Compare commits

..
Author SHA1 Message Date
claude-botandClaude Fable 5.1 6446616621 fix(ceph): OSD raw volume label must not contain "ceph"
All three OSD prepare jobs on the Hetzner rehearsal finished with zero
OSDs:

  cephosd: skipping device "sdb1": ["Used by ceph-disk"]

The partition is exactly what the machine config asked for (PARTLABEL
r-ceph-osd, plain Linux partition type). ceph-volume rejects it anyway —
its ceph-disk-member test is `"ceph" in PARTLABEL or PARTTYPE in
ceph_disk_guids` (ceph_volume/util/device.py) — so any label containing
"ceph" reads as a legacy ceph-disk partition and is never provisioned.

Talos derives the label from the RawVolumeConfig name (r-<name>), so the
name changes: ceph-osd -> osd-data, label r-osd-data. This is why the
rebuild branch called it osd-data; the #798 review renamed it back for
symmetry with the node label without this failure being known.

Production is unaffected today: the MS-01 OSDs match on the by-id half of
devicePathFilter and no r-ceph-osd partition exists there. The partlabel
half is the migration target and would have failed on the first
repartitioned drive.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 23:44:06 +02:00
claude-botandClaude Fable 5.1 5cb944a2d3 fix(infrastructure): move the LAPI ServersTransport behind the Traefik CRDs
A cold rebuild stops permanently at the infrastructure Kustomization:

  ServersTransport/crowdsec/crowdsec-lapi dry-run failed: no matches for
  kind "ServersTransport" in version "traefik.io/v1alpha1"

Flux dry-runs the whole apply set, so this one unknown kind rejects all of
it — including the Traefik HelmRelease that installs the CRD. Measured on
the Hetzner rehearsal: zero HelmReleases, zero CRDs, no progress. The
public LAPI endpoint (2026-09-02) landed after the post-crds sweep and
put a traefik.io consumer back into the layer that creates traefik.io.

