feat: certificate management with stage/activate/restore lifecycle
All checks were successful
CI / build (push) Successful in 1m8s
CI / docker (push) Successful in 45s

Provider-based architecture (Docker now, K8s later):
- CertificateManager interface + DockerCertificateManager (file-based)
- Atomic swap via .wip files for safe cert replacement
- Stage -> Activate -> Archive lifecycle with one-deep rollback
- Bootstrap supports user-supplied certs via CERT_FILE/KEY_FILE/CA_FILE
- CA bundle aggregates platform + tenant CAs, distributed to containers
- Vendor UI: Certificates page with upload, activate, restore, discard
- Stale tenant tracking (ca_applied_at) with restart banner
- Conditional TLS skip removal when CA bundle exists

Includes design spec, migration V012, service + controller tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-10 18:29:02 +02:00
parent 51a1aef10e
commit 45bcc954ac
23 changed files with 2035 additions and 7 deletions

View File

@@ -5,19 +5,57 @@ services:
entrypoint: ["sh", "-c"]
command:
- |
if [ ! -f /certs/cert.pem ]; then
if [ -f /certs/cert.pem ]; then
echo "Certs already exist, skipping"
exit 0
fi
# Option 1: User-supplied certificate
if [ -n "$$CERT_FILE" ] && [ -n "$$KEY_FILE" ]; then
apk add --no-cache openssl >/dev/null 2>&1
cp "$$CERT_FILE" /certs/cert.pem
cp "$$KEY_FILE" /certs/key.pem
if [ -n "$$CA_FILE" ]; then
cp "$$CA_FILE" /certs/ca.pem
fi
# Validate: key matches cert
CERT_MOD=$$(openssl x509 -noout -modulus -in /certs/cert.pem 2>/dev/null | md5sum)
KEY_MOD=$$(openssl rsa -noout -modulus -in /certs/key.pem 2>/dev/null | md5sum)
if [ "$$CERT_MOD" != "$$KEY_MOD" ]; then
echo "ERROR: Certificate and key do not match!"
rm -f /certs/cert.pem /certs/key.pem /certs/ca.pem
exit 1
fi
SELF_SIGNED=false
echo "Installed user-supplied certificate"
else
# Option 2: Generate self-signed
apk add --no-cache openssl >/dev/null 2>&1
openssl req -x509 -newkey rsa:4096 \
-keyout /certs/key.pem -out /certs/cert.pem \
-days 365 -nodes \
-subj "/CN=$$PUBLIC_HOST" \
-addext "subjectAltName=DNS:$$PUBLIC_HOST,DNS:*.$$PUBLIC_HOST"
SELF_SIGNED=true
echo "Generated self-signed cert for $$PUBLIC_HOST"
else
echo "Certs already exist, skipping"
fi
# Write metadata for SaaS app to seed DB
SUBJECT=$$(openssl x509 -noout -subject -in /certs/cert.pem 2>/dev/null | sed 's/subject=//')
FINGERPRINT=$$(openssl x509 -noout -fingerprint -sha256 -in /certs/cert.pem 2>/dev/null | sed 's/.*=//')
NOT_BEFORE=$$(openssl x509 -noout -startdate -in /certs/cert.pem 2>/dev/null | sed 's/notBefore=//')
NOT_AFTER=$$(openssl x509 -noout -enddate -in /certs/cert.pem 2>/dev/null | sed 's/notAfter=//')
HAS_CA=false
[ -f /certs/ca.pem ] && HAS_CA=true
cat > /certs/meta.json <<METAEOF
{"subject":"$$SUBJECT","fingerprint":"$$FINGERPRINT","selfSigned":$$SELF_SIGNED,"hasCa":$$HAS_CA,"notBefore":"$$NOT_BEFORE","notAfter":"$$NOT_AFTER"}
METAEOF
mkdir -p /certs/staged /certs/prev
environment:
PUBLIC_HOST: ${PUBLIC_HOST:-localhost}
CERT_FILE: ${CERT_FILE:-}
KEY_FILE: ${KEY_FILE:-}
CA_FILE: ${CA_FILE:-}
volumes:
- certs:/certs
@@ -133,6 +171,7 @@ services:
condition: service_completed_successfully
volumes:
- bootstrapdata:/data/bootstrap:ro
- certs:/certs
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/${POSTGRES_DB:-cameleer_saas}
SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER:-cameleer}