Skip to content

ACME Certificate Automation

Automated Certificate Management with cert-manager and Kubernetes

Document Version: 1.0.0
Last Updated: 2026-03-14


Overview

MazeVault implements an ACME server (RFC 8555) that allows Kubernetes clusters to automatically request, issue, and renew TLS certificates through cert-manager. This eliminates manual certificate management and integrates your existing PKI — including Microsoft ADCS — with cloud-native infrastructure.

sequenceDiagram
    participant CM as ⚙️ cert-manager
    participant MV as 🏦 MazeVault ACME Server
    participant CA as 📜 Backend CA (ADCS/Internal)

    rect rgb(235, 245, 251)
    Note over CM,MV: 🔑 Account Registration
    CM->>MV: POST /new-account (EAB)
    MV-->>CM: Account registered
    end

    rect rgb(255, 248, 225)
    Note over CM,CA: 📝 Certificate Order
    CM->>MV: POST /new-order
    MV-->>CM: Order + Authorizations
    CM->>MV: POST /finalize (CSR)
    MV->>CA: Sign CSR
    CA-->>MV: Signed certificate
    end

    rect rgb(232, 245, 233)
    Note over CM,MV: ✅ Certificate Delivery
    MV-->>CM: Certificate ready
    CM->>MV: GET /cert/:id
    MV-->>CM: PEM certificate chain
    end

Key Benefits

Feature Description
Zero-touch renewal cert-manager automatically renews certificates before expiry
ADCS bridge Issue certificates from Microsoft ADCS via standard ACME protocol
Template routing Map ACME profiles or domains to specific certificate templates
EAB security External Account Binding ensures only authorized clusters can register
Auto-approve Internal domains (.local, .internal, .lan, .corp) are approved instantly
Audit trail All ACME operations are logged in the MazeVault audit system

Prerequisites

  • MazeVault v1.0.17+ with ACME Server enabled
  • Kubernetes cluster with cert-manager v1.12+
  • At least one CA Account configured in MazeVault (ADCS, Internal CA, or external)
  • A Certificate Template configured for your use case

Step 1: Generate EAB Credentials

External Account Binding (EAB) credentials tie an ACME client to your MazeVault organization. Each set of credentials can only be used once.

Via Web Interface

  1. Navigate to Organization Settings → Certificate Authorities
  2. Scroll to the ACME Access section
  3. Click Generate EAB Credentials
  4. Fill in:
  5. Cluster Label — Descriptive name (e.g., prod-aks-westeurope)
  6. Default Project ID — Project where certificates will be stored (optional)
  7. Default Template ID — Certificate template to use (optional)
  8. Click Generate
  9. Copy and securely store the EAB Key ID and HMAC Key — the HMAC key is shown only once

Via API

curl -X POST https://vault.example.com/api/v1/organizations/{org_id}/acme-credentials \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "project_id": "proj_abc123",
    "default_template_id": "tmpl_def456",
    "cluster_label": "prod-aks-westeurope"
  }'

Response:

{
  "id": "cred_xyz789",
  "eab_key_id": "***REMOVED***",
  "eab_hmac_key": "***REMOVED***",
  "cluster_label": "prod-aks-westeurope",
  "created_at": "2026-03-14T10:00:00Z"
}

Save Credentials Immediately

The eab_hmac_key is returned only once at creation time. It cannot be retrieved later. Store it in a secure location (e.g., Azure Key Vault, HashiCorp Vault).


Step 2: Create the EAB Secret in Kubernetes

Store the HMAC key as a Kubernetes Secret for cert-manager to use:

apiVersion: v1
kind: Secret
metadata:
  name: mazevault-eab-secret
  namespace: cert-manager
type: Opaque
stringData:
  secret: "***REMOVED***"  # Your EAB HMAC Key
kubectl apply -f mazevault-eab-secret.yaml

Alternative: Create from CLI

kubectl create secret generic mazevault-eab-secret \
  --namespace cert-manager \
  --from-literal=secret="YOUR_EAB_HMAC_KEY"

Step 3: Configure ClusterIssuer

