From d6969ca390ede22c4538f6249a5dbbbacaaca02f Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Sun, 2 Aug 2026 17:27:27 +1000 Subject: [PATCH] Add bind-external namespace for externally-reachable zones We self-delegate _acme-challenge.unkin.net into an acme.unkin.net zone we serve, so cert-manager can solve Let's Encrypt DNS-01 over RFC2136/TSIG. That needs a publicly-reachable authoritative BIND, separate from the internal estate. - Add app bind-external (base + au-syd1 overlay) and register it in the platform ApplicationSet and AppProject destinations. - Add BindCluster bind-external: authoritative-only, recursion off, no forwarding, transfers denied except the keyed catalog/zone AXFR; 2 replicas; primaryService is a dmz-pinned PureLB LoadBalancer (198.18.199.53). - Add BindZone acme.unkin.net (primary, dynamicUpdate) and BindTSIGKey certmanager (hmac-sha256), whose Secret reflects into the cert-manager namespace for the rfc2136 solver. Claude-Session: https://claude.ai/code/session_01JUoARVdmhxKQHyyyp1pxeT --- apps/base/bind-external/cluster.yaml | 52 +++++++++++++++++++ apps/base/bind-external/kustomization.yaml | 9 ++++ apps/base/bind-external/namespace.yaml | 5 ++ apps/base/bind-external/tsigkey.yaml | 20 +++++++ apps/base/bind-external/zones.yaml | 19 +++++++ .../au-syd1/bind-external/kustomization.yaml | 6 +++ argocd/applicationsets/platform.yaml | 1 + argocd/projects/platform.yaml | 2 + 8 files changed, 114 insertions(+) create mode 100644 apps/base/bind-external/cluster.yaml create mode 100644 apps/base/bind-external/kustomization.yaml create mode 100644 apps/base/bind-external/namespace.yaml create mode 100644 apps/base/bind-external/tsigkey.yaml create mode 100644 apps/base/bind-external/zones.yaml create mode 100644 apps/overlays/au-syd1/bind-external/kustomization.yaml diff --git a/apps/base/bind-external/cluster.yaml b/apps/base/bind-external/cluster.yaml new file mode 100644 index 0000000..18d822a --- /dev/null +++ b/apps/base/bind-external/cluster.yaml @@ -0,0 +1,52 @@ +--- +# Externally-reachable authoritative BIND for zones we delegate to ourselves. +# First tenant: acme.unkin.net, the DNS-01 challenge zone Let's Encrypt validates +# via a one-time _acme-challenge.unkin.net CNAME. Authoritative-only, recursion +# off, no forwarding, no open transfers -- the primaryService is the single +# dmz-pinned LoadBalancer that public NAT targets and that cert-manager writes to. +apiVersion: bind.unkin.net/v1alpha1 +kind: BindCluster +metadata: + name: bind-external + namespace: bind-external +spec: + mode: authoritative + recursion: false + replicas: 2 + storageClassName: cephrbd-fast-delete + storageSize: 1Gi + # Public server: answer queries from anywhere (Let's Encrypt validates over the + # internet), deny recursion and open zone transfers. localhost + pod net are + # implied by "any" and cover in-pod nsupdate and secondary SOA refresh; per-zone + # allow-transfer (catalog + acme zone) still permits key-authenticated AXFR. + extraOptions: + - "allow-query { any; }" + - "allow-transfer { none; }" + service: + type: ClusterIP + primaryService: + type: LoadBalancer + externalTrafficPolicy: Local + annotations: + purelb.io/service-group: dmz + purelb.io/addresses: 198.18.199.53 + external-dns.alpha.kubernetes.io/hostname: bind-external-primary.k8s.syd1.au.unkin.net + resources: + requests: + cpu: 20m + memory: 128Mi + limits: + cpu: "1" + memory: 512Mi +--- +# Catalog zone so the acme zone replicates onto the secondary (AXFR/IXFR keyed +# with the certmanager TSIG key, reused here as the transfer key). +apiVersion: bind.unkin.net/v1alpha1 +kind: BindCatalogZone +metadata: + name: bind-external-catalog + namespace: bind-external +spec: + clusterRef: bind-external + zoneName: catalog.external + transferKeyRef: certmanager diff --git a/apps/base/bind-external/kustomization.yaml b/apps/base/bind-external/kustomization.yaml new file mode 100644 index 0000000..de5a685 --- /dev/null +++ b/apps/base/bind-external/kustomization.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - namespace.yaml + - cluster.yaml + - tsigkey.yaml + - zones.yaml diff --git a/apps/base/bind-external/namespace.yaml b/apps/base/bind-external/namespace.yaml new file mode 100644 index 0000000..51863e3 --- /dev/null +++ b/apps/base/bind-external/namespace.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: bind-external diff --git a/apps/base/bind-external/tsigkey.yaml b/apps/base/bind-external/tsigkey.yaml new file mode 100644 index 0000000..6517ad5 --- /dev/null +++ b/apps/base/bind-external/tsigkey.yaml @@ -0,0 +1,20 @@ +--- +# TSIG key cert-manager uses to send RFC2136 dynamic updates (the DNS-01 TXT +# records) to the primary, and that the secondary reuses for AXFR. The operator +# generates the material into Secret certmanager-tsig in this namespace; +# secretTemplate stamps emberstack reflector hints so the Secret is mirrored into +# the cert-manager namespace, where the rfc2136 solver reads its "secret" key. +apiVersion: bind.unkin.net/v1alpha1 +kind: BindTSIGKey +metadata: + name: certmanager + namespace: bind-external +spec: + clusterRef: bind-external + algorithm: hmac-sha256 + secretTemplate: + annotations: + reflector.v1.k8s.emberstack.com/reflection-allowed: "true" + reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces: "cert-manager" + reflector.v1.k8s.emberstack.com/reflection-auto-enabled: "true" + reflector.v1.k8s.emberstack.com/reflection-auto-namespaces: "cert-manager" diff --git a/apps/base/bind-external/zones.yaml b/apps/base/bind-external/zones.yaml new file mode 100644 index 0000000..05ece86 --- /dev/null +++ b/apps/base/bind-external/zones.yaml @@ -0,0 +1,19 @@ +--- +# Self-delegated ACME challenge zone. Google Cloud DNS holds a one-time +# _acme-challenge.unkin.net CNAME -> _acme-challenge.acme.unkin.net and an +# acme.unkin.net NS delegation pointing here; cert-manager writes the challenge +# TXT records via RFC2136 authenticated with the certmanager key. +apiVersion: bind.unkin.net/v1alpha1 +kind: BindZone +metadata: + name: acme-unkin-net + namespace: bind-external +spec: + clusterRef: bind-external + zoneName: acme.unkin.net + type: primary + defaultTTL: 60 + dynamicUpdate: true + updateKeyRef: certmanager + allowTransfer: + - key certmanager diff --git a/apps/overlays/au-syd1/bind-external/kustomization.yaml b/apps/overlays/au-syd1/bind-external/kustomization.yaml new file mode 100644 index 0000000..007dc53 --- /dev/null +++ b/apps/overlays/au-syd1/bind-external/kustomization.yaml @@ -0,0 +1,6 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - ../../../base/bind-external diff --git a/argocd/applicationsets/platform.yaml b/argocd/applicationsets/platform.yaml index 9b5a9db..2f7f87a 100644 --- a/argocd/applicationsets/platform.yaml +++ b/argocd/applicationsets/platform.yaml @@ -14,6 +14,7 @@ spec: - path: apps/overlays/*/artifactapi - path: apps/overlays/*/bind-system - path: apps/overlays/*/bind-internal + - path: apps/overlays/*/bind-external - path: apps/overlays/*/age-api - path: apps/overlays/*/cattle-system - path: apps/overlays/*/cert-manager diff --git a/argocd/projects/platform.yaml b/argocd/projects/platform.yaml index 1eabece..29ad278 100644 --- a/argocd/projects/platform.yaml +++ b/argocd/projects/platform.yaml @@ -25,6 +25,8 @@ spec: server: https://kubernetes.default.svc - namespace: 'bind-internal' server: https://kubernetes.default.svc + - namespace: 'bind-external' + server: https://kubernetes.default.svc - namespace: 'cert-manager' server: https://kubernetes.default.svc - namespace: 'certificates'