Skip to content

Commit ecbc873

Browse files
Merge pull request #3585 from bunkerity/dev
2 parents 58c238e + 85e6e65 commit ecbc873

29 files changed

Lines changed: 1266 additions & 216 deletions

.github/workflows/container-build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ jobs:
130130
# Check vulnerabilities with Docker Scout
131131
- name: Docker Scout CVE Analysis
132132
if: ${{ startsWith(inputs.CACHE_SUFFIX, 'arm') == false }}
133-
uses: docker/scout-action@bacf462e8d090c09660de30a6ccc718035f961e3 # v1.20.4
133+
uses: docker/scout-action@cd72f264beff1cd72735de31148b9d3244a0234a # v1.21.0
134134
with:
135135
command: cves,recommendations
136136
image: local/${{ inputs.IMAGE }}

.github/workflows/push-packagecloud.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ jobs:
4242
- name: Check out repository code
4343
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
4444
- name: Install ruby
45-
uses: ruby/setup-ruby@97ecb7b512899eb71ab1bf2310a624c6f1589ac6 # v1.308.0
45+
uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1.310.0
4646
with:
4747
ruby-version: "3.0"
4848
- name: Install packagecloud

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
# Changelog
22

3-
## v1.6.10
3+
## v1.6.11~rc1 - ????/??/??
4+
5+
- [BUGFIX] `letsencrypt` (core): fix self-propagating cache poisoning that caused fleet-wide `certbot AccountNotFound`; add CA-agnostic consistency gate (LE + ZeroSSL paths), server-scoped `select_account_id`, redacted-value `Configurator` WARN logs.
6+
- [SECURITY] `letsencrypt` (UI): harden delete + new heal flow — per-request scratch dir, `fcntl.flock`, `.`/`..` rejected in `cert_name`, DOMPurify + `markupsafe.escape` at every HTML sink, 500 on persistence failure; new `/letsencrypt/{orphans,accounts,cache-status,heal}` endpoints, per-row Heal button, sidebar orphan toast.
7+
- [FEATURE] `scheduler`: new `SCHEDULER_MAX_WORKERS` env var caps the job-executor thread pool to bound DB-pool pressure on shared MariaDB/MySQL/PostgreSQL; auto default tightened from `min(8, cpu*4)` to `min(8, max(2, cpu*2))` and a warning is emitted when the resolved value exceeds `DATABASE_POOL_SIZE` + `DATABASE_POOL_MAX_OVERFLOW`.
8+
- [SECURITY] `linux`: `after-remove` hooks now preserve `/var/log/bunkerweb`, `/etc/bunkerweb`, `/var/lib/bunkerweb` and `/var/tmp` upgrade backups on plain uninstall (only purge wipes configs + DB; logs and backups always kept, disposal commands printed); upgrade backups are written via `install -m 0600 -o root -g root` (atomic) and any pre-existing world-readable backups are retro-tightened, closing a local-read window on admin credentials and the SQLite DB.
9+
10+
## v1.6.10 - 2026/05/19
411

