Blueprint identifiers for PolicyBindings include `order`, so changing
the order value between commits created orphan bindings instead of
updating in-place. UI showed four bindings on the unifi Application:
two Admins (order 0 + 1) and two Users (order 0 + 10). Add explicit
state:absent entries for the order-0 Admins and order-10 Users
orphans; the keeper bindings (Users/0, Admins/1) are unchanged.
Entries left in the blueprint permanently — state:absent is
idempotent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Existing Synology Application admits both `Synology Users` and
`Synology Admins`; I only added the admins binding for Unifi which
left the pre-migration `Unifi Users` group (g/j/s.renner, t.geiger,
…) unable to bind via the unifi Application policy. Mirror the
Synology pattern with two PolicyBindings — Users at order 0, Admins
at order 1. The Unifi Users group is declared `state: present`
identifier-only so the M2M membership is preserved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
LDAP outpost dispatch in ldap_tls.go falls back to the outpost's
self-signed default whenever any attached provider has cert=nil — so
attaching the LE cert to Unifi and Synology alone wasn't enough,
because the all-identical-certs branch keeps failing on Jellyfin's
empty cert. Set Jellyfin's certificate + tls_server_name to the same
shared keypair; provider stays otherwise dormant.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Activates the dormant `LDAP Unifi` provider for UniFi Network admin
auth: rebases onto sibling `DC=unifi,DC=renner,DC=uno` to dodge the
existing Jellyfin/Unifi base-DN dispatch collision (same fix Synology
got in 90906e2), adds Unifi Admins group / unifi Application /
PolicyBinding. Exposes LDAPS on 636 using an LE cert from
cert-manager (DNS-01 via letsencrypt-cluster), imported into Authentik
as a CertificateKeyPair from env vars on the server + worker pods.
Cert is also attached to LDAP Synology so DSM's eventual 389→636
cutover only needs a DSM-side reconfigure.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DSM's smbd hardcodes the user lookup filter to
(&(uid=%u)(objectClass=sambaSamAccount)). Authentik's LDAP outpost
emits uid=<random User.uid hash> by default, so that filter never
matched and SMB authentication was failing at user lookup —
before NTLM verification even ran.
bridge commit b06668e: synthesise "uid": user.username into
user.attributes, so the outpost's AttributesToLDAP path overrides
the EnsureAttributes default. The override is gated by
SAMBA_BRIDGE_ENABLED_GROUP ("Synology Users"); users outside that
group still emit upstream's uid=<hash>.
Verified live before this rollout: smbclient -L //nas-1.renner.uno
-U k.renner authenticates and lists shares including the personal
"home" share, after directly injecting uid=k.renner via SQL into
the same JSONField the bridge writes. Bumping the bridge image so
the next password-change cycle (and the migration backfill for
existing users) writes this value automatically.
The ldap-outpost image tag is unchanged (sambaSamAccount +
sambaDomain GetUserOCs() patches already in 0140972; this commit
only updates Python in the server image).
DSM smbd live diagnostic on /var/log/samba/log.smbd revealed a
second LDAP gate after the sambaSamAccount one: every SMB session
attempt fires (&(sambaDomainName=synology)(objectClass=sambaDomain))
before user auth. Without a matching entry, smbd's
smbldap_search_domain_info tries to ADD one (outpost is read-only)
and bails with NT_STATUS_UNSUCCESSFUL, blocking SMB auth entirely.
Two changes:
- Patched outpost bumped to 2026.2.3-0140972, which extends
GetUserOCs() to recognise sambaDomain (in addition to
sambaSamAccount). Without this the outpost's search dispatcher
short-circuits sambaDomain queries at the gate before consulting
any user entry.
- New service-account user "samba-domain-synology" with
user.attributes carrying objectClass=sambaDomain and the required
sambaDomainName / sambaSID / sambaNextRid fields. The outpost
emits this entry on DSM's sambaDomain query; DSM accepts it as
the domain-info answer and proceeds with auth.
sambaSID on the marker MUST match SAMBA_BRIDGE_DOMAIN_SID and the
SID DSM has been pinned at, or DSM rejects the domain. All three
share S-1-5-21-700609483-19441523-1711414274 .
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
09b29dd carries default=True on SambaBridgeConfig so Django actually
picks our AppConfig subclass instead of falling back to the vanilla
django.apps.AppConfig (which has no tenant_schedule_specs property
and caused the scheduler exception-loop in the previous deploy).
Floating :latest doesn't trigger re-pull on Kubernetes reschedule —
need an explicit tag bump to roll the deployment. 279da54 carries the
ManagedAppConfig fix that resolves the tenant_schedule_specs
AttributeError seen in the 62f9540 rollout.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Deploys the authentik-samba-bridge stack:
gitea.renner.uno/k.renner/authentik-samba-bridge
Three pieces:
1. Server image override (helmrelease.yaml). global.image points at
the bridge-server image (Authentik server + our Django app that
subscribes to password_changed and writes Samba schema attrs to
user.attributes). global.envFrom adds SAMBA_BRIDGE_DOMAIN_SID,
SAMBA_BRIDGE_ENABLED_GROUP, SAMBA_BRIDGE_UID_OFFSET from a new
SOPS secret. Both server AND worker pods inherit the bridge —
the signal receiver registers in any Authentik server process
that calls User.set_password().
2. LDAP outpost image override (blueprints-configmap.yaml).
container_image on the LDAP Outpost record swaps the upstream
outpost for our patched build that adds "sambaSamAccount" to
GetUserOCs(). Without this, DSM's smbd filter
(&(uid=...)(objectClass=sambaSamAccount)) short-circuits to zero
entries inside the upstream outpost regardless of what's in
user.attributes.
3. samba-bridge-secret.sops.yaml — the bridge's env contract. The
domain SID was generated once from /dev/urandom and is now
frozen forever (must match the SID typed into DSM's "Configure
Samba Authentication" wizard).
Post-deploy: existing Synology Users members need to change their
password once via auth.renner.uno for the bridge to capture the
plaintext and synthesise the NT hash. Argon2 hashes already on
file can't be reverse-engineered into NT hashes; password reset
is the only path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DC=synology,DC=authentik,DC=renner,DC=uno is a child of the existing
Unifi/Jellyfin base; the outpost's HasSuffixNoCase dispatch matches
on first iteration without longest-match preference, so binds still
landed on Jellyfin's policy. Moving Synology to DC=synology,DC=renner,
DC=uno makes it a sibling subtree the others' suffix check can't claim.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Live binds from nas-1 land in the Jellyfin Application's policy
evaluation because the outpost dispatches by base_dn suffix match —
with LDAP Unifi/Jellyfin/Synology all sharing DC=authentik,DC=renner,
DC=uno, the alphabetically-first provider wins every request. Moving
Synology under DC=synology,DC=authentik,DC=renner,DC=uno gives the
outpost a unique suffix to route on. Unifi/Jellyfin are left alone;
neither is LAN-reachable yet so the duplicate-base-DN there is dormant
config, not in this change's scope.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Blueprint-manages a new LDAP integration for nas-1 DSM: dedicated
LDAPProvider, Application, Synology Users/Admins groups, and PolicyBindings.
Standalone LoadBalancer Service on the BGP pool (pinned 10.2.99.88/fd99::88,
LAN hostname ldap.renner.uno via external-dns-unifi) sidesteps the operator-
owned ClusterIP; CNP scopes :3389 ingress to the mgmt LAN so the cleartext
bind stays on a trusted segment until LDAPS is wired.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
paperless.k.renner.uno and docs.k.renner.uno were briefly stood up as
migration aliases and never put into use. Because they were orange-cloud
proxied via external-dns/cloudflare, Cloudflare advertised ECH for them,
but Cloudflare's Universal SSL cert only covers <zone> + *.<zone> — it
does not cover two-level subdomains like *.k.renner.uno. Chrome's ECH
handshake therefore reached Cloudflare, got back an empty/invalid
fallback cert, and surfaced ERR_ECH_FALLBACK_CERTIFICATE_INVALID
whenever someone landed on the .k. alias (bookmark, autocomplete, etc.).
Removed: paperless-aliases-ingress.yaml (Ingress + RedirectRegex
Middleware), the kustomization entry, both .k. URLs from
PAPERLESS_CSRF_TRUSTED_ORIGINS, the two paperless.k/docs.k checks in
e2e-test.sh, and stale alias references in surrounding comments.
Note: external-dns is upsert-only (no record cleanup), so the
paperless.k and docs.k CNAMEs in the Cloudflare zone need manual
deletion from the Cloudflare dashboard.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
These four endpoints have been reporting failure every monitor cycle:
the three HTTP hostnames (media/wiki/influx.renner.uno) are NXDOMAIN
with no Ingress in any namespace, and tcp://10.2.40.135:5432 no longer
responds (no TCP, no ICMP). Removed from in-cluster gatus to stop the
continuous false-failure noise; tombstones left in place for context.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
At Information level .NET emits 4 lines per outbound HTTP call
(Start/Sending/Received/End). With ~13 polls/sec to the Shelly fleet
that's ~53 log lines/sec — roughly 40% of total cluster log volume,
all of it duplicate per-request bookkeeping. Pin the named-handler
log level to Warning so real failures still surface.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The radius binary listens on :9300 like ldap/proxy do — verified by
port-forward, returns authentik_outpost_* metrics. But authentik's
Python operator at providers/radius/controllers/kubernetes.py omits
the DeploymentPort(9300, "http-metrics", "tcp", 9300) entry from its
`deployment_ports` list. ldap's controller has it, radius does not.
Net effect: the operator-managed Service exposes only UDP 1812 and
the operator-created ServiceMonitor points at a port that does not
exist on the Service it selects.
Add a sibling headless Service that selects the radius pod labels
and exposes 9300/TCP, plus a ServiceMonitor with the release label
so Prometheus scrapes it. Zero pod changes — the listener is
already there.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The goauthentik.io operator auto-creates ServiceMonitors per outpost
but does not stamp `release: kube-prometheus-stack`, so this cluster's
serviceMonitorSelector silently ignores them. Declare standalone SMs
with the correct label, following the Velero precedent.
Radius is omitted on purpose: its container exposes only UDP 1812,
no /metrics endpoint. The operator-created SM for radius is dead
regardless of label.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Server pods read 75m sustained CPU on a 100m request, tripping
KubeHpaMaxedOut without the cluster being under real load. The
asymmetry between pods correlates with outpost HTTP/2 connection
pinning + a brand_tls thundering herd in the Go router, but the
log-visible work only accounts for ~30m of the 75m. Authentik's
native /metrics endpoint (port 9300) was never enabled and the
chart's built-in ServiceMonitor was off, so per-handler and queue
metrics were invisible. Enable both and add the release label so
kube-prometheus-stack's serviceMonitorSelector picks it up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Applied live to k8s-{1,2,3}:
1. Removed stale `driver: virtio_net` interface entry — leftover from
before the 2026-05-13 busPath migration. Strategic merge keys
interfaces by deviceSelector, so changing the key from
`driver: virtio_net` to `busPath: 0000:06:12.0` left both entries
in the machineconfig, both claiming VIP 10.2.40.60.
2. Deduped the i40e Ceph `addresses` list. The strategic merge for
the addresses array appends rather than dedupes, so repeated
talosctl patch applications had accumulated 4× the same /29 entry.
k8s-edge was already clean — no Ceph i40e, never had a driver-key
entry.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both changes broke ingress: dummy0 claiming the LB pool prefixes
(10.2.99.0/24 + fd99::/64) on every node intercepted TCP destined for
Service VIPs at the kernel routing layer, beating Cilium's eBPF LB to
the punch and breaking Traefik / external-dns / cert-manager / many
others that talk to the K8s API ClusterIP. Repeated talosctl patch
applications also accumulated cruft in the live machineconfig
(duplicate addresses on i40e, two interface entries claiming the
control-plane VIP).
dummy0 has been removed from all four nodes live via JSON-patch
op:remove. sysctls block removed similarly. ICMP-to-LB-IP loop trip
is back on the table — Cilium 1.19's eBPF LB still doesn't intercept
ICMP, and the AnyIP fix needs to be designed not to interfere with
the eBPF LB's TCP/UDP path. The simple "claim the whole pool prefix
on dummy0" pattern does interfere; a per-VIP /32+/128 claim per
endpoint-hosting node is the next thing to try, but that's a separate
follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Talos defaults net.ipv4.conf.*.rp_filter to 1 (strict). Strict RPF
drops packets whose return path doesn't match the ingress interface,
which kills the dummy0 AnyIP trick: ICMP to an LB VIP arrives on eth0
but the dst is claimed by dummy0, so strict RPF treats it as spoofed
and drops silently. Loose (2) accepts the packet as long as the source
is routable via any interface — standard for Kubernetes BGP setups.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cilium's eBPF LB intercepts TCP/UDP destined for Service VIPs before
the kernel routing path, but ICMP/ICMPv6 isn't intercepted — packets
hit the kernel, the kernel has no interface owning the LB IP, default
route forwards back to the UDM, BGP routes back to a node, loop until
hop-limit exceeded. The "AnyIP" routing fix is to put the LB pool
prefixes on a dummy interface so the kernel claims them as local and
answers ICMP directly. Documented in the MetalLB BGP guide and in
Talos's machine.network.interfaces.dummy schema.
Applied to all four nodes (k8s-1/2/3/edge) since traffic may land on
any of them; the dummy interface is per-node. The /24 + /64 covers
the full bgp-pool range, future-proofing against new LB allocations
without further node config changes.
This is a kernel-network change — applying the patch requires a
rolling reboot of each node (talosctl patch --mode=auto + drain,
wait for Ceph HEALTH_OK between nodes per the wait-for-ceph rule).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 0.0.0.0 explicit bind constrained the listeners to IPv4 only,
which made TCP to the v6 LB VIP fd99::81 return Connection refused.
Without a bind address mosquitto binds to all interfaces (v4 + v6
wildcard), matching the dual-stack Service we just shipped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The single v4-transport multiprotocol session couldn't actually deliver
v6 routes: Cilium 1.19 BGPv2 advertises extended-nexthop capability
only for the IPv4 Unicast AFI/SAFI tuple, so v6 routes sent with the
IPv4-mapped MpReach next-hop landed in the UDM's BGP RIB but never
made it to the kernel FIB. The kernel can't resolve an IPv4-mapped v6
address as a v6 next-hop on a v6 interface, and there's no CRD field
in Cilium 1.19 to fix the capability advertisement.
Solution is structural: run two parallel sessions, one per family.
fd40::/64 already has SLAAC addresses on every cluster node and a
ULA fd40::1 on UDM's br40, so peering natively over v6 needs no new
addressing — just a second peer entry on each side and an additional
listen range on UDM. v6 routes flow with v6 next-hops, v4 routes
flow with v4 next-hops, neither path depends on extended-nexthop.
fd40::1 (ULA) is the v6 peer address rather than the GUA so the
session survives ISP renumber events.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cilium BGPv2 carries v6 unicast routes over the single IPv4 BGP session
with the peer's v4 source as the MpReach Nexthop (RFC 5549/8950). FRR
must advertise the extended-nexthop capability to accept and install
those routes; without it the v6 SAFI completed negotiation, Cilium sent
the /128s, but the UDM either rejected them or installed them as
unresolvable. Result: ping -6 mqtt.renner.uno fell through to the
default route and the ISP gateway returned ICMPv6 No Route.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
home-assistant-bgp, mosquitto-iot, ftp-scanner, syncthing-sync, and
alloy-syslog flipped from SingleStack/IPv4 to PreferDualStack with
fd99::<tail> pinned via the lbipam annotation. external-dns will
publish AAAA records alongside the existing A records; v6-capable
clients pick IPv6 first via Happy Eyeballs while legacy v4-only
clients keep working unchanged. ipFamilies is [IPv4, IPv6] because
Kubernetes forbids reordering the field on an existing Service —
flipping the primary stack would require deleting and recreating
each Service, deferred.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
vscode reaches the cluster only via Traefik at vscode.home.renner.uno;
the LoadBalancer at 10.2.99.0 was never consumed by anything else.
Traefik routes equally well to a ClusterIP, so the LB IP is dead
weight and the BGP advertisement for 10.2.99.0/32 goes away. Dropped
the lb-pool=bgp label and externalTrafficPolicy: Local along with it
since neither applies to ClusterIP.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
iot-pool no longer exists (removed in e1d8ecd). The static macvlan
addresses on v30iot — shelly-manager (.228), matter-server (.226),
otbr (.227) — live outside Cilium IPAM and don't need to reference a
deleted pool. Also dropped stale .224/.225 home-assistant/mosquitto
allocation numbers from the matter-server and otbr comments — those
Services were on .80/.81 (now retired/migrated), never on .224/.225.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The iot-pool LoadBalancerIPPool and iot CiliumL2AnnouncementPolicy
became orphaned when home-assistant-iot (the only L2-announced
Service) was retired in ac57502. No remaining Service uses the
iot-vlan label, so the policy never elects a lease holder and the
pool never allocates an IP.
Moved bgp-pool into cilium-bgp.yaml (it was the only other resource
in cilium-l2.yaml) and deleted cilium-l2.yaml entirely; the file's
name no longer described its contents. The matter-server, otbr, and
shelly-manager macvlan NetworkAttachments still hand-allocate IPs
from the 10.2.30.0/24 block — those are outside Cilium IPAM and
unaffected by this cleanup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
All Shellies (Gen1 and Gen2) are on MQTT to mosquitto-iot, so nothing
on v30iot needs the L2-announced HTTP entry point at 10.2.30.80 any
more. ha.renner.uno (10.2.99.87, bgp-pool, ETP=Local) covers the
remaining LAN-side HA HTTP traffic. Removing this also eliminates the
Cilium L2-lease-on-wrong-node trap that broke TTS earlier today.
iot-pool LoadBalancerIPPool and the iot CiliumL2AnnouncementPolicy in
infrastructure/post-crds/cilium-l2.yaml are now orphaned (no Services
match their selectors). Left in place: the macvlan NetworkAttachments
for shelly-manager, matter-server, and otbr still hand-allocate IPs
from the 10.2.30.0/24 block and reference iot-pool in comments. Pool
cleanup is a separate decision.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds home-assistant-bgp Service on the bgp-pool with ETP=Local and
external-dns publishing ha.renner.uno via the UniFi LAN zone. Avoids
the Cilium L2-announce path that was firing the ETP=Local-on-L2 trap
(lease landed on a non-pod-hosting node, traffic dropped). With BGP,
the VIP is advertised only from the HA pod's host, so v30iot and LAN
clients reach the correct node directly — useful for ESPHome devices
fetching TTS audio via a DNS hostname.
home-assistant-iot at 10.2.30.80 stays as the legacy entry for devices
with hard-coded IPs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
HA's ip_bans cleanup is done — the 50 stale 10.2.x.x entries that
collided with real per-device LAN IPs have been cleared, so flipping
Traefik back to ETP=Local no longer 403s LAN clients. The other six
BGP-pool Services have been on ETP=Local since 1146bc6 without
regression; Traefik now joins them. Source IPs at backends behind
Traefik are now real client IPs (preserved through the HA sidecar
chain via X-Forwarded-For).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previous comment implied the fix was to add LAN CIDRs to HA's
trusted_proxies; that misses the nginx-sidecar architecture HA uses
specifically to keep trusted_proxies loopback-only and avoid restart
churn on every ingress reschedule. Real fix path is ip_bans cleanup
plus login_attempts_threshold tuning in the HA config repo.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>