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.
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.
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
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 templates/cert-issuers.yaml ClusterIssuer resources: Let's Encrypt production and staging via Cloudflare DNS-01.
{{- $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.
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.
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.
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.
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 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.