feat: enhance custom SSL certificate support to accept plaintext PEM format in addition to base64 encoding

This commit is contained in:
Théophile Diot 2025-04-15 09:06:58 +02:00
parent 37bdef6622
commit 036c5340ef
No known key found for this signature in database
GPG key ID: FA995104A0BA376A
6 changed files with 141 additions and 72 deletions

View file

@ -96,3 +96,5 @@ docs/web-ui.md:hashicorp-tf-password:993
docs/web-ui.md:hashicorp-tf-password:1149
docs/web-ui.md:hashicorp-tf-password:1180
src/common/core/antibot/files/recaptcha.html:generic-api-key:26
src/common/core/customcert/README.md:private-key:84
docs/features.md:private-key:1847

View file

@ -4,6 +4,7 @@
- [DEPS] Update coreruleset-v4 version to v4.13.0
- [FEATURE] Add the possibility to choose a profile when generating certificates with Let's Encrypt using the `LETS_ENCRYPT_PROFILE` setting (`classic` (default), `tlsserver` for server-only validation, and `shortlived` for reduced 7-day validity) to provide flexibility in certificate configuration based on security requirements
- [FEATURE] Add the possibility to declare custom certificates and keys data as plaintext as well as base64-encoded data in the `customcert` plugin using the `CUSTOM_SSL_CERT_DATA` and `CUSTOM_SSL_KEY_DATA` settings
- [UI] Integrate Biscuit authentication and key management
## v1.6.2-rc1 - 2025/03/29

View file

