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:
- File existence: whether the configured certificate file exists on disk.
- Chain completeness: whether all intermediate certificates are present (no missing links).
- Trust anchor validity: whether the root CA is trusted by the system trust store.
- Expiration: days until the certificate expires, with tiered severity.
| Condition | Result |
|---|---|
| No TLS certificate configured | Skip |
| Certificate file not found | Fail |
| Certificate chain incomplete (missing intermediates) | Fail |
| Trust anchor not valid (unknown root CA) | Fail |
| Certificate already expired | Fail |
| Certificate expires within 7 days | Fail |
| Certificate expires within 30 days | Warn |
| Chain complete, trust anchor valid, not expiring soon | Pass |
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
- Certificate file was moved or deleted from the configured path
- Incorrect certificate path in configuration
- Missing intermediate certificates in the certificate bundle
- Incomplete certificate bundle (only leaf certificate, no intermediates)
- Root CA not added to the system trust store
- Self-signed certificate not explicitly trusted
- Certificate not renewed before expiration
- Automated renewal process failed silently
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
Related Checks
check.crypto.fips— FIPS compliance may impose certificate algorithm constraintscheck.crypto.eidas— eIDAS compliance requires specific signature algorithms on certificatescheck.crypto.hsm— HSM may store the private key associated with the certificatecheck.compliance.attestation-signing— attestation signing uses related key material
Related Documentation
See the Doctor reference for the full check catalog, CLI usage, and export bundles.