The Ingress is a core kind and stays. Rehearsal branch: one step. For
production this must ship add-then-remove, because both Kustomizations
prune.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 23:23:16 +02:00
claude-botandClaude Fable 5.1 c269d68639 rehearsal: flux components as the operator CLI (v2.8.3) writes them
Throwaway branch only. The local flux CLI is v2.8.3 while master carries
v2.8.6, so `flux bootstrap` committed a downgrade to GitHub; mirroring it
here keeps Gitea, GitHub and the cluster on one version for this run.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 23:20:15 +02:00
claude-botandClaude Fable 5.1 394ab82093 rehearsal: flux sync manifests for the GitHub break-glass source
Throwaway branch only. Gitea push-mirror force-syncs every branch on every
push, which erased the commit `flux bootstrap github` made here; carrying
the same gotk-sync on the Gitea side keeps mirror and cluster in step.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 23:17:00 +02:00
17 changed files with 75 additions and 575 deletions
+1 -28
View File
@@ -89,7 +89,7 @@ steps:
--destination gitea.renner.uno/gitea_admin/ci-k8s:latest
- name: build-vmtest
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.18-debug
image: gcr.io/kaniko-project/executor:v1.23.2-debug
directory: ci/images/vmtest
depends_on: []
when:
@@ -349,30 +349,3 @@ steps:
--dockerfile Containerfile \
--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
# 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
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.18-debug
directory: ci/images/netbird-proxy-services
depends_on: []
when:
- event: deployment
- event: push
path: ["ci/images/netbird-proxy-services/**", ".woodpecker/build-ci-images.yml"]
environment:
GITEA_USER: gitea_admin
GITEA_TOKEN:
from_secret: ci_images_token
commands:
- mkdir -p /kaniko/.docker
- AUTH=$(printf '%s:%s' "$GITEA_USER" "$GITEA_TOKEN" | base64 | tr -d '\n')
- printf '{"auths":{"gitea.renner.uno":{"auth":"%s"}}}' "$AUTH" > /kaniko/.docker/config.json
- TAG=$(date +%Y-%m-%d)
- |
/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
@@ -32,13 +32,8 @@ netbird_watchdog_fail_threshold: 3
# Minimum seconds between watchdog-issued restarts, so a genuinely broken
# backend can't put the proxy into a restart loop. Shared by both the URL-probe
# watchdog and the flood watchdog below. One minute: the route-manager wedge
# recurs on every routing-peer loss, and rolling node reboots lose one peer
# every few minutes; a suppressed restart there leaves the wedge standing
# until the URL probe's three rounds fire (3.5 min of 000 on every domain,
# measured 2026-09-07). A minute still caps a loop at one restart per
# probe interval.
netbird_watchdog_cooldown_seconds: 60
# watchdog and the flood watchdog below.
netbird_watchdog_cooldown_seconds: 600
# Log fingerprint of the reverse-proxy route-manager wedge, matched as a plain
# substring in the proxy's docker logs by the flood watchdog. On this build the
@@ -1,12 +0,0 @@
# netbird-proxy-services: 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.
FROM python:3.13-slim
RUN useradd --system --no-create-home --shell /usr/sbin/nologin controller
COPY controller.py /opt/controller.py
ENV PYTHONUNBUFFERED=1
USER controller
ENTRYPOINT ["python3", "/opt/controller.py"]
@@ -1,275 +0,0 @@
#!/usr/bin/env python3
"""netbird-proxy-services: 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
service in TLS-passthrough mode on :443, targeting Traefik's ClusterIP through a
subnet resource that covers the cluster's service CIDR. SNI and Host reach
Traefik untouched, the proxy terminates nothing, and no domain resource is
involved -- the routing peers stay ordinary IP routers. The target sends a
PROXY protocol v2 header so Traefik (and CrowdSec behind its access log) sees
the WAN client address instead of the routing peer's pod IP.
Ownership is derived from NetBird itself, not from local state: a service is
managed iff its mode is "tls" and its target is this controller's subnet
resource. Services that do not match (the hand-made HTTP/domain ones) are never
touched; a desired host held by such a service is reported and skipped until
that service is removed by hand.
Credentials: the NetBird PAT is read from OpenBao on every pass through the
Kubernetes auth role, exactly like the setup-key rotator; it never lands in a
Kubernetes Secret or in a log line.
"""
import json
import logging
import os
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
LOG = logging.getLogger("netbird-proxy-services")
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_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")
NETWORK_NAME = os.environ.get("NETWORK_NAME", "k8s public ingress")
ROUTER_GROUP = os.environ.get("ROUTER_GROUP_NAME", "k8s-ingress-router")
SOURCE_GROUP = os.environ.get("SOURCE_GROUP_NAME", "All")
TRAEFIK_NS, TRAEFIK_SVC = os.environ.get("TRAEFIK_SERVICE", "ingress/traefik").split("/", 1)
PUBLIC_LABEL = os.environ.get("PUBLIC_LABEL", "dns.renner.uno/public")
LISTEN_PORT = int(os.environ.get("LISTEN_PORT", "443"))
INTERVAL = int(os.environ.get("INTERVAL_SECONDS", "60"))
HTTP_TIMEOUT = 20
K8S_API = "https://kubernetes.default.svc"
K8S_TOKEN_FILE = "/var/run/secrets/kubernetes.io/serviceaccount/token"
K8S_CA_FILE = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
class ApiError(Exception):
pass
def http(method, url, body=None, headers=None, cafile=None):
data = None if body is None else json.dumps(body).encode()
req = urllib.request.Request(url, data=data, method=method)
req.add_header("Accept", "application/json")
if data is not None:
req.add_header("Content-Type", "application/json")
for k, v in (headers or {}).items():
req.add_header(k, v)
ctx = None
if cafile:
import ssl
ctx = ssl.create_default_context(cafile=cafile)
try:
with urllib.request.urlopen(req, timeout=HTTP_TIMEOUT, context=ctx) as resp:
raw = resp.read()
return json.loads(raw) if raw else None
except urllib.error.HTTPError as e:
detail = e.read().decode(errors="replace")[:300]
raise ApiError(f"{method} {url} -> {e.code}: {detail}") from None
# --- Kubernetes -------------------------------------------------------------
def k8s(path):
with open(K8S_TOKEN_FILE) as f:
token = f.read().strip()
return http("GET", K8S_API + path, headers={"Authorization": f"Bearer {token}"}, cafile=K8S_CA_FILE)
def desired_hosts():
sel = urllib.parse.quote(f"{PUBLIC_LABEL}=true")
ingresses = k8s(f"/apis/networking.k8s.io/v1/ingresses?labelSelector={sel}")
hosts = {}
for ing in ingresses.get("items", []):
ref = f'{ing["metadata"]["namespace"]}/{ing["metadata"]["name"]}'
for rule in ing.get("spec", {}).get("rules", []):
host = rule.get("host")
if not host or host.startswith("*"):
continue
hosts.setdefault(host, ref)
return hosts
def traefik_cluster_ip():
svc = k8s(f"/api/v1/namespaces/{TRAEFIK_NS}/services/{TRAEFIK_SVC}")
for ip in svc["spec"].get("clusterIPs", []):
if ":" not in ip:
return ip
raise ApiError(f"{TRAEFIK_NS}/{TRAEFIK_SVC} has no IPv4 ClusterIP")
# --- OpenBao ----------------------------------------------------------------
def netbird_pat():
with open(K8S_TOKEN_FILE) as f:
jwt = f.read().strip()
login = http("POST", f"{BAO_ADDR}/v1/auth/kubernetes/login", {"role": BAO_ROLE, "jwt": jwt})
bao_token = login["auth"]["client_token"]
kv = http("GET", f"{BAO_ADDR}/v1/{BAO_PAT_PATH}", headers={"X-Vault-Token": bao_token})
pat = kv["data"]["data"].get(BAO_PAT_KEY)
if not pat:
raise ApiError(f"OpenBao {BAO_PAT_PATH} has no key {BAO_PAT_KEY}")
return pat
# --- NetBird management -----------------------------------------------------
class NetBird:
def __init__(self, pat):
self.h = {"Authorization": f"Token {pat}"}
def get(self, path):
return http("GET", f"{NB}/api/{path}", headers=self.h)
def list(self, path):
# An empty collection comes back as an empty body / null, not [].
return self.get(path) or []
def post(self, path, body):
return http("POST", f"{NB}/api/{path}", body, headers=self.h)
def put(self, path, body):
return http("PUT", f"{NB}/api/{path}", body, headers=self.h)
def delete(self, path):
return http("DELETE", f"{NB}/api/{path}", headers=self.h)
def group_id(nb, name):
for g in nb.list("groups"):
if g["name"] == name:
return g["id"]
raise ApiError(f"NetBird group {name!r} not found")
def ensure_network(nb):
for n in nb.list("networks"):
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"})
LOG.info("created network %s (%s)", NETWORK_NAME, n["id"])
return n["id"]
def ensure_resource(nb, net):
for r in nb.list(f"networks/{net}/resources"):
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",
"enabled": True, "groups": []})
LOG.info("created subnet resource %s (%s)", SERVICE_CIDR, r["id"])
return r["id"]
def ensure_router(nb, net, router_group):
for r in nb.list(f"networks/{net}/routers"):
if router_group in (r.get("peer_groups") or []):
return r["id"]
r = nb.post(f"networks/{net}/routers", {"peer_groups": [router_group], "metric": 9999,
"masquerade": True, "enabled": True})
LOG.info("created router on group %s (%s)", ROUTER_GROUP, r["id"])
return r["id"]
def ensure_policy(nb, resource, source_group):
name = f"{NETWORK_NAME} access"
for p in nb.list("policies"):
if p["name"] == name:
return p["id"]
p = nb.post("policies", {"name": name, "enabled": True, "rules": [{
"name": name, "enabled": True, "action": "accept", "protocol": "tcp",
"ports": [str(LISTEN_PORT)], "bidirectional": False, "sources": [source_group],
"destinationResource": {"id": resource, "type": "subnet"}}]})
LOG.info("created policy %s (%s)", name, p["id"])
return p["id"]
def service_body(host, resource, target_ip):
# proxy_protocol: PROXY v2 header carries the WAN client address to
# Traefik (proxyProtocol.trustedIPs on websecure); TCP/TLS modes only.
return {"name": host, "domain": host, "mode": "tls", "listen_port": LISTEN_PORT, "enabled": True,
"targets": [{"target_type": "subnet", "target_id": resource, "protocol": "tcp",
"host": target_ip, "port": 443, "enabled": True,
"options": {"proxy_protocol": True}}]}
def is_managed(svc, resource):
return svc.get("mode") == "tls" and any(t.get("target_id") == resource for t in svc.get("targets") or [])
def target_matches(svc, target_ip):
if svc.get("listen_port") != LISTEN_PORT or not svc.get("enabled"):
return False
ts = svc.get("targets") or []
return len(ts) == 1 and ts[0].get("host") == target_ip and ts[0].get("port") == 443 \
and ts[0].get("protocol") == "tcp" and ts[0].get("enabled") \
and (ts[0].get("options") or {}).get("proxy_protocol") is True
def reconcile():
hosts = desired_hosts()
target_ip = traefik_cluster_ip()
nb = NetBird(netbird_pat())
router_group = group_id(nb, ROUTER_GROUP)
source_group = group_id(nb, SOURCE_GROUP)
net = ensure_network(nb)
resource = ensure_resource(nb, net)
ensure_router(nb, net, router_group)
ensure_policy(nb, resource, source_group)
by_domain = {s["domain"]: s for s in nb.list("reverse-proxies/services")}
created = updated = deleted = held = 0
for host, ref in sorted(hosts.items()):
svc = by_domain.get(host)
if svc is None:
nb.post("reverse-proxies/services", service_body(host, resource, target_ip))
LOG.info("created %s (from %s) -> %s:443", host, ref, target_ip)
created += 1
elif not is_managed(svc, resource):
LOG.warning("%s is held by unmanaged service %s (mode %s); remove it to migrate",
host, svc["id"], svc.get("mode"))
held += 1
elif not target_matches(svc, target_ip):
nb.put(f"reverse-proxies/services/{svc['id']}", service_body(host, resource, target_ip))
LOG.info("updated %s -> %s:443", host, target_ip)
updated += 1
for domain, svc in by_domain.items():
if is_managed(svc, resource) and domain not in hosts:
nb.delete(f"reverse-proxies/services/{svc['id']}")
LOG.info("deleted %s (no public Ingress carries it any more)", domain)
deleted += 1
LOG.info("reconciled: %d hosts desired, %d created, %d updated, %d deleted, %d held by unmanaged services",
len(hosts), created, updated, deleted, held)
def main():
logging.basicConfig(level=os.environ.get("LOG_LEVEL", "INFO"), stream=sys.stdout,
format="%(asctime)s %(levelname)s %(message)s")
if os.environ.get("ONCE") == "1":
reconcile()
return
while True:
try:
reconcile()
except Exception as e: # keep running; the next pass retries
LOG.error("reconcile failed: %s", e)
time.sleep(INTERVAL)
if __name__ == "__main__":
main()
@@ -21,9 +21,6 @@ metadata:
namespace: audiobookshelf
labels:
app.kubernetes.io/name: audiobookshelf
annotations:
# envFrom does not restart the pod when the ConfigMap changes; Reloader does.
configmap.reloader.stakater.com/reload: audiobookshelf-config
spec:
replicas: 1
strategy:
-3
View File
@@ -35,9 +35,6 @@ metadata:
namespace: immich
labels:
app.kubernetes.io/name: immich-server
annotations:
# envFrom does not restart the pod when the ConfigMap changes; Reloader does.
configmap.reloader.stakater.com/reload: immich-config
spec:
# Scaled to 0 alongside immich-machine-learning during single-node
# maintenance; see the note there. The CNPG Postgres cluster stays running,
@@ -1,6 +1,6 @@
---
# This manifest was generated by flux. DO NOT EDIT.
# Flux Version: v2.8.6
# Flux Version: v2.8.3
# Components: source-controller,kustomize-controller,helm-controller,notification-controller,image-reflector-controller,image-automation-controller
apiVersion: v1
kind: Namespace
@@ -8,7 +8,7 @@ metadata:
labels:
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/warn-version: latest
name: flux-system
@@ -19,7 +19,7 @@ metadata:
labels:
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
name: allow-egress
namespace: flux-system
spec:
@@ -39,7 +39,7 @@ metadata:
labels:
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
name: allow-scraping
namespace: flux-system
spec:
@@ -59,7 +59,7 @@ metadata:
labels:
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
name: allow-webhooks
namespace: flux-system
spec:
@@ -78,7 +78,7 @@ metadata:
labels:
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
name: critical-pods-flux-system
namespace: flux-system
spec:
@@ -98,7 +98,7 @@ metadata:
labels:
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
name: crd-controller-flux-system
rules:
- apiGroups:
@@ -204,7 +204,7 @@ metadata:
labels:
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
rbac.authorization.k8s.io/aggregate-to-admin: "true"
rbac.authorization.k8s.io/aggregate-to-edit: "true"
name: flux-edit-flux-system
@@ -231,7 +231,7 @@ metadata:
labels:
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
rbac.authorization.k8s.io/aggregate-to-admin: "true"
rbac.authorization.k8s.io/aggregate-to-edit: "true"
rbac.authorization.k8s.io/aggregate-to-view: "true"
@@ -257,7 +257,7 @@ metadata:
labels:
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
name: cluster-reconciler-flux-system
roleRef:
apiGroup: rbac.authorization.k8s.io
@@ -277,7 +277,7 @@ metadata:
labels:
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
name: crd-controller-flux-system
roleRef:
apiGroup: rbac.authorization.k8s.io
@@ -315,7 +315,7 @@ metadata:
app.kubernetes.io/component: source-controller
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
name: buckets.source.toolkit.fluxcd.io
spec:
group: source.toolkit.fluxcd.io
@@ -702,7 +702,7 @@ metadata:
app.kubernetes.io/component: source-controller
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
name: externalartifacts.source.toolkit.fluxcd.io
spec:
group: source.toolkit.fluxcd.io
@@ -898,7 +898,7 @@ metadata:
app.kubernetes.io/component: source-controller
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
name: gitrepositories.source.toolkit.fluxcd.io
spec:
group: source.toolkit.fluxcd.io
@@ -1385,7 +1385,7 @@ metadata:
app.kubernetes.io/component: source-controller
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
name: helmcharts.source.toolkit.fluxcd.io
spec:
group: source.toolkit.fluxcd.io
@@ -1744,7 +1744,7 @@ metadata:
app.kubernetes.io/component: source-controller
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
name: helmrepositories.source.toolkit.fluxcd.io
spec:
group: source.toolkit.fluxcd.io
@@ -2070,7 +2070,7 @@ metadata:
app.kubernetes.io/component: source-controller
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
name: ocirepositories.source.toolkit.fluxcd.io
spec:
group: source.toolkit.fluxcd.io
@@ -2484,7 +2484,7 @@ metadata:
app.kubernetes.io/component: source-controller
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
name: source-controller
namespace: flux-system
---
@@ -2495,7 +2495,7 @@ metadata:
app.kubernetes.io/component: source-controller
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
control-plane: controller
name: source-controller
namespace: flux-system
@@ -2516,7 +2516,7 @@ metadata:
app.kubernetes.io/component: source-controller
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
control-plane: controller
name: source-controller
namespace: flux-system
@@ -2537,7 +2537,7 @@ spec:
app.kubernetes.io/component: source-controller
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
spec:
containers:
- args:
@@ -2560,7 +2560,7 @@ spec:
resourceFieldRef:
containerName: manager
resource: limits.memory
image: ghcr.io/fluxcd/source-controller:v1.8.3
image: ghcr.io/fluxcd/source-controller:v1.8.1
imagePullPolicy: IfNotPresent
livenessProbe:
httpGet:
@@ -2624,7 +2624,7 @@ metadata:
app.kubernetes.io/component: kustomize-controller
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
name: kustomizations.kustomize.toolkit.fluxcd.io
spec:
group: kustomize.toolkit.fluxcd.io
@@ -3338,7 +3338,7 @@ metadata:
app.kubernetes.io/component: kustomize-controller
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
name: kustomize-controller
namespace: flux-system
---
@@ -3349,7 +3349,7 @@ metadata:
app.kubernetes.io/component: kustomize-controller
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
control-plane: controller
name: kustomize-controller
namespace: flux-system
@@ -3368,7 +3368,7 @@ spec:
app.kubernetes.io/component: kustomize-controller
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
spec:
containers:
- args:
@@ -3387,7 +3387,7 @@ spec:
resourceFieldRef:
containerName: manager
resource: limits.memory
image: ghcr.io/fluxcd/kustomize-controller:v1.8.4
image: ghcr.io/fluxcd/kustomize-controller:v1.8.2
imagePullPolicy: IfNotPresent
livenessProbe:
httpGet:
@@ -3444,7 +3444,7 @@ metadata:
app.kubernetes.io/component: helm-controller
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
name: helmreleases.helm.toolkit.fluxcd.io
spec:
group: helm.toolkit.fluxcd.io
@@ -4197,11 +4197,8 @@ spec:
rollback has been performed.
type: boolean
force:
description: |-
Force forces resource updates through a replacement strategy
that avoids 3-way merge conflicts on client-side apply.
This field is ignored for server-side apply (which always
forces conflicts with other field managers).
description: Force forces resource updates through a replacement
strategy.
type: boolean
recreate:
description: |-
@@ -4405,11 +4402,8 @@ spec:
upgrade has been performed.
type: boolean
force:
description: |-
Force forces resource updates through a replacement strategy
that avoids 3-way merge conflicts on client-side apply.
This field is ignored for server-side apply (which always
forces conflicts with other field managers).
description: Force forces resource updates through a replacement
strategy.
type: boolean
preserveValues:
description: |-
@@ -4894,7 +4888,7 @@ metadata:
app.kubernetes.io/component: helm-controller
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
name: helm-controller
namespace: flux-system
---
@@ -4905,7 +4899,7 @@ metadata:
app.kubernetes.io/component: helm-controller
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
control-plane: controller
name: helm-controller
namespace: flux-system
@@ -4924,7 +4918,7 @@ spec:
app.kubernetes.io/component: helm-controller
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
spec:
containers:
- args:
@@ -4943,7 +4937,7 @@ spec:
resourceFieldRef:
containerName: manager
resource: limits.memory
image: ghcr.io/fluxcd/helm-controller:v1.5.4
image: ghcr.io/fluxcd/helm-controller:v1.5.3
imagePullPolicy: IfNotPresent
livenessProbe:
httpGet:
@@ -5000,7 +4994,7 @@ metadata:
app.kubernetes.io/component: notification-controller
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
name: alerts.notification.toolkit.fluxcd.io
spec:
group: notification.toolkit.fluxcd.io
@@ -5394,7 +5388,7 @@ metadata:
app.kubernetes.io/component: notification-controller
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
name: providers.notification.toolkit.fluxcd.io
spec:
group: notification.toolkit.fluxcd.io
@@ -5809,7 +5803,7 @@ metadata:
app.kubernetes.io/component: notification-controller
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
name: receivers.notification.toolkit.fluxcd.io
spec:
group: notification.toolkit.fluxcd.io
@@ -5933,10 +5927,7 @@ spec:
secretRef:
description: |-
SecretRef specifies the Secret containing the token used
to validate the payload authenticity. The Secret must contain a 'token'
key. For GCR receivers, the Secret must also contain an 'email' key
with the IAM service account email configured on the Pub/Sub push
subscription, and an 'audience' key with the expected OIDC token audience.
to validate the payload authenticity.
properties:
name:
description: Name of the referent.
@@ -6293,7 +6284,7 @@ metadata:
app.kubernetes.io/component: notification-controller
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
name: notification-controller
namespace: flux-system
---
@@ -6304,7 +6295,7 @@ metadata:
app.kubernetes.io/component: notification-controller
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
control-plane: controller
name: notification-controller
namespace: flux-system
@@ -6325,7 +6316,7 @@ metadata:
app.kubernetes.io/component: notification-controller
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
control-plane: controller
name: webhook-receiver
namespace: flux-system
@@ -6346,7 +6337,7 @@ metadata:
app.kubernetes.io/component: notification-controller
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
control-plane: controller
name: notification-controller
namespace: flux-system
@@ -6365,7 +6356,7 @@ spec:
app.kubernetes.io/component: notification-controller
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
spec:
containers:
- args:
@@ -6383,7 +6374,7 @@ spec:
resourceFieldRef:
containerName: manager
resource: limits.memory
image: ghcr.io/fluxcd/notification-controller:v1.8.4
image: ghcr.io/fluxcd/notification-controller:v1.8.2
imagePullPolicy: IfNotPresent
livenessProbe:
httpGet:
@@ -6445,7 +6436,7 @@ metadata:
app.kubernetes.io/component: image-reflector-controller
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
name: imagepolicies.image.toolkit.fluxcd.io
spec:
group: image.toolkit.fluxcd.io
@@ -7037,7 +7028,7 @@ metadata:
app.kubernetes.io/component: image-reflector-controller
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
name: imagerepositories.image.toolkit.fluxcd.io
spec:
group: image.toolkit.fluxcd.io
@@ -7647,7 +7638,7 @@ metadata:
app.kubernetes.io/component: image-reflector-controller
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
name: image-reflector-controller
namespace: flux-system
---
@@ -7658,7 +7649,7 @@ metadata:
app.kubernetes.io/component: image-reflector-controller
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
control-plane: controller
name: image-reflector-controller
namespace: flux-system
@@ -7677,7 +7668,7 @@ spec:
app.kubernetes.io/component: image-reflector-controller
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
spec:
containers:
- args:
@@ -7756,7 +7747,7 @@ metadata:
app.kubernetes.io/component: image-automation-controller
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
name: imageupdateautomations.image.toolkit.fluxcd.io
spec:
group: image.toolkit.fluxcd.io
@@ -8585,7 +8576,7 @@ metadata:
app.kubernetes.io/component: image-automation-controller
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
name: image-automation-controller
namespace: flux-system
---
@@ -8596,7 +8587,7 @@ metadata:
app.kubernetes.io/component: image-automation-controller
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
control-plane: controller
name: image-automation-controller
namespace: flux-system
@@ -8615,7 +8606,7 @@ spec:
app.kubernetes.io/component: image-automation-controller
app.kubernetes.io/instance: flux-system
app.kubernetes.io/part-of: flux
app.kubernetes.io/version: v2.8.6
app.kubernetes.io/version: v2.8.3
spec:
containers:
- args:
@@ -8634,7 +8625,7 @@ spec:
resourceFieldRef:
containerName: manager
resource: limits.memory
image: ghcr.io/fluxcd/image-automation-controller:v1.1.2
image: ghcr.io/fluxcd/image-automation-controller:v1.1.1
imagePullPolicy: IfNotPresent
livenessProbe:
httpGet:
@@ -8,10 +8,10 @@ metadata:
spec:
interval: 1m0s
ref:
branch: master
branch: rehearsal-san
secretRef:
name: flux-system
url: https://gitea.renner.uno/k.renner/homelab.git
name: flux-system-breakglass
url: https://github.com/konairius/Homelab.git
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
@@ -18,7 +18,6 @@ resources:
- cloudnative-pg/
- mariadb-operator/
- netbird-ingress/
- netbird-proxy-services/
- coredns-config/
- k8s-gateway/
- external-dns-unifi/
@@ -1,108 +0,0 @@
# 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).
#
# 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.
apiVersion: v1
kind: ServiceAccount
metadata:
name: netbird-proxy-services
namespace: openbao
labels:
app.kubernetes.io/name: netbird-proxy-services
---
# 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
rules:
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["services"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: netbird-proxy-services-read
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: netbird-proxy-services-read
subjects:
- kind: ServiceAccount
name: netbird-proxy-services
namespace: openbao
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: netbird-proxy-services
namespace: openbao
labels:
app.kubernetes.io/name: netbird-proxy-services
spec:
replicas: 1
# One writer against the management API at a time.
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: netbird-proxy-services
template:
metadata:
labels:
app.kubernetes.io/name: netbird-proxy-services
spec:
serviceAccountName: netbird-proxy-services
securityContext:
runAsNonRoot: true
# Numeric, or the kubelet refuses the image's named user with
# "cannot verify user is non-root". The script needs no home and no
# writable path, so nobody/nogroup is enough.
runAsUser: 65534
runAsGroup: 65534
seccompProfile:
type: RuntimeDefault
containers:
- name: controller
# Pinned by digest: the date tag is rebuilt on every push of the
# 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
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
env:
- name: NB_MANAGEMENT_URL
value: https://netbird.renner.uno
- name: BAO_ADDR
value: http://openbao.openbao.svc.cluster.local:8200
- name: BAO_ROLE
value: netbird-proxy-services
# Must match cluster.network.serviceSubnets in talos/patches/cluster.yaml.
- name: SERVICE_CIDR
value: "10.96.0.0/12"
- name: TRAEFIK_SERVICE
value: ingress/traefik
- name: INTERVAL_SECONDS
value: "60"
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
memory: 64Mi
@@ -1,4 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
@@ -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"]
}
@@ -95,18 +95,12 @@ spec:
# Dedicated Ceph cluster network on VLAN 49, attached to the OSDs through
# Multus. The attachment is san-network-attachment.yaml.
#
# Only `cluster` is selected. Do not add `public`: under provider: host
# that puts every daemon including the mons on host networking. A mon's
# address then becomes the node's DHCP lease, and when a lease moves the mon
# crash-loops on "Cannot assign requested address" with no node free to
# fail over to, because all three already hold the mon host ports.
#
# addressRanges.cluster is stated, not discovered. Without it Rook runs a
# canary pod on an OSD node to read the CIDR off the attachment on EVERY
# reconcile; the attachment is host-device, so the OSD on that node already
# holds the NIC and the canary can never attach — the reconcile blocks for
# 15 minutes, fails, and repeats. The value must equal the whereabouts range
# in san-network-attachment.yaml.
# Only `cluster` is selected. Do not add `public`, and do not reach for
# `addressRanges` — that is honoured only under provider: host, which puts
# every daemon including the mons on host networking. A mon's address then
# becomes the node's DHCP lease, and when a lease moves the mon crash-loops
# on "Cannot assign requested address" with no node free to fail over to,
# because all three already hold the mon host ports.
#
# With `public` unset, client traffic stays on the pod network and VLAN 49
# carries Ceph-internal traffic only — which is also why Rook's Multus
@@ -123,9 +117,6 @@ spec:
provider: multus
selectors:
cluster: rook-ceph/ceph-san
addressRanges:
cluster:
- 10.99.0.0/29
# Nodes listed explicitly, not useAllNodes: under useAllNodes the operator
# re-provisions an OSD on any node whose device still carries a BlueStore
# signature, so purging an OSD before pulling its drive is impossible — it
@@ -93,17 +93,13 @@ spec:
# Real client-IP visibility. The netbird-ingress routing peer
# re-originates every mesh + WAN-via-proxy connection from its own
# pod IP (rootless userspace WireGuard), so Traefik's TCP peer for
# 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);
# 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.
proxyProtocol:
trustedIPs:
- "10.244.0.0/16"
- "fd10:244::/56"
# that traffic is always a pod IP (e.g. fd10:244::2de1) — collapsing
# all external clients to one source. The NetBird reverse-proxy
# (HTTP mode, on the VPS) sets X-Forwarded-For with the real WAN
# client IP; trust the pod CIDRs so Traefik reads it instead. Without
# this, a CrowdSec bouncer bans the shared pod IP and 403s everyone
# (the 2026-06-27 IPv6 outage). Harmless no-op when no XFF is present
# (direct LAN/mesh) — those keep their RemoteAddr.
forwardedHeaders:
trustedIPs:
- "10.244.0.0/16"
-7
View File
@@ -14,13 +14,6 @@ machine:
# Disabling v6 on a VLAN parent does not affect the VLAN child.
net.ipv6.conf.enp2s0f0np0.disable_ipv6: "1"
net.ipv6.conf.enp87s0.disable_ipv6: "1"
# That network also carries the mgmt VLAN's ARP. Answer ARP only on the
# link that owns the address, or the UDM learns the node IP against
# enp87s0's MAC and delivers IPv4 to a NIC Cilium is not attached to (#793).
net.ipv4.conf.all.arp_ignore: "1"
net.ipv4.conf.default.arp_ignore: "1"
net.ipv4.conf.all.arp_announce: "2"
net.ipv4.conf.default.arp_announce: "2"
install:
# Select disks by serial only. A broad selector such as
# `disk.transport == 'nvme'` matches every drive in the machine, and the
-5
View File
@@ -12,11 +12,6 @@ machine:
# cluster IPv6 egress.
net.ipv6.conf.enp2s0f0np0.disable_ipv6: "1"
net.ipv6.conf.enp87s0.disable_ipv6: "1"
# Answer ARP only on the link that owns the address — see k8s-1.yaml (#793).
net.ipv4.conf.all.arp_ignore: "1"
net.ipv4.conf.default.arp_ignore: "1"
net.ipv4.conf.all.arp_announce: "2"
net.ipv4.conf.default.arp_announce: "2"
install:
# Select the installer target by serial so it cannot land on the OSD drive
# or on EPHEMERAL, whatever order the kernel enumerates them in.
-5
View File
@@ -8,11 +8,6 @@ machine:
# k8s-1.yaml.
net.ipv6.conf.enp2s0f0np0.disable_ipv6: "1"
net.ipv6.conf.enp87s0.disable_ipv6: "1"
# Answer ARP only on the link that owns the address — see k8s-1.yaml (#793).
net.ipv4.conf.all.arp_ignore: "1"
net.ipv4.conf.default.arp_ignore: "1"
net.ipv4.conf.all.arp_announce: "2"
net.ipv4.conf.default.arp_announce: "2"
install:
# Select the installer target by serial so it cannot land on the OSD drive
# or on EPHEMERAL, whatever order the kernel enumerates them in.