Inhaltsverzeichnis

Kubernetes Cert-Manager

Complexity: High
Duration: 2-3 hours setup
Prerequisite: Kubernetes 1.25+, Helm

Automatic certificate management in Kubernetes with cert-manager and your own PKI.


Architecture

flowchart TB subgraph K8S["KUBERNETES CLUSTER"] subgraph CM["cert-manager"] I[Issuer/ClusterIssuer] C[Certificate] CR[CertificateRequest] end subgraph APP["Application"] P[Pod] S[Secret] ING[Ingress] end C --> CR --> I I --> S S --> P S --> ING end subgraph EXTERNAL["EXTERNAL CA"] CA[Internal CA] V[Vault PKI] LE[Let's Encrypt] end I --> CA I --> V I --> LE style CM fill:#e3f2fd style I fill:#fff3e0


Installation

# Add Helm repository
helm repo add jetstack https://charts.jetstack.io
helm repo update
 
# Install cert-manager
helm install cert-manager jetstack/cert-manager \
    --namespace cert-manager \
    --create-namespace \
    --set installCRDs=true \
    --set prometheus.enabled=true
 
# Verify installation
kubectl get pods -n cert-manager

Issuer Types

Issuer Type Use Case PQ Support
————-———-————
SelfSigned Testing, Bootstrapping No
CA Internal PKI Yes (with WvdS)
Vault HashiCorp Vault PKI Partial
ACME Let's Encrypt, public CAs No
Venafi Enterprise PKI Partial

ClusterIssuer with Own CA

Step 1: Create CA Secret

# CA certificate and key as secret
kubectl create secret tls ca-key-pair \
    --cert=intermediate-ca.pem \
    --key=intermediate-ca.key \
    --namespace cert-manager

Or as YAML:

# ca-secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: ca-key-pair
  namespace: cert-manager
type: kubernetes.io/tls
data:
  tls.crt: <base64-encoded-ca-cert>
  tls.key: <base64-encoded-ca-key>

Step 2: Define ClusterIssuer

# cluster-issuer.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: internal-ca-issuer
spec:
  ca:
    secretName: ca-key-pair
kubectl apply -f cluster-issuer.yaml
 
# Check status
kubectl get clusterissuer internal-ca-issuer

Certificate Resource

# certificate.yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: app-tls
  namespace: production
spec:
  # Secret to be created
  secretName: app-tls-secret
 
  # Validity period
  duration: 2160h    # 90 days
  renewBefore: 360h  # Renew 15 days before expiry
 
  # Subject
  subject:
    organizations:
      - EMSR DATA
  commonName: app.example.com
 
  # Key configuration
  privateKey:
    algorithm: ECDSA
    size: 384
    rotationPolicy: Always
 
  # Usage
  usages:
    - server auth
    - client auth
 
  # DNS/IP SANs
  dnsNames:
    - app.example.com
    - app.production.svc.cluster.local
  ipAddresses:
    - 10.0.0.100
 
  # Issuer reference
  issuerRef:
    name: internal-ca-issuer
    kind: ClusterIssuer
kubectl apply -f certificate.yaml
 
# Check status
kubectl get certificate -n production
kubectl describe certificate app-tls -n production
 
# View secret
kubectl get secret app-tls-secret -n production -o yaml

Ingress Annotation (Automatic)

# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-ingress
  namespace: production
  annotations:
    # cert-manager creates certificate automatically
    cert-manager.io/cluster-issuer: "internal-ca-issuer"
spec:
  tls:
    - hosts:
        - app.example.com
      secretName: app-ingress-tls
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: app-service
                port:
                  number: 80

Vault PKI Integration

# vault-issuer.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: vault-issuer
spec:
  vault:
    server: https://vault.example.com
    path: pki/sign/server-role
    caBundle: <base64-encoded-vault-ca>
    auth:
      kubernetes:
        role: cert-manager
        mountPath: /v1/auth/kubernetes
        secretRef:
          name: vault-token
          key: token

Vault Configuration:

# Enable PKI engine
vault secrets enable pki
vault secrets tune -max-lease-ttl=87600h pki
 
# Generate root CA (or import)
vault write pki/root/generate/internal \
    common_name="Internal Root CA" \
    ttl=87600h
 
# Role for cert-manager
vault write pki/roles/server-role \
    allowed_domains="example.com,svc.cluster.local" \
    allow_subdomains=true \
    max_ttl=720h
 
# Kubernetes auth
vault auth enable kubernetes
vault write auth/kubernetes/config \
    kubernetes_host="https://kubernetes.default.svc"

Monitoring

Prometheus Metrics

# ServiceMonitor for cert-manager
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: cert-manager
  namespace: monitoring
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: cert-manager
  endpoints:
    - port: http-metrics

Important Metrics:

Metric Description Alert Threshold
——–————-—————–
certmanager_certificate_expiration_timestamp_seconds Expiry time < 7 days
certmanager_certificate_ready_status Ready status != 1
certmanager_certificate_renewal_timestamp_seconds Last renewal -

Alert Rules

# PrometheusRule
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: cert-manager-alerts
  namespace: monitoring
spec:
  groups:
    - name: cert-manager
      rules:
        - alert: CertificateExpiringSoon
          expr: |
            certmanager_certificate_expiration_timestamp_seconds - time() < 604800
          for: 1h
          labels:
            severity: warning
          annotations:
            summary: "Certificate {{ $labels.name }} expires in < 7 days"

        - alert: CertificateNotReady
          expr: |
            certmanager_certificate_ready_status != 1
          for: 10m
          labels:
            severity: critical
          annotations:
            summary: "Certificate {{ $labels.name }} not ready"

Troubleshooting

# Certificate status
kubectl get certificate -A
kubectl describe certificate <name> -n <namespace>
 
# Check CertificateRequest
kubectl get certificaterequest -A
kubectl describe certificaterequest <name>
 
# cert-manager logs
kubectl logs -n cert-manager -l app=cert-manager -f
 
# Events
kubectl get events -n cert-manager --sort-by='.lastTimestamp'

Common Problems:

Problem Cause Solution
—————-———-
Issuer not found ClusterIssuer vs. Issuer Check kind
Secret not found CA secret not created Create secret
Failed to generate CSR Key algorithm Check algorithm/size
Challenge failed ACME problem Check DNS/HTTP challenge

Post-Quantum with Own CA

PQ Certificates with cert-manager:

Cert-manager itself does not support PQ algorithms, but you can use a PQ-enabled CA and create the certificates manually.

// Create Kubernetes secret with PQ certificate
using var intermediate = new X509Certificate2("intermediate.pfx", "password");
 
// Load CSR from Kubernetes
var csr = CertificateRequest.LoadSigningRequest(csrBytes, HashAlgorithmName.SHA384);
 
// Issue PQ hybrid certificate
var cert = csr.Create(
    intermediate,
    DateTimeOffset.UtcNow,
    DateTimeOffset.UtcNow.AddDays(90),
    Guid.NewGuid().ToByteArray(),
    CryptoMode.Hybrid);
 
// Save as Kubernetes secret
var secret = new V1Secret
{
    Metadata = new V1ObjectMeta { Name = "pq-tls-secret" },
    Type = "kubernetes.io/tls",
    Data = new Dictionary<string, byte[]>
    {
        ["tls.crt"] = Encoding.UTF8.GetBytes(cert.ExportCertificatePem()),
        ["tls.key"] = Encoding.UTF8.GetBytes(cert.GetECDsaPrivateKey().ExportPkcs8PrivateKeyPem())
    }
};

Checklist

# Checkpoint Done
——————
1 cert-manager installed
2 ClusterIssuer configured
3 CA secret created
4 Test certificate works
5 Prometheus monitoring active
6 Alerts configured


« <- CI/CD Code Signing | -> Scheduled Renewal »


Wolfgang van der Stille @ EMSR DATA d.o.o. - Post-Quantum Cryptography Professional