CRL (Certificate Revocation List) Configuration#
This guide explains how to configure the Certificate Revocation List (CRL) endpoint for external system integration.
Overview#
When Kleidia issues certificates for YubiKeys, each certificate contains a CRL Distribution Point URL. External systems (Azure Entra ID, Bitbucket, Active Directory) use this URL to check if a certificate has been revoked.
Certificate issued by Kleidia:
├── Subject: CN=user@company.com
├── Key Usage: Digital Signature, Client Authentication
└── CRL Distribution Points:
└── URI: https://kleidia.example.com/api/pki/crl ← External systems fetch thisArchitecture#
Kleidia’s backend serves CRL requests with in-memory caching to minimize load on OpenBao:
External System Kleidia Backend OpenBao
(Entra ID, AD, etc.) │ │
│ │ │
│ GET /api/pki/crl │ │
│─────────────────────────────►│ │
│ │ │
│ │ Cache HIT? │
│ │ ├── Yes: Return cached CRL │
│ │ └── No: Fetch from OpenBao │
│ │ ─────────────────────►
│ │◄─────────────────────────────│
│ │ Cache CRL (1 hour TTL) │
│◄─────────────────────────────│ │
│ CRL (DER format) │ │Performance Characteristics#
| Metric | Value |
|---|---|
| Cache TTL | 1 hour |
| Cache size | ~50KB (typical) |
| Response time (cache hit) | <1ms |
| Response time (cache miss) | 10-50ms |
| Memory overhead | Negligible |
Configuration#
Automatic URL Detection#
No manual CRL URL configuration is needed. Kleidia automatically derives the PKI URL from your domain configuration:
| Configuration | Resulting PKI URL |
|---|---|
global.siteUrl: "https://kleidia.example.com" | https://kleidia.example.com/api/pki |
global.domain: "kleidia.example.com" | https://kleidia.example.com/api/pki |
| Neither set | Internal URL (not externally accessible) |
⚠️ Important: The CRL URL is embedded in every issued certificate. Once certificates are issued, the URL cannot be changed without re-issuing all certificates.
Standard Helm Configuration#
Just set your domain during Helm install - PKI URLs are derived automatically:
# In your values.yaml
global:
domain: "kleidia.example.com" # PKI URL auto-detected from this
# OR
siteUrl: "https://kleidia.example.com" # Takes precedence if setThis automatically configures:
- CRL URL:
https://kleidia.example.com/api/pki/crl - CA URL:
https://kleidia.example.com/api/pki/ca - CA Chain:
https://kleidia.example.com/api/pki/ca_chain
Optional: Override PKI URL#
Only needed if you want a different URL than {siteUrl}/api/pki:
openbao:
pki:
urls:
# Override auto-detected URL (rarely needed)
externalBaseUrl: "https://pki.example.com/custom/path"
crlExpiry: "24h"Load Balancer Configuration#
Ensure your load balancer routes PKI endpoints to the Kleidia backend:
External Request Load Balancer Backend Service
│ │ │
│ GET /api/pki/crl │ │
│─────────────────────────────────►│ │
│ │ Route to backend:8080 │
│ │───────────────────────────────►│Example HAProxy configuration:
# Kleidia backend (includes PKI endpoints)
frontend kleidia_frontend
bind *:443 ssl crt /etc/ssl/kleidia.pem
# Route API requests (including /api/pki/*) to backend
acl is_api path_beg /api
use_backend kleidia_backend if is_api
backend kleidia_backend
balance roundrobin
server backend1 10.0.0.1:32570 check
server backend2 10.0.0.2:32570 checkExample nginx Ingress:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: kleidia-ingress
spec:
rules:
- host: kleidia.example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: backend
port:
number: 8080Endpoints#
GET /api/pki/crl#
Returns the Certificate Revocation List in DER format.
Response:
- Content-Type:
application/pkix-crl - Cache-Control:
public, max-age=3600
Headers:
X-CRL-Cache:HITorMISS(indicates cache status)X-CRL-Age: Duration since last fetch (e.g.,5m30s)
GET /api/pki/ca#
Returns the CA certificate in PEM format.
Response:
- Content-Type:
application/x-pem-file - Cache-Control:
public, max-age=86400
GET /api/pki/ca_chain#
Returns the full CA chain in PEM format.
Response:
- Content-Type:
application/x-pem-file - Cache-Control:
public, max-age=86400
GET /api/pki/crl/status#
Returns CRL cache status (for monitoring).
Response:
{
"cached": true,
"size_bytes": 1234,
"age": "15m30s",
"expires_in": "44m30s",
"fetched_at": "2025-12-18T10:00:00Z",
"cache_ttl": "1h0m0s",
"stale_ttl": "24h0m0s",
"is_stale": false
}The is_stale field indicates if the cache is past its TTL but still being served (stale-while-revalidate). Stale CRLs are served for up to 24 hours if OpenBao becomes temporarily unavailable.
Integration Examples#
Azure Entra ID (Certificate-Based Authentication)#
Set your domain in Helm values (PKI URL auto-detected):
global: domain: "kleidia.example.com"HTTP accessibility for CRL (Azure consideration):
- HTTPS works for most Azure Entra ID scenarios (modern CBA flows)
- HTTP (port 80) required only for: legacy on-premises AD federation, certain hybrid identity configurations, or specific compliance requirements
- If HTTP is required, configure your load balancer to also serve
/api/pki/crlon port 80
Note: Test with HTTPS first. Only configure HTTP if you encounter CRL fetch errors in Azure.
Export CA certificate and import into Azure:
curl -o ca.pem https://kleidia.example.com/api/pki/ca # Upload ca.pem to Azure Entra ID > Security > Certificate authorities
Bitbucket Data Center (Code Signing)#
Export CA chain:
curl -o ca_chain.pem https://kleidia.example.com/api/pki/ca_chainImport into Bitbucket:
- Navigate to Administration > Security > Signing certificates
- Click “Add certificate chain”
- Paste the CA chain content
Bitbucket automatically fetches CRL updates every 24 hours.
Windows Active Directory#
Export CA certificate:
curl -o kleidia-ca.pem https://kleidia.example.com/api/pki/caConvert to DER format and import:
certutil -decode kleidia-ca.pem kleidia-ca.crt certutil -addstore -enterprise -f "Root" kleidia-ca.crt
Windows caches CRL based on the Next Update field (typically 24 hours).
Monitoring#
Health Check#
Check CRL availability:
# Should return binary CRL data
curl -sf https://kleidia.example.com/api/pki/crl -o /dev/null && echo "CRL OK"
# Check cache status
curl -s https://kleidia.example.com/api/pki/crl/status | jq .Prometheus Metrics#
Monitor CRL endpoint with standard HTTP metrics:
# Example Prometheus scrape config
- job_name: 'kleidia-pki'
metrics_path: /api/pki/crl/status
static_configs:
- targets: ['kleidia.example.com']Troubleshooting#
CRL Not Accessible Externally#
Symptoms:
- Certificate validation fails in external systems
- “CRL fetch failed” errors in Entra ID / Bitbucket
Solutions:
Verify the external URL is correctly configured:
curl -v https://kleidia.example.com/api/pki/crlCheck load balancer is routing to backend service
Ensure firewall allows inbound traffic on ports 80/443
Azure Requires HTTP#
Symptoms:
- Azure CRL validation fails despite HTTPS working
Solution: Azure Entra ID requires CRL over HTTP (port 80). Configure your load balancer:
# Add HTTP frontend for CRL only
frontend kleidia_http
bind *:80
acl is_crl path_beg /api/pki/crl
use_backend kleidia_backend if is_crl
# Redirect all other HTTP to HTTPS
redirect scheme https if !is_crlCertificates Have Wrong CRL URL#
Symptoms:
- Issued certificates point to internal Kubernetes URL
- External systems cannot reach CRL
Solution:
The global.domain or global.siteUrl was not set before certificate issuance. You must:
- Update Helm values with correct
global.domainorglobal.siteUrl - Reinstall OpenBao (or manually reconfigure PKI URLs)
- Re-enroll affected YubiKeys to get new certificates
Traffic Estimation#
For capacity planning, CRL traffic is minimal due to caching:
| Scenario | Daily CRL Requests | Bandwidth/Day |
|---|---|---|
| 1,000 users | ~1,000 | ~50 MB |
| 10,000 users | ~10,000 | ~500 MB |
| 100,000 users | ~100,000 | ~5 GB |
Notes:
- Each workstation/client fetches CRL once per 24 hours (cached)
- Shared workstations reduce request count significantly
- CRL size is typically 10-50 KB