# ============================================================================= # L2 BEHAVIORAL — split-horizon INTERNAL resolution (R-DNS-01 / R-DNS-03) [RUNNABLE] # Catalog: docs/14 "DNS/exposure (R-DNS-*)" # Spec: docs/09 "Split-horizon resolution (R-DNS-03)" — internal clients resolve # .${BASE_DOMAIN} to the INTERNAL Gateway VIP, not the public hairpin. # Impl: platform/coredns/coredns-custom.yaml (k3s CoreDNS `import *.server`; template plugin # answers the whole ${BASE_DOMAIN} zone with ${GATEWAY_VIP}). # # ENVIRONMENT-AGNOSTIC (R-ENV-02): this test asks the CLUSTER IT IS POINTED AT for its own # ${BASE_DOMAIN} and ${GATEWAY_VIP} — the same flux-system/cluster-vars ConfigMap Flux # substitutes from — via test-level chainsaw bindings (see `spec.bindings`). Nothing here is # per-environment and no flag is passed at run time, so the suite cannot be run against the # wrong expectation: dev asserts dev's zone/VIP, prod asserts prod's. # # GIVEN the split-horizon coredns-custom ConfigMap is reconciled into k3s CoreDNS, # WHEN an in-cluster pod resolves a platform hostname over its PUBLIC name # (keycloak.${BASE_DOMAIN}) using the default cluster resolver (CoreDNS 10.43.0.10), # THEN the answer is EXACTLY that cluster's ${GATEWAY_VIP} (the address coredns-custom is # templated with, and the `shared` Gateway's own status address) — NOT a public/off-cluster # IP. This is what lets pod->public-hostname traffic hairpin through the # Gateway with the correct SNI (*.${BASE_DOMAIN}) so the wildcard cert validates # (R-DNS-01 "services reachable at .${BASE_DOMAIN}"; R-DNS-03 split-horizon). # # Complements dns-exposure (R-DNS-02, the EXTERNAL/public-record side). Together they cover the # split-horizon pair: external answer = public record (R-DNS-02); internal answer = Gateway VIP # (R-DNS-01/03). # # CLEANUP: chainsaw deletes the per-test namespace (Job included) at teardown. Read-only otherwise. # chainsaw v0.2.12: Job + status assert; no test-level finally. # ============================================================================= apiVersion: chainsaw.kyverno.io/v1alpha1 kind: Test metadata: name: dns-internal-resolution labels: conformance.home.k3s/R-DNS-01: "true" conformance.home.k3s/R-DNS-03: "true" annotations: conformance.home.k3s/requirements: "R-DNS-01,R-DNS-03" conformance.home.k3s/layer: "L2" conformance.home.k3s/safety-critical: "false" conformance.home.k3s/executable: "true" conformance.home.k3s/intent: >- An in-cluster pod resolving keycloak.${BASE_DOMAIN} via cluster DNS gets that cluster's internal Gateway VIP, not the public hairpin — split-horizon in-cluster resolution. spec: timeouts: apply: 60s assert: 2m exec: 2m # -- R-ENV-02: the environment's identity is READ FROM THE CLUSTER UNDER TEST, never authored # here. flux-system/cluster-vars is the ConfigMap Flux itself substitutes from # (clusters/base/cluster-vars.yaml + the dev/prod overlay patch), so these bindings are the # same source of truth the platform is rendered from. Test-level => in scope for every step # (unlike step `outputs`, which are step-scoped). bindings: - name: baseDomain value: (x_k8s_get($client, 'v1', 'ConfigMap', 'flux-system', 'cluster-vars').data.BASE_DOMAIN) - name: gatewayVip value: (x_k8s_get($client, 'v1', 'ConfigMap', 'flux-system', 'cluster-vars').data.GATEWAY_VIP) steps: - name: then-cluster-dns-answers-with-the-internal-gateway-vip try: - apply: resource: apiVersion: batch/v1 kind: Job metadata: name: dns-internal-probe spec: backoffLimit: 2 activeDeadlineSeconds: 100 template: spec: restartPolicy: Never containers: - name: probe image: busybox:1.36.1 # The cluster's OWN identity, resolved from flux-system/cluster-vars by the # test-level bindings — never a literal (R-ENV-02). env: - name: BASE_DOMAIN value: ($baseDomain) - name: GATEWAY_VIP value: ($gatewayVip) command: - sh - -c - | set -u host="keycloak.${BASE_DOMAIN}" # Resolve via the DEFAULT cluster resolver (CoreDNS, /etc/resolv.conf -> # 10.43.0.10). Split-horizon must answer with THIS cluster's Gateway VIP # (the address coredns-custom is templated with), proving pods hairpin # through the Gateway. Exact match on ${GATEWAY_VIP}: stricter than the # old hardcoded 192.168.1.53-.60 range AND correct in both environments. out="$(nslookup "$host" 2>/dev/null)" echo "$out" ip="$(echo "$out" | awk '/^Address: /{print $2}' | grep -Fx "${GATEWAY_VIP}" | head -1)" if [ -n "$ip" ]; then echo "INTERNAL_RESOLUTION_OK $host -> $ip (in-cluster Gateway VIP)" exit 0 fi echo "FAIL: $host did not resolve to this cluster's Gateway VIP (${GATEWAY_VIP});" echo " split-horizon coredns-custom not in effect (got a public/off-cluster answer)." exit 1 - assert: resource: apiVersion: batch/v1 kind: Job metadata: name: dns-internal-probe status: succeeded: 1