diff --git a/kubernetes/infrastructure/openbao/rotation/netbird-ingress.yaml b/kubernetes/infrastructure/openbao/rotation/netbird-ingress.yaml index 640f0345..08d4e0ea 100644 --- a/kubernetes/infrastructure/openbao/rotation/netbird-ingress.yaml +++ b/kubernetes/infrastructure/openbao/rotation/netbird-ingress.yaml @@ -1,20 +1,18 @@ --- -# Class-B automated rotation of the NetBird ingress setup-key. +# Class-B rotation of the netbird-ingress setup key. Mints a fresh reusable +# setup key via the secret-rotator binary (netbird-key mode): +# POST /api/setup-keys, then patches OpenBao. Create-new, not roll-in-place — +# the old key is left to expire (ephemeral, 1yr), so nothing is deleted and +# there is no window: the old key stays valid while the ingress re-enrolls. # -# Unlike the Class-A jobs (which just generate a random value), this calls the -# self-hosted NetBird MANAGEMENT API to mint a fresh setup key, then writes it to -# OpenBao. openbao-ns pattern (intra-namespace OpenBao access; the public NetBird -# mgmt API is reachable via the openbao ns `world` egress). ESO syncs the -# netbird-ingress-setup-key Secret (ns ingress, 10m refresh); Reloader restarts -# the netbird-ingress DaemonSet so its peers re-enroll with the new key. Setup -# keys are disposable/reusable by design, so this is inherently low-risk. +# openbao-ns pattern; the NetBird management API (netbird.renner.uno, valid TLS) +# is reachable via the openbao ns `world` egress. ESO syncs the +# netbird-ingress-setup-key Secret (ns ingress, 10m); Reloader restarts the +# netbird-ingress DaemonSet, whose pods re-enroll on the new key. # -# Tested + enabled 2026-07-19. The minted key MUST match the ingress key's -# settings: ephemeral=true (peers auto-reaped when offline — these DaemonSet peers -# re-enroll fresh on every restart) and allow_extra_dns_labels=true (the `DNS` -# capability), in the k8s-ingress-router group. It KEEPS a bounded 1y expiry on -# purpose: an auto-rotated key SHOULD expire, so a silently-broken rotation -# eventually surfaces (new enrollments fail) instead of a key living forever. +# An auto-rotated key SHOULD expire (bounded blast radius if a rotation ever +# silently breaks). The rotator NEVER deletes keys — a past manual delete-filter +# bug wiped unrelated keys, so automated deletion is deliberately absent. apiVersion: v1 kind: ServiceAccount metadata: @@ -23,8 +21,6 @@ metadata: labels: app.kubernetes.io/name: netbird-ingress app.kubernetes.io/component: secret-rotator - # OpenBao: auth/kubernetes/role/rotator-netbird binds this SA to rotator-netbird - # (read bootstrap PAT + patch the setup-key path). --- apiVersion: batch/v1 kind: CronJob @@ -35,7 +31,7 @@ metadata: app.kubernetes.io/name: netbird-ingress app.kubernetes.io/component: secret-rotator spec: - suspend: false # tested + enabled 2026-07-19 + suspend: false schedule: "0 4 1 1,4,7,10 *" # quarterly, Europe/Berlin timeZone: Europe/Berlin concurrencyPolicy: Forbid @@ -63,55 +59,29 @@ spec: type: RuntimeDefault containers: - name: rotate - # Class-B jobs need an HTTP client + JSON. python:alpine provides both - # via stdlib (urllib/json) with NO runtime package install, so it runs - # cleanly under the non-root securityContext (apk/apt need root and fail - # here — the openbao ns enforces restricted PSA). - image: python:3.13-alpine + image: gitea.renner.uno/gitea_admin/secret-rotator@sha256:49bfddd8eeb5fd871cbeb05bc3b4e2fcaaab7d9e24cc3007caae6084b4634c18 + args: ["netbird-key"] securityContext: allowPrivilegeEscalation: false + readOnlyRootFilesystem: true capabilities: drop: ["ALL"] env: - name: BAO_ADDR value: http://openbao.openbao.svc.cluster.local:8200 + - name: BAO_ROLE + value: rotator-netbird + - name: BOOTSTRAP_PATH + value: homelab/infrastructure/secret-rotators-bootstrap/netbird + - name: BAO_PATH + value: homelab/infrastructure/netbird-ingress - name: NB_MANAGEMENT_URL - value: "https://netbird.renner.uno" - # NetBird group ID(s) for the new key — the k8s-ingress-router group. + value: https://netbird.renner.uno - name: NB_AUTO_GROUPS - value: "d8umic2oc9cs73d4e1f0" - command: ["python3", "-c"] - args: - - | - import json, os, urllib.request as u - bao = os.environ["BAO_ADDR"]; mgmt = os.environ["NB_MANAGEMENT_URL"] - groups = [g for g in os.environ["NB_AUTO_GROUPS"].split(",") if g] - jwt = open("/var/run/secrets/kubernetes.io/serviceaccount/token").read() - def call(method, url, headers=None, body=None, ct="application/json"): - data = json.dumps(body).encode() if body is not None else None - req = u.Request(url, data=data, method=method, headers=headers or {}) - if data is not None: - req.add_header("Content-Type", ct) - with u.urlopen(req, timeout=30) as r: - return json.loads(r.read() or b"{}") - # 1. OpenBao login (k8s auth) - tok = call("POST", bao + "/v1/auth/kubernetes/login", - body={"role": "rotator-netbird", "jwt": jwt})["auth"]["client_token"] - # 2. read the bootstrap NetBird PAT - pat = call("GET", bao + "/v1/homelab/data/infrastructure/secret-rotators-bootstrap/netbird", - {"X-Vault-Token": tok})["data"]["data"]["pat"] - # 3. mint a new reusable setup key via the NetBird mgmt API - key = call("POST", mgmt + "/api/setup-keys", {"Authorization": "Token " + pat}, - {"name": "ingress-rotated", "type": "reusable", "expires_in": 31536000, - "usage_limit": 0, "ephemeral": True, "allow_extra_dns_labels": True, - "auto_groups": groups})["key"] - # 4. patch the new key into OpenBao (KV v2 merge-patch) - call("PATCH", bao + "/v1/homelab/data/infrastructure/netbird-ingress", - {"X-Vault-Token": tok}, {"data": {"setup-key": key}}, "application/merge-patch+json") - print("rotated setup-key at homelab/infrastructure/netbird-ingress (value not logged)") + value: d8umic2oc9cs73d4e1f0 resources: requests: cpu: 25m - memory: 48Mi + memory: 16Mi limits: - memory: 96Mi + memory: 48Mi