Skip to content

Cert-Manager with Let's Encrypt & Route53 DNS-01

cert-manager Let's Encrypt EKS logo Route53

Cert-Manager is an open-source X.509 certificate management controller for Kubernetes. It automatically provisions, validates, and renews TLS certificates from public Certificate Authorities like Let's Encrypt or private CAs before expiration.

This guide demonstrates how to configure Cert-Manager on Amazon EKS using Route53 DNS-01 challenge validation, EKS Pod Identity, and automatic TLS termination with Gateway API / Ingress.


📺 Video Tutorial


Architecture: DNS-01 ACME Challenge with Route53

sequenceDiagram
    participant Gateway as Gateway / Ingress
    participant CM as Cert-Manager Controller
    participant LE as Let's Encrypt ACME Server
    participant R53 as AWS Route53

    Gateway->>CM: Request Certificate (Annotation)
    CM->>LE: Initiate Order & DNS-01 Challenge
    LE-->>CM: Challenge Token & TXT Record Key
    CM->>R53: Create _acme-challenge TXT Record (via Pod Identity)
    LE->>R53: Query & Verify TXT Record
    LE-->>CM: Issue Signed Certificate & Chain
    CM->>Gateway: Store in Kubernetes TLS Secret
    CM->>R53: Clean up TXT Record

Step 1. Configure IAM Policy & EKS Pod Identity

Cert-Manager needs permissions to create and remove _acme-challenge TXT records in Route53.

cert-manager-iam.tf

# 1. IAM Policy for Route53 DNS-01 Challenges
resource "aws_iam_policy" "cert_manager_dns" {
  name        = "CertManagerRoute53Policy-${module.eks.cluster_name}"
  description = "Allows Cert-Manager to manage DNS-01 ACME challenge TXT records in Route53"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect   = "Allow"
        Action   = "route53:GetChange"
        Resource = "arn:aws:route53:::change/*"
      },
      {
        Effect = "Allow"
        Action = [
          "route53:ListHostedZones",
          "route53:ListHostedZonesByName",
          "route53:ListResourceRecordSets"
        ]
        Resource = "*"
      },
      {
        Effect = "Allow"
        Action = [
          "route53:ChangeResourceRecordSets"
        ]
        Resource = "arn:aws:route53:::hostedzone/*"
      }
    ]
  })
}

# 2. IAM Role with Pod Identity Trust
data "aws_iam_policy_document" "pod_id_trust" {
  statement {
    effect = "Allow"
    principals {
      type        = "Service"
      identifiers = ["pods.eks.amazonaws.com"]
    }
    actions = ["sts:AssumeRole", "sts:TagSession"]
  }
}

resource "aws_iam_role" "cert_manager" {
  name               = "cert-manager-role-${module.eks.cluster_name}"
  assume_role_policy = data.aws_iam_policy_document.pod_id_trust.json
}

resource "aws_iam_role_policy_attachment" "cert_manager" {
  role       = aws_iam_role.cert_manager.name
  policy_arn = aws_iam_policy.cert_manager_dns.arn
}

# 3. Pod Identity Association
resource "aws_eks_pod_identity_association" "cert_manager" {
  cluster_name    = module.eks.cluster_name
  namespace       = "cert-manager"
  service_account = "cert-manager"
  role_arn        = aws_iam_role.cert_manager.arn
}


Step 2. Install Cert-Manager via Helm

cert-manager-values.yaml

installCRDs: true
config:
  apiVersion: controller.config.cert-manager.io/v1alpha1
  kind: ControllerConfiguration
  enableGatewayAPI: true
serviceAccount:
  create: true
  name: "cert-manager"

helm repo add jetstack https://charts.jetstack.io
helm repo update

helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --version v1.16.2 \
  -f cert-manager-values.yaml

Step 3. Create Let's Encrypt ClusterIssuer

cluster-issuer.yaml

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-route53
spec:
  acme:
    email: admin@vettom.online
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-route53-account-key
    solvers:
      - selector:
          dnsZones:
            - vettom.online
            - "*.vettom.online"
        dns01:
          route53:
            region: eu-west-1

kubectl apply -f cluster-issuer.yaml
kubectl get clusterissuer letsencrypt-route53

Step 4. Attach TLS Certificate to Gateway API or Ingress

gateway.yaml

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: external-gateway
  namespace: gateway
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-route53
spec:
  gatewayClassName: external-gatewayclass
  listeners:
    - name: https
      hostname: "*.vettom.online"
      port: 443
      protocol: HTTPS
      allowedRoutes:
        namespaces:
          from: All
      tls:
        certificateRefs:
          - kind: Secret
            group: ""
            name: "wildcard-vettom-cert"
            namespace: gateway


Verification

# Check certificate provisioning status
kubectl get certificate -n gateway
kubectl describe certificate wildcard-vettom-cert -n gateway

# Inspect generated secret
kubectl get secret wildcard-vettom-cert -n gateway