OCI Certificates Service: Private CA, Auto-Renewal, and TLS Deployment with Terraform

Certificate management done manually is a series of incidents waiting to happen. An engineer generates a cert, deploys it, and nobody sets a renewal reminder. The certificate expires at 3am and the load balancer starts rejecting connections. OCI Certificates Service automates the full lifecycle: issuance from a private CA, automatic renewal before expiry, and deployment to OCI Load Balancer and API Gateway without manual steps.

Step 1: Private Root CA

resource "oci_certificates_management_certificate_authority" "internal_ca" {
  compartment_id = var.compartment_id
  name           = "internal-root-ca"
  kms_key_id     = var.vault_master_key_id

  certificate_authority_config {
    config_type = "ROOT_CA_GENERATED_INTERNALLY"
    subject {
      common_name         = "Internal Root CA"
      organization        = "OsamaOracle"
      organizational_unit = "Platform Engineering"
      country             = "SA"
    }
    validity {
      time_of_validity_not_before = "2026-09-08T00:00:00Z"
      time_of_validity_not_after  = "2031-09-08T00:00:00Z"
    }
    signing_algorithm = "SHA512_WITH_RSA"
    version_name      = "v1"
  }

  defined_tags = { "Operations.ManagedBy" = "terraform", "Operations.Environment" = "production" }
}

output "ca_id" { value = oci_certificates_management_certificate_authority.internal_ca.id }

Step 2: Issue Certificate with Auto-Renewal

resource "oci_certificates_management_certificate" "orders_api_tls" {
  compartment_id = var.compartment_id
  name           = "orders-api-tls"

  certificate_config {
    config_type              = "ISSUED_BY_INTERNAL_CA"
    certificate_authority_id = oci_certificates_management_certificate_authority.internal_ca.id
    certificate_profile_type = "TLS_SERVER_OR_CLIENT"
    key_algorithm            = "RSA2048"
    signature_algorithm      = "SHA256_WITH_RSA"
    version_name             = "v1"

    subject { common_name = "orders-api.internal.example.com" }

    subject_alternative_names { type = "DNS"; value = "orders-api.internal.example.com" }
    subject_alternative_names { type = "DNS"; value = "orders-api-v2.internal.example.com" }

    validity {
      time_of_validity_not_before = "2026-09-08T00:00:00Z"
      time_of_validity_not_after  = "2027-09-08T00:00:00Z"
    }
  }

  certificate_rules {
    rule_type              = "CERTIFICATE_RENEWAL_RULE"
    advance_renewal_period = "P30D"
    renewal_interval       = "P90D"
  }

  defined_tags = { "Operations.ManagedBy" = "terraform", "Operations.Environment" = "production" }
}

output "certificate_id" { value = oci_certificates_management_certificate.orders_api_tls.id }

Renewal creates a new certificate version with the same OCID. Any downstream resource referencing the certificate by OCID receives the renewed version automatically on the next TLS handshake. No reconfiguration of load balancers, API gateways, or any other consumer is needed when a certificate renews.

Step 3: Deploy to OCI Load Balancer

resource "oci_load_balancer_listener" "https_listener" {
  load_balancer_id         = var.load_balancer_id
  name                     = "orders-api-https"
  default_backend_set_name = var.backend_set_name
  port                     = 443
  protocol                 = "HTTP"

  ssl_configuration {
    certificate_ids         = [oci_certificates_management_certificate.orders_api_tls.id]
    verify_peer_certificate = false
    server_order_preference = "ENABLED"
    cipher_suite_name       = "oci-default-ssl-cipher-suite-v1"
    protocols               = ["TLSv1.2", "TLSv1.3"]
  }
}

Step 4: Deploy to API Gateway

resource "oci_apigateway_gateway" "production" {
  compartment_id             = var.compartment_id
  display_name               = "production-api-gateway"
  endpoint_type              = "PRIVATE"
  subnet_id                  = var.private_subnet_id
  network_security_group_ids = [var.api_gateway_nsg_id]
  certificate_id             = oci_certificates_management_certificate.orders_api_tls.id

  defined_tags = { "Operations.ManagedBy" = "terraform", "Operations.Environment" = "production" }
}

Step 5: Import an Externally Issued Certificate

resource "oci_certificates_management_certificate" "public_tls" {
  compartment_id = var.compartment_id
  name           = "public-api-tls"

  certificate_config {
    config_type     = "IMPORTED"
    version_name    = "letsencrypt-2026-09"
    certificate_pem = file("${path.module}/certs/fullchain.pem")
    private_key_pem = var.certificate_private_key
    cert_chain_pem  = file("${path.module}/certs/chain.pem")
  }

  certificate_rules {
    rule_type              = "CERTIFICATE_RENEWAL_RULE"
    advance_renewal_period = "P14D"
    renewal_interval       = "P30D"
  }
}

Step 6: Expiry Alarm

resource "oci_monitoring_alarm" "cert_expiry" {
  compartment_id        = var.compartment_id
  display_name          = "certificate-expiry-45-days"
  is_enabled            = true
  metric_compartment_id = var.compartment_id
  namespace             = "oci_certificates"
  query                 = "DaysToExpiry[1d]{certificateId = '${oci_certificates_management_certificate.orders_api_tls.id}'}.min() < 45"
  severity              = "WARNING"
  pending_duration      = "PT24H"
  destinations          = [var.ops_notification_topic_id]
  body                  = "TLS certificate for orders-api expires in less than 45 days. Verify auto-renewal is configured and the CA is accessible."
}

Step 7: Check Certificate Status with Python

import oci
from datetime import datetime, timezone

def check_certificate_expiry(compartment_id: str, warn_days: int = 45) -> list:
    config = oci.config.from_file()
    client = oci.certificates_management.CertificatesManagementClient(config)

    certs = client.list_certificates(
        compartment_id=compartment_id
    ).data.items

    warnings = []
    now = datetime.now(timezone.utc)

    for cert in certs:
        if cert.lifecycle_state != "ACTIVE":
            continue

        expiry = cert.current_version.validity.time_of_validity_not_after
        days_left = (expiry - now).days

        if days_left 4} days | {cert['name']:<40} | expires {cert['expires']}")

Operational Notes

Store private keys in OCI Vault, not in Terraform code or state files. For imported certificates, pass the private key as a sensitive Terraform variable sourced from Vault at apply time. Certificate PEM and chain PEM files are not sensitive and can be stored in version control. The private key must never appear in state, logs, or source code.

Set the expiry alarm at 45 days even with auto-renewal configured. Auto-renewal depends on the CA being accessible and the signing key being available in Vault. The alarm gives you 45 days to intervene manually if auto-renewal silently fails, which is enough time to act without incident-level pressure.

Regards,
Osama

#OCI #OracleCloud #TLS #PKI #Terraform #IaC #CloudSecurity #TechBlog #Oracle #PlatformEngineering #DevSecOps #CertificateManagement #LoadBalancer #APIGateway #OracleCloudInfrastructure #AutoRenewal #PrivateCA #Encryption #ZeroTrust #Certificates

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.