Compare commits

...
Author SHA1 Message Date
claude-botandClaude Opus 5.5 aea4be734c feat(bird): put node0 in the pod routing table (#576 step 2)
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Pod-sourced traffic is routed from table 47. Once the node IP moves to
VLAN 46 (step 3), pods reach node addresses there: the kubernetes
ClusterIP (socket LB rewrites it to the apiserver's advertise address),
hostNetwork pod IPs, and replies to kubelet probes. Without the attached
node0 route in table 47 those flows take the default to the UDM and every
in-cluster API client breaks. Taking it from the interface, like the
other attached networks, keeps the table in one owner.

Refs #576

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 13:28:57 +02:00
k.renner 60361d4c8a Merge pull request 'chore(talos): drop the #832 stage labels' (#917) from chore/cni-drop-stage-labels into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#917
2026-09-23 10:46:35 +00:00
claude-botandClaude Opus 5.5 a8cef5de1b chore(talos): drop the #832 stage labels
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Nothing selects cni.renner.uno/cilium-uninstall or
cni.renner.uno/multus-sole-writer since stage B (#915, #916). Removed
live from all three nodes with a $patch: delete strategic merge patch
(--mode=no-reboot); Talos dropped them from the Kubernetes nodes and
left the other labels in place.

Closes #832

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 12:40:10 +02:00
k.renner 505802dad5 Merge pull request 'feat(cilium): write the conflist where only Multus reads it (stage B2 of #832)' (#916) from feat/cni-sole-writer-converge-b2 into master
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#916
2026-09-23 10:38:00 +00:00
claude-botandClaude Opus 5.5 ba66b604d4 feat(cilium): write the conflist where only Multus reads it (stage B2 of #832)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
cni.confPath: /etc/cni/net.d/multus mounts that host directory as the
agent's /host/etc/cni/net.d, so write-cni-conf-when-ready (unchanged,
.../05-cilium.conflist) lands in /etc/cni/net.d/multus/05-cilium.conflist,
the file the per-node CiliumNodeConfigs already produced on all three
nodes. The agent finds it unchanged and does not rewrite it.

updateStrategy maxUnavailable: 1 (chart default 2) so the roll takes one
node's datapath at a time.

bootstrap.sh renders the chart from these values, so a cold rebuild
lands directly in this layout.

Refs #832

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 11:55:12 +02:00
k.renner 4fbc49fc8c Merge pull request 'feat(cni): converge Multus as sole CNI writer, part 1 (stage B1 of #832)' (#915) from feat/cni-sole-writer-converge-b1 into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#915
2026-09-23 09:53:24 +00:00
claude-botandClaude Opus 5.5 f493c9fe52 feat(cni): converge Multus as sole CNI writer, part 1 (stage B1 of #832)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
All three nodes run the sole-writer layout. Converge the Multus side and
drop the per-node Cilium overrides:
- kube-multus-ds-subdir becomes the only Multus DaemonSet, no node
  affinity; kube-multus-ds (0 pods) and its ConfigMap are pruned.
- /etc/cni/net.d/multus is mounted DirectoryOrCreate so Multus waits for
  the Cilium conflist instead of exiting on a missing directory.
- CiliumNodeConfigs removed. They only act when a cilium pod is created;
  stage B2 moves the same path into the HelmRelease (cni.confPath) and
  rolls the agents.

Refs #832

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 11:43:48 +02:00
k.renner 8a3c66eb2e Merge pull request 'chore(talos): record #832 stage labels on k8s-1' (#914) from chore/cni-stage-k8s-1 into master
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#914
2026-09-23 09:31:07 +00:00
claude-botandClaude Opus 5.5 9928999da4 chore(talos): record #832 stage labels on k8s-1
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
k8s-1 carries cni.renner.uno/cilium-uninstall and
cni.renner.uno/multus-sole-writer, applied live with talosctl patch.
Verified on the node: /etc/cni/net.d holds only 00-multus.conf at the
top level, and after a reboot with immediate uncordon the OSD came up
with net1 (10.99.0.1) and no restarts. All three nodes are converted.

Refs #832

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 11:15:45 +02:00
k.renner b3def28f7c Merge pull request 'fix(whereabouts): let the reconciler respect cordons' (#913) from fix/whereabouts-reconciler-cordon into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#913
2026-09-23 09:04:29 +00:00
claude-botandClaude Opus 5.5 93dbcdb2fb fix(whereabouts): let the reconciler respect cordons
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
The Deployment tolerated every NoSchedule taint (operator: Exists, no
key), including node.kubernetes.io/unschedulable. During the #832 node
reboots its ReplicaSet kept placing replacement pods on the cordoned,
shutting-down node; the kubelet rejected each with NodeShutdown, about
200 failed pods per reboot. Tolerate only the control-plane taint the
comment names.

Refs #832

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 10:56:50 +02:00
k.renner 47bf16cfac Merge pull request 'chore(talos): record #832 stage labels on k8s-2' (#912) from chore/cni-stage-k8s-2 into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#912
2026-09-23 08:45:30 +00:00
claude-botandClaude Opus 5.5 4664de8f97 chore(talos): record #832 stage labels on k8s-2
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
k8s-2 carries cni.renner.uno/cilium-uninstall and
cni.renner.uno/multus-sole-writer, applied live with talosctl patch.
Verified on the node: /etc/cni/net.d holds only 00-multus.conf at the
top level, and after a reboot with immediate uncordon the OSD came up
with net1 (10.99.0.2) and no restarts.

Refs #832

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 10:44:17 +02:00
k.renner fb52e4be88 Merge pull request 'chore(talos): record #832 stage labels on k8s-3' (#911) from chore/cni-stage-k8s-3 into master
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#911
2026-09-23 08:28:40 +00:00
claude-botandClaude Opus 5.5 f11a71c33f chore(talos): record #832 stage labels on k8s-3
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
k8s-3 carries cni.renner.uno/cilium-uninstall and
cni.renner.uno/multus-sole-writer, applied live with talosctl patch.
Verified on the node: /etc/cni/net.d holds only 00-multus.conf at the
top level, Cilium writes multus/05-cilium.conflist, and after a reboot
with immediate uncordon the OSD came up with net1 (10.99.0.3) and no
restarts.

Refs #832

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 10:25:53 +02:00
k.renner 29326eead3 Merge pull request 'feat(cni): per-node switch for Multus as sole CNI writer (stage A of #832)' (#910) from feat/cni-staged-sole-writer into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#910
2026-09-23 07:52:22 +00:00
claude-botandClaude Opus 5.5 eb09b1b2a3 feat(cni): per-node switch for Multus as sole CNI writer (stage A of #832)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Nothing changes until a node is labelled:
- post-crds/cilium-nodeconfig.yaml: CiliumNodeConfig cni-uninstall
  (label cni.renner.uno/cilium-uninstall) arms the agent's preStop
  conflist cleanup; multus-sole-writer (label
  cni.renner.uno/multus-sole-writer) moves write-cni-conf-when-ready to
  /host/etc/cni/net.d/multus/05-cilium.conflist, a subdirectory libcni
  does not read.
- multus/multus-subdir.yaml: kube-multus-ds-subdir with
  multusAutoconfigDir /host/etc/cni/net.d/multus, scheduled only on
  labelled nodes; kube-multus-ds gets the inverse affinity.

The kube-multus-ds template change rolls Multus node by node once on
merge.

Refs #832

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 09:47:16 +02:00
k.renner dd3a1f1e90 Merge pull request 'feat(netbird-service-sync): run the controller in its own namespace (stage 2 of #876)' (#909) from feat/netbird-service-sync-phase2 into master
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#909
2026-09-23 06:55:25 +00:00
claude-botandClaude Opus 5.5 98a0462137 feat(netbird-service-sync): run the controller in its own namespace (stage 2 of #876)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
- kubernetes/infrastructure/netbird-proxy-services -> netbird-service-sync:
  SA, ClusterRole(Binding) and Deployment renamed, in namespace
  netbird-service-sync, image gitea_admin/netbird-service-sync pinned to
  the master build digest, BAO_ROLE=netbird-service-sync.
- Old policy doc netbird-proxy-services.hcl removed; Traefik comment
  follows the name.

The infrastructure KS owns both old and new objects, so prune and apply
happen in one pass. OpenBao policy and role netbird-service-sync are
already written.

Closes #876

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 08:41:20 +02:00
k.renner 9aef2e0669 Merge pull request 'feat(netbird-service-sync): prepare rename and own namespace (stage 1 of #876)' (#908) from feat/netbird-service-sync-phase1 into master
ci/woodpecker/push/build-ci-images Pipeline was successful
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#908
2026-09-23 06:35:14 +00:00
claude-botandClaude Opus 5.5 fd7597ddc2 chore(netbird-service-sync): drop stray __pycache__
ci/woodpecker/push/build-ci-images Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 08:30:28 +02:00
claude-botandClaude Opus 5.5 10b0c1b853 feat(netbird-service-sync): prepare rename and own namespace (stage 1 of #876)
ci/woodpecker/push/build-ci-images Pipeline was canceled
Additive; the running controller is untouched:
- ci/images/netbird-proxy-services -> ci/images/netbird-service-sync,
  build step and image name renamed, so the merge builds
  gitea_admin/netbird-service-sync.
- Namespace netbird-service-sync with a baseline policy: probes in;
  out only to kube-dns, kube-apiserver, OpenBao :8200 and world :443
  (NetBird management API).
- openbao-ns admits netbird-service-sync on :8200, like wiki-index.
- policies/netbird-service-sync.hcl documents the new policy and role
  (applied by hand, as before).

Refs #876

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 08:30:15 +02:00
k.renner 674169cb08 Merge pull request 'chore(gitea): drop the apps-namespace grants for Gitea (stage 3 of #878)' (#907) from chore/gitea-namespace-phase3 into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#907
2026-09-23 06:20:17 +00:00
claude-botandClaude Opus 5.5 0c6174b7a6 chore(gitea): drop the apps-namespace grants for Gitea (stage 3 of #878)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Gitea runs in the gitea namespace since #906. Remove what only served it
in apps:
- openbao rotator egress (flux-git, review-bot): apps/gitea target.
- ai-ns wiki-index ingress: apps/gitea source.
- apps default-deny egress to wiki-index :8080 (gitea's webhook; now in
  gitea-ns.yaml).
- Velero schedule comments.

Refs #878

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 08:09:30 +02:00
k.renner 76888c3771 Merge pull request 'feat(gitea): move Gitea into the gitea namespace (stage 2 of #878)' (#906) from feat/gitea-namespace-phase2 into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#906
2026-09-23 06:05:33 +00:00
20 changed files with 171 additions and 104 deletions
+7 -7
View File
@@ -350,17 +350,17 @@ steps:
--destination gitea.renner.uno/gitea_admin/heating-can:$TAG \
--destination gitea.renner.uno/gitea_admin/heating-can:latest
# netbird-proxy-services: the controller that reconciles NetBird reverse-proxy
# netbird-service-sync: the controller that reconciles NetBird reverse-proxy
# services (public Ingress hosts -> TLS passthrough -> Traefik ClusterIP).
# Runtime artifact like heating-can; same registry so the audit tracks it.
- name: build-netbird-proxy-services
- name: build-netbird-service-sync
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.18-debug
directory: ci/images/netbird-proxy-services
directory: ci/images/netbird-service-sync
depends_on: []
when:
- event: deployment
- event: push
path: ["ci/images/netbird-proxy-services/**", ".woodpecker/build-ci-images.yml"]
path: ["ci/images/netbird-service-sync/**", ".woodpecker/build-ci-images.yml"]
environment:
GITEA_USER: gitea_admin
GITEA_TOKEN:
@@ -374,11 +374,11 @@ steps:
/kaniko/executor \
--context . \
--dockerfile Containerfile \
--destination gitea.renner.uno/gitea_admin/netbird-proxy-services:$TAG \
--destination gitea.renner.uno/gitea_admin/netbird-proxy-services:latest
--destination gitea.renner.uno/gitea_admin/netbird-service-sync:$TAG \
--destination gitea.renner.uno/gitea_admin/netbird-service-sync:latest
# wiki-index: embedding index over the Notes repo's wiki/, fed from Gitea and
# queried by ~/Notes/bin/wiki_rank.py. Runtime artifact like netbird-proxy-services.
# queried by ~/Notes/bin/wiki_rank.py. Runtime artifact like netbird-service-sync.
- name: build-wiki-index
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.18-debug
directory: ci/images/wiki-index
@@ -1,4 +1,4 @@
# netbird-proxy-services: reconciles NetBird reverse-proxy services (public
# netbird-service-sync: reconciles NetBird reverse-proxy services (public
# domains -> TLS passthrough -> Traefik ClusterIP) from Ingresses carrying the
# public label. Standard library only, so the image is the interpreter plus one
# file; runs unprivileged as its own user.
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""netbird-proxy-services: reconcile NetBird reverse-proxy services from Ingresses.
"""netbird-service-sync: reconcile NetBird reverse-proxy services from Ingresses.
Source of truth: every Ingress carrying the public label (dns.renner.uno/public).
For each host on such an Ingress the NetBird management gets one reverse-proxy
@@ -30,11 +30,11 @@ import urllib.error
import urllib.parse
import urllib.request
LOG = logging.getLogger("netbird-proxy-services")
LOG = logging.getLogger("netbird-service-sync")
NB = os.environ.get("NB_MANAGEMENT_URL", "https://netbird.renner.uno").rstrip("/")
BAO_ADDR = os.environ.get("BAO_ADDR", "http://openbao.openbao.svc.cluster.local:8200").rstrip("/")
BAO_ROLE = os.environ.get("BAO_ROLE", "netbird-proxy-services")
BAO_ROLE = os.environ.get("BAO_ROLE", "netbird-service-sync")
BAO_PAT_PATH = os.environ.get("BAO_PAT_PATH", "homelab/data/infrastructure/secret-rotators-bootstrap/netbird")
BAO_PAT_KEY = os.environ.get("BAO_PAT_KEY", "pat")
SERVICE_CIDR = os.environ.get("SERVICE_CIDR", "10.96.0.0/12")
@@ -156,7 +156,7 @@ def ensure_network(nb):
if n["name"] == NETWORK_NAME:
return n["id"]
n = nb.post("networks", {"name": NETWORK_NAME,
"description": "managed by netbird-proxy-services; public ingress via TLS passthrough"})
"description": "managed by netbird-service-sync; public ingress via TLS passthrough"})
LOG.info("created network %s (%s)", NETWORK_NAME, n["id"])
return n["id"]
@@ -166,7 +166,7 @@ def ensure_resource(nb, net):
if r.get("address") == SERVICE_CIDR:
return r["id"]
r = nb.post(f"networks/{net}/resources", {"name": "cluster service CIDR", "address": SERVICE_CIDR,
"description": "managed by netbird-proxy-services",
"description": "managed by netbird-service-sync",
"enabled": True, "groups": []})
LOG.info("created subnet resource %s (%s)", SERVICE_CIDR, r["id"])
return r["id"]
+1 -1
View File
@@ -23,7 +23,7 @@ protocol device { scan time 10; }
# down. The usual advice is not to export these to the kernel because it
# maintains them itself — true of main, not of a table the kernel never fills.
protocol direct attached {
interface "mgmt0", "ingress0", "pod0";
interface "mgmt0", "ingress0", "pod0", "node0";
ipv4 { table master4; };
ipv6 { table master6; };
}
@@ -118,14 +118,21 @@ spec:
# traffic out to the VIP and back (which fails with L2 announcements).
lbExternalClusterIP: true
cni:
# Multus (kubernetes/infrastructure/multus) runs alongside Cilium and
# writes 00-multus.conf to /etc/cni/net.d so kubelet picks multus
# first; multus then delegates to Cilium's 05-cilium.conflist via
# clusterNetwork. The default `exclusive: true` makes cilium-cni's
# install step rename anything alphabetically before its own config
# to *.cilium_bak on every reconcile, which kept nuking multus.
# Setting it false lets multus's config coexist.
# Multus is the only writer of /etc/cni/net.d (#832): Cilium's conflist
# goes to /etc/cni/net.d/multus/, which the container runtime does not
# read, and Multus delegates to it via clusterNetwork. Without this a
# pod created after a reboot, before Multus rewrote 00-multus.conf, came
# up with Cilium alone. See kubernetes/infrastructure/multus.
confPath: /etc/cni/net.d/multus
# The default `exclusive: true` renames other configs in the conf dir to
# *.cilium_bak on every reconcile, which kept nuking multus.
exclusive: false
# One agent at a time: the chart default of 2 restarts two of three
# nodes' datapath at once.
updateStrategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
# Talos-specific settings
securityContext:
capabilities:
+1 -1
View File
@@ -19,7 +19,7 @@ resources:
- cloudnative-pg/
- mariadb-operator/
- netbird-ingress/
- netbird-proxy-services/
- netbird-service-sync/
- coredns-config/
- k8s-gateway/
- external-dns-unifi/
@@ -111,10 +111,18 @@ metadata:
name: multus
namespace: kube-system
---
# Multus is the only writer of /etc/cni/net.d (#832). Cilium writes its conflist
# to /etc/cni/net.d/multus/, a subdirectory the container runtime does not read
# (libcni skips directories); Multus picks it up there via multusAutoconfigDir
# and writes 00-multus.conf. Until that file exists the runtime has no network
# config at all, so no pod can be created with Cilium alone after a reboot.
#
# The "-subdir" names date from the node-by-node rollout; renaming would roll
# Multus again for nothing.
kind: ConfigMap
apiVersion: v1
metadata:
name: multus-daemon-config
name: multus-daemon-config-subdir
namespace: kube-system
labels:
tier: node
@@ -127,7 +135,7 @@ data:
"logLevel": "verbose",
"logToStderr": true,
"cniConfigDir": "/host/etc/cni/net.d",
"multusAutoconfigDir": "/host/etc/cni/net.d",
"multusAutoconfigDir": "/host/etc/cni/net.d/multus",
"multusConfigFile": "auto",
"socketDir": "/host/run/multus/"
}
@@ -135,16 +143,16 @@ data:
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: kube-multus-ds
name: kube-multus-ds-subdir
namespace: kube-system
labels:
tier: node
app: multus
name: multus
name: multus-subdir
spec:
selector:
matchLabels:
name: multus
name: multus-subdir
updateStrategy:
type: RollingUpdate
template:
@@ -152,7 +160,7 @@ spec:
labels:
tier: node
app: multus
name: multus
name: multus-subdir
spec:
# Upstream ships this DaemonSet with no priority class, so it runs at 0
# and can preempt nothing. Any event that makes the pod need a fresh
@@ -224,6 +232,10 @@ spec:
volumeMounts:
- name: cni
mountPath: /host/etc/cni/net.d
# Exists before Multus starts, so it waits for Cilium's conflist
# instead of exiting on a missing directory.
- name: cni-cilium-conf
mountPath: /host/etc/cni/net.d/multus
# multus-daemon expects that cnibin path must be identical between pod and container host.
# e.g. if the cni bin is in '/opt/cni/bin' on the container host side, then it should be mount to '/opt/cni/bin' in multus-daemon,
# not to any other directory, like '/opt/bin' or '/usr/bin'.
@@ -279,6 +291,10 @@ spec:
- name: cni
hostPath:
path: /etc/cni/net.d
- name: cni-cilium-conf
hostPath:
path: /etc/cni/net.d/multus
type: DirectoryOrCreate
- name: cnibin
hostPath:
path: /opt/cni/bin
@@ -287,7 +303,7 @@ spec:
path: /
- name: multus-daemon-config
configMap:
name: multus-daemon-config
name: multus-daemon-config-subdir
items:
- key: daemon-config.json
path: daemon-config.json
@@ -39,9 +39,11 @@ spec:
name: whereabouts-reconciler
spec:
serviceAccountName: whereabouts
# Every node here is a control plane node.
# Every node here is a control plane node. Keyed: a keyless Exists would
# also tolerate node.kubernetes.io/unschedulable and ignore cordons.
tolerations:
- operator: Exists
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
containers:
- name: whereabouts-reconciler
@@ -187,6 +187,14 @@ kind: Namespace
metadata:
name: gitea
---
# netbird-service-sync, moving out of `openbao` (issue #876). Declared here like
# `woodpecker` above, so it exists before infrastructure-crds applies its
# CiliumNetworkPolicies. No pod-security labels: the cluster default applies.
apiVersion: v1
kind: Namespace
metadata:
name: netbird-service-sync
---
# Local LLM inference (kubernetes/apps/llama-server). Declared here, like
# `woodpecker` above, so the `infrastructure` KS creates it before
# `infrastructure-crds` applies the namespace's CiliumNetworkPolicies.
@@ -1,28 +1,28 @@
# Reconciles NetBird reverse-proxy services from Ingresses that carry
# dns.renner.uno/public: one TLS-passthrough service per host on :443,
# targeting Traefik's ClusterIP through a subnet resource over the service
# CIDR (ci/images/netbird-proxy-services/controller.py).
# CIDR (ci/images/netbird-service-sync/controller.py).
#
# openbao namespace, like the rotators: its default-deny already allows egress
# to OpenBao, DNS, the apiserver and `world` (netbird.renner.uno), and the
# NetBird PAT is read from OpenBao on every pass through the
# netbird-proxy-services role — policies/netbird-proxy-services.hcl, applied
# once by hand like the rotator roles. Until that role exists the pod logs an
# auth error each minute and does nothing.
# Own namespace; its egress (DNS, apiserver, OpenBao, NetBird API) is in
# network-policies/netbird-service-sync-ns.yaml. The NetBird PAT is read from
# OpenBao on every pass through the netbird-service-sync role —
# policies/netbird-service-sync.hcl, applied once by hand like the rotator
# roles. Until that role exists the pod logs an auth error each minute and does
# nothing.
apiVersion: v1
kind: ServiceAccount
metadata:
name: netbird-proxy-services
namespace: openbao
name: netbird-service-sync
namespace: netbird-service-sync
labels:
app.kubernetes.io/name: netbird-proxy-services
app.kubernetes.io/name: netbird-service-sync
---
# Read-only, cluster-wide: public Ingresses live in many namespaces, the
# Traefik Service in ingress/.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: netbird-proxy-services-read
name: netbird-service-sync-read
rules:
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses"]
@@ -34,23 +34,23 @@ rules:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: netbird-proxy-services-read
name: netbird-service-sync-read
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: netbird-proxy-services-read
name: netbird-service-sync-read
subjects:
- kind: ServiceAccount
name: netbird-proxy-services
namespace: openbao
name: netbird-service-sync
namespace: netbird-service-sync
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: netbird-proxy-services
namespace: openbao
name: netbird-service-sync
namespace: netbird-service-sync
labels:
app.kubernetes.io/name: netbird-proxy-services
app.kubernetes.io/name: netbird-service-sync
spec:
replicas: 1
# One writer against the management API at a time.
@@ -58,13 +58,13 @@ spec:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: netbird-proxy-services
app.kubernetes.io/name: netbird-service-sync
template:
metadata:
labels:
app.kubernetes.io/name: netbird-proxy-services
app.kubernetes.io/name: netbird-service-sync
spec:
serviceAccountName: netbird-proxy-services
serviceAccountName: netbird-service-sync
securityContext:
runAsNonRoot: true
# Numeric, or the kubelet refuses the image's named user with
@@ -80,7 +80,7 @@ spec:
# image directory (branch and master alike) and the registry keeps
# only the newest manifest per tag, so the digest must come from
# the MASTER build of the code change.
image: gitea.renner.uno/gitea_admin/netbird-proxy-services:2026-09-07@sha256:caffa308d802c6fcc6aea919ac94657c08b993651baa6b8d18de395bcc40523e
image: gitea.renner.uno/gitea_admin/netbird-service-sync:2026-09-23@sha256:64f6057c508640e1dbc50457dc26e9b2de058a111fd46ca096021d7444b5bbff
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
@@ -92,7 +92,7 @@ spec:
- name: BAO_ADDR
value: http://openbao.openbao.svc.cluster.local:8200
- name: BAO_ROLE
value: netbird-proxy-services
value: netbird-service-sync
# Must match cluster.network.serviceSubnets in talos/patches/cluster.yaml.
- name: SERVICE_CIDR
value: "10.96.0.0/12"
@@ -166,9 +166,6 @@ spec:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": ingress
app.kubernetes.io/name: traefik
- matchLabels:
"k8s:io.kubernetes.pod.namespace": apps
app.kubernetes.io/name: gitea
- matchLabels:
"k8s:io.kubernetes.pod.namespace": gitea
app.kubernetes.io/name: gitea
@@ -45,16 +45,6 @@ spec:
- ports:
- port: "1883"
protocol: TCP
# gitea delivers the notes repo's push webhook to wiki-index (ai ns) on :8080.
# wiki-index's own ingress policy scopes this to the gitea pod.
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": ai
app.kubernetes.io/name: wiki-index
toPorts:
- ports:
- port: "8080"
protocol: TCP
- toEntities:
- host
- remote-node
@@ -23,6 +23,7 @@ resources:
- woodpecker-ci-ns.yaml
- woodpecker-ns.yaml
- gitea-ns.yaml
- netbird-service-sync-ns.yaml
- trmnl-ns.yaml
- wordpress-ns.yaml
- ai-ns.yaml
@@ -0,0 +1,43 @@
---
# Baseline policy for the `netbird-service-sync` namespace (issue #876).
# The controller serves nothing; it only calls out: the Kubernetes API
# (Ingress/Service reads), OpenBao (NetBird PAT) and the NetBird management API.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: default-deny
namespace: netbird-service-sync
spec:
endpointSelector: {}
ingress:
- fromEntities:
- host
- remote-node
egress:
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
- port: "53"
protocol: TCP
- toEntities:
- kube-apiserver
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": openbao
app.kubernetes.io/name: openbao
toPorts:
- ports:
- port: "8200"
protocol: TCP
# NetBird management API (netbird.renner.uno).
- toEntities:
- world
toPorts:
- ports:
- port: "443"
protocol: TCP
@@ -41,6 +41,16 @@ spec:
- ports:
- port: "8200"
protocol: TCP
# netbird-service-sync logs in via auth/kubernetes/role/netbird-service-sync
# and reads the NetBird PAT on every pass.
- fromEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": netbird-service-sync
app.kubernetes.io/name: netbird-service-sync
toPorts:
- ports:
- port: "8200"
protocol: TCP
# (Secret-rotation CronJobs run in THIS namespace since 2026-07-18, so their
# OpenBao access is covered by the intra-namespace ingress rule above — no
# per-app rule needed.)
@@ -127,7 +137,7 @@ spec:
# Egress complement to gitea-allow-flux-rotator (apps/gitea/networkpolicy.yaml).
# The flux-git rotator mints a PAT against the in-cluster gitea Service; the
# openbao default-deny only permits TCP egress to openbao/kube-dns/apiserver/
# world, and gitea (apps ns) is in-cluster, so it needs an explicit allow.
# world, and gitea (gitea ns) is in-cluster, so it needs an explicit allow.
# Scoped to the flux-git rotator label and gitea :3000.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
@@ -141,9 +151,6 @@ spec:
app.kubernetes.io/name: flux-git
egress:
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": apps
app.kubernetes.io/name: gitea
- matchLabels:
"k8s:io.kubernetes.pod.namespace": gitea
app.kubernetes.io/name: gitea
@@ -153,10 +160,10 @@ spec:
protocol: TCP
---
# Egress for the review-bot rotator. Its CronJob has two steps: the init step
# mints review-bot's PAT against gitea (apps :3000), the main step upserts the
# mints review-bot's PAT against gitea (gitea :3000), the main step upserts the
# Woodpecker org secret against the woodpecker server (woodpecker :8000). Both run
# under the same pod labels, so one egress policy covers both hops. Complements
# gitea-allow-review-bot-rotator (apps) + woodpecker-allow-rotators (woodpecker).
# gitea-allow-review-bot-rotator (gitea) + woodpecker-allow-rotators (woodpecker).
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
@@ -169,9 +176,6 @@ spec:
app.kubernetes.io/name: review-bot
egress:
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": apps
app.kubernetes.io/name: gitea
- matchLabels:
"k8s:io.kubernetes.pod.namespace": gitea
app.kubernetes.io/name: gitea
@@ -1,23 +0,0 @@
# OpenBao policy: netbird-proxy-services
#
# The reverse-proxy-services controller (Deployment netbird-proxy-services in
# the openbao namespace) reads the NetBird PAT on every reconcile pass and
# calls the management API with it. Read-only on that one path; it shares the
# bootstrap credential with rotator-netbird but not that role's PATCH rights.
#
# NOT applied by Flux. Apply ONCE with an admin token:
#
# bao policy write netbird-proxy-services kubernetes/infrastructure/openbao/policies/netbird-proxy-services.hcl
# bao write auth/kubernetes/role/netbird-proxy-services \
# bound_service_account_names=netbird-proxy-services \
# bound_service_account_namespaces=openbao \
# policies=netbird-proxy-services ttl=15m
#
# The PAT itself is the one rotator-netbird already uses
# (homelab/infrastructure/secret-rotators-bootstrap/netbird, key `pat`).
#
# Do NOT set token_no_default_policy=true (kv-v2 preflight needs the default policy).
path "homelab/data/infrastructure/secret-rotators-bootstrap/netbird" {
capabilities = ["read"]
}
@@ -0,0 +1,24 @@
# OpenBao policy: netbird-service-sync
#
# The controller that syncs public Ingresses into NetBird reverse-proxy services
# (Deployment netbird-service-sync in the netbird-service-sync namespace) reads
# the NetBird PAT on every reconcile pass and calls the management API with it.
# Read-only on that one path; it shares the bootstrap credential with
# rotator-netbird but not that role's PATCH rights.
#
# NOT applied by Flux. Apply ONCE with an admin token:
#
# bao policy write netbird-service-sync kubernetes/infrastructure/openbao/policies/netbird-service-sync.hcl
# bao write auth/kubernetes/role/netbird-service-sync \
# bound_service_account_names=netbird-service-sync \
# bound_service_account_namespaces=netbird-service-sync \
# policies=netbird-service-sync ttl=15m
#
# The PAT itself is the one rotator-netbird already uses
# (homelab/infrastructure/secret-rotators-bootstrap/netbird, key `pat`).
#
# Do NOT set token_no_default_policy=true (kv-v2 preflight needs the default policy).
path "homelab/data/infrastructure/secret-rotators-bootstrap/netbird" {
capabilities = ["read"]
}
@@ -225,8 +225,7 @@ metadata:
labels:
app.kubernetes.io/part-of: homelab-backup
spec:
# Daily at 07:15 local. Every PVC in the shared `apps` namespace: Gitea's
# data-gitea-0 until it moves to the `gitea` namespace (#878), gatus-data.
# Daily at 07:15 local. Every PVC in the shared `apps` namespace (gatus-data).
# No CNPG cluster runs here, so no labelSelector exclusion is needed.
schedule: "15 7 * * *"
useOwnerReferencesInBackup: false
@@ -247,8 +246,7 @@ metadata:
labels:
app.kubernetes.io/part-of: homelab-backup
spec:
# Daily at 07:45 local. data-gitea-0 (SQLite, repos, LFS, packages) once
# Gitea runs in the `gitea` namespace (#878).
# Daily at 07:45 local. data-gitea-0 (SQLite, repos, LFS, packages).
schedule: "45 7 * * *"
useOwnerReferencesInBackup: false
template:
@@ -96,7 +96,7 @@ spec:
# that traffic is always a pod IP — collapsing all external clients
# to one source. The NetBird reverse-proxy services are TLS
# passthrough and carry the WAN client address in a PROXY protocol
# v2 header (options.proxy_protocol, set by netbird-proxy-services);
# v2 header (options.proxy_protocol, set by netbird-service-sync);
# X-Forwarded-For stays trusted for HTTP-mode services. Both are
# accepted from the pod CIDRs only; connections without a header
# (direct LAN/mesh) keep their RemoteAddr.