Skip to content

Certificate Management

PKI Operations — Request, Revoke, Import, Export, and CRL Management

Document Version: 1.3.0
Last Updated: 2026-06-21


1. Certificate Lifecycle

graph LR
    A["📝 Request<br/>(CSR)"] --> B["⏳ Approval"]
    B --> C["✅ Issuance"]
    C --> D["🔒 Active"]
    D --> E["🔄 Renewal"]
    E --> C
    D --> F["❌ Revocation"]
    F --> G["📜 CRL Update"]
    D --> H["⏰ Expiration"]

    classDef request fill:#EBF5FB,stroke:#2196F3,stroke-width:2px,color:#1565C0
    classDef active fill:#E8F5E9,stroke:#4CAF50,stroke-width:2px,color:#2E7D32
    classDef warning fill:#FFF8E1,stroke:#FF9800,stroke-width:2px,color:#E65100
    classDef danger fill:#FFEBEE,stroke:#F44336,stroke-width:2px,color:#C62828

    class A,B request
    class C,D active
    class E,H warning
    class F,G danger

2. Requesting a Certificate

Via Web Interface

  1. Navigate to Certificates → Request Certificate
  2. Choose a Certificate Template.
Template Key Usage Extended Key Usage Typical Validity
Web Server Digital Signature, Key Encipherment Server Authentication 1 year
Client Auth Digital Signature Client Authentication 1 year
Code Signing Digital Signature Code Signing 2 years
Email (S/MIME) Digital Signature, Key Encipherment Email Protection 1 year
  1. Select a request source:
  2. Upload CSR when your tooling already generated the private key and CSR.
  3. Generate in MazeVault when MazeVault should create the private key and CSR.
  4. Generate in Azure Key Vault when the key must remain inside Azure Key Vault and only the CSR is exposed for signing.
  5. Fill in the request details:
  6. Common Name (CN): Primary domain or identifier.
  7. Template subject fields: Review the subject attributes inherited from the template.
  8. Additional SANs: Add allowed DNS, IP, email, or URI SAN entries as permitted by the template.
  9. Key custody: For MazeVault-generated requests, keep the key in MazeVault or store it through a configured secret manager integration. For Azure Key Vault generation, select the Azure Key Vault integration that will own the key.
  10. Submit the request. Depending on approval policy, the request is either issued immediately or waits for approval.

Via API

curl -X POST https://vault.example.com/api/v1/certificates/csr \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "template_id": "550e8400-e29b-41d4-a716-446655440010",
    "project_id": "550e8400-e29b-41d4-a716-446655440001",
    "requested_cn": "api.example.com",
    "csr_source_mode": "mazevault_generated",
    "requested_san": {
      "dns_names": ["api.example.com", "api-internal.example.com"]
    },
    "requested_subject": {
      "common_name": "api.example.com",
      "organization": "Example Corp"
    },
    "requested_key_algorithm": "RSA",
    "requested_key_size": 2048,
    "key_storage_mode": "local",
    "dcv_method": "dns",
    "order_years": 2,
    "tech_contact": {"firstname": "Jana", "lastname": "Nováková", "email": "pki@example.com"}
  }'

For Azure Key Vault-native generation, set csr_source_mode to azure_key_vault_generated and include key_integration_id for the Azure Key Vault integration that should own the key material.

dcv_method, ca_product_id, order_years, requested_validity_days, tech_contact and voucher are optional per-request overrides for commercial CAs; omitted values inherit the template, then the CA account (see Commercial CA Orders).

3. Certificate Approval

Depending on your organization's policy:

Policy Behavior
Auto-approve Certificate issued immediately after request
Admin approval Certificate enters pending_approval state; an administrator must approve
Manager approval Project manager can approve certificates for their project

Approving a Request

  1. Navigate to Certificates → Pending Requests
  2. Review the certificate details
  3. Click Approve or Reject

4. Revoking a Certificate

When to Revoke

  • Private key has been compromised
  • Certificate holder's affiliation has changed
  • Certificate has been superseded by a new one
  • Service using the certificate has been decommissioned

