QCecuring - Enterprise Security Solutions

Infrastructure as Code and PKI

Shivam Sharma

Key Takeaways

  • IaC declares certificates as resources: domain, issuer, key type, and validity are defined in code and provisioned automatically
  • Terraform manages cloud certificates (ACM, GCP, Azure) and private CA infrastructure. cert-manager is the Kubernetes-native equivalent.
  • Private keys in IaC state files are a security risk — use references (ARNs, Secret names) instead of storing key material in state
  • GitOps for certificates means the desired certificate state is in Git — drift is detected and corrected automatically

Infrastructure as Code (IaC) applies software engineering practices — version control, code review, automated testing, and declarative configuration — to PKI infrastructure. Instead of manually creating CAs, requesting certificates through web portals, or running ad-hoc scripts, you declare your PKI resources (CAs, certificates, trust stores, policies) in code. Terraform provisions cloud CA infrastructure and certificates. Ansible configures certificate deployment on servers. GitOps controllers (ArgoCD, Flux) ensure the declared certificate state matches reality in Kubernetes.


Why it matters

  • Reproducibility — the entire PKI infrastructure can be recreated from code. Disaster recovery becomes “run terraform apply” instead of “find the person who set this up 3 years ago.”
  • Version control — certificate configurations are tracked in Git. You can see who changed what, when, and why. Rollback is a git revert.
  • Code review — certificate changes go through pull requests. A second pair of eyes catches misconfigurations (wrong SAN, too-long validity, wrong CA) before they reach production.
  • Drift detection — GitOps controllers continuously compare declared state to actual state. If someone manually modifies a certificate or Secret, the drift is detected and corrected.
  • Environment consistency — the same IaC modules provision certificates across dev, staging, and production. No more “staging uses self-signed, production uses real certs” inconsistencies.

How it works

  1. Declare PKI resources — define CAs, certificates, and trust configurations in IaC files (Terraform HCL, Ansible YAML, Kubernetes manifests)
  2. Store in version control — commit to Git with proper branching and review workflows
  3. Plan/previewterraform plan or ansible --check shows what will change before applying
  4. Apply — IaC tool provisions the resources (creates CA, requests certificate, deploys to target)
  5. State management — Terraform stores resource state (certificate ARNs, expiry dates). Kubernetes stores state in etcd.
  6. Drift detection — periodic reconciliation detects manual changes and reverts them to the declared state
  7. Lifecycle updates — certificate renewal, CA rotation, and policy changes are code changes that go through the same review process

In real systems

Terraform — AWS Private CA + certificates:

# Create a private CA
resource "aws_acmpca_certificate_authority" "internal" {
  type = "SUBORDINATE"
  certificate_authority_configuration {
    key_algorithm     = "EC_prime256v1"
    signing_algorithm = "SHA256WITHECDSA"
    subject {
      common_name  = "Internal Issuing CA"
      organization = "My Org"
    }
  }
}

# Issue a certificate from the private CA
resource "aws_acmpca_certificate" "service" {
  certificate_authority_arn   = aws_acmpca_certificate_authority.internal.arn
  certificate_signing_request = tls_cert_request.service.cert_request_pem
  signing_algorithm           = "SHA256WITHECDSA"
  validity {
    type  = "DAYS"
    value = 365
  }
}

# Public certificate via ACM (auto-validated with Route53)
resource "aws_acm_certificate" "public" {
  domain_name       = "api.example.com"
  validation_method = "DNS"
}

Ansible — deploy certificates to servers:

- name: Deploy TLS certificate
  hosts: webservers
  tasks:
    - name: Copy certificate
      copy:
        src: "certs/{{ inventory_hostname }}.pem"
        dest: /etc/ssl/certs/server.pem
        mode: '0644'

    - name: Copy private key
      copy:
        src: "keys/{{ inventory_hostname }}.key"
        dest: /etc/ssl/private/server.key
        mode: '0600'

    - name: Reload Nginx
      service:
        name: nginx
        state: reloaded
      when: cert_changed.changed or key_changed.changed

GitOps with ArgoCD + cert-manager:

# Certificate declared in Git — ArgoCD ensures it exists in cluster
# apps/production/certificates/api-tls.yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: api-tls
  namespace: production
spec:
  secretName: api-tls-secret
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
  dnsNames:
    - api.example.com

# ArgoCD syncs this to the cluster
# If someone manually deletes the Secret, ArgoCD recreates it
# If someone modifies the Certificate resource, ArgoCD reverts it

Terraform — Vault PKI backend:

resource "vault_mount" "pki" {
  path        = "pki"
  type        = "pki"
  max_lease_ttl_seconds = 315360000  # 10 years
}

resource "vault_pki_secret_backend_root_cert" "root" {
  backend     = vault_mount.pki.path
  type        = "internal"
  common_name = "My Org Root CA"
  ttl         = "315360000"
}

resource "vault_pki_secret_backend_role" "web_server" {
  backend          = vault_mount.pki.path
  name             = "web-server"
  allowed_domains  = ["example.com"]
  allow_subdomains = true
  max_ttl          = "2592000"  # 30 days
  key_type         = "ec"
  key_bits         = 256
}

Where it breaks

Private keys in Terraform state — Terraform’s tls_private_key resource stores the private key in the state file (plaintext JSON by default). If state is stored in S3 without encryption, or shared among team members, private keys are exposed. Never generate private keys in Terraform for production. Use: cloud-managed keys (ACM, KMS), cert-manager (keys stay in K8s Secrets), or Vault (keys stay in Vault). If you must use Terraform for keys, encrypt state and restrict access.

IaC drift on certificates managed externally — Terraform declares a certificate, but cert-manager or ACME also manages it (renewing it independently). Terraform sees the renewed certificate as “drift” and tries to revert it to the original. Or Terraform’s state shows the old certificate while the live system has the renewed one. Choose one management plane per certificate: either IaC provisions it once and something else renews it, or IaC manages the full lifecycle. Don’t mix.

Secrets in Git — a developer accidentally commits a private key or CA credentials to the Git repository. Even if removed in a subsequent commit, it’s in Git history forever. Use .gitignore for key files, pre-commit hooks that scan for secrets (gitleaks, trufflehog), and reference secrets by name (Vault path, AWS Secrets Manager ARN) rather than embedding values in IaC code.


Operational insight

The tension in IaC + PKI is between “everything declared in code” and “secrets must not be in code.” The resolution: declare the certificate specification in code (domain, issuer, key algorithm, validity) but let the provisioning system handle the secret material (private key generation, storage, rotation). Terraform declares “I need a certificate for api.example.com from Let’s Encrypt” — ACM handles the key. cert-manager declares “I need a certificate with these SANs” — it generates the key internally. The IaC defines what you want; the certificate management system handles the sensitive how. This separation keeps your Git repository secret-free while maintaining the benefits of declarative infrastructure.


Ready to Secure Your Enterprise?

Experience how our cryptographic solutions simplify, centralize, and automate identity management for your entire organization.

Stay ahead on cryptography & PKI

Get monthly insights on certificate management, post-quantum readiness, and enterprise security. No spam.

We respect your privacy. Unsubscribe anytime.