Skip to content

TLS and Certificates

Wildcard Cloudflare Origin CA certificates for the shared Gateway. Three zones: *.unipuka.app, *.mansety.com, *.us-academy.net.

Architecture

Cloudflare Dashboard (Origin Server -> Create Certificate)
    ↓  (CF Origin CA issues wildcard cert, 15yr)
Doppler base  CF_ORIGIN_CERT_<ZONE> / CF_ORIGIN_KEY_<ZONE>
    ↓  (ESO ClusterSecretStore + ExternalSecret, refreshInterval=1h)
K8s Secret  type=kubernetes.io/tls  in gateway ns
    ↓  (Gateway listener TLS secretRef)
Shared Gateway  (Cilium Gateway API, BD-3.7)
    ↓  (TLS termination at origin)
Cloudflare Edge  (re-encrypts browser<->CF with edge cert)

CF terminates browser TLS with an auto-managed edge cert. CF re-encrypts to the origin (our Gateway) using the Origin CA cert. The Origin CA cert is NOT a public CA cert - it is only trusted by Cloudflare.

Zone to Secret mapping

Zone Doppler keys K8s Secret Gateway listener
*.unipuka.app CF_ORIGIN_CERT/KEY_UNIPUKA_APP tls-unipuka-app argocd, grafana, oep-stg api/ws
*.mansety.com CF_ORIGIN_CERT/KEY_MANSETY_COM tls-mansety-com mansety-prd api/ws
*.us-academy.net CF_ORIGIN_CERT/KEY_US_ACADEMY_NET tls-us-academy-net us-prd api/ws

Rotation runbook

Certs are valid 15 years. Rotate only on: private key compromise, or proactively before expiry.

# 1. Reissue in CF dashboard: SSL/TLS -> Origin Server -> existing cert -> Revoke + reissue
#    Download new cert + key PEMs.

# 2. Upload to Doppler (example for unipuka.app zone)
doppler secrets set --project oep --config base --silent \
  "CF_ORIGIN_CERT_UNIPUKA_APP=$(cat new-unipuka-app.cert.pem)" \
  "CF_ORIGIN_KEY_UNIPUKA_APP=$(cat new-unipuka-app.key.pem)"

# 3. ESO syncs within refreshInterval (1h). Force immediate:
kubectl annotate externalsecret tls-unipuka-app -n gateway \
  force-sync=$(date +%s) --overwrite

# 4. Verify new cert loaded
kubectl -n gateway get secret tls-unipuka-app \
  -o jsonpath='{.data.tls\.crt}' | base64 -d \
  | openssl x509 -noout -dates -ext subjectAltName

# 5. Shred local PEM files
shred -uz new-unipuka-app.cert.pem new-unipuka-app.key.pem

Also see rotate-secrets.md for the full BD-3 cert rotation flow.

Debugging

ExternalSecret not Ready

kubectl -n gateway describe externalsecret tls-unipuka-app
# Check: Status.Conditions[type=Ready].Message
# Common: ClusterSecretStore not Valid, key name wrong, Doppler API unreachable

Gateway listener Programmed=False

kubectl -n gateway describe gateway shared
# Look for: Listeners[].Conditions[type=ResolvedRefs].Message
# Common: TLS secret not found, wrong namespace, secret type not kubernetes.io/tls

Verify cert SAN + expiry

for s in tls-unipuka-app tls-mansety-com tls-us-academy-net; do
  echo "== $s =="
  kubectl -n gateway get secret $s \
    -o jsonpath='{.data.tls\.crt}' | base64 -d \
    | openssl x509 -noout -subject -ext subjectAltName -dates
done