feat: certificate management with stage/activate/restore lifecycle
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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user