Overview
Full remediation of all Critical, High, and Medium findings identified in the post-release 360-degree codebase audit. Every item on the list is fixed and verified. Zero regressions: 174 passed, 1 expected skip, 0 failed — including a real certificate lifecycle against certmate.org via Cloudflare DNS.
Critical Fixes
TOCTOU race in settings (DI-1)
Concurrent POST /api/settings requests could overwrite each other silently. Added threading.Lock to SettingsManager and a new atomic_update() method that performs load → merge → protect → save atomically.
Non-atomic cert file writes during renewal (DI-2)
renew_certificate() used direct open() writes that left corrupt PEM files on a mid-copy process kill. New _atomic_binary_copy() static method writes to a .tmp sibling, then renames atomically.
High Fixes
- API-1:
create_certificate()raisesFileExistsErrorif the domain already has a certificate — prevents silent overwrites - API-2:
renew_certificate()raisesFileNotFoundErrorbefore calling certbot if the domain has no certificate - API-4:
delete_certificate()raisesRuntimeErrorif a create/renew is in progress for the domain (per-domain lock check) - API-5: Cert expiry uses
datetime.utcnow()— fixes off-by-hours on non-UTC servers - SEC-1: Deploy hooks: 1024-char length cap + blocklist regex rejecting commands that reference credential/settings files
- SB-2: HashiCorp Vault token auto-renewed every 6 hours via
auth.token.renew_self(); re-authenticates on renewal failure - SB-3:
_with_retry()decorator (3 attempts, exponential back-off) onstore/retrieve/list_certificatesfor Azure, AWS, and Vault backends - SB-4: All four cloud backends strip whitespace from credentials at initialization
Medium Fixes
- SEC-2:
GET /api/settingsmasks alltoken/secret/password/key/credentialfields as'********' - SEC-4: Session timeout configurable via
SESSION_TIMEOUT_HOURSenv var (default: 8 hours) - SEC-5: Last-admin guard counts all admins regardless of
enabledstate - DI-3:
cert_dir/data_dirresolved to absolute paths at startup (no CWD dependency) - DI-4: Startup scan removes orphan
.tmpfiles left by previous hard kills - DI-5: Corrupt
settings.jsontriggers backup restore before falling back to factory defaults - DI-7: Domain migration guard uses
all(isinstance(d, str) for d in domains)instead of fragiledomains[0]check - API-3: Batch certificate creation capped at 50 domains per request
- INFRA-1:
GET /healthnow reports scheduler state, cert directory, and disk free space; always returns HTTP 200 - INFRA-3: Nginx Docker Compose profile includes a healthcheck directive
- SB-5: Azure
_sanitize_secret_name()appends a CRC32 suffix to prevent secret key collisions between domains - SB-6: All
.decode('utf-8')calls in storage backends useerrors='replace' - SB-7:
LocalFileSystemBackendsetschmod 0o600onmetadata.json - SB-8: APScheduler SQLite job store uses WAL mode and
synchronous=NORMAL - API-6:
POST /api/usersrejects usernames over 64 characters or passwords over 256 characters
Low Fixes
shutilandremoved to module-level imports instorage_backends.py- Domain migration guard is safe for mixed-type lists
Test Suite
174 passed, 1 expected skip, 0 failed
The single skip is test_welcome_banner_visible: when the cert lifecycle tests run first (creating a real certificate), the welcome banner is intentionally hidden — the test correctly skips itself in that scenario. Two pre-existing UI test bugs were also fixed: the Cloudflare DNS provider radio button click (was targeting the hidden sr-only input instead of its label) and the add-account modal field selectors.