checkId: check.crypto.certchain plugin: stellaops.doctor.crypto severity: warn tags: [crypto, certificate, tls, security]

Certificate Chain Validation

Doctor check check.crypto.certchain — for operators and DevOps engineers confirming that the configured TLS certificate has a complete, trusted, and unexpired chain before a handshake failure takes the platform offline.

What It Checks

Verifies certificate chain completeness, trust anchor validity, and expiration for the configured TLS certificate. The check reads the certificate path from Crypto:TlsCertPath, Kestrel:Certificates:Default:Path, or Server:TlsCertificate and validates:

ConditionResult
No TLS certificate configuredSkip
Certificate file not foundFail
Certificate chain incomplete (missing intermediates)Fail
Trust anchor not valid (unknown root CA)Fail
Certificate already expiredFail
Certificate expires within 7 daysFail
Certificate expires within 30 daysWarn
Chain complete, trust anchor valid, not expiring soonPass

Evidence collected: CertPath, ChainLength, MissingIntermediates, TrustAnchorValid, TrustAnchorIssuer, ExpirationDate, DaysRemaining.

This check always runs (no precondition), but skips if no TLS certificate path is configured.

Why It Matters

An incomplete certificate chain causes TLS handshake failures for clients that do not have intermediate certificates cached. An untrusted root CA triggers browser and API client warnings or outright connection refusal. An expired certificate causes immediate service outage for all HTTPS connections. Certificate issues affect every component that communicates over TLS, including the UI, API, inter-service communication, and external integrations.

Common Causes

How to Fix

Docker Compose

# Check if certificate file exists at configured path
docker compose exec gateway ls -la /certs/

# Verify certificate details
docker compose exec gateway openssl x509 -in /certs/server.crt -noout -dates -subject -issuer

# Verify certificate chain
docker compose exec gateway openssl verify -untrusted /certs/chain.pem /certs/server.crt

# Bundle certificates correctly (leaf + intermediates)
cat server.crt intermediate.crt > fullchain.pem

# Update configuration in .env or compose override
# Crypto__TlsCertPath=/certs/fullchain.pem

# Set up automated renewal notification
# Notify__CertExpiry__ThresholdDays=14

Bare Metal / systemd

# Verify certificate file exists
ls -la /etc/stellaops/certs/server.crt

# Check certificate expiration
openssl x509 -in /etc/stellaops/certs/server.crt -noout -enddate

# Download missing intermediates using the AIA URL embedded in the cert.
# Extract the issuer's "CA Issuers" URI, fetch it, then concatenate.
issuer_url=$(openssl x509 -in /etc/stellaops/certs/server.crt -noout -text \
  | awk '/CA Issuers - URI:/{print $NF}')
curl -sSL "$issuer_url" -o /tmp/intermediate.crt
# Convert DER -> PEM if needed:
openssl x509 -inform DER -in /tmp/intermediate.crt -out /tmp/intermediate.pem 2>/dev/null \
  || cp /tmp/intermediate.crt /tmp/intermediate.pem
cat /etc/stellaops/certs/server.crt /tmp/intermediate.pem \
  > /etc/stellaops/certs/fullchain.pem

# Add CA to system trust store (Debian/Ubuntu)
sudo cp root-ca.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates

# Renew certificate via your existing PKI / ACME tooling
# (e.g. certbot, step-ca, or the org CA portal). Stella Ops does not own
# certificate issuance — point your renewal job at the same path:
#   /etc/stellaops/certs/server.crt
# and reload the gateway after rotation.

# Persist the new chain path in appsettings.crypto.yaml (or appsettings.json):
#   crypto:
#     tlsCertPath: /etc/stellaops/certs/fullchain.pem
# Save, then restart the gateway.

sudo systemctl restart stellaops-gateway

Kubernetes / Helm

# Check certificate secret
kubectl get secret stellaops-tls-cert -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -noout -dates

# Verify certificate chain
kubectl get secret stellaops-tls-cert -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl verify

# Update TLS certificate secret
kubectl create secret tls stellaops-tls-cert \
  --cert=fullchain.pem \
  --key=server.key \
  --dry-run=client -o yaml | kubectl apply -f -
# values.yaml - use cert-manager for automated renewal
certManager:
  enabled: true
  issuer: letsencrypt-prod
  renewBefore: 360h  # 15 days before expiry

Verification

stella doctor run --check check.crypto.certchain

See the Doctor reference for the full check catalog, CLI usage, and export bundles.