@ -1502,7 +1502,7 @@ CrowdSec is a modern, open-source security engine that detects and blocks malici
};
destination d_file {
file("/var/log/bunkerweb/bunkerweb.log" template(t_imp));
file("/var/log/bunkerweb.log" template(t_imp));
};
log {
@ -1765,7 +1765,7 @@ The Custom SSL certificate plugin allows you to use your own SSL/TLS certificate
**How it works:**
1. You provide BunkerWeb with your certificate and private key files, either by specifying file paths or by providing the data in base64-encoded format.
1. You provide BunkerWeb with your certificate and private key files, either by specifying file paths or by providing the data in base64-encoded or plaintext PEM format.
2. BunkerWeb validates your certificate and key to ensure they are properly formatted and usable.
3. When a secure connection is established, BunkerWeb serves your custom certificate instead of the auto-generated one.
4. BunkerWeb automatically monitors your certificate's validity and displays warnings if it is approaching expiration.
@ -1779,9 +1779,9 @@ The Custom SSL certificate plugin allows you to use your own SSL/TLS certificate
Follow these steps to configure and use the Custom SSL certificate feature:
1. **Enable the feature:** Set the `USE_CUSTOM_SSL` setting to `yes` to enable custom certificate support.
2. **Choose a method:** Decide whether to provide certificates via file paths or as base64-encoded data, and set the priority using `CUSTOM_SSL_CERT_PRIORITY`.
2. **Choose a method:** Decide whether to provide certificates via file paths or as base64-encoded/plaintext data, and set the priority using `CUSTOM_SSL_CERT_PRIORITY`.
3. **Provide certificate files:** If using file paths, specify the locations of your certificate and private key files.
4. **Or provide certificate data:** If using base64 data, provide your certificate and key as base64-encoded strings.
4. **Or provide certificate data:** If using data, provide your certificate and key as either base64-encoded strings or plaintext PEM format.
5. **Let BunkerWeb handle the rest:** Once configured, BunkerWeb automatically uses your custom certificates for all HTTPS connections.
!!! tip "Stream Mode Configuration"
@ -1795,8 +1795,8 @@ Follow these steps to configure and use the Custom SSL certificate feature:
| `CUSTOM_SSL_CERT_PRIORITY` | `file` | multisite | no | **Certificate Priority:** Choose whether to prioritize the certificate from file path or from base64 data (`file` or `data`). |
| `CUSTOM_SSL_CERT` | | multisite | no | **Certificate Path:** Full path to your SSL certificate or certificate bundle file. |
| `CUSTOM_SSL_KEY` | | multisite | no | **Private Key Path:** Full path to your SSL private key file. |
| `CUSTOM_SSL_CERT_DATA` | | multisite | no | **Certificate Data:** Your certificate encoded in base64 format. |
| `CUSTOM_SSL_KEY_DATA` | | multisite | no | **Private Key Data:** Your private key encoded in base64 format. |
| `CUSTOM_SSL_CERT_DATA` | | multisite | no | **Certificate Data:** Your certificate encoded in base64 format or as plaintext PEM. |
| `CUSTOM_SSL_KEY_DATA` | | multisite | no | **Private Key Data:** Your private key encoded in base64 format or as plaintext PEM. |
!!! warning "Security Considerations"
When using custom certificates, ensure your private key is properly secured and has appropriate permissions. The files must be readable by the BunkerWeb scheduler.
@ -1832,6 +1832,23 @@ Follow these steps to configure and use the Custom SSL certificate feature:
CUSTOM_SSL_KEY_DATA: "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSEV...base64 encoded key...Cg=="
```
=== "Using Plaintext PEM Data"
A configuration using plaintext certificate and key data in PEM format:
```yaml
USE_CUSTOM_SSL: "yes"
CUSTOM_SSL_CERT_PRIORITY: "data"
CUSTOM_SSL_CERT_DATA: |
-----BEGIN CERTIFICATE-----
MIIDdzCCAl+gAwIBAgIUJH...certificate content...AAAA
-----END CERTIFICATE-----
CUSTOM_SSL_KEY_DATA: |
-----BEGIN PRIVATE KEY-----
MIIEvQIBADAN...key content...AAAA
-----END PRIVATE KEY-----
```
=== "Fallback Configuration"
A configuration that prioritizes files but falls back to base64 data if files are unavailable:
@ -2835,18 +2852,21 @@ STREAM support :x:
Provides load balancing feature to group of upstreams with optional healthchecks.
| Setting | Default | Context | Multiple | Description |
| ----------------------------------------- | --------- | ------- | -------- | ------------------------------------------------------------------ |
| `LOADBALANCER_HEALTHCHECK_DICT_SIZE` | `10m` | global | no | Shared dict size (datastore for all healthchecks). |
| `LOADBALANCER_UPSTREAM_NAME` | | global | yes | Name of the upstream (used in REVERSE_PROXY_HOST). |
| `LOADBALANCER_UPSTREAM_SERVERS` | | global | yes | List of servers/IPs in the server group. |
| `LOADBALANCER_HEALTHCHECK_URL` | `/status` | global | yes | The healthcheck URL. |
| `LOADBALANCER_HEALTHCHECK_INTERVAL` | `2000` | global | yes | Healthcheck interval in milliseconds. |
| `LOADBALANCER_HEALTHCHECK_TIMEOUT` | `1000` | global | yes | Healthcheck timeout in milliseconds. |
| `LOADBALANCER_HEALTHCHECK_FALL` | `3` | global | yes | Number of failed healthchecks before marking the server as down. |
| `LOADBALANCER_HEALTHCHECK_RISE` | `1` | global | yes | Number of successful healthchecks before marking the server as up. |
| `LOADBALANCER_HEALTHCHECK_VALID_STATUSES` | `200` | global | yes | HTTP status considered valid in healthchecks. |
| `LOADBALANCER_HEALTHCHECK_CONCURRENCY` | `10` | global | yes | Maximum number of concurrent healthchecks. |
| Setting | Default | Context | Multiple | Description |
| ----------------------------------------- | ------------------- | ------- | -------- | ----------------------------------------------------------------------------------- |
| `LOADBALANCER_HEALTHCHECK_DICT_SIZE` | `10m` | global | no | Shared dict size (datastore for all healthchecks). |
| `LOADBALANCER_UPSTREAM_NAME` | | global | yes | Name of the upstream (used in REVERSE_PROXY_HOST). |
| `LOADBALANCER_UPSTREAM_SERVERS` | | global | yes | List of servers/IPs in the server group. |
| `LOADBALANCER_UPSTREAM_MODE` | `round-robin` | global | yes | Load balancing mode (round-robin or sticky). |
| `LOADBALANCER_UPSTREAM_STICKY` | `srv_id expires=4h` | global | yes | Settings when load balancing mode is set to sticky (see sticky directive of nginx). |
| `LOADBALANCER_UPSTREAM_RESOLVE` | `no` | global | yes | Dynamically resolve upstream hostnames. |
| `LOADBALANCER_HEALTHCHECK_URL` | `/status` | global | yes | The healthcheck URL. |
| `LOADBALANCER_HEALTHCHECK_INTERVAL` | `2000` | global | yes | Healthcheck interval in milliseconds. |
| `LOADBALANCER_HEALTHCHECK_TIMEOUT` | `1000` | global | yes | Healthcheck timeout in milliseconds. |
| `LOADBALANCER_HEALTHCHECK_FALL` | `3` | global | yes | Number of failed healthchecks before marking the server as down. |
| `LOADBALANCER_HEALTHCHECK_RISE` | `1` | global | yes | Number of successful healthchecks before marking the server as up. |
| `LOADBALANCER_HEALTHCHECK_VALID_STATUSES` | `200` | global | yes | HTTP status considered valid in healthchecks. |
| `LOADBALANCER_HEALTHCHECK_CONCURRENCY` | `10` | global | yes | Maximum number of concurrent healthchecks. |
## Metrics

View file

@ -2,7 +2,7 @@ The Custom SSL certificate plugin allows you to use your own SSL/TLS certificate
**How it works:**
1. You provide BunkerWeb with your certificate and private key files, either by specifying file paths or by providing the data in base64-encoded format.
1. You provide BunkerWeb with your certificate and private key files, either by specifying file paths or by providing the data in base64-encoded or plaintext PEM format.
2. BunkerWeb validates your certificate and key to ensure they are properly formatted and usable.
3. When a secure connection is established, BunkerWeb serves your custom certificate instead of the auto-generated one.
4. BunkerWeb automatically monitors your certificate's validity and displays warnings if it is approaching expiration.
@ -16,9 +16,9 @@ The Custom SSL certificate plugin allows you to use your own SSL/TLS certificate
Follow these steps to configure and use the Custom SSL certificate feature:
1. **Enable the feature:** Set the `USE_CUSTOM_SSL` setting to `yes` to enable custom certificate support.
2. **Choose a method:** Decide whether to provide certificates via file paths or as base64-encoded data, and set the priority using `CUSTOM_SSL_CERT_PRIORITY`.
2. **Choose a method:** Decide whether to provide certificates via file paths or as base64-encoded/plaintext data, and set the priority using `CUSTOM_SSL_CERT_PRIORITY`.
3. **Provide certificate files:** If using file paths, specify the locations of your certificate and private key files.
4. **Or provide certificate data:** If using base64 data, provide your certificate and key as base64-encoded strings.
4. **Or provide certificate data:** If using data, provide your certificate and key as either base64-encoded strings or plaintext PEM format.
5. **Let BunkerWeb handle the rest:** Once configured, BunkerWeb automatically uses your custom certificates for all HTTPS connections.
!!! tip "Stream Mode Configuration"
@ -32,8 +32,8 @@ Follow these steps to configure and use the Custom SSL certificate feature:
| `CUSTOM_SSL_CERT_PRIORITY` | `file` | multisite | no | **Certificate Priority:** Choose whether to prioritize the certificate from file path or from base64 data (`file` or `data`). |
| `CUSTOM_SSL_CERT` | | multisite | no | **Certificate Path:** Full path to your SSL certificate or certificate bundle file. |
| `CUSTOM_SSL_KEY` | | multisite | no | **Private Key Path:** Full path to your SSL private key file. |
| `CUSTOM_SSL_CERT_DATA` | | multisite | no | **Certificate Data:** Your certificate encoded in base64 format. |
| `CUSTOM_SSL_KEY_DATA` | | multisite | no | **Private Key Data:** Your private key encoded in base64 format. |
| `CUSTOM_SSL_CERT_DATA` | | multisite | no | **Certificate Data:** Your certificate encoded in base64 format or as plaintext PEM. |
| `CUSTOM_SSL_KEY_DATA` | | multisite | no | **Private Key Data:** Your private key encoded in base64 format or as plaintext PEM. |
!!! warning "Security Considerations"
When using custom certificates, ensure your private key is properly secured and has appropriate permissions. The files must be readable by the BunkerWeb scheduler.
@ -69,6 +69,23 @@ Follow these steps to configure and use the Custom SSL certificate feature:
CUSTOM_SSL_KEY_DATA: "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSEV...base64 encoded key...Cg=="
```
=== "Using Plaintext PEM Data"
A configuration using plaintext certificate and key data in PEM format:
```yaml
USE_CUSTOM_SSL: "yes"
CUSTOM_SSL_CERT_PRIORITY: "data"
CUSTOM_SSL_CERT_DATA: |
-----BEGIN CERTIFICATE-----
MIIDdzCCAl+gAwIBAgIUJH...certificate content...AAAA
-----END CERTIFICATE-----
CUSTOM_SSL_KEY_DATA: |
-----BEGIN PRIVATE KEY-----
MIIEvQIBADAN...key content...AAAA
-----END PRIVATE KEY-----
```
=== "Fallback Configuration"
A configuration that prioritizes files but falls back to base64 data if files are unavailable:

View file

@ -8,7 +8,7 @@ from sys import exit as sys_exit, path as sys_path
from base64 import b64decode
from tempfile import NamedTemporaryFile
from traceback import format_exc
from typing import Tuple, Union
from typing import Tuple, Union, Optional, Literal
for deps_path in [join(sep, "usr", "share", "bunkerweb", *paths) for paths in (("deps", "python"), ("utils",), ("db",))]:
if deps_path not in sys_path:
@ -22,6 +22,48 @@ LOGGER = setup_logger("CUSTOM-CERT")
JOB = Job(LOGGER, __file__)
def process_ssl_data(data: str, file_path: Optional[str], data_type: Literal["cert", "key"], server_name: str) -> Union[bytes, Path, None]:
"""Process SSL certificate or key data from file path or direct data (base64 or plain text)"""
try:
if file_path:
path_obj = Path(file_path)
if not path_obj.is_file():
LOGGER.error(f"{data_type.capitalize()} file {file_path} is not a valid file for {server_name}")
return None
return path_obj
if not data:
return None
# Try base64 decode first
try:
decoded = b64decode(data)
return decoded
except BaseException:
LOGGER.debug(format_exc())
LOGGER.warning(f"Failed to decode {data_type} data as base64 for server {server_name}, trying as plain text")
# Try using the data directly as plain text
try:
text_data = data.encode()
# Quick validation check
if data_type == "cert" and not text_data.strip().startswith(b"-----BEGIN CERTIFICATE-----"):
LOGGER.error(f"Invalid certificate format for server {server_name}")
return None
elif data_type == "key" and (not text_data.strip().startswith(b"-----BEGIN") or not (b"PRIVATE KEY" in text_data)):
LOGGER.error(f"Invalid key format for server {server_name}")
return None
return text_data
except BaseException:
LOGGER.debug(format_exc())
LOGGER.error(f"Error while processing {data_type} data for server {server_name}")
return None
except BaseException as e:
LOGGER.debug(format_exc())
LOGGER.error(f"Error processing {data_type} for {server_name}: {e}")
return None
def check_cert(cert_file: Union[Path, bytes], key_file: Union[Path, bytes], first_server: str) -> Tuple[bool, Union[str, BaseException]]:
try:
ret = False
@ -112,59 +154,46 @@ try:
LOGGER.info(f"Service {first_server} is using custom SSL certificates, checking ...")
cert_priority = getenv(f"{first_server}_CUSTOM_SSL_CERT_PRIORITY", getenv("CUSTOM_SSL_CERT_PRIORITY", "file"))
cert_file = getenv(f"{first_server}_CUSTOM_SSL_CERT", getenv("CUSTOM_SSL_CERT", ""))
key_file = getenv(f"{first_server}_CUSTOM_SSL_KEY", getenv("CUSTOM_SSL_KEY", ""))
cert_file_path = getenv(f"{first_server}_CUSTOM_SSL_CERT", getenv("CUSTOM_SSL_CERT", ""))
key_file_path = getenv(f"{first_server}_CUSTOM_SSL_KEY", getenv("CUSTOM_SSL_KEY", ""))
cert_data = getenv(f"{first_server}_CUSTOM_SSL_CERT_DATA", getenv("CUSTOM_SSL_CERT_DATA", ""))
key_data = getenv(f"{first_server}_CUSTOM_SSL_KEY_DATA", getenv("CUSTOM_SSL_KEY_DATA", ""))
if (cert_file or cert_data) and (key_file or key_data):
if (cert_priority == "file" or not cert_data) and cert_file:
cert_file = Path(cert_file)
else:
try:
cert_file = b64decode(cert_data)
except BaseException as e:
LOGGER.debug(format_exc())
LOGGER.error(f"Error while decoding cert data, skipping server {first_server} :\n{e}")
skipped_servers.append(first_server)
status = 2
continue
# Use file or data based on priority
use_cert_file = cert_priority == "file" and cert_file_path
use_key_file = cert_priority == "file" and key_file_path
if (cert_priority == "file" or not key_data) and key_file:
key_file = Path(key_file)
else:
try:
key_file = b64decode(key_data)
except BaseException as e:
LOGGER.debug(format_exc())
LOGGER.error(f"Error while decoding key data, skipping server {first_server} :\n{e}")
skipped_servers.append(first_server)
status = 2
continue
cert_file = process_ssl_data(cert_data if not use_cert_file else "", cert_file_path if use_cert_file else None, "cert", first_server)
LOGGER.info(f"Checking certificate for {first_server} ...")
need_reload, err = check_cert(cert_file, key_file, first_server)
if isinstance(err, BaseException):
LOGGER.error(f"Exception while checking {first_server}'s certificate, skipping ... \n{err}")
skipped_servers.append(first_server)
status = 2
continue
elif err:
LOGGER.warning(f"Error while checking {first_server}'s certificate : {err}")
skipped_servers.append(first_server)
status = 2
continue
elif need_reload:
LOGGER.info(f"Detected change in {first_server}'s certificate")
status = 1
continue
key_file = process_ssl_data(key_data if not use_key_file else "", key_file_path if use_key_file else None, "key", first_server)
LOGGER.info(f"No change in {first_server}'s certificate")
elif not cert_file or not key_file:
if not cert_file or not key_file:
LOGGER.warning(
"Variables (CUSTOM_SSL_CERT or CUSTOM_SSL_CERT_DATA) and (CUSTOM_SSL_KEY or CUSTOM_SSL_KEY_DATA) have to be set to use custom certificates"
"Variables (CUSTOM_SSL_CERT or CUSTOM_SSL_CERT_DATA) and (CUSTOM_SSL_KEY or CUSTOM_SSL_KEY_DATA) "
f"have to be set and valid to use custom certificates for {first_server}"
)
skipped_servers.append(first_server)
status = 2
continue
LOGGER.info(f"Checking certificate for {first_server} ...")
need_reload, err = check_cert(cert_file, key_file, first_server)
if isinstance(err, BaseException):
LOGGER.error(f"Exception while checking {first_server}'s certificate, skipping ... \n{err}")
skipped_servers.append(first_server)
status = 2
continue
elif err:
LOGGER.warning(f"Error while checking {first_server}'s certificate : {err}")
skipped_servers.append(first_server)
status = 2
continue
elif need_reload:
LOGGER.info(f"Detected change in {first_server}'s certificate")
status = 1
continue
LOGGER.info(f"No change in {first_server}'s certificate")
for first_server in skipped_servers:
JOB.del_cache("cert.pem", service_id=first_server)

View file

@ -45,18 +45,18 @@
"CUSTOM_SSL_CERT_DATA": {
"context": "multisite",
"default": "",
"help": "Certificate data encoded in base64.",
"help": "Certificate data encoded in base64 or plaintext PEM format.",
"id": "custom-ssl-cert-data",
"label": "Certificate data (base64)",
"label": "Certificate data (base64/plaintext)",
"regex": "^.*$",
"type": "text"
},
"CUSTOM_SSL_KEY_DATA": {
"context": "multisite",
"default": "",
"help": "Key data encoded in base64.",
"help": "Key data encoded in base64 or plaintext PEM format.",
"id": "custom-ssl-key-data",
"label": "Key data (base64)",
"label": "Key data (base64/plaintext)",
"regex": "^.*$",
"type": "text"
}