Skip to content
Chris Zinda edited this page Mar 6, 2026 · 1 revision

GitOps Certificate Management

GitOps Certificate Management brings declarative, version-controlled certificate lifecycle management to the lab. Certificates are defined as desired state in a YAML file, and a reconciler script compares the desired state against the live PKI infrastructure to issue, revoke, or renew certificates as needed.

Concept

┌──────────────────────┐        ┌──────────────────┐        ┌─────────────────┐
│  desired-state.yaml  │──────► │  Reconciler      │──────► │  Dogtag PKI     │
│  (Git-tracked)       │        │  (gitops-        │        │  (9 CAs across  │
│                      │        │   reconcile.py)  │        │   3 hierarchies)│
└──────────────────────┘        └──────────────────┘        └─────────────────┘
         │                              │
    Git commit                   Dry-run / Apply
    & PR review                  mode selection

Benefits:

  • Certificates defined as code, tracked in Git
  • Pull request reviews for certificate changes
  • Audit trail for all certificate operations
  • Automated drift detection between desired and actual state
  • Bulk certificate management across all PKI hierarchies

Desired State File

Location: configs/gitops/desired-state.yaml

Structure

certificates:
  # Web server certificates
  - cn: web.cert-lab.local
    pki_type: rsa
    ca_level: intermediate
    profile: caServerCert
    state: present
    san:
      - web.cert-lab.local
      - www.cert-lab.local
    validity_days: 365

  - cn: api.cert-lab.local
    pki_type: ecc
    ca_level: intermediate
    profile: caECServerCert
    state: present
    san:
      - api.cert-lab.local
    validity_days: 365

  # IoT device certificates
  - cn: sensor-001.iot.cert-lab.local
    pki_type: rsa
    ca_level: iot
    profile: caServerCert
    state: present
    validity_days: 90

  - cn: sensor-002.iot.cert-lab.local
    pki_type: ecc
    ca_level: iot
    profile: caECServerCert
    state: present
    validity_days: 90

  # Post-quantum protected service
  - cn: vault.cert-lab.local
    pki_type: pqc
    ca_level: intermediate
    profile: caMLDSAServerCert
    state: present
    san:
      - vault.cert-lab.local
    validity_days: 365

  # Decommissioned device — should be revoked
  - cn: old-sensor.iot.cert-lab.local
    pki_type: rsa
    ca_level: iot
    state: absent
    revocation_reason: cessation_of_operation

  # Compromised device — should be revoked
  - cn: compromised.cert-lab.local
    pki_type: rsa
    ca_level: intermediate
    state: absent
    revocation_reason: key_compromise

Certificate Entry Fields

Field Required Description Values
cn Yes Common Name (fully qualified domain name) Any valid hostname
pki_type Yes PKI hierarchy rsa, ecc, pqc
ca_level Yes Target CA within the hierarchy root, intermediate, iot
profile No Certificate profile (auto-selected if omitted) See profile mapping
state Yes Desired state present or absent
san No Subject Alternative Names List of DNS names
validity_days No Certificate validity period in days Integer (default: 365)
revocation_reason No Reason for revocation (required when state: absent) See reasons below

Revocation reasons: unspecified, key_compromise, ca_compromise, affiliation_changed, superseded, cessation_of_operation, certificate_hold, privilege_withdrawn

Profile Mapping

When profile is not specified, the reconciler selects the correct profile based on pki_type:

PKI Type Profile Algorithm
rsa caServerCert SHA512withRSA (RSA-4096)
ecc caECServerCert SHA384withEC (P-384)
pqc caMLDSAServerCert ML-DSA-87 (FIPS 204)

CA Mapping

The reconciler maps pki_type + ca_level to the correct Dogtag CA container and port:

PKI Type CA Level Container Port
rsa root rsa-root-ca 8443
rsa intermediate rsa-intermediate-ca 8444
rsa iot rsa-iot-ca 8445
ecc root ecc-root-ca 8463
ecc intermediate ecc-intermediate-ca 8464
ecc iot ecc-iot-ca 8465
pqc root pq-root-ca 8453
pqc intermediate pq-intermediate-ca 8454
pqc iot pq-iot-ca 8455

Reconciler Script

Location: scripts/gitops-reconcile.py

Usage

