feat(ingress): NetBird mesh ingress (routing peer) alongside cloudflared #27

Merged
k.renner merged 2 commits from worktree-netbird-mesh-ingress into master 2026-06-25 17:49:19 +00:00
7 changed files with 195 additions and 0 deletions
@@ -14,6 +14,7 @@ resources:
- matter-server.yaml
- mmwave-configurator.yaml
- music-assistant.yaml
- netbird-ingress.yaml
- paperless-ngx.yaml
- protonmail-bridge.yaml
- pve-exporter.yaml
@@ -0,0 +1,26 @@
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImageRepository
metadata:
name: netbird-ingress
namespace: flux-system
spec:
image: netbirdio/netbird
interval: 1h
---
# Semver. filterTags restricts candidates to bare X.Y.Z release tags so
# pre-release (-rc.N) and the -rootless variant can't slip through; the
# -rootless image is userspace/netstack and can't act as a routing peer.
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImagePolicy
metadata:
name: netbird-ingress
namespace: flux-system
spec:
imageRepositoryRef:
name: netbird-ingress
filterTags:
pattern: '^(?P<ver>\d+\.\d+\.\d+)$'
extract: '$ver'
policy:
semver:
range: ">=0.73.0"
@@ -16,6 +16,7 @@ resources:
- ceph-csi-drivers/
- cloudnative-pg/
- cloudflared/
- netbird-ingress/
- coredns-config/
- k8s-gateway/
- external-dns-unifi/
@@ -0,0 +1,92 @@
# NetBird mesh ingress — a second, parallel ingress path alongside
# cloudflared. This pod enrolls as a NetBird *routing peer* with the
# self-hosted management server (netbird.renner.uno) and advertises the
# Traefik VIP (10.2.99.86) into the mesh, so mesh peers can reach every
# Traefik-fronted service. Mesh DNS (10.2.40.53) already resolves
# <app>.renner.uno → 10.2.99.86; the only missing piece was making that
# VIP routable over the overlay, which this peer provides.
#
# Flow: mesh peer (100.96.x.x) --wg--> this pod --(masq + fwd)--> 10.2.99.86 (Traefik) --> service
#
# Unlike the upstream NetBird k8s operator, this pod is NOT privileged.
# The NET_ADMIN/SYS_ADMIN/SYS_RESOURCE caps are the irreducible minimum
# for an L3 WireGuard routing peer (the netbird entrypoint mknods
# /dev/net/tun and programs the wg interface + masquerade itself). Its
# egress is locked down by the netbird-ingress-egress CiliumNetworkPolicy
# (network-policies/ingress-isolation.yaml), mirroring cloudflared.
apiVersion: apps/v1
kind: Deployment
metadata:
name: netbird-ingress
namespace: ingress
labels:
app: netbird-ingress
spec:
# Single routing peer = single advertised next-hop into the mesh.
# HA (multiple peers + distribution) is a later iteration.
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: netbird-ingress
template:
metadata:
labels:
app: netbird-ingress
spec:
containers:
- name: netbird
image: netbirdio/netbird:0.73.2 # {"$imagepolicy": "flux-system:netbird-ingress"}
env:
- name: NB_SETUP_KEY
valueFrom:
secretKeyRef:
name: netbird-ingress-setup-key
key: setup-key
- name: NB_MANAGEMENT_URL
value: "https://netbird.renner.uno"
- name: NB_HOSTNAME
value: "netbird-ingress"
- name: NB_LOG_LEVEL
value: "info"
securityContext:
capabilities:
add:
- NET_ADMIN
- SYS_RESOURCE
- SYS_ADMIN
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
memory: 256Mi
livenessProbe:
exec:
command: ["netbird", "status", "--check", "live"]
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
exec:
command: ["netbird", "status", "--check", "ready"]
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
startupProbe:
exec:
command: ["netbird", "status", "--check", "startup"]
periodSeconds: 2
timeoutSeconds: 10
failureThreshold: 30
volumeMounts:
- name: state
mountPath: /var/lib/netbird
volumes:
# Ephemeral state: the setup key is reusable, so a pod restart
# cleanly re-enrolls a fresh peer (old one is reaped as ephemeral).
- name: state
emptyDir: {}
@@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- secret.sops.yaml
- deployment.yaml
@@ -0,0 +1,23 @@
apiVersion: ENC[AES256_GCM,data:n5g=,iv:AS2cmrgB/sOiONg/DoPRT6ZmPdLOPbWjaQqilYTJpUg=,tag:ulxs3Ez1IixOUIRDktDtew==,type:str]
kind: ENC[AES256_GCM,data:G2FirIAV,iv:xiJ8bkbXCRC8diE4UwYP3rnTBYG1BTszYOKcPTcMgCU=,tag:/vMwZg4Tu0NHwpzxX8RJ0g==,type:str]
metadata:
name: ENC[AES256_GCM,data:HTnb3axigMpnO7jTqK3XQ+V7TehSfDPH+Q==,iv:8TEl6Yvgpj54ZtY1aF1dJ6Hkb9M0bVjJR7fUhA0+RUk=,tag:u5QEgWAc4udwwwKMLB+F2g==,type:str]
namespace: ENC[AES256_GCM,data:BMF0U6V7GA==,iv:cImq31ZJLipyvkHOVcixDKyUakWKFTTQposOS6NDYqM=,tag:RGfZgiX9w4nEX2C9YXytPQ==,type:str]
type: ENC[AES256_GCM,data:EtCwuXbU,iv:jmjZOjGDXhK+GZ/kjd7cynRYfp+BxCMU6IuvS+2p5AU=,tag:Stc7GZZf6UjdR4svy2+M6Q==,type:str]
stringData:
setup-key: ENC[AES256_GCM,data:+ivBRI8oBZaiKT8CYIIT4ECQk4tpX17BmCbncMJ3we3kmCq+,iv:7q51hlOukOAxUFEjWw3el0x6tTyJQdvelMXUxA0T5Ks=,tag:JoDZRAri8QO47Ouc5sJK0w==,type:str]
sops:
age:
- recipient: age1yqjvqkwwwxdj7mr546zfs5ckzuwndp69ya2n2drd4vypwns8w4ys04qfy3
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBvOWNLQXg3NUpiZFQrQmx5
bUxkYlZ5U1ZuU0VGN1ZiSit1QjNtVTNxelRNCkRoYW1Kb0lDZlFXWmlOcjRETUw2
SzV2OGlVZXNPUzlISkEwc3crcWdTNlEKLS0tIEVjeFFHakZWOUJBdVdQMGJXN1kr
c1gybVVEOVV5b1dwR2V6VEpqbUtPSlkKdrtsKN4ExpeRfT8tk+s0yUVlBmD20Tbw
zSGHaQvuFeAG4HJW6QBlFcJ9c2GGhlvYhAH2E31tHKzwPaqYLQYPww==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-06-25T17:46:34Z"
mac: ENC[AES256_GCM,data:gxxN4X4m3YTxL2nqMhG6eTrbOIhr2bMIhUdVZs1SeDldIgO+iXu8S8ComrEFHb7IeYh55OaCZjFz9CG9uR36JzAX7o2d7F7/HM7g3kzkoA7vV0PGX7BgGLWJsT5LxVuU7/300KEdaiqPXPubwAhHOEbVkVdyn8IXnrQRCHbkkzs=,iv:inS8+v2tbGYZAbD3zG3Bf5uVIckTTFMK8Tk0fPsVXXg=,tag:7JMmk6irsBCfM/za2CdkaQ==,type:str]
unencrypted_suffix: _unencrypted
version: 3.10.2
@@ -42,6 +42,53 @@ spec:
- port: "443"
protocol: TCP
---
# NetBird mesh-ingress routing peer: same egress-lockdown discipline as
# cloudflared above. It may reach (a) traefik (by label selector — Cilium
# applies LB/DNAT before egress policy, so forwarded mesh→VIP traffic is
# enforced on traefik's identity, no static VIP needed); (b) DNS; (c) the
# NetBird control/data plane on netbird.renner.uno — mgmt/signal/relay
# over TCP/443, STUN/TURN over UDP/3478, and WireGuard over UDP/51820.
# Relay-over-443 is the guaranteed fallback when direct UDP P2P is blocked.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: netbird-ingress-egress
namespace: ingress
spec:
endpointSelector:
matchLabels:
app: netbird-ingress
egress:
- toEndpoints:
- matchLabels:
app.kubernetes.io/name: traefik
toPorts:
- ports:
- port: "8443"
protocol: TCP
- port: "8000"
protocol: TCP
# Allow DNS resolution
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
# NetBird control + data plane (netbird.renner.uno, external)
- toEntities:
- world
toPorts:
- ports:
- port: "443"
protocol: TCP
- port: "3478"
protocol: UDP
- port: "51820"
protocol: UDP
---
# Default deny ingress to ingress namespace from external sources only.
# Any in-cluster pod may reach traefik (required for east-west OIDC
# flows via k8s_gateway — audiobookshelf → Authentik etc.); we don't