Revocation Procedure

  1. Navigate to Certificates → Active and find the certificate
  2. Click Revoke
  3. Select a Revocation Reason (RFC 5280):
  4. keyCompromise — Private key is compromised
  5. affiliationChanged — Subject's affiliation changed
  6. superseded — Replaced by a new certificate
  7. cessationOfOperation — Service decommissioned
  8. certificateHold — Temporary hold (can be unreleased)
  9. Add an optional comment
  10. Click Confirm Revocation

The CRL is automatically regenerated after revocation.

Unrevoking a Certificate

Certificates revoked with reason certificateHold can be unreleased:

  1. Navigate to Certificates → Revoked and find the certificate
  2. Click Unrevoke
  3. Confirm the action

5. Importing Certificates

Single Certificate Import

  1. Navigate to Certificates → Import
  2. Upload or paste:
  3. Certificate (PEM format, required)
  4. Private Key (PEM format, optional)
  5. CA Chain (PEM format, optional)
  6. Select the target project
  7. Click Import

Bulk Import (PEM Bundle)

  1. Navigate to Certificates → Import → Bulk Import
  2. Upload a PEM bundle containing multiple certificates
  3. Select the target project
  4. Click Import

The system parses the bundle, identifies individual certificates, and imports them with automatic chain detection.

Multi-File Import (Certificate + Separate Key File)

When you upload several files at once (for example server.pem and server.key), MazeVault pairs each standalone .key file with the certificate whose public key it matches during the preview step. The paired certificate shows a Private Key badge and a source label such as server.pem + server.key. On import, the key is sent together with its certificate in one request, so it is stored with that certificate — including certificates that already exist in MazeVault without key custody (see below). Keys that match none of the uploaded certificates are reported as an error and never imported on their own.

Adding a Private Key to an Existing Certificate

Certificates signed from an uploaded CSR are stored with key custody none, because MazeVault never saw the private key. The same applies to certificates imported without a key. To bring the key under MazeVault custody later, use either path — both end in the same backend logic that verifies the key against the certificate's public key before storing it:

  1. Certificate detail → Overview → Import private key (recommended). Upload a PEM key file, an encrypted PEM key, or a PFX/P12 bundle, or paste the PEM key. The request is pinned to that certificate (expected_certificate_id), so material that belongs to a different certificate is rejected instead of creating a new record.
  2. Certificates → Import: import a PFX/P12, a PEM containing both the certificate and the key, or a multi-file upload with the certificate file plus its .key file into the same project. MazeVault detects the existing record by SHA-256 thumbprint and attaches the key.

Rules that apply in both paths:

  • The key must match the certificate; a mismatch returns private key does not match certificate.
  • A certificate that already has a private key (local or external custody) is never overwritten; the import reports already has a private key.
  • Importing into a different project than the one owning the certificate is rejected.
  • In Orchestrator Mode the key is offloaded to the selected secret manager integration; local storage is not offered.
  • Re-importing a certificate without a key when it already exists returns a hint explaining how to attach the key.

Completing a Certificate That Has No Body

A managed certificate adopted from discovery or synchronised from a CA listing may hold metadata only (no certificate body) if it was created before MazeVault started persisting bodies, or if it was discovered only by an agent on the legacy protocol. Such a certificate cannot be exported, deployed or renewed, and a private key cannot be verified against it. It is completed by any of these paths — the result is identical:

  1. Automatically — the next agent scan or CA synchronisation that sees the certificate attaches the body (matched by SHA-256 thumbprint).
  2. Certificate detail → Complete certificate — upload the public certificate (CRT/PEM/DER), optionally together with its private key, or a PFX/P12. The upload must match the stored thumbprint.
  3. Certificates → Import — importing the public certificate alone completes the record instead of reporting "already exists"; the import result item carries message: "existing certificate record completed with the uploaded certificate body".