# Dry-run mode (default) — shows planned actions without making changes
python scripts/gitops-reconcile.py --state configs/gitops/desired-state.yaml

# Apply mode — executes the reconciliation plan
python scripts/gitops-reconcile.py --state configs/gitops/desired-state.yaml --apply

Options

Flag Description Default
--state Path to the desired state YAML file Required
--dry-run Show plan without executing (default behavior) true
--apply Execute the reconciliation plan false

Reconciliation Logic

The reconciler performs these steps:

  1. Parse the desired state YAML file
  2. Query each target CA for existing certificates (by CN)
  3. Compare desired state against actual state
  4. Plan actions based on the difference
  5. Execute (if --apply is set) or display the plan

Action Types

Desired State Actual State Action
present Not found Issue new certificate
present Valid certificate exists No action (already compliant)
present Expired certificate Renew (issue new, revoke old)
present Revoked certificate Re-issue new certificate
absent Valid certificate exists Revoke with specified reason
absent Already revoked No action (already compliant)
absent Not found No action (already compliant)

Dry-Run Output

================================================================================
  GitOps Certificate Reconciliation Plan
  State file: configs/gitops/desired-state.yaml
  Mode: DRY-RUN
================================================================================

  ACTION     CN                                PKI    CA             DETAILS
  ─────────────────────────────────────────────────────────────────────────────
  ISSUE      web.cert-lab.local                rsa    intermediate   365 days, SAN: web, www
  ISSUE      api.cert-lab.local                ecc    intermediate   365 days, SAN: api
  ISSUE      sensor-001.iot.cert-lab.local     rsa    iot            90 days
  NO-OP      sensor-002.iot.cert-lab.local     ecc    iot            Already present (serial: 0x3F)
  ISSUE      vault.cert-lab.local              pqc    intermediate   365 days, SAN: vault
  REVOKE     old-sensor.iot.cert-lab.local     rsa    iot            Reason: cessation_of_operation
  NO-OP      compromised.cert-lab.local        rsa    intermediate   Already revoked

================================================================================
  Summary: 4 ISSUE | 1 REVOKE | 2 NO-OP
  Run with --apply to execute this plan
================================================================================

Apply Output

================================================================================
  GitOps Certificate Reconciliation
  State file: configs/gitops/desired-state.yaml
  Mode: APPLY
================================================================================

  [1/5] Issuing web.cert-lab.local (rsa/intermediate)... OK (serial: 0x4A)
  [2/5] Issuing api.cert-lab.local (ecc/intermediate)... OK (serial: 0x12)
  [3/5] Issuing sensor-001.iot.cert-lab.local (rsa/iot)... OK (serial: 0x7B)
  [4/5] Issuing vault.cert-lab.local (pqc/intermediate)... OK (serial: 0x09)
  [5/5] Revoking old-sensor.iot.cert-lab.local (rsa/iot, serial: 0x3E)... OK

================================================================================
  Summary: 4 issued | 1 revoked | 0 errors
================================================================================

Workflow Example

A typical GitOps workflow for certificate management:

# 1. Create a branch for the change
git checkout -b certs/add-new-api-server

# 2. Edit the desired state
vi configs/gitops/desired-state.yaml
# Add: cn: new-api.cert-lab.local, state: present, pki_type: ecc ...

# 3. Dry-run to verify the plan
python scripts/gitops-reconcile.py --state configs/gitops/desired-state.yaml

# 4. Commit and push
git add configs/gitops/desired-state.yaml
git commit -m "Add new-api.cert-lab.local certificate"
git push -u origin certs/add-new-api-server

# 5. Create merge request, get review

# 6. After merge, apply the changes
python scripts/gitops-reconcile.py --state configs/gitops/desired-state.yaml --apply

Decommissioning Devices

To revoke a certificate via GitOps, change its state to absent and specify a revocation reason:

# Before (active device)
- cn: old-sensor.iot.cert-lab.local
  pki_type: rsa
  ca_level: iot
  state: present
  validity_days: 90

# After (decommissioned)
- cn: old-sensor.iot.cert-lab.local
  pki_type: rsa
  ca_level: iot
  state: absent
  revocation_reason: cessation_of_operation

The reconciler will find the active certificate by CN, look up its serial number, and revoke it with the specified reason.

Clone this wiki locally