github fabriziosalmi/certmate v2.2.2
v2.2.2 — Comprehensive Security and Reliability Hardening (31 Findings)

8 hours ago

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() raises FileExistsError if the domain already has a certificate — prevents silent overwrites
  • API-2: renew_certificate() raises FileNotFoundError before calling certbot if the domain has no certificate
  • API-4: delete_certificate() raises RuntimeError if 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) on store/retrieve/list_certificates for Azure, AWS, and Vault backends
  • SB-4: All four cloud backends strip whitespace from credentials at initialization

Medium Fixes

  • SEC-2: GET /api/settings masks all token/secret/password/key/credential fields as '********'
  • SEC-4: Session timeout configurable via SESSION_TIMEOUT_HOURS env var (default: 8 hours)
  • SEC-5: Last-admin guard counts all admins regardless of enabled state
  • DI-3: cert_dir/data_dir resolved to absolute paths at startup (no CWD dependency)
  • DI-4: Startup scan removes orphan .tmp files left by previous hard kills
  • DI-5: Corrupt settings.json triggers backup restore before falling back to factory defaults
  • DI-7: Domain migration guard uses all(isinstance(d, str) for d in domains) instead of fragile domains[0] check
  • API-3: Batch certificate creation capped at 50 domains per request
  • INFRA-1: GET /health now 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 use errors='replace'
  • SB-7: LocalFileSystemBackend sets chmod 0o600 on metadata.json
  • SB-8: APScheduler SQLite job store uses WAL mode and synchronous=NORMAL
  • API-6: POST /api/users rejects usernames over 64 characters or passwords over 256 characters

Low Fixes

  • shutil and re moved to module-level imports in storage_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.

Don't miss a new certmate release

NewReleases is sending notifications on new releases.