|
| 1 | +# LabDA: Wildcard-DNS + Cilium-LB + Vault-Zertifikate pro Cluster |
| 2 | + |
| 3 | +Wie ein Cluster in LabDA seine Services unter eigenen Namen erreichbar macht — eine LoadBalancer-IP, ein Wildcard-Record, Zertifikate aus Vault-PKI. |
| 4 | + |
| 5 | +**Am 2026-08-21 auf `seed-labda-1` (rke2, `10.100.136.52`) Ende zu Ende durchgemessen** und von einem Laptop ausserhalb des Cluster-Subnetzes per Namen aufgerufen. Vorlage fuer den naechsten Platform-Cluster. |
| 6 | + |
| 7 | +``` |
| 8 | +Cilium gibt eine LB-IP frei |
| 9 | + -> PowerDNS: *.<cluster>.4sthings.tiab.ssc.sva.de -> diese IP |
| 10 | + -> Service antwortet |
| 11 | + -> Zertifikat aus Vault-PKI |
| 12 | +``` |
| 13 | + |
| 14 | +## Die Landschaft, bevor es losgeht |
| 15 | + |
| 16 | +Es gibt in LabDA **zwei getrennte DNS-Welten**, und die Verwechslung kostet am meisten Zeit: |
| 17 | + |
| 18 | +| Zone | Autoritaet | fuer uns | |
| 19 | +|---|---|---| |
| 20 | +| `tiab.labda.sva.de`, `labda.sva.de` | Infoblox (`infoblox01.labwi`, `infoblox02.labda`, …) | Konzern-DNS, **nicht schreibbar**. Hier leben die Node-Namen. | |
| 21 | +| `4sthings.tiab.ssc.sva.de` | **PowerDNS `10.100.136.115`** (`pdns-vsphere.tiab.labda.sva.de`), API auf `:8443` | unsere Zone. Hier entstehen die Wildcards. | |
| 22 | + |
| 23 | +Der PowerDNS antwortet auf `tiab.labda.sva.de` mit **REFUSED** — wer dort Cluster-Namen sucht, fragt den falschen Server. Die Delegation von Infoblox auf ihn ist korrekt eingetragen (`4sthings.tiab.ssc.sva.de NS -> pdns-vsphere.tiab.labda.sva.de`). |
| 24 | + |
| 25 | +**Das Muster in der Zone ist ein Wildcard pro Cluster**, TTL 60, Typ A. Bestand am 2026-08-21: `*.app`/`*.mgmt`/`*.preapp`/`*.sthings-infra-dev` -> `.220` (Altbestand, antwortet nicht mehr), `*.test2` -> `.222`, `*.dev7` -> `.223`, `*.dev8` -> `.226`, `*.dev11` -> `.228`, `*.seed-labda-1` -> `.221`. |
| 26 | + |
| 27 | +**IP-Bereich:** `10.100.136.220–229` ist fuer uns reserviert und im DNS ausgenommen. Es gibt in LabDA (Stand 2026-08-21) **kein clusterbook**, die IP wird also von Hand vergeben — freie pruefen, indem man die Wildcards der Zone auflistet (siehe Schritt 3). |
| 28 | + |
| 29 | +Der API-Key des PowerDNS steht in Vault unter `apps/powerdns`, nicht in diesem Dokument. |
| 30 | + |
| 31 | +--- |
| 32 | + |
| 33 | +## Schritt 1 — Vault: PKI-Rolle und Recht darauf |
| 34 | + |
| 35 | +Der von der Platform emittierte Issuer (`vault-pki-k8s`) signiert `pki/sign/tiab.labda.sva.de` — die **falsche Domain** fuer diese Zone. `pkiRole` im Platform-XRD ist ein einzelner String und deckt nur einen Issuer ab, es braucht also einen zweiten. |
| 36 | + |
| 37 | +Auf dem **LabDA-Vault** (`vault-vsphere.tiab.labda.sva.de:8200`) existiert die Rolle `4sthings.tiab.ssc.sva.de` bereits. Was fehlt, ist das Recht der cert-manager-Rolle darauf. |
| 38 | + |
| 39 | +**1a. Policy anlegen** (Token mit Schreibrecht auf `sys/policies/acl/*`): |
| 40 | + |
| 41 | +```hcl |
| 42 | +# vault policy write pki-issue-4sthings - |
| 43 | +path "pki/sign/4sthings.tiab.ssc.sva.de" { |
| 44 | + capabilities = ["create", "update"] |
| 45 | +} |
| 46 | +``` |
| 47 | + |
| 48 | +Nur `sign`, **nicht `issue`**: cert-manager benutzt ausschliesslich den sign-Endpunkt; `issue` wuerde zusaetzlich erlauben, sich den privaten Schluessel von Vault erzeugen zu lassen. |
| 49 | + |
| 50 | +Eine eigene Policy statt `pki-issue` zu erweitern, weil `pki-issue` der XRD-Default fuer **jeden** Cluster ist — sie zu erweitern gaebe den Signierpfad an alles frei, was sie traegt. |
| 51 | + |
| 52 | +**1b. Der Rolle zuordnen**, im ClusterStack (`stuttgart-things`, `crossplane/xrs/clusterstack/labda/<cluster>.yaml`): |
| 53 | + |
| 54 | +```yaml |
| 55 | +spec: |
| 56 | + platform: |
| 57 | + vaultIssuer: |
| 58 | + pkiRole: tiab.labda.sva.de |
| 59 | + tokenPolicies: |
| 60 | + - pki-issue |
| 61 | + - pki-issue-4sthings |
| 62 | +``` |
| 63 | +
|
| 64 | +`tokenPolicies`, nicht `policies` — letzteres verlangt einen AppRole, der Policies schreiben darf. Nach dem Anwenden reconciled der vault-auth-Workspace und schreibt die Policies an die Kubernetes-Auth-Rolle. Kontrolle: |
| 65 | + |
| 66 | +```bash |
| 67 | +kubectl -n crossplane-system get workspace <cluster>-certmanager-vault-auth \ |
| 68 | + -o json | jq -r '.spec.forProvider.vars[]|select(.key=="token_policies")|.value' |
| 69 | +``` |
| 70 | + |
| 71 | +## Schritt 2 — ClusterIssuer fuer die Zone |
| 72 | + |
| 73 | +Gleiche Anmeldung wie der vorhandene Issuer, nur ein anderer Pfad: |
| 74 | + |
| 75 | +```yaml |
| 76 | +apiVersion: cert-manager.io/v1 |
| 77 | +kind: ClusterIssuer |
| 78 | +metadata: |
| 79 | + name: vault-pki-4sthings |
| 80 | +spec: |
| 81 | + vault: |
| 82 | + server: https://vault-vsphere.tiab.labda.sva.de:8200 |
| 83 | + path: pki/sign/4sthings.tiab.ssc.sva.de |
| 84 | + caBundleSecretRef: |
| 85 | + key: ca.crt |
| 86 | + name: vault-pki-ca # legt der vaultIssuer-Zweig der Platform an |
| 87 | + auth: |
| 88 | + kubernetes: |
| 89 | + mountPath: /v1/auth/<cluster>-certmanager |
| 90 | + role: certmanager |
| 91 | + serviceAccountRef: |
| 92 | + name: cert-manager |
| 93 | +``` |
| 94 | + |
| 95 | +> **`Ready: True` heisst hier NICHT, dass er signieren darf.** cert-manager prueft beim Verify nur die Anmeldung. Der Beweis ist das erste ausgestellte Zertifikat. |
| 96 | + |
| 97 | +## Schritt 3 — Cilium: LoadBalancer-IP |
| 98 | + |
| 99 | +Voraussetzung ist `enable-l2-announcements=true` in der cilium-Config (auf den machinery-Clustern gesetzt). **L2-Announcement traegt nur auf einem echten Netzsegment** — auf kind waere die IP von aussen tot. |
| 100 | + |
| 101 | +Freie IP finden: |
| 102 | + |
| 103 | +```bash |
| 104 | +curl -sSk -H "X-API-Key: $PDNS_KEY" \ |
| 105 | + https://pdns-vsphere.tiab.labda.sva.de:8443/api/v1/servers/localhost/zones/4sthings.tiab.ssc.sva.de. \ |
| 106 | + | jq -r '.rrsets[]|select(.name|startswith("*"))|"\(.name) -> \(.records[0].content)"' |
| 107 | +``` |
| 108 | + |
| 109 | +**Achtung auf die API-Versionen** — bei Cilium 1.19.3 sind sie gemischt: |
| 110 | + |
| 111 | +```yaml |
| 112 | +apiVersion: cilium.io/v2 # Pool: schon v2 |
| 113 | +kind: CiliumLoadBalancerIPPool |
| 114 | +metadata: |
| 115 | + name: <cluster>-lb |
| 116 | +spec: |
| 117 | + blocks: |
| 118 | + - start: 10.100.136.221 |
| 119 | + stop: 10.100.136.221 |
| 120 | +--- |
| 121 | +apiVersion: cilium.io/v2alpha1 # L2-Policy: noch v2alpha1 |
| 122 | +kind: CiliumL2AnnouncementPolicy |
| 123 | +metadata: |
| 124 | + name: <cluster>-l2 |
| 125 | +spec: |
| 126 | + loadBalancerIPs: true |
| 127 | + nodeSelector: |
| 128 | + matchLabels: |
| 129 | + kubernetes.io/os: linux |
| 130 | +``` |
| 131 | + |
| 132 | +Kontrolle: `kubectl get ciliumloadbalancerippool` muss `IPS AVAILABLE 1` und `CONFLICTING False` zeigen. |
| 133 | + |
| 134 | +## Schritt 4 — Wildcard im PowerDNS |
| 135 | + |
| 136 | +```bash |
| 137 | +Z=4sthings.tiab.ssc.sva.de. |
| 138 | +curl -sSk -H "X-API-Key: $PDNS_KEY" -X PATCH \ |
| 139 | + -d '{"rrsets":[{"name":"*.<cluster>.'"$Z"'","type":"A","ttl":60, |
| 140 | + "changetype":"REPLACE","records":[{"content":"10.100.136.221","disabled":false}]}]}' \ |
| 141 | + "https://pdns-vsphere.tiab.labda.sva.de:8443/api/v1/servers/localhost/zones/$Z" |
| 142 | +``` |
| 143 | + |
| 144 | +HTTP **204** heisst angenommen. Kontrolle direkt am autoritativen Server, nicht ueber den eigenen Resolver: |
| 145 | + |
| 146 | +```bash |
| 147 | +dig +short @10.100.136.115 beliebig.<cluster>.4sthings.tiab.ssc.sva.de |
| 148 | +``` |
| 149 | + |
| 150 | +## Schritt 5 — Service mit Zertifikat |
| 151 | + |
| 152 | +**Ein Zertifikat pro Service**, nicht eines pro Cluster — siehe „Bekannte Grenzen". |
| 153 | + |
| 154 | +```yaml |
| 155 | +apiVersion: cert-manager.io/v1 |
| 156 | +kind: Certificate |
| 157 | +metadata: {name: test-svc, namespace: default} |
| 158 | +spec: |
| 159 | + secretName: test-svc-tls |
| 160 | + issuerRef: {name: vault-pki-4sthings, kind: ClusterIssuer} |
| 161 | + commonName: test.<cluster>.4sthings.tiab.ssc.sva.de |
| 162 | + dnsNames: ["test.<cluster>.4sthings.tiab.ssc.sva.de"] |
| 163 | +--- |
| 164 | +apiVersion: v1 |
| 165 | +kind: Service |
| 166 | +metadata: {name: test-svc, namespace: default} |
| 167 | +spec: |
| 168 | + type: LoadBalancer |
| 169 | + loadBalancerIP: 10.100.136.221 |
| 170 | + selector: {app: test-svc} |
| 171 | + ports: [{port: 443, targetPort: 443}] |
| 172 | +``` |
| 173 | + |
| 174 | +Ausstellung dauert ~20 s. Der Rest ist ein beliebiger Pod, der TLS mit `test-svc-tls` terminiert. |
| 175 | + |
| 176 | +## Verifikation |
| 177 | + |
| 178 | +Die Reihenfolge ist bewusst so — jeder Schritt schliesst eine Fehlerursache aus: |
| 179 | + |
| 180 | +```bash |
| 181 | +# 1. autoritativ: gibt es den Namen? |
| 182 | +dig +short @10.100.136.115 test.<cluster>.4sthings.tiab.ssc.sva.de |
| 183 | +
|
| 184 | +# 2. LB, L2 und Zertifikat, am DNS vorbei |
| 185 | +curl --cacert vault-ca.pem \ |
| 186 | + --resolve test.<cluster>.4sthings.tiab.ssc.sva.de:443:10.100.136.221 \ |
| 187 | + https://test.<cluster>.4sthings.tiab.ssc.sva.de/ |
| 188 | +
|
| 189 | +# 3. mit echter Aufloesung, vom Arbeitsplatz |
| 190 | +curl -k https://test.<cluster>.4sthings.tiab.ssc.sva.de/ |
| 191 | +``` |
| 192 | + |
| 193 | +Schritt 2 **mit `--cacert` gegen die Vault-CA**, nicht mit `-k`: sonst prueft man nur, dass irgendein TLS steht. |
| 194 | + |
| 195 | +--- |
| 196 | + |
| 197 | +## Fallen, die uns Zeit gekostet haben |
| 198 | + |
| 199 | +**Vaults `403 permission denied` bedeutet drei verschiedene Dinge:** verboten, Pfad existiert nicht, oder die PKI-Rolle lehnt die Anfrage ab (z. B. ein Wildcard bei `allow_glob_domains: false`). Der Fehler unterscheidet das nicht — er verraet absichtlich nicht, ob ein Pfad existiert. Nur ein Blick in `pki/roles/<name>` trennt es auf. |
| 200 | + |
| 201 | +**Ein `Ready`-Status ist kein Koennen.** Der ClusterIssuer meldete `VaultVerified`, obwohl jede Signieranfrage mit 403 scheiterte. |
| 202 | + |
| 203 | +**macOS: `dig` und `curl` loesen unterschiedlich auf.** `dig` fragt die Nameserver der Reihe nach, der System-Resolver (und damit `curl` und Browser) fragt sie **parallel und nimmt die schnellste Antwort**. Ein Heimrouter, der in 7 ms NXDOMAIN sagt, schlaegt den Firmen-Resolver mit 20 ms. Abhilfe pro Arbeitsplatz: |
| 204 | + |
| 205 | +```bash |
| 206 | +sudo tee /etc/resolver/4sthings.tiab.ssc.sva.de >/dev/null <<'EOF' |
| 207 | +nameserver 10.100.136.115 |
| 208 | +EOF |
| 209 | +sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder |
| 210 | +``` |
| 211 | + |
| 212 | +Dabei den **PowerDNS selbst** eintragen, nicht den Firmen-Resolver: bei Split-Tunneling wird `10.100.0.0/16` geroutet, `10.10.0.0/16` aber nicht — und `scutil`s `reach: Reachable` ist da optimistisch. Dass der PowerDNS nicht rekursiv ist, stoert nicht, die Datei leitet ihm nur seine eigene Zone zu. |
| 213 | + |
| 214 | +**Negative Antworten haelt die Zone 60 Minuten** (SOA-Minimum 3600 s), waehrend die Records selbst TTL 60 haben. Ein NXDOMAIN auf einen frisch angelegten Namen ist trotzdem zuerst ein *Resolver*-Verdacht, kein Cache-Verdacht. |
| 215 | + |
| 216 | +## Bekannte Grenzen |
| 217 | + |
| 218 | +**Keine Wildcard-Zertifikate.** Die PKI-Rolle `4sthings.tiab.ssc.sva.de` hat `allow_glob_domains: false` (`allowed_domains: 4sthings.tiab.ssc.sva.de`, `allow_subdomains: true`, `allow_bare_domains: false`). Ein `*.<cluster>.…` wird mit 403 abgelehnt, ein normaler Name darunter in ~20 s ausgestellt. Der Wildcard im **DNS** ist davon unberuehrt. Wer ein Wildcard-**Zertifikat** will, muss die Rolle im Vault aendern — dann traegt allerdings ein einziger kompromittierter Pod den Schluessel fuer alle Namen des Clusters. |
| 219 | + |
| 220 | +**In-cluster loest die Zone nicht auf.** CoreDNS forwardet `.` an die `/etc/resolv.conf` des Nodes, dort steht `10.100.101.5` — und dieser Resolver erreicht `10.100.136.115:53` nicht (er beantwortet die Elternzonen und die NS-Abfrage, laeuft aber beim Folgen der Delegation in den Timeout). Von aussen ist das egal; fuer Service-zu-Service ueber diese Namen braucht es einen CoreDNS-Stub auf `10.100.136.115`, den ein Pod nachweislich direkt erreicht. |
| 221 | + |
| 222 | +**Der PowerDNS selbst ist alt.** 4.5.2, Uptime 40,8 Monate, Pflicht-Sicherheitsadvisory (2022-01) fuellt 99 % des Log-Rings, kein DNSSEC, kein NS-Record im Apex der Zone (die Delegation traegt der Elternteil). RFC2136-Updates werden abgelehnt — fuer statische Wildcards irrelevant. |
| 223 | + |
| 224 | +**Fuer ein ganzes Team gehoert die Zone ins Split-DNS des VPN.** Sonst braucht jeder Arbeitsplatz die `/etc/resolver`-Datei, und wer sie vergisst, haelt den Dienst fuer kaputt. |
0 commit comments