512
- [SECURITY] `nginx` : update nginx to 1.30.1 to fix various CVEs
613
- [BUGFIX] `reverseproxy`: pin a `USE_UI=yes` service upstream to HTTP/1.1 so a global `REVERSE_PROXY_HTTP_VERSION=2` no longer locks out the web UI. (Fixes #3550)

docs/de/integrations.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1777,6 +1777,7 @@ Der Scheduler ist der Control-Plane-Worker, der Einstellungen liest, Konfigurati
17771777
| `DISABLE_CONFIGURATION_TESTING` | Konfigtests vor dem Anwenden überspringen | `yes` oder `no` | `no` |
17781778
| `IGNORE_FAIL_SENDING_CONFIG` | Fortfahren, auch wenn einige Instanzen keine Konfig erhalten | `yes` oder `no` | `no` |
17791779
| `IGNORE_REGEX_CHECK` | Regex-Validierung für Einstellungen überspringen (geteilt mit Autoconf) | `yes` oder `no` | `no` |
1780+
| `SCHEDULER_MAX_WORKERS` | Maximale Anzahl an Worker-Threads im Job-Executor des Schedulers. Jeder laufende Thread kann eine DB-Verbindung halten, was die DB-Pool-Belastung auf Scheduler-Seite begrenzt. Beim Start wird eine Warnung ausgegeben, wenn der ermittelte Wert `DATABASE_POOL_SIZE` + `DATABASE_POOL_MAX_OVERFLOW` überschreitet. | Positive Ganzzahl | `min(8, max(2, cpu_count*2))` |
17801781
| `TZ` | Zeitzone für Scheduler-Logs, Cron-ähnliche Jobs, Backups und Zeitstempel | TZ-Datenbank-Name (z. B. `UTC`, `Europe/Paris`) | unset (Container-Standard, meist UTC) |
17811782

17821783
##### Datenbank

docs/es/integrations.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1778,6 +1778,7 @@ El programador es el worker del plano de control que lee configuraciones, genera
17781778
| `DISABLE_CONFIGURATION_TESTING` | Saltar pruebas de configuración antes de aplicar | `yes` o `no` | `no` |
17791779
| `IGNORE_FAIL_SENDING_CONFIG` | Continuar incluso si algunas instancias no reciben la configuración | `yes` o `no` | `no` |
17801780
| `IGNORE_REGEX_CHECK` | Omitir validación regex de configuraciones (compartido con autoconf) | `yes` o `no` | `no` |
1781+
| `SCHEDULER_MAX_WORKERS` | Número máximo de hilos en el ejecutor de jobs del Scheduler. Cada hilo activo puede mantener una conexión a la BD, limitando la presión sobre el pool desde el Scheduler. Al iniciar se emite una advertencia si el valor resuelto supera `DATABASE_POOL_SIZE` + `DATABASE_POOL_MAX_OVERFLOW`. | Entero positivo | `min(8, max(2, cpu_count*2))` |
17811782
| `TZ` | Zona horaria para logs del programador, jobs tipo cron, backups y marcas de tiempo | Nombre en base TZ (ej. `UTC`, `Europe/Paris`) | unset (default de contenedor, suele ser UTC) |
17821783

17831784
##### Base de datos

docs/fr/integrations.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1786,6 +1786,7 @@ Le Scheduler est le worker du plan de contrôle qui lit les paramètres, rend le
17861786
| `DISABLE_CONFIGURATION_TESTING` | Sauter les tests de configuration avant application | `yes` ou `no` | `no` |
17871787
| `IGNORE_FAIL_SENDING_CONFIG` | Continuer même si certaines instances ne reçoivent pas la config | `yes` ou `no` | `no` |
17881788
| `IGNORE_REGEX_CHECK` | Ignorer la validation regex des paramètres (partagé avec autoconf) | `yes` ou `no` | `no` |
1789+
| `SCHEDULER_MAX_WORKERS` | Nombre maximal de threads dans l'exécuteur de jobs du Scheduler. Chaque thread peut détenir une connexion DB, ce qui borne la pression sur le pool côté Scheduler. Un avertissement est émis au démarrage si la valeur résolue dépasse `DATABASE_POOL_SIZE` + `DATABASE_POOL_MAX_OVERFLOW`. | Entier positif | `min(8, max(2, cpu_count*2))` |
17891790
| `TZ` | Fuseau horaire pour les logs du Scheduler, tâches type cron, sauvegardes et dates | Nom de base TZ (ex. `UTC`, `Europe/Paris`) | unset (défaut conteneur, généralement UTC) |
17901791

17911792
##### Base de données

docs/integrations.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1792,6 +1792,7 @@ The scheduler is the control-plane worker that reads settings, renders configs,
17921792
| `DISABLE_CONFIGURATION_TESTING` | Skip config tests before applying | `yes` or `no` | `no` |
17931793
| `IGNORE_FAIL_SENDING_CONFIG` | Proceed even if some instances fail to receive a config | `yes` or `no` | `no` |
17941794
| `IGNORE_REGEX_CHECK` | Skip regex validation for settings (shared with autoconf) | `yes` or `no` | `no` |
1795+
| `SCHEDULER_MAX_WORKERS` | Max worker threads in the scheduler's job executor. Each running thread can hold one DB connection, so this caps scheduler-side DB-pool pressure. A startup warning is emitted if the resolved value exceeds `DATABASE_POOL_SIZE` + `DATABASE_POOL_MAX_OVERFLOW`. | Positive integer | `min(8, max(2, cpu_count*2))` |
17951796
| `TZ` | Time zone for scheduler logs, cron-like jobs, backups, and timestamps | TZ database name (e.g., `UTC`, `Europe/Paris`) | unset (container default, usually UTC) |
17961797

17971798
##### Database

docs/zh/integrations.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1777,6 +1777,7 @@ volumes:
17771777
| `DISABLE_CONFIGURATION_TESTING` | 应用前跳过配置测试 | `yes` 或 `no` | `no` |
17781778
| `IGNORE_FAIL_SENDING_CONFIG` | 即便部分实例未收到配置也继续 | `yes` 或 `no` | `no` |
17791779
| `IGNORE_REGEX_CHECK` | 跳过设置的正则校验(与 autoconf 共享) | `yes` 或 `no` | `no` |
1780+
| `SCHEDULER_MAX_WORKERS` | 调度器作业执行器的最大工作线程数。每个运行线程可占用一个数据库连接,从而限制调度器侧的连接池压力。若解析值超过 `DATABASE_POOL_SIZE` + `DATABASE_POOL_MAX_OVERFLOW`,启动时会输出警告。 | 正整数 | `min(8, max(2, cpu_count*2))` |
17801781
| `TZ` | 调度器日志、类 cron 任务、备份和时间戳使用的时区 | TZ 数据库名(如 `UTC`、`Europe/Paris`) | unset(容器默认,通常为 UTC) |
17811782

17821783
##### 数据库

src/common/core/letsencrypt/jobs/certbot-new.py

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
ZEROSSL_BOT_SCRIPT,
8181
build_certbot_env,
8282
get_expected_acme_directory,
83+
letsencrypt_cache_consistent,
8384
prepare_logs_dir,
8485
resolve_certbot_entrypoint,
8586
)
@@ -724,8 +725,19 @@ def certbot_new(
724725
else:
725726
command.append("--register-unsafely-without-email")
726727

728+
# Always scope account lookup to the target ACME server's URL.
729+
# Without this, an LE account id can be passed as `--account` to a ZeroSSL
730+
# certbot certonly invocation (and vice versa) — certbot then constructs the
731+
# account directory from its own --server path and fails with AccountNotFound.
732+
acme_server_url = str(config.get("acme_server_url") or "")
733+
727734
if config.get("acme_server") == "letsencrypt":
728-
account_id = select_account_id(paths.config_dir.joinpath("accounts"), bool(config["staging"]), str(config.get("email") or ""))
735+
account_id = select_account_id(
736+
paths.config_dir.joinpath("accounts"),
737+
bool(config["staging"]),
738+
str(config.get("email") or ""),
739+
server_url=acme_server_url,
740+
)
729741
if account_id:
730742
command.extend(["--account", account_id])
731743
else:
@@ -744,7 +756,12 @@ def certbot_new(
744756
if zerossl_api_key:
745757
cmd_env["LETS_ENCRYPT_ZEROSSL_API_KEY"] = zerossl_api_key
746758

747-
account_id = select_account_id(paths.config_dir.joinpath("accounts"), bool(config["staging"]), str(config.get("email") or ""))
759+
account_id = select_account_id(
760+
paths.config_dir.joinpath("accounts"),
761+
bool(config["staging"]),
762+
str(config.get("email") or ""),
763+
server_url=acme_server_url,
764+
)
748765
if account_id:
749766
command.extend(["--account", account_id])
750767

@@ -1156,11 +1173,25 @@ def generate_certificate(service: str, config: Dict[str, Union[str, bool, int, D
11561173
elif not DATA_PATH.is_dir() or not any(DATA_PATH.glob("live/*/fullchain.pem")):
11571174
LOGGER.warning("Skipping db cache update: no live certificates found under DATA_PATH/live/*/fullchain.pem.")
11581175
else:
1159-
cached, err = JOB.cache_dir(DATA_PATH)
1160-
if not cached:
1161-
LOGGER.error(f"Error while saving data to db cache : {err}")
1176+
# Refuse to re-cache when renewal/ references account IDs that are missing from accounts/.
1177+
# That snapshot would self-propagate certbot AccountNotFound errors across every renew.
1178+
consistent, reason = letsencrypt_cache_consistent(DATA_PATH)
1179+
if not consistent:
1180+
LOGGER.error(
1181+
"Skipping db cache update to avoid persisting an inconsistent Let's Encrypt state "
1182+
f"({reason}). The DB cache row is left untouched; investigate accounts/ recovery before the next renew."
1183+
)
1184+
# If certbot itself succeeded, the fresh certs are already on disk — signal a reload
1185+
# (ret=1) so nginx picks them up. Persistence failure is logged separately above; do
1186+
# not escalate to status=2 here, otherwise JobScheduler suppresses the reload.
1187+
if status == 0:
1188+
status = 1
11621189
else:
1163-
LOGGER.info("Successfully saved data to db cache")
1190+
cached, err = JOB.cache_dir(DATA_PATH)
1191+
if not cached:
1192+
LOGGER.error(f"Error while saving data to db cache : {err}")
1193+
else:
1194+
LOGGER.info("Successfully saved data to db cache")
11641195
except SystemExit as e:
11651196
status = e.code
11661197
except BaseException as e:

src/common/core/letsencrypt/jobs/certbot-renew.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
build_certbot_env,
2424
certbot_log_backup_flags,
2525
is_zerossl_used_in_env,
26+
letsencrypt_cache_consistent,
2627
prepare_logs_dir,
2728
resolve_certbot_entrypoint,
2829
)
@@ -115,11 +116,26 @@
115116
elif not DATA_PATH.is_dir() or not any(DATA_PATH.glob("live/*/fullchain.pem")):
116117
LOGGER.warning("Skipping db cache update: no live certificates found under DATA_PATH/live/*/fullchain.pem.")
117118
else:
118-
cached, err = JOB.cache_dir(DATA_PATH)
119-
if not cached:
120-
LOGGER.error(f"Error while saving Let's Encrypt data to db cache : {err}")
119+
# Refuse to re-cache when renewal/ references account IDs that are missing from accounts/.
120+
# That snapshot would self-propagate certbot AccountNotFound errors across every renew.
121+
consistent, reason = letsencrypt_cache_consistent(DATA_PATH)
122+
if not consistent:
123+
LOGGER.error(
124+
"Skipping db cache update to avoid persisting an inconsistent Let's Encrypt state "
125+
f"({reason}). The DB cache row is left untouched; investigate accounts/ recovery before the next renew."
126+
)
127+
# If certbot itself succeeded, the fresh certs are already on disk — signal a reload
128+
# (ret=1) so nginx picks them up. Persistence failure is logged separately above; do
129+
# not escalate to status=2 here, otherwise JobScheduler suppresses the reload and the
130+
# newly-renewed certs sit unused until the next restart.
131+
if status == 0:
132+
status = 1
121133
else:
122-
LOGGER.info("Successfully saved Let's Encrypt data to db cache")
134+
cached, err = JOB.cache_dir(DATA_PATH)
135+
if not cached:
136+
LOGGER.error(f"Error while saving Let's Encrypt data to db cache : {err}")
137+
else:
138+
LOGGER.info("Successfully saved Let's Encrypt data to db cache")
123139
except SystemExit as e:
124140
status = e.code
125141
except BaseException as e:

0 commit comments

Comments
 (0)