mirror of
https://github.com/PegaProx/project-pegaprox.git
synced 2026-08-12 15:27:47 +08:00
When TLS was the intended posture and the cert could not be read or generated, the bootstrap printed a WARNING and then bound plaintext HTTP on the port meant for TLS - a silent downgrade of the management plane (TLS clients got "Invalid http version: \x16\x03\x01..." while the service reported itself healthy). The os.path.exists() gate also collapsed EACCES into ENOENT, so a present-but-unreadable cert was reported "missing" and generation tried to write over it. The config/ssl mkdir/chmod/migrate ran at import time, so a root-run importer could plant root-owned certs and lock the service user out - the trigger behind the reported case. Adapted from #637 by @SpyrosPsarras, with follow-up hardening from an adversarial review: - Fail closed: the inline TLS setup in main() is now _resolve_ssl_context(), which returns None only for a reverse proxy or the explicit PEGAPROX_ALLOW_PLAINTEXT=1 opt-in, and otherwise raises SystemExit. Both plaintext bind sites (gevent + the Flask dev-server fallback) read that single context, so there is no path left where TLS is intended yet cleartext binds. - Unreadable != missing: readability is probed by opening the file; only a true ENOENT leads to generation, and a present-but-unreachable cert is never overwritten. The error names the path, dir owner/mode and the process uid/gid. - Import-time side effects in constants.py now run only when config/ is owned by the current euid; update.sh repairs ownership on upgrade. - (MK) readable != loadable: _unloadable() actually load_cert_chain()s the pair in the resolver, so a corrupt/mismatched cert gets the same actionable fail-closed message instead of a raw ssl.SSLError crash downstream. Uses the byte-identical call the real bind uses, so it can never reject a cert the server would accept. - (MK) a half-present pair (one file there, the other ENOENT) fails closed instead of regenerating over the surviving half. - (MK) systemd StartLimitBurst so a permanently-broken cert lands in `failed`, not an endless 5s crash-loop. Tests: tests/test_ssl_bootstrap.py drives the real _resolve_ssl_context; the fixture now uses a real throwaway pair so the loadability path is exercised, plus corrupt-cert and half-pair cases. 18/19 pass here; the one gap is the generation-SUCCESS case, blocked by this box being a broken-pyOpenSSL env (it too fails closed) - it and a real systemd/TLS-bind E2E are owed on a proper host. Closes #633.