Create a ClusterIssuer (cluster-wide) or Issuer (namespace-scoped) resource:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: mazevault-issuer
spec:
  acme:
    # MazeVault ACME directory URL
    server: https://vault.example.com/api/acme/directory

    # External Account Binding
    externalAccountBinding:
      keyID: "***REMOVED***"          # Your EAB Key ID
      keySecretRef:
        name: mazevault-eab-secret
        key: secret
      keyAlgorithm: HS256

    # Private key for the ACME account (auto-generated by cert-manager)
    privateKeySecretRef:
      name: mazevault-acme-account-key

    # Challenge solver for non-auto-approved domains
    solvers:
      - http01:
          ingress:
            ingressClassName: nginx
kubectl apply -f mazevault-clusterissuer.yaml

Namespace-Scoped Issuer

apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: mazevault-issuer
  namespace: my-app
spec:
  acme:
    server: https://vault.example.com/api/acme/directory
    externalAccountBinding:
      keyID: "***REMOVED***"
      keySecretRef:
        name: mazevault-eab-secret
        key: secret
      keyAlgorithm: HS256
    privateKeySecretRef:
      name: mazevault-acme-account-key
    solvers:
      - http01:
          ingress:
            ingressClassName: nginx

Verify Issuer Status

kubectl get clusterissuer mazevault-issuer -o wide

Expected output:

NAME               READY   STATUS                                                 AGE
mazevault-issuer   True    The ACME account was registered with the ACME server   30s

Step 4: Request Certificates

Option A: Certificate Resource

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: api-tls
  namespace: my-app
spec:
  secretName: api-tls-secret
  duration: 8760h       # 1 year
  renewBefore: 720h     # Renew 30 days before expiry
  issuerRef:
    name: mazevault-issuer
    kind: ClusterIssuer
  commonName: api.example.com
  dnsNames:
    - api.example.com
    - api-internal.example.com

Option B: Ingress Annotation (Auto-TLS)

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-ingress
  namespace: my-app
  annotations:
    cert-manager.io/cluster-issuer: mazevault-issuer
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - api.example.com
      secretName: api-tls-secret
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: api-service
                port:
                  number: 8080

Option C: ACME Profiles (cert-manager v1.18+)

ACME profiles allow cert-manager to select a specific certificate template by name:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: web-tls
  namespace: my-app
spec:
  secretName: web-tls-secret
  issuerRef:
    name: mazevault-issuer
    kind: ClusterIssuer
  commonName: web.example.com
  dnsNames:
    - web.example.com
  # Profile maps to a MazeVault Certificate Template with matching ACMEProfileName
  profileName: web-server

Profile Configuration

To use ACME profiles, set the ACME Profile Name field on your Certificate Template in MazeVault. The profile name in the Certificate resource must match exactly.


Step 5: Verify Certificate Issuance

# Check Certificate status
kubectl get certificate -n my-app
NAME      READY   SECRET           AGE
api-tls   True    api-tls-secret   2m
# Inspect the issued certificate
kubectl describe certificate api-tls -n my-app

# View the actual TLS certificate
kubectl get secret api-tls-secret -n my-app -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -noout -text

Domain Auto-Approval

For internal domains, MazeVault skips the HTTP-01 challenge and approves certificates immediately:

Domain Suffix Example Challenge Required
.local app.mycompany.local No (auto-approved)
.internal api.cluster.internal No (auto-approved)
.lan server.office.lan No (auto-approved)
.corp vault.mycompany.corp No (auto-approved)
All others api.example.com Yes (HTTP-01)

Internal Domains Are Fastest

For Kubernetes services that use internal DNS names, certificates are issued instantly without challenge validation. This is ideal for service mesh, internal APIs, and microservice-to-microservice TLS.


Template Routing

MazeVault routes ACME certificate requests to the correct CA backend using these resolution rules (in priority order):

  1. ACME Profile — If the request includes a profileName, the template with matching ACMEProfileName is used
  2. Domain Rules — If domain rules are configured on the EAB credential, the first matching rule determines the template
  3. Default Template — Falls back to the default template set on the EAB credential or ACME account
