Skip to content

Certificate Issuer

Certificate Issuer

Category: Infrastructure Type: Cluster Service Tags: cluster-level · networking · tls · dns


Overview

The Certificate Manager plugin installs the cert-manager controller, but cert-manager issues nothing until a ClusterIssuer exists. This plugin creates that issuer from the app store, so a cluster can go from a fresh cert-manager install to issuing Let's Encrypt certificates without applying YAML by hand.

Once installed, any Ingress in the cluster can request a certificate by carrying the cert-manager.io/cluster-issuer annotation and a tls block. Workload templates that support custom domains reference this issuer through their tls_issuer field.


How It Works

Cluster Service - Installed once per cluster by an administrator. It creates a single ClusterIssuer and, when a DNS-01 challenge is used, the Secret holding the provider credential.

Two challenge types are supported:

  • HTTP-01 serves a token over port 80 on the hostname being certified. The hostname must already resolve to this cluster before a certificate can be issued. It cannot issue wildcard certificates.
  • DNS-01 proves ownership by writing a TXT record, so the hostname does not need to resolve yet, and it is the only option that can issue a wildcard certificate such as *.apps.example.com. It needs a credential for the DNS provider.

Prerequisites

  • The Certificate Manager plugin installed and healthy
  • No existing ClusterIssuer with the name you choose. Check with kubectl get clusterissuer. Many clusters already have one called letsencrypt-prod, and if so this install takes it over, replaces its configuration, and deletes it on uninstall, which breaks every certificate that renews through it. Pick another name instead
  • For HTTP-01, a hostname already resolving to the cluster ingress address
  • For DNS-01, a credential Secret in the cert-manager namespace, see Credentials, or IRSA for Route53

Installation

  1. For DNS-01, create the credential Secret first, see Credentials
  2. Open Terra and navigate to the Plugin Marketplace
  3. Search for "Certificate Issuer"
  4. Click Install
  5. Fill in the configuration fields below
  6. Click Confirm to deploy

Configuration

Install-Time Fields

Field Details
issuer_name string · Required · Default: letsencrypt-prod
Name of the ClusterIssuer. This is the value workloads put in their tls_issuer field. Must not already exist on the cluster, see Prerequisites
email string · Required
Contact address registered with the ACME account, used for account and policy notices. Let's Encrypt stopped sending expiry warnings in June 2025, so renewal relies on cert-manager
acme_server select · Required · Default: production
Let's Encrypt production or staging directory
solver select · Required · Default: http01
http01 or dns01
ingress_class string · Optional · Default: nginx
Ingress class used to serve the HTTP-01 challenge
dns_provider select · Optional · Default: cloudflare
cloudflare or route53, used only with dns01
secret_namespace string · Optional · Default: cert-manager
Namespace the credential Secret is created in. Must be the namespace cert-manager runs in
secret_name string · Optional
Name of the credential Secret in secret_namespace. Required for Cloudflare. For Route53, leave empty to use ambient credentials such as IRSA
secret_key string · Optional
Key holding the Cloudflare API token. Route53 reads fixed key names, see Credentials
aws_hosted_zone_id string · Optional
Route53 hosted zone id. Leave empty to let cert-manager discover the zone

Credentials

The plugin never takes a credential as a form value. For DNS-01 you create a Secret, then point the plugin at it with secret_namespace, secret_name and secret_key.

secret_namespace has to be the namespace cert-manager runs in, cert-manager when installed from Terra, which is the default. A ClusterIssuer's Secret references carry no namespace of their own: cert-manager always reads them from its own namespace, so a Secret created anywhere else is never found. The field is there so the Secret's location is stated in the form, as it is for the other plugins that take a credential Secret, not because the Secret can live elsewhere.

Cloudflare

Store the API token under a single key:

kubectl create secret generic cloudflare-api-token \
  --namespace cert-manager \
  --from-literal=api-token=YOUR_TOKEN

Then set secret_name to cloudflare-api-token and secret_key to api-token. The token needs Zone:Read and DNS:Edit on the zones being certified, nothing else.

Route53

cert-manager needs the access key id and the secret access key as two separate values, so the Secret holds both under these fixed key names:

kubectl create secret generic aws-dns-credentials \
  --namespace cert-manager \
  --from-literal=aws_access_key_id=YOUR_ACCESS_KEY \
  --from-literal=aws_secret_access_key=YOUR_SECRET_KEY

Then set secret_name to aws-dns-credentials. secret_key is not used for Route53.

On EKS with IRSA, leave secret_name empty and cert-manager uses the role attached to its own ServiceAccount.