The body is stored together with the identity derived from it (serial, thumbprints, subject, issuer, validity, algorithms, key usage, SAN, type). An active status is refreshed from the certificate's validity; a revoked or pending record keeps its status. If a private key is supplied with the certificate: it is attached when the record has no custody, accepted silently when it matches an existing custody, and rejected with an explicit error after the body was stored when it does not match. A locally stored key that could not be verified earlier is verified when the body arrives; a mismatch is flagged in the certificate metadata and the audit log but the key is never deleted — review and replace it deliberately.

Uploaded CSR Validation Against the Template

Uploaded CSRs are checked against the selected template before anything is sent to the CA, so policy violations fail fast:

  • The CSR must be well-formed and its self-signature must verify.
  • If the template defines allowed_key_sizes, the CSR key size must be listed (for example a 4096-bit RSA CSR is rejected by a template that only allows 2048/3072).
  • If the template has an explicit SAN policy in san_config (allowed_san_types, san_validation_pattern, max_san_count), the CSR's SAN entries must satisfy it. The top-level allow_additional_sans / max_san_count columns are not applied to uploaded CSRs.
  • Set san_config.require_san = true on a template to reject CN-only CSRs, which modern TLS clients do not accept.
  • System templates (agent and gateway mTLS) are exempt from these checks.

If the CA requires manual approval (for example an ADCS CA with manager approval), the request moves to submitted_to_ca and the certificate is finalized automatically once the CA approves it. If automatic issuance fails for another reason, an auto-approved request is marked failed with the error recorded, while a manually approved request is returned to pending so the approver can retry.

Renewal Preparation After Import

  • If an imported certificate is linked to a managed CA account and meets rotation prerequisites, MazeVault can prepare renewal metadata automatically.
  • Automatically prepared rotation configurations stay disabled until an operator explicitly enables them. This prevents silent private-key rotation immediately after import.
  • The initial renewal lead time is resolved from the effective certificate, template, and CA-account policy hierarchy, so imported certificates inherit the intended renewal window instead of a hidden default.

Per-Certificate Renewal Key Policy

For renewable certificates, you can override key handling on a per-certificate basis:

  • Regenerate issues a new private key during renewal.
  • Reuse keeps the current key material when the provider, custody mode, and downstream deployment path support reuse.
  • Leaving the value unset defers to the template or higher-level policy.

Use Reuse only when downstream systems require key continuity or re-enrollment overhead is unacceptable.

Readiness Review Before Enabling Automation

  • Review the Project Rotations view before enabling certificate automation.
  • Renewal and deployment resources now expose inline readiness and preflight state.
  • Manual Review Required means MazeVault cannot validate a target automatically and an operator should confirm the deployment path before production rollout.

6. Exporting Certificates

Export Formats

Format Extension Includes Key Use Case
PEM .pem, .crt Optional Linux/Unix servers, Apache, Nginx
DER .der, .cer No Java applications, Windows
PKCS#12 .p12, .pfx Yes (password-protected) Windows, Java KeyStore import

