Cert-manager

Issues and renews the wildcard TLS certificate using Cloudflare DNS-01

cert-manager issues and renews every certificate in the cluster. The wildcard certificate for opi5cluster.co.uk is obtained with a Cloudflare DNS-01 challenge, stored as a secret, and consumed by the Istio gateway. Nothing needs inbound reachability to prove domain control.

ArgoCD configuration

Excerpt from argocd-apps/values.yaml in the argocd-apps chart, with annotations added for this site:

cert-manager:
  # true: standard app-of-apps entry
  application: true
  project: k3s-services
  # false: certificate issuer changes affect every TLS secret in the cluster
  autoSync: false

Chart values

The full cert-manager/values.yaml from the service's own chart:

namespace: cert-manager

ClusterIssuer:
  opi5-cluster-lets-encrypt-issuer:
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeyName: opi5-cluster-lets-encrypt-secret
  opi5-cluster-lets-encrypt-issuer-stag:
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    privateKeyName: opi5-cluster-lets-encrypt-secret-stag

email: admin@opi5cluster.co.uk
apiTokenSecretRefName: cloudflare-api-token-secret
apiTokenSecretRefKey: api-token

cert-manager:
  dns01RecursiveNameserversOnly: true
  dns01RecursiveNameservers: amir.ns.cloudflare.com:53,clara.ns.cloudflare.com:53
  installCRDs: true

Manifests & templates

templates/cert-issuers.yaml

ClusterIssuer resources: Let's Encrypt production and staging via Cloudflare DNS-01.

Show manifest
{{- $email := .Values.email -}}
{{- $apiTokenSecretRefName  := .Values.apiTokenSecretRefName -}}
{{- $apiTokenSecretRefKey  := .Values.apiTokenSecretRefKey -}}
{{- range $item, $property  := .Values.ClusterIssuer }}
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: {{ $item }}
spec:
  acme:
    server: {{ $property.server  }}
    privateKeySecretRef:
      name: {{ $property.privateKeyName  }}
    solvers:
    - dns01:
        cloudflare:
          email: {{ $email }}
          apiTokenSecretRef:
            name:  {{ $apiTokenSecretRefName }}
            key:  {{ $apiTokenSecretRefKey }}
---
{{- end -}}
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: opi5-cluster-self-signed-issuer
spec:
  selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: opi5-cluster-ca-issuer
spec:
  ca:
    secretName: root-secret

templates/cloudflare-external-secret.yaml

ExternalSecret for the Cloudflare API token the DNS-01 challenge uses.

Show manifest
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: cloudflare-external-secret
  namespace: {{ .Values.namespace }}
spec:
  refreshInterval: 24h
  secretStoreRef:
    kind: ClusterSecretStore
    name: vault-cluster-secret-store
  target:
    name: {{ .Values.apiTokenSecretRefName }}
    creationPolicy: Owner
    template:
      engineVersion: v2
      data:
         {{ .Values.apiTokenSecretRefKey }}: "{{ `{{ .api_token }}` }}"
  data:
  - secretKey: api_token
    remoteRef:
      key: cloudflare
      property: api_token

templates/opi5cluster-co-uk-domain-certificate.yaml

Certificate resource for the opi5cluster.co.uk wildcard.

Show manifest
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: opi5cluster-co-uk-domain-certificate
  namespace:  {{ .Values.namespace }}
spec:
  secretTemplate:
    annotations:
      reflector.v1.k8s.emberstack.com/reflection-allowed: "true"
      reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces: "istio,media-systems"
      reflector.v1.k8s.emberstack.com/reflection-auto-enabled: "true"
      reflector.v1.k8s.emberstack.com/reflection-auto-namespaces: "istio,media-systems"
  secretName: opi5cluster-co-uk-domain-secret
  privateKey:
    rotationPolicy: Always
  duration: 26298h # 3 years (36 months)
  renewBefore: 24837h # (34 months)
  issuerRef:
    name: opi5-cluster-lets-encrypt-issuer
    kind: ClusterIssuer
    group: cert-manager.io
  dnsNames:
  - "*.opi5cluster.co.uk"

templates/rustfs-subdomain-certificate.yaml

Certificate for the RustFS subdomain.

Show manifest
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: rustfs-subdomain-certificate
  namespace:  {{ .Values.namespace }}
spec:
  secretTemplate:
    annotations:
      reflector.v1.k8s.emberstack.com/reflection-allowed: "true"
      reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces: "istio"
      reflector.v1.k8s.emberstack.com/reflection-auto-enabled: "true"
      reflector.v1.k8s.emberstack.com/reflection-auto-namespaces: "istio"
  secretName: rustfs-subdomain-secret
  privateKey:
    rotationPolicy: Always
  duration: 26298h # 3 years (36 months)
  renewBefore: 24837h # (34 months)
  issuerRef:
    name: opi5-cluster-lets-encrypt-issuer
    kind: ClusterIssuer
    group: cert-manager.io
  dnsNames:
  - "*.rustfs.opi5cluster.co.uk"

templates/self-sign-cert.yaml

Self-signed certificate for internal-only endpoints.

Show manifest
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: opi5-cluster-selfsigned-ca
  namespace: {{ .Values.namespace }}
spec:
  isCA: true
  commonName: opi5-cluster-selfsigned-ca
  secretName: root-secret
  privateKey:
    algorithm: ECDSA
    size: 256
  issuerRef:
    name: opi5-cluster-self-signed-issuer
    kind: ClusterIssuer
    group: cert-manager.io

Trade-offs

Decision. Issue one wildcard certificate via DNS-01

Alternative. Per-host HTTP-01 certificates

Why. A single certificate covers every subdomain, including internal-only hosts that could never answer an HTTP-01 challenge.

Decision. Use a zone-scoped Cloudflare API token from Vault

Alternative. The account-wide Global API key

Why. Least privilege: the token can only edit DNS records in one zone, and it is rotated from Vault like every other secret.

← Back to Platform & Infrastructure · All service groups