Skip to content
Open
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
6fc701b
feat: add context7 configuration file with URL and public key
TheophileDiot Mar 6, 2026
ab24f60
Revert "feat: add context7 configuration file with URL and public key"
TheophileDiot Mar 6, 2026
230f840
use OCSP files for stapling responses
mkf-sysangels Mar 12, 2026
80236ed
Update ssl-certificate-lua.conf
mkf-sysangels Mar 12, 2026
6d75d8a
remove redis integration
mkf-sysangels Mar 12, 2026
d444061
fix "end" entry
mkf-sysangels Mar 12, 2026
18b58de
fix tab errors
mkf-sysangels Mar 12, 2026
a731fe8
Add a 3 days TTL cache for OCSP responses
mkf-sysangels Mar 13, 2026
05c6926
add customcert processing + refresh TTL
mkf-sysangels Mar 13, 2026
f824b41
fix cleanup run, fix job run
mkf-sysangels Mar 13, 2026
3762f36
use python cryptography + add cleanup + SAN fix + fix race condition
mkf-sysangels Mar 13, 2026
a4b10af
add checksum checking, cleanup function
mkf-sysangels Mar 13, 2026
55df73d
Trigger OCSP stapling refresh
mkf-sysangels Mar 13, 2026
d6ee173
OCSP Response Signature is NOT Cryptographically Verified
mkf-sysangels Mar 13, 2026
6c60a9a
fix multiple vulnerability issues
mkf-sysangels Mar 13, 2026
5fba3f4
def is_safe_url
mkf-sysangels Mar 13, 2026
6b5bef6
Update ocsp-refresh.py
mkf-sysangels Mar 13, 2026
981178e
Update ocsp-refresh.py
mkf-sysangels Mar 13, 2026
ffa38d7
add locking, error handling, verification
mkf-sysangels Mar 13, 2026
1321d90
fix tlinter errors
mkf-sysangels Mar 13, 2026
426cc52
add rate limiting
mkf-sysangels Mar 13, 2026
3d259d4
database: batch inserts
mkf-sysangels Mar 13, 2026
bfc6869
trigger OCSP update
mkf-sysangels Mar 13, 2026
bc2da87
improve caching logic
mkf-sysangels Mar 13, 2026
a860bf5
shorter TTL for caching
mkf-sysangels Mar 13, 2026
7f92fea
clean up expired OCSP entries
mkf-sysangels Mar 13, 2026
3857c04
fix linter hints
mkf-sysangels Mar 13, 2026
1fe1087
Merge pull request #3321 from bunkerity/dev
TheophileDiot Mar 13, 2026
1c57d48
fixing _get_cert_checksums errors
mkf-sysangels Mar 13, 2026
7e933c1
optimizing execution time
mkf-sysangels Mar 13, 2026
eac3511
fix: Trigger OCSP stapling refresh for newly issued certificates
mkf-sysangels Mar 13, 2026
3a793b5
add wildcard support
mkf-sysangels Mar 13, 2026
1675847
Update ssl-certificate-lua.conf
mkf-sysangels Mar 13, 2026
81fac61
Update ssl-certificate-lua.conf
mkf-sysangels Mar 13, 2026
00aeb9e
Update ocsp-refresh.py
mkf-sysangels Mar 13, 2026
2748e0e
Update ocsp-refresh.py
mkf-sysangels Mar 13, 2026
7ce0e7e
Update ocsp-refresh.py
mkf-sysangels Mar 13, 2026
273279c
fix wildcard error
mkf-sysangels Mar 13, 2026
4e9af34
fix merge issue
mkf-sysangels Mar 13, 2026
7bd3c3f
Merge branch 'dev' into OCSP-SSL-Stappling-#1592
mkf-sysangels Mar 13, 2026
787e8e6
Merge remote-tracking branch 'upstream/master' into OCSP-SSL-Stapplin…
mkf-sysangels Mar 14, 2026
184f7bb
Update ssl-certificate-lua.conf
mkf-sysangels Mar 14, 2026
8b8c546
Potential fix for pull request finding
mkf-sysangels Mar 15, 2026
b735de9
Address Copilot review: OCSP timeouts, SSL_USE_OCSP_STAPLING, stats s…
mkf-sysangels Mar 15, 2026
1841a32
Update ssl-certificate-lua.conf
mkf-sysangels Mar 15, 2026
bee29ca
fix OCSP certificate verification + optimize log level
mkf-sysangels Mar 15, 2026
3b3ac2f
fallback to tls on error
mkf-sysangels Mar 15, 2026
94c4d52
fix ocsp loading error
mkf-sysangels Mar 15, 2026
7616e4b
fix SN comparison, check OCSP existence early, fallback to native tls…
mkf-sysangels Mar 16, 2026
edd608c
add redis backend
mkf-sysangels Mar 16, 2026
2fba405
fix openssl file race condition
mkf-sysangels Mar 16, 2026
c196df2
add fail safe
mkf-sysangels Mar 16, 2026
eb5fe1f
use safe module loading + implement OCSP-must-stapple + remove redis
mkf-sysangels Mar 16, 2026
4a1b226
add --force-fetch + cleanup
mkf-sysangels Mar 16, 2026
c7de21a
optimize lookup mechanism
mkf-sysangels Mar 19, 2026
b916f35
code fixes
mkf-sysangels Mar 19, 2026
b8814b3
fix path issue
mkf-sysangels Mar 19, 2026
4259817
fix closing section error
mkf-sysangels Mar 19, 2026
1a6aab5
fix key mapping
mkf-sysangels Mar 19, 2026
5309d9b
fix calc of fingerprints
mkf-sysangels Mar 19, 2026
697130a
optimize ocsp validation/lookup+ verbose logging
mkf-sysangels Mar 20, 2026
0e2d186
optimize orphan cleanup + hash colision check
mkf-sysangels Mar 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 144 additions & 11 deletions src/common/confs/server-http/ssl-certificate-lua.conf
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ ssl_certificate_by_lua_block {
local helpers = require "bunkerweb.helpers"
local utils = require "bunkerweb.utils"
local cdatastore = require "bunkerweb.datastore"
local cclusterstore = require "bunkerweb.clusterstore"
local cjson = require "cjson"
local ssl = require "ngx.ssl"
local ssl = require "ngx.ssl"
local ocsp = require "ngx.ocsp"

local ngx = ngx
local ngx_req = ngx.req
Expand All @@ -87,7 +87,11 @@ ssl_certificate_by_lua_block {
local require_plugin = helpers.require_plugin
local new_plugin = helpers.new_plugin
local call_plugin = helpers.call_plugin
local tostring = tostring
local tostring = tostring
local insert = table.insert
local lower = string.lower
local match = string.match
local concat = table.concat

-- Start ssl_certificate phase
local logger = clogger:new("SSL-CERTIFICATE")
Expand All @@ -101,17 +105,143 @@ ssl_certificate_by_lua_block {
return
end

local function get_phase_order(ord, phase, server_name)
if ord.per_site and server_name and ord.per_site[server_name] and ord.per_site[server_name][phase] then
return ord.per_site[server_name][phase]
elseif ord.global and ord.global[phase] then
return ord.global[phase]
-- Resolve per-site plugin order
local function get_phase_order(ord, phase, server_name)
if ord.per_site and server_name and ord.per_site[server_name] and ord.per_site[server_name][phase] then
return ord.per_site[server_name][phase]
elseif ord.global and ord.global[phase] then
return ord.global[phase]
end
return ord[phase]
end

local server_name = ssl.server_name()
local phase_order = get_phase_order(order, "ssl_certificate", server_name)

-- Helper: check if OCSP stapling is enabled for this site
local function is_ocsp_stapling_enabled()
-- TODO: Check SSL_USE_OCSP_STAPLING variable when variable loading is available in ssl_certificate phase
-- For now, always attempt OCSP stapling if files exist (graceful fallback if not)
Comment thread
mkf-sysangels marked this conversation as resolved.
Outdated
return true
end

-- Helper: sanitize domain/cert name for filesystem (replace * with _wildcard_)
local function sanitize_name(name)
if not name then return nil end
return name:gsub("%*", "_wildcard_")
end

-- Helper: set OCSP stapling from cache (internalstore + ocsp.der disk files)
local function set_ocsp_from_cache()
logger:log(INFO, "OCSP set_ocsp_from_cache() called for server_name=" .. (server_name or "nil"))

if not is_ocsp_stapling_enabled() then
logger:log(INFO, "OCSP stapling disabled via SSL_USE_OCSP_STAPLING setting")
Comment thread
mkf-sysangels marked this conversation as resolved.
Outdated
return
end

if not server_name or server_name == "" then
logger:log(ERR, "OCSP no server_name available")
return
end

local cache_key = "SSL:ocsp_status:" .. server_name
local resp

-- 1) Local shared dict lookup (per-worker cache)
local db_val, gerr = internalstore:get(cache_key, true)
if db_val then
resp = db_val
elseif gerr and gerr ~= "not found" then
logger:log(ERR, "OCSP error while getting response from internalstore: " .. gerr)
end

-- 2) Fallback to on-disk OCSP response (/var/cache/bunkerweb/ssl/{domain}/ocsp.der)
if not resp then
local base_path = "/var/cache/bunkerweb/ssl/"

-- Generate candidates for lookup
local candidates = {}

-- Direct matches
insert(candidates, server_name)
insert(candidates, server_name .. "-ecdsa")
insert(candidates, server_name .. "-rsa")
insert(candidates, "customcert-" .. server_name)
insert(candidates, "customcert-" .. server_name .. "-ecdsa")
insert(candidates, "customcert-" .. server_name .. "-rsa")

-- Wildcard and apex matches (e.g., if server_name is www.example.com, try *.example.com and example.com)
local labels = {}
for label in server_name:gmatch("[^.]+") do
insert(labels, label)
end
return ord[phase]

-- Only try wildcard/apex if we have at least subdomain.example.com (2+ labels)
if #labels >= 2 then
for i = 2, #labels do
Comment thread
mkf-sysangels marked this conversation as resolved.
Outdated
local suffix = table.concat(labels, ".", i)
if suffix and suffix ~= "" then
-- Apex domain (e.g. example.com) — wildcard certs are often stored under apex name
insert(candidates, suffix)
insert(candidates, suffix .. "-ecdsa")
insert(candidates, suffix .. "-rsa")
insert(candidates, "customcert-" .. suffix)
insert(candidates, "customcert-" .. suffix .. "-ecdsa")
insert(candidates, "customcert-" .. suffix .. "-rsa")
-- Wildcard form (e.g. *.example.com)
local wildcard_base = "*." .. suffix
insert(candidates, wildcard_base)
insert(candidates, wildcard_base .. "-ecdsa")
insert(candidates, wildcard_base .. "-rsa")
insert(candidates, "customcert-" .. wildcard_base)
insert(candidates, "customcert-" .. wildcard_base .. "-ecdsa")
insert(candidates, "customcert-" .. wildcard_base .. "-rsa")
end
end
end

for _, name in ipairs(candidates) do
if name and name ~= "" then
local sanitized = sanitize_name(name)
local path = base_path .. sanitized .. "/ocsp.der"
local f = io.open(path, "rb")
if f then
local data = f:read("*a")
f:close()
if data and #data > 0 then
logger:log(INFO, "OCSP loaded response (" .. #data .. " bytes) from " .. path .. " (matches " .. name .. ")")
resp = data
-- Cache in shared memory to avoid disk I/O on every handshake (TTL 300s)
local ok_set, serr = internalstore:set(cache_key, resp, 300, true)
if not ok_set then
logger:log(ERR, "OCSP error while caching file response into internalstore: " .. (serr or "unknown"))
end
break
end
else
-- Check if file exists but couldn't be read (permission denied)
local file_exists = os.execute("test -f " .. path .. " 2>/dev/null")
if file_exists == 0 then
logger:log(ERR, "OCSP permission denied reading " .. path .. " (check file permissions)")
end
Comment thread
mkf-sysangels marked this conversation as resolved.
end
end
end
end

if not resp then
logger:log(ngx.DEBUG, "OCSP not found for " .. server_name)
return
end

local server_name = ssl.server_name()
local phase_order = get_phase_order(order, "ssl_certificate", server_name)
local ok_set, oerr = ocsp.set_ocsp_status_resp(resp)
if not ok_set then
logger:log(ERR, "OCSP failed to set stapling: " .. oerr)
else
logger:log(INFO, "OCSP stapling set from cache for " .. server_name)
end
end

-- Call ssl_certificate() methods
logger:log(INFO, "calling ssl_certificate() methods of plugins ...")
Expand Down Expand Up @@ -151,6 +281,9 @@ ssl_certificate_by_lua_block {
if not ok then
logger:log(ERR, "error while setting private key : " .. err)
else
-- Try to set OCSP stapling from cache (if enabled)
logger:log(INFO, "DEBUG: About to call set_ocsp_from_cache() for " .. server_name)
set_ocsp_from_cache()
logger:log(INFO, "certificate set by " .. plugin_id)
return true
end
Expand Down
23 changes: 22 additions & 1 deletion src/common/core/customcert/jobs/custom-cert.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env python3

from os import getenv, sep
from os import environ, getenv, sep
Comment thread
mkf-sysangels marked this conversation as resolved.
Outdated
from os.path import join
from pathlib import Path
from subprocess import DEVNULL, run
Expand Down Expand Up @@ -159,6 +159,7 @@ def check_cert(cert_file: Union[Path, bytes], key_file: Union[Path, bytes], firs
sys_exit(0)

skipped_servers = []
changed_domains = [] # Track which domains had certificate changes
if not multisite:
all_domains = [all_domains[0]]
if getenv("USE_CUSTOM_SSL", "no") == "no":
Expand Down Expand Up @@ -210,6 +211,7 @@ def check_cert(cert_file: Union[Path, bytes], key_file: Union[Path, bytes], firs
continue
elif need_reload:
LOGGER.info(f"Detected change in {first_server}'s certificate")
changed_domains.append(first_server) # Track this domain as changed
status = 1
continue

Expand All @@ -218,6 +220,25 @@ def check_cert(cert_file: Union[Path, bytes], key_file: Union[Path, bytes], firs
for first_server in skipped_servers:
JOB.del_cache("cert.pem", service_id=first_server)
JOB.del_cache("key.pem", service_id=first_server)

# Trigger OCSP stapling refresh when certificates changed (AFTER caching)
# OCSP job will compare new certs with cached ones and process differential updates
if changed_domains and getenv("SSL_USE_OCSP_STAPLING", "yes").lower() == "yes":
LOGGER.info(f"🔄 OCSP triggering refresh for {len(changed_domains)} changed custom cert(s): {', '.join(changed_domains)}")
try:
import sys

ocsp_script = join(sep, "usr", "share", "bunkerweb", "core", "ssl", "jobs", "ocsp-refresh.py")
result = run([sys.executable, ocsp_script, "--force"], stdin=DEVNULL, capture_output=True, text=True, timeout=300)
Comment thread
mkf-sysangels marked this conversation as resolved.
Outdated
if result.returncode == 0:
LOGGER.info("✓ OCSP refresh completed successfully after cert change")
else:
LOGGER.warning(f"⚠️ OCSP refresh returned exit code {result.returncode}")
if result.stderr:
for line in result.stderr.strip().splitlines():
LOGGER.debug(f"OCSP: {line}")
except Exception as e:
LOGGER.warning(f"⚠️ OCSP post-change refresh failed (non-fatal): {e}")
except SystemExit as e:
status = e.code
except BaseException as e:
Expand Down
21 changes: 20 additions & 1 deletion src/common/core/letsencrypt/jobs/certbot-new.py
Original file line number Diff line number Diff line change
Expand Up @@ -1141,13 +1141,32 @@ def generate_certificate(service: str, config: Dict[str, Union[str, bool, int, D

save_zerossl_api_key_hashes(updated_zerossl_api_key_hashes)

# * Save data to db cache
# * Save data to db cache (full LE directory)
if DATA_PATH.is_dir() and list(DATA_PATH.iterdir()):
cached, err = JOB.cache_dir(DATA_PATH)
if not cached:
LOGGER.error(f"Error while saving data to db cache : {err}")
else:
LOGGER.info("Successfully saved data to db cache")

# * Trigger OCSP stapling refresh for newly issued certificates (AFTER database save)
# OCSP job will compare new certs with cached ones and process differential updates
if status == 1 and getenv("SSL_USE_OCSP_STAPLING", "yes").lower() == "yes":
LOGGER.info("🔄 OCSP triggering refresh for newly issued certificates")
try:
import sys

ocsp_script = join(sep, "usr", "share", "bunkerweb", "core", "ssl", "jobs", "ocsp-refresh.py")
result = run([sys.executable, ocsp_script, "--force"], stdin=DEVNULL, capture_output=True, text=True, timeout=300)
Comment thread
mkf-sysangels marked this conversation as resolved.
Outdated
if result.returncode == 0:
LOGGER.info("✓ OCSP refresh completed successfully after issuance")
else:
LOGGER.warning(f"⚠️ OCSP refresh returned exit code {result.returncode}")
if result.stderr:
for line in result.stderr.strip().splitlines():
LOGGER.debug(f"OCSP: {line}")
except Exception as e:
LOGGER.warning(f"⚠️ OCSP post-issuance refresh failed (non-fatal): {e}")
except SystemExit as e:
status = e.code
except BaseException as e:
Expand Down
44 changes: 42 additions & 2 deletions src/common/core/letsencrypt/jobs/certbot-renew.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from os import getenv, sep
from os.path import join
from subprocess import DEVNULL, PIPE, Popen
from subprocess import DEVNULL, PIPE, Popen, TimeoutExpired, run
from sys import exit as sys_exit, path as sys_path
from time import monotonic
from traceback import format_exc
Expand All @@ -29,9 +29,11 @@
LOGGER = getLogger("LETS-ENCRYPT.RENEW")

LOGGER_CERTBOT = getLogger("LETS-ENCRYPT.RENEW.CERTBOT")
CERTBOT_TIMEOUT = 600 # seconds
CERTBOT_TIMEOUT = 900 # 15 minutes max for a single certbot invocation
Comment thread
mkf-sysangels marked this conversation as resolved.
Outdated
status = 0


try:
# Check if we're using let's encrypt
use_letsencrypt = False
Expand Down Expand Up @@ -81,10 +83,28 @@
]
+ (["-v"] if getenv("CUSTOM_LOG_LEVEL", getenv("LOG_LEVEL", "INFO")).upper() == "DEBUG" else []),
stdin=DEVNULL,
stdout=PIPE,
stderr=PIPE,
universal_newlines=True,
env=cmd_env,
)
try:
stdout, stderr = process.communicate(timeout=CERTBOT_TIMEOUT)
except TimeoutExpired:
Comment thread
mkf-sysangels marked this conversation as resolved.
LOGGER.error(f"certbot renew timed out after {CERTBOT_TIMEOUT}s, killing process.")
process.kill()
stdout, stderr = process.communicate()
status = 2
if stdout:
for line in stdout.splitlines():
line_str = line.strip()
if line_str:
LOGGER_CERTBOT.info(line_str)
if "(success)" in line_str or "Congratulations" in line_str:
status = 1
if stderr:
for line in stderr.splitlines():
LOGGER_CERTBOT.info(line.strip())
deadline = monotonic() + CERTBOT_TIMEOUT
while process.poll() is None:
if monotonic() > deadline:
Expand All @@ -101,13 +121,33 @@
status = 2
LOGGER.error("Certificates renewal failed")

# Save Let's Encrypt data to db cache
# Save Let's Encrypt data to db cache (full directory)
if DATA_PATH.is_dir() and list(DATA_PATH.iterdir()):
cached, err = JOB.cache_dir(DATA_PATH)
if not cached:
LOGGER.error(f"Error while saving Let's Encrypt data to db cache : {err}")
else:
LOGGER.info("Successfully saved Let's Encrypt data to db cache")

# Trigger OCSP refresh after successful renewal (AFTER database save)
# OCSP job will compare new certs with cached ones and process differential updates
if status == 1 and getenv("SSL_USE_OCSP_STAPLING", "yes").lower() == "yes":
LOGGER.info("🔄 OCSP triggering refresh for renewed certificates")

try:
import sys

ocsp_script = join(sep, "usr", "share", "bunkerweb", "core", "ssl", "jobs", "ocsp-refresh.py")
result = run([sys.executable, ocsp_script, "--force"], stdin=DEVNULL, capture_output=True, text=True, timeout=300)
if result.returncode == 0:
Comment thread
mkf-sysangels marked this conversation as resolved.
LOGGER.info("✓ OCSP refresh completed successfully after renewal")
else:
LOGGER.warning(f"⚠️ OCSP refresh returned exit code {result.returncode}")
if result.stderr:
for line in result.stderr.strip().splitlines():
LOGGER.debug(f"OCSP: {line}")
except Exception as e:
LOGGER.warning(f"⚠️ OCSP post-renewal refresh failed (non-fatal): {e}")
except SystemExit as e:
status = e.code
except BaseException as e:
Expand Down
Loading
Loading