graph TD
    A["📨 ACME Order"] --> B{"Profile<br/>specified?"}
    B -->|Yes| C["✅ Use matching template"]
    B -->|No| D{"Domain rules<br/>match?"}
    D -->|Yes| E["✅ Use rule's template"]
    D -->|No| F{"Default template<br/>set?"}
    F -->|Yes| G["✅ Use default template"]
    F -->|No| H["❌ Error: No template"]

    classDef start fill:#EBF5FB,stroke:#2196F3,stroke-width:2px,color:#1565C0
    classDef decision fill:#FFF8E1,stroke:#FF9800,stroke-width:2px,color:#E65100
    classDef success fill:#E8F5E9,stroke:#4CAF50,stroke-width:2px,color:#2E7D32
    classDef error fill:#FFEBEE,stroke:#F44336,stroke-width:2px,color:#C62828

    class A start
    class B,D,F decision
    class C,E,G success
    class H error

Managing EAB Credentials

List Credentials

Navigate to Organization Settings → Certificate Authorities → ACME Access to view all EAB credentials with their status:

Status Meaning
Available Not yet used, ready for registration
Used Already consumed by an ACME client
Revoked Disabled by an administrator

Revoking a Credential

To prevent an ACME client from registering (or to decommission a cluster):

  1. Navigate to the ACME Access section
  2. Find the credential in the table
  3. Click the Revoke button

Existing Accounts Unaffected

Revoking an EAB credential does not affect accounts already registered with it. To deactivate an existing account, use the ACME account deactivation flow.


ACME Endpoints Reference

Endpoint Method Description
/api/acme/directory GET ACME directory (discovery)
/api/acme/new-nonce HEAD, POST Get replay-protection nonce
/api/acme/new-account POST Register new account (EAB required)
/api/acme/new-order POST Create certificate order
/api/acme/order/:id POST Get order status
/api/acme/authz/:id POST Get authorization details
/api/acme/challenge/:id POST Trigger challenge validation
/api/acme/finalize/:id POST Submit CSR for signing
/api/acme/cert/:id POST Download issued certificate

Troubleshooting

ClusterIssuer shows False READY status

kubectl describe clusterissuer mazevault-issuer

Common causes:

Error Solution
401 externalAccountRequired EAB credentials are missing or incorrect
403 unauthorized: EAB credential already used Generate new EAB credentials — each can only be used once
403 unauthorized: EAB credential revoked The credential was revoked, generate a new one
Connection refused Verify the MazeVault URL is reachable from the cluster
TLS error Ensure the cluster trusts MazeVault's TLS certificate

Certificate stays in Pending state

kubectl describe certificate api-tls -n my-app
kubectl describe certificaterequest -n my-app
kubectl describe order -n my-app
kubectl describe challenge -n my-app

Common causes:

Issue Solution
Challenge failed Ensure the Ingress controller can serve /.well-known/acme-challenge/
No template found Configure a default template on the EAB credential or use ACME profiles
Order expired Orders expire after 24 hours; check for stuck challenges

Useful Commands

# View cert-manager logs
kubectl logs -n cert-manager deploy/cert-manager -f

# Check ACME account registration
kubectl get secret mazevault-acme-account-key -n cert-manager -o yaml

# Force certificate renewal
kubectl delete secret api-tls-secret -n my-app
# cert-manager will automatically re-issue

Complete Example: End-to-End Setup

Here is a complete working example that provisions a TLS certificate for a web application:

---
# 1. EAB Secret
apiVersion: v1
kind: Secret
metadata:
  name: mazevault-eab-secret
  namespace: cert-manager
type: Opaque
stringData:
  secret: "YOUR_EAB_HMAC_KEY"

---
# 2. ClusterIssuer
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: mazevault-issuer
spec:
  acme:
    server: https://vault.example.com/api/acme/directory
    externalAccountBinding:
      keyID: "YOUR_EAB_KEY_ID"
      keySecretRef:
        name: mazevault-eab-secret
        key: secret
      keyAlgorithm: HS256
    privateKeySecretRef:
      name: mazevault-acme-account-key
    solvers:
      - http01:
          ingress:
            ingressClassName: nginx

---
# 3. Certificate
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: myapp-tls
  namespace: default
spec:
  secretName: myapp-tls-secret
  duration: 8760h
  renewBefore: 720h
  issuerRef:
    name: mazevault-issuer
    kind: ClusterIssuer
  commonName: myapp.example.com
  dnsNames:
    - myapp.example.com
    - www.myapp.example.com

---
# 4. Ingress using the certificate
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: myapp-ingress
  namespace: default
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - myapp.example.com
        - www.myapp.example.com
      secretName: myapp-tls-secret
  rules:
    - host: myapp.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: myapp
                port:
                  number: 80