Export Procedure

  1. Navigate to the certificate detail page
  2. Click Export
  3. Select format and options:
  4. Include CA chain
  5. Include private key (requires certificates.export_key permission)
  6. Set password (for PKCS#12)
  7. Download the file

Private Key Export

Exporting a certificate with its private key is a security-sensitive operation recorded in the audit log. Ensure the private key is transferred securely.

How the CA Chain Is Determined

With Include CA chain enabled, MazeVault exports the certificate's effective issuer chain. The same chain is shown on the certificate's Chain tab, returned by the API and installed by agents, so every consumer sees identical material.

  1. Stored chain first. A chain captured when the certificate was imported or issued (for example from a PFX or from the CA at issuance) is exported exactly as stored. MazeVault never appends anything to it — if your issuer delivered leaf + intermediate without the root, that is what you get.
  2. Inventory only when nothing is stored. If the certificate has no stored chain (typical for an intermediate CA imported on its own), MazeVault searches the CA certificates managed in the same organization — including the internal CA — for the issuer. A candidate is used only when it cryptographically verifies the certificate's signature; a matching name alone is never enough. The chain is followed up to the self-signed root.
  3. Transparency. The Chain tab labels the source (Stored with certificate / Resolved from inventory / No chain), shows whether the chain is complete to the root, and lists warnings such as a revoked issuer. Nothing is written back to the certificate by viewing or exporting it.

Intermediate CA export

To export an intermediate CA together with its root, import the root CA certificate into any project of the same organization once. From then on, the root is included automatically in exports, in the Chain tab and in agent deployments of that intermediate.

Deployments and trust stores

Agents split the exported bundle the same way on every target: the certificate and key go to the keystore (JKS, PKCS#12, Windows My), the issuer chain goes to the trust store (Windows Root/CA, Linux CA directory) when the agent's install_chain_to_trust_store option is enabled. Windows Root/CA targets receive public certificates only — never a private key.

7. ACME Certificate Automation

MazeVault includes a built-in ACME server (RFC 8555) that enables automated certificate issuance directly from Kubernetes using cert-manager.

Quick Overview

  1. Generate EAB credentials in MazeVault (Organization Settings → ACME Access)
  2. Create a Kubernetes Secret with the HMAC key
  3. Deploy a ClusterIssuer pointing to your MazeVault ACME directory
  4. Annotate Ingresses or create Certificate resources — cert-manager handles the rest
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
    privateKeySecretRef:
      name: mazevault-acme-account-key
    solvers:
      - http01:
          ingress:
            ingressClassName: nginx

Internal domains (.local, .internal, .lan, .corp) are auto-approved without HTTP-01 challenge.

Full ACME Guide

For step-by-step instructions, YAML examples, ACME profiles, and troubleshooting, see the dedicated ACME Certificate Automation Guide.


8. CRL Management

Certificate Revocation List (CRL)

MazeVault generates CRLs automatically when certificates are revoked. CRLs are available at:

Format URL
DER https://vault.example.com/api/v1/crl
PEM https://vault.example.com/api/v1/crl/pem

CRL Distribution Point

Configure the CRL Distribution Point in your certificates to point to your MazeVault CRL URL. This is automatically included in certificates issued by MazeVault CAs.

Force CRL Regeneration

If needed, regenerate the CRL manually:

  1. Navigate to Certificates → CRL
  2. Click Regenerate CRL
  3. Select type: Full or Delta

8. OCSP Validation

OCSP provides real-time certificate status checking as an alternative to CRL:

Feature CRL OCSP
Real-time ❌ (periodic generation) ✅
Bandwidth Higher (full list) Lower (per-certificate)
Privacy Client downloads all Client queries specific cert
Offline support ✅ (cached CRL) ❌ (requires connectivity)

OCSP URL

https://vault.example.com/ocsp

Testing OCSP

openssl ocsp \
  -issuer ca.pem \
  -cert server.pem \
  -url https://vault.example.com/ocsp \
  -noverify

9. Commercial CA Orders (SSLmarket)

Public TLS certificates bought through a commercial CA behave differently from internal or ACME issuance: an order is paid, it cannot be cancelled through the API, it is processed asynchronously, and it is bound to a multi-year subscription that outlives any single certificate. MazeVault treats such an order as a long-running process rather than a request/response call.

Where settings come from (request → template → CA account)

Setting Request (per certificate) Certificate template CA account
Product (validation level, wildcard/SAN limits) ca_product_id ca_product_id product_code default
Domain validation method dcv_method (dns, email, file) dcv_method dv_auth_method
Subscription length (years) order_years order_years 1 year
Technical contact tech_contact tech_contact tech_* defaults
Voucher voucher — voucher
Owner, authorised contact, invoice — — owner_*, auth_*, invoice_*

The request wins, the template fills in what the request leaves empty, and the CA account supplies the business data. The Request Certificate dialog shows a CA order settings section with exactly the fields the issuing CA allows to be overridden; every field displays the template value as its default, so leaving it empty always means "inherit".

Before anything is ordered, the resolved settings are validated against the product catalog synced from the CA (unsupported validation method, too many SANs, wildcard on a non-wildcard product, subscription longer than the product allows). A request the CA would reject therefore fails when it is submitted, with an actionable message — never as a paid order. The resolved settings and their sources are stored with the request (issuance_spec) and shown in the request detail.

Subscription years are not certificate validity

Since March 2026 public TLS certificates are valid for at most 200 days. order_years is the length of the paid order at SSLmarket (1–6 years, limited by the product). Within that subscription the CA issues follow-up certificates itself — see below.

Asynchronous issuance and attentive waiting

  1. Submission — the request enters submitting_to_ca while the order is placed. If the CA does not answer clearly (timeout, 5xx, restart), MazeVault does not retry blindly: it looks the order up by the exact CSR and either continues with it or, after a grace period, returns the request to approved. An order is never placed twice for one request.
  2. Waiting — the request is submitted_to_ca; MazeVault polls the CA with increasing intervals (2 → 5 → 15 → 60 minutes) while the CA-side state does not change and resets the interval when it does.
  3. Escalation — when an order has not moved for longer than the CA account's Alert when an order has not progressed for (hours) setting (default 24 h), MazeVault opens one incident, comments the DCV ticket if there is one and keeps waiting. It never gives up on a paid order by itself.
  4. Issuance — the certificate is downloaded, stored with its external_order_id, and deployment continues as for any other certificate.

Domain control validation (DCV)

The CA proves domain control before issuing. How the proof is placed is decided per order:

Situation What MazeVault does What you do
DNS validation and the CA account has a DNS provider configured (dns_solver_type + dns_solver_config) Creates the _dnsauth.<domain> TXT record itself, waits for the CA, removes the record afterwards Nothing
DNS/file validation and the organization has a ticketing integration (Jira) Opens one ticket with the exact record (name, type, value) and the order id; continues automatically once the CA validates; closes the ticket with a comment Add the record from the ticket to your DNS or web server
Neither Opens an incident dcv_manual_action_required and shows the record in the DCV panel Add the record; MazeVault keeps polling
E-mail validation Waits for the approval e-mail; Resend E-mail asks the CA to send it again Click the link in the CA's e-mail

Nothing needs to be confirmed in MazeVault after the record is in place — the CA validates, MazeVault notices and continues.

Renewal of a subscription certificate (follow-up)

When a certificate issued under an SSLmarket order is due for renewal, MazeVault first inspects the order:

  • The subscription is still running (or the CA renewed the order into a child order, or a reissue is pending): the renewal enters awaiting_ca_followup. No new order is placed. When the CA issues the follow-up certificate, MazeVault downloads it, verifies that it uses the same public key as the managed certificate (the private key stays, custody is inherited), runs the deployment preflight, deploys, runs post-install steps and only then marks the previous certificate as superseded. If the predecessor is about to expire and the follow-up has not appeared, one incident subscription_followup_overdue is opened.
  • The follow-up was issued with a different key (e.g. a manual reissue in the SSLmarket portal with a new CSR): MazeVault cannot deploy it — the renewal goes to manual_review with an incident subscription_followup_key_mismatch. Either reissue the order with the managed key, import the new key, or trigger a renewal with Place a new order at the CA.
  • The subscription ends with this certificate: a new order is placed as described above.

Certificates discovered from the CA account carry their order id as well, so adopted certificates take part in this logic too.

Revocation

The SSLmarket API revokes DigiCert Client Premium (S/MIME) certificates only. TLS certificates ordered via SSLmarket are revoked in the SSLmarket customer portal; MazeVault shows this note on the template and request forms.

Renewal states and what they mean

State Meaning Your action
submitting_to_ca Order is being placed; outcome reconciled by CSR if unclear None
submitted_to_ca Order accepted; waiting for the CA None
pending_dcv Domain validation outstanding (see table above) Depends on placement mode
awaiting_ca_followup Subscription running; waiting for the CA's follow-up certificate None
manual_review Automatic continuation is not safe; see the incident Decide: reissue / import key / new order
deploying Certificate received; deploying — the previous certificate stays active None
failed with resume Deployment / post-install / hand-off failed on some targets; nothing was handed over. While this item exists no new renewal is started automatically or manually (a second paid order would be placed) Fix the target, then Resume renewal — only the failed targets are redeployed; or Cancel the item explicitly

Certificate Issuance Pipeline — Architecture

The following diagram shows the complete end-to-end flow from a browser request to a signed certificate, covering all supported CA types.

flowchart TD
    subgraph Frontend["🖥️ Frontend (React)"]
        FE_MODAL["RequestCertificateModal\n(template select, CN, SANs,\nkey algorithm, subject DN)"]
        FE_POLL["Status Polling\nafter submission"]
    end

    subgraph API["🔌 API Layer (Go/Gin)"]
        API_SUBMIT["POST /projects/:id/csr\nSubmitProjectCSR\n+ OrganizationID from JWT"]
        API_ISSUE["IssueCertificate"]
    end

    subgraph Template["📋 Template Engine"]
        TPL_RESOLVE["ResolveTemplateForProject\n(inheritance merge)"]
        TPL_MERGE["mergeTemplates\nValidityDays + ValidDays sync\n(BUG-01 fixed)"]
        TPL_OVERRIDE["OverrideValidityDays\nfrom project_template_assignments\n(BUG-05 fixed)"]
    end

    subgraph CA_Resolution["🔀 CA Resolution"]
        CA_DUAL["Dual-probe:\n1. org CA accounts\n2. certificate_authorities\n(BUG FP-1 fixed)"]
    end

    subgraph CA_Providers["🏛️ CA Providers"]
        CA_INT["Internal CA\n(crypto/x509)\nvalidDays = ValidityDays || ValidDays || 365"]
        CA_SMALL["Smallstep (step-ca REST)\nnotAfter from ValidityDays\n(1-day cert bug fixed)"]
        CA_DIGI["DigiCert CertCentral v2\nValidityYears from ValidityDays"]
        CA_SSL["SSLMarket.cz REST\nvalidityMonths from ValidityDays"]
        CA_VENAFI["Venafi (vcert v5)"]
        CA_ACME_OUT["ACME Client (outbound)\nDNS-01 + HTTP-01"]
        CA_ADCS["ADCS (Web/WCCE/DCOM)\nStatus=5 → ErrADCSPendingApproval\n(type-safe check)"]
        CA_VAULT["HashiCorp Vault"]
    end

    subgraph ACME_Server["🔐 ACME Server (MazeVault as CA)"]
        ACME_NEW["NewOrder\nhttp-01 + dns-01\ntls-alpn-01 → explicit 400"]
        ACME_VALIDATE["ValidateChallenge\nHTTP probe / DNS TXT lookup"]
        ACME_FINALIZE["FinalizeOrder\nSignCSR → CA provider"]
        ACME_ADCS_POLL["pollPendingADCSOrders\n(5 min ticker)\nadcs_request_id → resume"]
    end

    subgraph Pending["⏳ ADCS Pending Approval"]
        ADCS_FIELD["ACMEServerOrder.adcs_request_id\n(migration 000165)"]
        ADCS_POLLER["ADCSApprovalPoller\nRenewalQueue path"]
    end

    subgraph Key_Offload["🔑 Key Offload (Orchestrator Mode)"]
        KO_CTX["WithKeyOffloadOrgID(ctx)\ntenant scope (BUG-07a fixed)"]
        KO_AZURE["Azure Key Vault\nbase64 decode on read\n(BUG-07b fixed)"]
        KO_AWS["AWS Secrets Manager"]
        KO_HC["HashiCorp Vault"]
    end

    FE_MODAL --> API_SUBMIT
    API_SUBMIT --> TPL_RESOLVE
    TPL_RESOLVE --> TPL_MERGE
    TPL_MERGE --> TPL_OVERRIDE
    TPL_OVERRIDE --> CA_DUAL
    CA_DUAL --> CA_INT & CA_SMALL & CA_DIGI & CA_SSL & CA_VENAFI & CA_ACME_OUT & CA_ADCS & CA_VAULT
    CA_INT & CA_SMALL & CA_DIGI & CA_SSL --> API_ISSUE
    CA_ADCS -->|"Status=5 pending"| ADCS_FIELD
    ADCS_FIELD --> ACME_ADCS_POLL & ADCS_POLLER
    ACME_ADCS_POLL -->|"approved → valid"| ACME_FINALIZE
    ADCS_POLLER -->|"approved → completed"| API_ISSUE
    API_ISSUE --> KO_CTX
    KO_CTX --> KO_AZURE & KO_AWS & KO_HC
    API_ISSUE --> FE_POLL
    FE_POLL -.->|"poll GET /projects/:id/csr/:id"| API_SUBMIT

    subgraph cert_mgr["🤖 cert-manager (K8s)"]
        CM_ACME["ACME NewOrder\nhttp-01 / dns-01 challenges"]
    end

    cert_mgr --> ACME_NEW
    ACME_NEW --> ACME_VALIDATE --> ACME_FINALIZE
    ACME_FINALIZE --> CA_INT & CA_ADCS

    classDef frontend fill:#EBF5FB,stroke:#2196F3,color:#1565C0
    classDef api fill:#E8F5E9,stroke:#4CAF50,color:#2E7D32
    classDef template fill:#F3E5F5,stroke:#9C27B0,color:#4A148C
    classDef ca fill:#FFF3E0,stroke:#FF9800,color:#E65100
    classDef acme fill:#E0F2F1,stroke:#009688,color:#00695C
    classDef pending fill:#FFEBEE,stroke:#F44336,color:#C62828
    classDef offload fill:#E8EAF6,stroke:#3F51B5,color:#1A237E

    class FE_MODAL,FE_POLL frontend
    class API_SUBMIT,API_ISSUE api
    class TPL_RESOLVE,TPL_MERGE,TPL_OVERRIDE template
    class CA_INT,CA_SMALL,CA_DIGI,CA_SSL,CA_VENAFI,CA_ACME_OUT,CA_ADCS,CA_VAULT ca
    class ACME_NEW,ACME_VALIDATE,ACME_FINALIZE,ACME_ADCS_POLL acme
    class ADCS_FIELD,ADCS_POLLER pending
    class KO_CTX,KO_AZURE,KO_AWS,KO_HC offload

Key Architectural Decisions (2026-06-21)

Component Issue Fix Migration
mergeTemplates ValidDays alias not synced after inheritance merge → 1-day certs on Smallstep Add merged.ValidDays = child.ValidityDays —
All CA providers Read ValidDays (alias) not ValidityDays (DB col) effectiveDays := template.ValidityDays; if == 0 { effectiveDays = template.ValidDays } —
ResolveTemplateForProject OverrideValidityDays in project_template_assignments was never applied New ResolveTemplateForProject(ctx, id, projectID) reads override —
IssueCertificate CA lookup CAAccountID UUID probed only in organization_ca_accounts → internal CA requests fail Dual-probe: try organization_ca_accounts → fallback certificate_authorities —
SubmitProjectCSR OrganizationID not set → cert request missing tenant scope Read org_id from JWT context —
ACME FinalizeOrder strings.Contains for ADCS pending — fragile, dead-end on approval errors.As(err, &adcsPending) + ADCSRequestID field + 5-min poller 000165
ACME findOrderByCert LIMIT 500 full scan — revocation silently fails for large tenants Serial-number JOIN on certificates table —
ACME ValidateChallenge tls-alpn-01 silently falls through to http-01 Explicit unsupportedIdentifier ACME error —
getIntegrationByProvider System-wide first-match → cross-tenant key vault leakage Scope by organization_id via WithKeyOffloadOrgID(ctx, orgID) —
Azure KV reader path GetExternalSecretValue returns raw bytes, base64.Decode was skipped → double-base64 Decode base64 in retrieval path —