BunkerWeb comes with a plugin system making it possible to easily add new features. Once a plugin is installed, you can manage it using additional settings defined by the plugin.
Here is the list of "official" plugins that we maintain (see the [bunkerweb-plugins](https://github.com/bunkerity/bunkerweb-plugins) repository for more information) :
| **ClamAV** | 1.10 | Automatically scans uploaded files with the ClamAV antivirus engine and denies the request when a file is detected as malicious. | [bunkerweb-plugins/clamav](https://github.com/bunkerity/bunkerweb-plugins/tree/main/clamav) |
| **Coraza** | 1.10 | Inspect requests using the Coraza WAF (alternative of ModSecurity). | [bunkerweb-plugins/coraza](https://github.com/bunkerity/bunkerweb-plugins/tree/main/coraza) |
| **Discord** | 1.10 | Send security notifications to a Discord channel using a Webhook. | [bunkerweb-plugins/discord](https://github.com/bunkerity/bunkerweb-plugins/tree/main/discord) |
| **Slack** | 1.10 | Send security notifications to a Slack channel using a Webhook. | [bunkerweb-plugins/slack](https://github.com/bunkerity/bunkerweb-plugins/tree/main/slack) |
| **VirusTotal** | 1.10 | Automatically scans uploaded files with the VirusTotal API and denies the request when a file is detected as malicious. | [bunkerweb-plugins/virustotal](https://github.com/bunkerity/bunkerweb-plugins/tree/main/virustotal) |
| **WebHook** | 1.10 | Send security notifications to a custom HTTP endpoint using a Webhook. | [bunkerweb-plugins/webhook](https://github.com/bunkerity/bunkerweb-plugins/tree/main/webhook) |
If you want to quickly install external plugins, you can use the `EXTERNAL_PLUGIN_URLS` setting. It takes a list of URLs separated by spaces, each pointing to a compressed (zip format) archive containing one or more plugins.
You can use the following value if you want to automatically install the official plugins : `EXTERNAL_PLUGIN_URLS=https://github.com/bunkerity/bunkerweb-plugins/archive/refs/tags/v1.10.zip`
The first step is to install the plugin by placing its files inside the corresponding `plugins` data folder. The procedure depends on your integration :
When using the [Docker integration](integrations.md#docker), plugins must be placed in the volume mounted on `/data/plugins` in the scheduler container.
The scheduler runs as an **unprivileged user with UID 101 and GID 101** inside the container. The reason behind this is security : in case a vulnerability is exploited, the attacker won't have full root (UID/GID 0) privileges.
But there is a downside : if you use a **local folder for the persistent data**, you will need to **set the correct permissions** so the unprivileged user can write data to it. Something like that should do the trick :
If you are using [Docker in rootless mode](https://docs.docker.com/engine/security/rootless) or [podman](https://podman.io/), UIDs and GIDs in the container will be mapped to different ones in the host. You will first need to check your initial subuid and subgid :
```shell
grep ^$(whoami): /etc/subuid && \
grep ^$(whoami): /etc/subgid
```
For example, if you have a value of **100000**, the mapped UID/GID will be **100100** (100000 + 100) :
When using the [Docker autoconf integration](integrations.md#docker-autoconf), plugins must be placed in the volume mounted on `/data/plugins` in the scheduler container.
The Swarm integration is deprecated and will be removed in a future release. Please consider using the [Kubernetes integration](integrations.md#kubernetes) instead.
Configuring a Swarm volume that will persist when the scheduler service is running on different nodes is not covered is in this documentation. We will assume that you have a shared folder mounted on `/shared` across all nodes.
When using the [Kubernetes integration](integrations.md#kubernetes), plugins must be placed in the volume mounted on `/data/plugins` in the scheduler container.
The first thing to do is to declare a [PersistentVolumeClaim](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) that will contain our plugins data :
If the documentation is not enough, you can have a look at the existing source code of [official plugins](https://github.com/bunkerity/bunkerweb-plugins) and the [core plugins](https://github.com/bunkerity/bunkerweb/tree/v1.6.11-rc1/src/common/core) (already included in BunkerWeb but they are plugins, technically speaking).
- **actions.py** : Script to execute on the Flask server. This script runs in a Flask context, giving you access to libraries and utilities like `jinja2` and `requests`.
This folder is used to override existing Flask blueprints or create new ones. Inside, you can include blueprint files and an optional **templates** subfolder for blueprint-specific templates.
- **my-template.json** : Add [custom templates](concepts.md#templates) to override the default values of settings and apply custom configurations easily.
- **plugin.lua** : Code executed on NGINX using the [NGINX LUA module](https://github.com/openresty/lua-nginx-module).
- **plugin.json** : Metadata, settings, and job definitions for your plugin.
| `file` | yes | string | Name of the file inside the jobs folder. |
| `every` | yes | string | Job scheduling frequency : `minute`, `hour`, `day`, `week` or `once` (no frequency, only once before (re)generating the configuration). |
Plugins can extend the `bwcli` tool with custom commands that run under `bwcli plugin <plugin_id> ...`:
1. Add a `bwcli` directory in your plugin and drop one file per command (for example `bwcli/list.py`). The CLI adds the plugin path to `sys.path` before executing the file.
2. Declare the commands in the optional `bwcli` section of `plugin.json`, mapping each command name to its executable file name.
```json
{
"bwcli": {
"list": "list.py",
"save": "save.py"
}
}
```
The scheduler automatically exposes the declared commands once the plugin is installed. Core plugins, such as `backup` in `src/common/core/backup`, follow the same pattern.
You can add custom NGINX configurations by adding a folder named **confs** with content similar to the [custom configurations](advanced.md#custom-configurations). Each subfolder inside the **confs** will contain [jinja2](https://jinja.palletsprojects.com) templates that will be generated and loaded at the corresponding context (`http`, `server-http`, `default-server-http`, `stream`, `server-stream`, `modsec`, `modsec-crs`, `crs-plugins-before` and `crs-plugins-after`).
Under the hood, BunkerWeb is using the [NGINX LUA module](https://github.com/openresty/lua-nginx-module) to execute code within NGINX. Plugins that need to execute code must provide a lua file at the root directory of the plugin folder using the `id` value of **plugin.json** as its name. Here is an example named **myplugin.lua** :
| `init` | [init_by_lua](https://github.com/openresty/lua-nginx-module#init_by_lua) | Called when NGINX just started or received a reload order. the typical use case is to prepare any data that will be used by your plugin. | `ret`, `msg`<ul><li>`ret` (boolean) : true if no error or else false</li><li>`msg` (string) : success or error message</li></ul> |
| `set` | [set_by_lua](https://github.com/openresty/lua-nginx-module#set_by_lua) | Called before each request received by the server.The typical use case is for computing before access phase. | `ret`, `msg`<ul><li>`ret` (boolean) : true if no error or else false</li><li>`msg` (string) : success or error message</li></ul> |
| `access` | [access_by_lua](https://github.com/openresty/lua-nginx-module#access_by_lua) | Called on each request received by the server. The typical use case is to do the security checks here and deny the request if needed. | `ret`, `msg`,`status`,`redirect`<ul><li>`ret` (boolean) : true if no error or else false</li><li>`msg` (string) : success or error message</li><li>`status` (number) : interrupt current process and return [HTTP status](https://github.com/openresty/lua-nginx-module#http-status-constants)</li><li>`redirect` (URL) : if set will redirect to given URL</li></ul> |
| `log` | [log_by_lua](https://github.com/openresty/lua-nginx-module#log_by_lua) | Called when a request has finished (and before it gets logged to the access logs). The typical use case is to make stats or compute counters for example. | `ret`, `msg`<ul><li>`ret` (boolean) : true if no error or else false</li><li>`msg` (string) : success or error message</li></ul> |
| `log_default` | [log_by_lua](https://github.com/openresty/lua-nginx-module#log_by_lua) | Same as `log` but only called on the default server. | `ret`, `msg`<ul><li>`ret` (boolean) : true if no error or else false</li><li>`msg` (string) : success or error message</li></ul> |
| `preread` | [preread_by_lua](https://github.com/openresty/stream-lua-nginx-module#preread_by_lua_block) | Similar to the `access` function but for stream mode. | `ret`, `msg`,`status`<ul><li>`ret` (boolean) : true if no error or else false</li><li>`msg` (string) : success or error message</li><li>`status` (number) : interrupt current process and return [status](https://github.com/openresty/lua-nginx-module#http-status-constants)</li></ul> |
| `log_stream` | [log_by_lua](https://github.com/openresty/stream-lua-nginx-module#log_by_lua_block) | Similar to the `log` function but for stream mode. | `ret`, `msg`<ul><li>`ret` (boolean) : true if no error or else false</li><li>`msg` (string) : success or error message</li></ul> |
All directives from [NGINX LUA module](https://github.com/openresty/lua-nginx-module) and are available and [NGINX stream LUA module](https://github.com/openresty/stream-lua-nginx-module). On top of that, you can use the LUA libraries included within BunkerWeb : see [this script](https://github.com/bunkerity/bunkerweb/blobsrc/deps/clone.sh) for the complete list.
If you need additional libraries, you can put them in the root folder of the plugin and access them by prefixing them with your plugin ID. Here is an example file named **mylibrary.lua** :
If you want to see the full list of available functions, you can have a look at the files present in the [lua directory](https://github.com/bunkerity/bunkerweb/tree/v1.6.11-rc1/src/bw/lua/bunkerweb) of the repository.
BunkerWeb uses an internal job scheduler for periodic tasks like renewing certificates with certbot, downloading blacklists, downloading MMDB files, ... You can add tasks of your choice by putting them inside a subfolder named **jobs** and listing them in the **plugin.json** metadata file. Don't forget to add the execution permissions for everyone to avoid any problems when a user is cloning and installing your plugin.
Everything related to the web UI is located inside the subfolder **ui** as we seen in the [previous structure section.](#structure).
#### Prerequisites
When you want to create a plugin page, you need two files :
- **template.html** that will be accessible with a **GET /plugins/<*plugin_id*>**.
- **actions.py** where you can add some scripting and logic with a **POST /plugins/<*plugin_id*>**. Notice that this file **need a function with the same name as the plugin** to work. This file is needed even if the function is empty.
We can put aside the **actions.py** file and start **only using the template on a GET situation**. The template can access app context and libs, so you can use Jinja, request or flask utils.
You have two functions by default in **actions.py** :
**pre_render function**
This allows you to retrieve data when you **GET** the template, and to use the data with the pre_render variable available in Jinja to display content more dynamically.
BunkerWeb's Web UI includes a set of pre-installed Python libraries that you can use in your plugin's `actions.py` or other UI-related scripts. These are available out-of-the-box without needing additional installations.
If you need libraries not listed above, install them inside the `ui` folder of your plugin and import them using the classical `import` directive. Ensure compatibility with the existing environment to avoid conflicts.
This documentation outlines the lifecycle hooks used for managing different stages of a request within the application. Each hook is associated with a specific phase.
=== "before_request"
These hooks are executed **before** processing an incoming request. They are typically used for pre-processing tasks such as authentication, validation, or logging.
If the hook returns a response object, Flask will skip the request handling and return the response directly. This can be useful for short-circuiting the request processing pipeline.
# Perform authentication, validation, or logging here
if not is_valid_request(request): # We are in the app context
return Response("Invalid request!", status=400)
def is_valid_request(request):
# Dummy validation logic
return "user" in request
```
=== "after_request"
These hooks that run **after** the request has been processed. They are ideal for post-processing tasks such as cleanup, additional logging, or modifying the response before it is sent back.
They receive the response object as an argument and can modify it before returning it. The first after_request hook to return a response will be used as the final response.
# Perform logging, cleanup, or response modifications here
log_response(response)
return response
def log_response(response):
# Dummy logging logic
print("Response logged:", response, flush=True)
```
=== "teardown_request"
These hooks are invoked when the request context is being torn down. These hooks are used for releasing resources or handling errors that occurred during the request lifecycle.
**Example:**
```python
def teardown_request(error=None):
print("Teardown-request: Cleaning up resources...", flush=True)
# Perform cleanup, release resources, or handle errors here
if error:
handle_error(error)
cleanup_resources()
def handle_error(error):
# Dummy error handling logic
print("Error encountered:", error, flush=True)
def cleanup_resources():
# Dummy resource cleanup logic
print("Resources have been cleaned up.", flush=True)
```
=== "context_processor"
These hooks are used to inject additional context into templates or views. They enrich the runtime context by passing common data (like user information or configuration settings) to the templates.
If a context processor returns a dictionary, the keys and values will be added to the context for all templates. This allows you to share data across multiple views or templates.
# Return a dictionary containing context data for templates/views
return {
"current_user": "John Doe",
"app_version": "1.0.0",
"feature_flags": {"new_ui": True}
}
```
This lifecycle hook design provides a modular and systematic approach to managing various aspects of a request's lifecycle:
- **Modularity:** Each hook is responsible for a distinct phase, ensuring that concerns are separated.
- **Maintainability:** Developers can easily add, modify, or remove hook implementations without impacting other parts of the request lifecycle.
- **Extensibility:** The framework is flexible, allowing for additional hooks or enhancements as application requirements evolve.
By clearly defining the responsibilities of each hook and their associated logging prefixes, the system ensures that each stage of request processing is transparent and manageable.
### Blueprints
In Flask, **blueprints** serve as a modular way to organize related components—such as views, templates, and static files—within your application. They allow you to group functionality logically and can be used to create new sections of your app or override existing ones.
#### Creating a Blueprint
To define a blueprint, you create an instance of the `Blueprint` class, specifying its name and import path. You then define routes and views associated with this blueprint.
**Example: Defining a New Blueprint**
```python
from os.path import dirname
from flask import Blueprint, render_template
# Define the blueprint
my_blueprint = Blueprint('my_blueprint', __name__, template_folder=dirname(__file__) + '/templates') # The template_folder is set to avoid conflicts with the original blueprint
# Define a route within the blueprint
@my_blueprint.route('/my_blueprint')
def my_blueprint_page():
return render_template('my_blueprint.html')
```
In this example, a blueprint named `my_blueprint` is created, and a route `/my_blueprint` is defined within it.
#### Overriding an Existing Blueprint
Blueprints can also override existing ones to modify or extend functionality. To do this, ensure that the new blueprint has the same name as the one you're overriding and register it after the original.
**Example: Overriding an Existing Blueprint**
```python
from os.path import dirname
from flask import Flask, Blueprint
# Original blueprint
instances = Blueprint('instances', __name__, template_folder=dirname(__file__) + '/templates') # The template_folder is set to avoid conflicts with the original blueprint
@instances.route('/instances')
def override_instances():
return "My new instances page"
```
In this scenario, accessing the URL `/instances` will display "My new instances page" because the `instances` blueprint, registered last, overrides the original `instances` blueprint.
!!! warning "About overriding"
Be cautious when overriding existing blueprints, as it can impact the behavior of the application. Ensure that the changes align with the application's requirements and do not introduce unexpected side effects.
All existing routes will be removed from he original blueprint, so you will need to re-implement them if needed.
#### Naming Conventions
!!! danger "Important"
Ensure the blueprint’s name matches the blueprint variable name, else it will not be considered as a valid blueprint and will not be registered.
For consistency and clarity, it's advisable to follow these naming conventions:
- **Blueprint Names**: Use short, all-lowercase names. Underscores can be used for readability, e.g., `user_auth`.
- **File Names**: Match the filename to the blueprint name, ensuring it's all lowercase with underscores as needed, e.g., `user_auth.py`.
This practice aligns with Python's module naming conventions and helps maintain a clear project structure.
**Example: Blueprint and File Naming**
```
plugin /
ui / blueprints / user_auth.py
templates / user_auth.html
```
In this structure, `user_auth.py` contains the `user_auth` blueprint, and `user_auth.html` is the associated template, adhering to the recommended naming conventions.