mirror of
https://github.com/apidoorman/doorman.git
synced 2026-05-02 22:29:54 -05:00
305 lines
11 KiB
Python
305 lines
11 KiB
Python
"""
|
|
Test SSL Certificate Validation on Startup
|
|
Documents the implementation of SSL certificate existence checks to prevent cryptic startup failures
|
|
"""
|
|
|
|
def test_ssl_certificate_validation():
|
|
"""Test SSL certificate validation patterns"""
|
|
|
|
print("SSL Certificate Validation - Implementation")
|
|
print("=" * 70)
|
|
print()
|
|
|
|
print("P0 Security Risk:")
|
|
print(" Server starts without SSL certificates")
|
|
print(" → Crashes with cryptic error after startup")
|
|
print(" → No clear indication of missing certificate files")
|
|
print(" → Production deployment fails in unclear ways")
|
|
print()
|
|
print("=" * 70)
|
|
print()
|
|
|
|
print("Implementation Location:")
|
|
print()
|
|
print(" File: backend-services/doorman.py")
|
|
print(" Lines: 121-132 (production validation block)")
|
|
print(" Context: app_lifespan() startup validation")
|
|
print()
|
|
print("=" * 70)
|
|
print()
|
|
|
|
print("Validation Logic:")
|
|
print()
|
|
print("1. Check if HTTPS is enabled (HTTPS_ONLY or HTTPS_ENABLED)")
|
|
print("2. If HTTPS_ONLY=true, require both SSL_CERTFILE and SSL_KEYFILE")
|
|
print("3. If SSL_CERTFILE is set, verify file exists on filesystem")
|
|
print("4. If SSL_KEYFILE is set, verify file exists on filesystem")
|
|
print()
|
|
print("=" * 70)
|
|
print()
|
|
|
|
print("Code Implementation:")
|
|
print()
|
|
print(" if https_only or https_enabled:")
|
|
print(" cert = os.getenv('SSL_CERTFILE')")
|
|
print(" key = os.getenv('SSL_KEYFILE')")
|
|
print(" if https_only and (not cert or not key):")
|
|
print(" raise RuntimeError(")
|
|
print(" 'SSL_CERTFILE and SSL_KEYFILE required when HTTPS_ONLY=true'")
|
|
print(" )")
|
|
print(" if cert and not os.path.exists(cert):")
|
|
print(" raise RuntimeError(f'SSL certificate not found: {cert}')")
|
|
print(" if key and not os.path.exists(key):")
|
|
print(" raise RuntimeError(f'SSL private key not found: {key}')")
|
|
print()
|
|
print("=" * 70)
|
|
print()
|
|
|
|
print("Validation Scenarios:")
|
|
print()
|
|
|
|
scenarios = [
|
|
{
|
|
'scenario': 'HTTPS_ONLY=true, no SSL_CERTFILE/SSL_KEYFILE',
|
|
'result': 'FAIL',
|
|
'error': 'SSL_CERTFILE and SSL_KEYFILE required when HTTPS_ONLY=true',
|
|
'reason': 'Cannot enforce HTTPS without certificates'
|
|
},
|
|
{
|
|
'scenario': 'HTTPS_ONLY=true, SSL_CERTFILE set but file missing',
|
|
'result': 'FAIL',
|
|
'error': 'SSL certificate not found: /path/to/cert.pem',
|
|
'reason': 'Certificate file does not exist on filesystem'
|
|
},
|
|
{
|
|
'scenario': 'HTTPS_ONLY=true, SSL_KEYFILE set but file missing',
|
|
'result': 'FAIL',
|
|
'error': 'SSL private key not found: /path/to/key.pem',
|
|
'reason': 'Private key file does not exist on filesystem'
|
|
},
|
|
{
|
|
'scenario': 'HTTPS_ENABLED=true, SSL_CERTFILE/SSL_KEYFILE exist',
|
|
'result': 'PASS',
|
|
'error': None,
|
|
'reason': 'Certificates exist and accessible'
|
|
},
|
|
{
|
|
'scenario': 'HTTPS_ONLY=false, HTTPS_ENABLED=false (dev mode)',
|
|
'result': 'SKIP',
|
|
'error': None,
|
|
'reason': 'SSL validation skipped when HTTPS not enabled'
|
|
},
|
|
{
|
|
'scenario': 'HTTPS_ENABLED=true, no SSL_CERTFILE (optional)',
|
|
'result': 'PASS',
|
|
'error': None,
|
|
'reason': 'HTTPS_ENABLED allows HTTP, certs optional'
|
|
}
|
|
]
|
|
|
|
for i, scenario in enumerate(scenarios, 1):
|
|
print(f"{i}. {scenario['scenario']}")
|
|
print(f" Result: {scenario['result']}")
|
|
if scenario['error']:
|
|
print(f" Error: {scenario['error']}")
|
|
print(f" Reason: {scenario['reason']}")
|
|
print()
|
|
|
|
print("=" * 70)
|
|
print()
|
|
print("BEFORE (Vulnerable):")
|
|
print()
|
|
print(" Server startup sequence:")
|
|
print(" 1. FastAPI app starts (app_lifespan begins)")
|
|
print(" 2. Routes registered")
|
|
print(" 3. uvicorn starts listening on port")
|
|
print(" 4. First HTTPS request arrives")
|
|
print(" 5. uvicorn tries to load SSL certificate")
|
|
print(" 6. FileNotFoundError: [Errno 2] No such file or directory: '/path/to/cert.pem'")
|
|
print(" 7. → Server crashes with cryptic stack trace")
|
|
print()
|
|
print("Problems:")
|
|
print(" - Server appears to start successfully")
|
|
print(" - Crashes on first request (or during uvicorn SSL setup)")
|
|
print(" - Unclear error message (deep in uvicorn/SSL stack)")
|
|
print(" - Production deployment fails after seeming healthy")
|
|
print(" - Health checks may pass before crash")
|
|
print()
|
|
print("=" * 70)
|
|
print()
|
|
print("AFTER (Secure):")
|
|
print()
|
|
print(" Server startup sequence:")
|
|
print(" 1. FastAPI app starts (app_lifespan begins)")
|
|
print(" 2. Production validation checks run:")
|
|
print(" - HTTPS enforcement check")
|
|
print(" - SSL certificate existence check ← NEW")
|
|
print(" 3. If certificates missing:")
|
|
print(" → RuntimeError with clear message")
|
|
print(" → Server refuses to start")
|
|
print(" → Exit code 1")
|
|
print(" 4. If certificates exist:")
|
|
print(" → Startup continues normally")
|
|
print()
|
|
print("Benefits:")
|
|
print(" ✓ Fail fast with clear error message")
|
|
print(" ✓ Certificate issues detected before listening on port")
|
|
print(" ✓ Production deployments fail immediately if misconfigured")
|
|
print(" ✓ Clear indication of missing files with full path")
|
|
print(" ✓ Prevents cryptic SSL errors deep in stack")
|
|
print()
|
|
print("=" * 70)
|
|
print()
|
|
|
|
print("Environment Variable Configuration:")
|
|
print()
|
|
print("Required for HTTPS_ONLY=true:")
|
|
print(" SSL_CERTFILE=/path/to/fullchain.pem")
|
|
print(" SSL_KEYFILE=/path/to/privkey.pem")
|
|
print()
|
|
print("Optional for HTTPS_ENABLED=true:")
|
|
print(" SSL_CERTFILE=/path/to/fullchain.pem (optional, but recommended)")
|
|
print(" SSL_KEYFILE=/path/to/privkey.pem (optional, but recommended)")
|
|
print()
|
|
print("Development (HTTPS disabled):")
|
|
print(" SSL_CERTFILE not required")
|
|
print(" SSL_KEYFILE not required")
|
|
print()
|
|
print("=" * 70)
|
|
print()
|
|
|
|
print("Certificate File Paths:")
|
|
print()
|
|
print("Common locations:")
|
|
print(" Let's Encrypt: /etc/letsencrypt/live/yourdomain.com/")
|
|
print(" - fullchain.pem (certificate + intermediates)")
|
|
print(" - privkey.pem (private key)")
|
|
print()
|
|
print(" Custom certificates:")
|
|
print(" - /etc/ssl/certs/yoursite.crt")
|
|
print(" - /etc/ssl/private/yoursite.key")
|
|
print()
|
|
print(" Docker mounts:")
|
|
print(" - /app/certs/fullchain.pem")
|
|
print(" - /app/certs/privkey.pem")
|
|
print()
|
|
print("Note: Paths must be absolute and accessible to Doorman process")
|
|
print()
|
|
print("=" * 70)
|
|
print()
|
|
|
|
print("Error Messages:")
|
|
print()
|
|
print("1. Missing environment variables (HTTPS_ONLY=true):")
|
|
print(" RuntimeError: SSL_CERTFILE and SSL_KEYFILE required when HTTPS_ONLY=true")
|
|
print()
|
|
print("2. Certificate file not found:")
|
|
print(" RuntimeError: SSL certificate not found: /path/to/cert.pem")
|
|
print()
|
|
print("3. Private key file not found:")
|
|
print(" RuntimeError: SSL private key not found: /path/to/key.pem")
|
|
print()
|
|
print("All errors raised during startup, preventing server from starting")
|
|
print()
|
|
print("=" * 70)
|
|
print()
|
|
|
|
print("Production Deployment Checklist:")
|
|
print()
|
|
print("Before deploying to production:")
|
|
print(" 1. Set ENV=production")
|
|
print(" 2. Set HTTPS_ONLY=true (or HTTPS_ENABLED=true)")
|
|
print(" 3. Set SSL_CERTFILE to full path of certificate")
|
|
print(" 4. Set SSL_KEYFILE to full path of private key")
|
|
print(" 5. Verify files exist: ls -l $SSL_CERTFILE $SSL_KEYFILE")
|
|
print(" 6. Verify permissions: Doorman process user can read files")
|
|
print(" 7. Test startup: python doorman.py (should not crash)")
|
|
print()
|
|
print("=" * 70)
|
|
print()
|
|
|
|
print("Testing Recommendations:")
|
|
print()
|
|
print("1. Test missing certificate file:")
|
|
print(" - Set SSL_CERTFILE=/nonexistent/cert.pem")
|
|
print(" - Set HTTPS_ONLY=true, ENV=production")
|
|
print(" - Verify startup fails with clear error")
|
|
print()
|
|
print("2. Test missing private key file:")
|
|
print(" - Set SSL_KEYFILE=/nonexistent/key.pem")
|
|
print(" - Set HTTPS_ONLY=true, ENV=production")
|
|
print(" - Verify startup fails with clear error")
|
|
print()
|
|
print("3. Test missing environment variables:")
|
|
print(" - Unset SSL_CERTFILE and SSL_KEYFILE")
|
|
print(" - Set HTTPS_ONLY=true, ENV=production")
|
|
print(" - Verify startup fails with clear error")
|
|
print()
|
|
print("4. Test valid configuration:")
|
|
print(" - Create temporary certificate files")
|
|
print(" - Set SSL_CERTFILE and SSL_KEYFILE to valid paths")
|
|
print(" - Verify startup succeeds")
|
|
print()
|
|
print("5. Test development mode (skip validation):")
|
|
print(" - Set ENV=development (or unset)")
|
|
print(" - Unset SSL_CERTFILE and SSL_KEYFILE")
|
|
print(" - Verify startup succeeds (validation skipped)")
|
|
print()
|
|
print("=" * 70)
|
|
print()
|
|
|
|
print("Integration with uvicorn SSL:")
|
|
print()
|
|
print("Doorman validates certificates exist before uvicorn starts.")
|
|
print("Uvicorn SSL configuration (in doorman.py, near end of file):")
|
|
print()
|
|
print(" if https_only or https_enabled:")
|
|
print(" ssl_keyfile = os.getenv('SSL_KEYFILE')")
|
|
print(" ssl_certfile = os.getenv('SSL_CERTFILE')")
|
|
print(" uvicorn.run(")
|
|
print(" app,")
|
|
print(" host='0.0.0.0',")
|
|
print(" port=8000,")
|
|
print(" ssl_keyfile=ssl_keyfile,")
|
|
print(" ssl_certfile=ssl_certfile")
|
|
print(" )")
|
|
print()
|
|
print("Validation flow:")
|
|
print(" 1. app_lifespan() validates files exist (new check)")
|
|
print(" 2. If validation passes, uvicorn starts")
|
|
print(" 3. uvicorn loads SSL certificates (already validated)")
|
|
print(" 4. Server listens on HTTPS port")
|
|
print()
|
|
print("=" * 70)
|
|
print()
|
|
|
|
print("Security Impact:")
|
|
print()
|
|
print("Prevents:")
|
|
print(" ✓ Server appearing to start but crashing on first request")
|
|
print(" ✓ Cryptic SSL errors deep in uvicorn/OpenSSL stack")
|
|
print(" ✓ Production deployments with missing certificates")
|
|
print(" ✓ Delayed failures after health checks pass")
|
|
print()
|
|
print("Improves:")
|
|
print(" ✓ Clear error messages at startup")
|
|
print(" ✓ Fail-fast behavior for misconfigurations")
|
|
print(" ✓ Production deployment reliability")
|
|
print(" ✓ Operator experience (clear errors vs cryptic SSL errors)")
|
|
print()
|
|
print("=" * 70)
|
|
print()
|
|
|
|
print("P0 Risk Mitigated:")
|
|
print(" Server starts, then crashes with unclear error if cert files missing")
|
|
print()
|
|
print("Production Impact:")
|
|
print(" ✓ Certificate issues detected immediately at startup")
|
|
print(" ✓ Clear error messages with full file paths")
|
|
print(" ✓ Prevents production deployments with missing certs")
|
|
print(" ✓ Fail-fast instead of delayed crashes")
|
|
print()
|
|
|
|
if __name__ == '__main__':
|
|
test_ssl_certificate_validation()
|