The IAM identity needs route53:GetChange, route53:ChangeResourceRecordSets and route53:ListResourceRecordSets on the zone, plus route53:ListHostedZonesByName when aws_hosted_zone_id is left empty.

cert-manager does not share credentials with the ExternalDNS plugin even when both talk to the same provider, so each holds its own Secret in its own namespace.


Requesting a Certificate

Annotate an Ingress and give it a tls block:

metadata:
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  tls:
    - hosts: [app.example.com]
      secretName: app-tls

cert-manager runs the challenge, writes the certificate into app-tls, and renews it before expiry.


Notes

  • Test with the staging directory first. Production Let's Encrypt limits duplicate certificates to five per week, and a misconfigured issuer can burn through that while you debug
  • Check progress with kubectl describe clusterissuer <name> and kubectl get certificate -A. A certificate stuck in False usually means the challenge cannot be reached, not that the issuer is wrong
  • Wildcard certificates require dns01. HTTP-01 has no way to prove ownership of every name under a domain
  • The issuer is cluster wide, so one install serves every project and every workload
  • Changing issuer_name after workloads reference it leaves those workloads pointing at an issuer that no longer exists. Keep the name stable
  • Route53 has no regional endpoints, but the AWS SDK still wants a region to sign requests, so the issuer sets us-east-1. cert-manager ignores it on EKS with IRSA, where the pod identity webhook provides the region
  • Choosing Cloudflare without naming a Secret and key stops the install with a message saying what to set, rather than deploying an issuer that can never become ready

plugins/cert-issuer/terra.yaml
resource_id: cert-issuer
name: Certificate Issuer
icon: https://github.com/juno-fx/Terra-Official-Plugins/blob/main/plugins/cert-issuer/assets/icon.svg?raw=true
description: Creates the ACME ClusterIssuer that cert-manager needs before it can issue any certificate. Supports Let's Encrypt over HTTP-01, or DNS-01 with Route53 and Cloudflare for wildcard certificates. https://cert-manager.io/docs/configuration/acme/
category: Infrastructure
tags:
  - cluster-level
  - networking
  - tls
  - dns
editable: true
fields:
  - name: issuer_name
    description: Name of the ClusterIssuer. Workloads reference this name in their tls_issuer field. Check kubectl get clusterissuer before installing, since an existing issuer with this name is taken over and its configuration replaced, and uninstalling deletes it.
    required: true
    default: letsencrypt-prod
    type: string
  - name: email
    description: Contact address registered with the ACME account, used by Let's Encrypt for account and policy notices. It no longer sends expiry warnings, cert-manager renews automatically.
    required: true
    type: string
  - name: acme_server
    description: ACME directory to register against. Use staging while testing, it issues untrusted certificates but has far higher rate limits.
    required: true
    default: https://acme-v02.api.letsencrypt.org/directory
    type: select
    options:
      - https://acme-v02.api.letsencrypt.org/directory
      - https://acme-staging-v02.api.letsencrypt.org/directory
  - name: solver
    description: How domain ownership is proven. http01 needs the hostname to already resolve to this cluster. dns01 writes a TXT record instead and is the only option that can issue wildcard certificates.
    required: true
    default: http01
    type: select
    options:
      - http01
      - dns01
  - name: ingress_class
    description: Ingress class used to serve the HTTP-01 challenge. Must match the class Orion routes through.
    required: false
    default: nginx
    type: string
  - name: dns_provider
    description: DNS provider used for the dns01 challenge.
    required: false
    default: cloudflare
    type: select
    options:
      - cloudflare
      - route53
  - name: secret_namespace
    description: Namespace the credential Secret is created in. A ClusterIssuer's Secret references carry no namespace, so cert-manager only reads them from the namespace it runs in. Leave at cert-manager unless cert-manager was installed somewhere else.
    required: false
    default: cert-manager
    type: string
  - name: secret_name
    description: Name of an existing Secret holding the DNS provider credential, in secret_namespace. This plugin does not create the Secret, it must be created with kubectl first (see the plugin README). Required for Cloudflare. For Route53, leave empty to use ambient credentials such as IRSA.
    required: false
    type: string
  - name: secret_key
    description: Name of the key inside that Secret, for example api-token. This is only a key name, never the token itself, which lives in the Secret. Route53 reads the fixed key names aws_access_key_id and aws_secret_access_key, so this is only used for Cloudflare.
    required: false
    type: string
  - name: aws_hosted_zone_id
    description: Route53 hosted zone id. Leave empty to let cert-manager discover the zone.
    required: false
    type: string