UniFi Endpoint devices authenticate against the UDM RADIUS and send their real account name as the outer EAP identity; authentik clients send anonymous. FreeRADIUS now routes on that:
Outer identity anonymous / anonymous@… (any case): unchanged — EAP-TTLS terminated here, inner PAP to the authentik outpost, VLAN from dot1x_vlan, reject without VLAN.
Any other outer identity: the whole EAP conversation is proxied unprocessed to new home server udm (10.2.47.1:1812, the UDM on VLAN 47, secret UDM_RADIUS_SECRET from radius-secrets). post-proxy / post-auth / Post-Auth-Type REJECT leave UDM replies untouched (Tunnel-* VLAN, MS-MPPE keys, EAP-Success/Failure). No EAP state is kept locally for these, so rounds can land on either replica. A wrong authentik password is never tried against the UDM.
Accounting stays answered locally.
home_server udm: response_window 5 (< proxy_dedup_window 10, the rule that avoids the 3.2.10 proxy-hash segfault), require_message_authenticator yes (RFC 3579), status_check request with user udm-status-probe.invalid while zombie/dead.
freeradius.yaml: env UDM_RADIUS_SECRET (secretKeyRef radius-secrets; Reloader already watches the Secret).
freeradius-cnp.yaml: freeradius-udm-egress allows only 10.2.47.1/32 UDP 1812 (the namespace baseline has no LAN egress). Pod traffic to the LAN is not masqueraded (cilium-bgp.yaml), so the UDM sees the FreeRADIUS pod IPs as RADIUS source.
tests/radius: mock_udm.py on a second internal bridge at the udm address from proxy.conf. New checks: both EAP rounds (with State) of a non-anonymous login reach the mock; its Accept reaches the NAS with VLAN 200 and decryptable MS-MPPE keys; its Reject passes; IPv6 NAS works; Anonymous@Example.Org still goes to authentik; no anonymous login or accounting reaches the mock. run.sh: 55/55 pass.
docs/radius/dot1x.md: routing section; the anonymous outer identity is mandatory for authentik clients (Android/Linux steps updated).
UniFi Endpoint devices authenticate against the UDM RADIUS and send their real account name as the outer EAP identity; authentik clients send `anonymous`. FreeRADIUS now routes on that:
- **Outer identity `anonymous` / `anonymous@…`** (any case): unchanged — EAP-TTLS terminated here, inner PAP to the authentik outpost, VLAN from `dot1x_vlan`, reject without VLAN.
- **Any other outer identity**: the whole EAP conversation is proxied unprocessed to new home server `udm` (`10.2.47.1:1812`, the UDM on VLAN 47, secret `UDM_RADIUS_SECRET` from `radius-secrets`). post-proxy / post-auth / `Post-Auth-Type REJECT` leave UDM replies untouched (Tunnel-* VLAN, MS-MPPE keys, EAP-Success/Failure). No EAP state is kept locally for these, so rounds can land on either replica. A wrong authentik password is never tried against the UDM.
- Accounting stays answered locally.
`home_server udm`: `response_window 5` (< `proxy_dedup_window 10`, the rule that avoids the 3.2.10 proxy-hash segfault), `require_message_authenticator yes` (RFC 3579), `status_check request` with user `udm-status-probe.invalid` while zombie/dead.
- `freeradius.yaml`: env `UDM_RADIUS_SECRET` (secretKeyRef `radius-secrets`; Reloader already watches the Secret).
- `freeradius-cnp.yaml`: `freeradius-udm-egress` allows only `10.2.47.1/32` UDP 1812 (the namespace baseline has no LAN egress). Pod traffic to the LAN is not masqueraded (`cilium-bgp.yaml`), so the UDM sees the FreeRADIUS pod IPs as RADIUS source.
- `tests/radius`: `mock_udm.py` on a second internal bridge at the `udm` address from `proxy.conf`. New checks: both EAP rounds (with State) of a non-anonymous login reach the mock; its Accept reaches the NAS with VLAN 200 and decryptable MS-MPPE keys; its Reject passes; IPv6 NAS works; `Anonymous@Example.Org` still goes to authentik; no anonymous login or accounting reaches the mock. `run.sh`: 55/55 pass.
- `docs/radius/dot1x.md`: routing section; the anonymous outer identity is mandatory for authentik clients (Android/Linux steps updated).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Outer identity anonymous[@realm] (any case) keeps the local EAP-TTLS ->
authentik path. Any other outer identity is a UniFi Endpoint account: the
outer server proxies every EAP round to new home server udm (10.2.47.1:1812,
UDM_RADIUS_SECRET) without local EAP processing; post-proxy/post-auth leave
UDM replies untouched. Endpoint account names look like ordinary usernames,
so routing keys on the anonymous identity, not a name pattern.
home_server udm: response_window 5 < proxy_dedup_window 10,
require_message_authenticator yes (RFC 3579, EAP replies), status_check
request with a user name that cannot exist.
CNP freeradius-udm-egress allows only 10.2.47.1/32 UDP 1812.
Harness: mock_udm.py on a second internal bridge at the udm address from
proxy.conf. Asserts both EAP rounds (State included) reach it, its Accept
reaches the NAS with Tunnel-* VLAN 200 and MS-MPPE keys intact, its Reject
passes, and anonymous logins and accounting never reach it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
k.renner
requested review from review-bot 2026-11-04 12:15:45 +00:00
🤖Automated review — model claude-opus-5-5, commit bc698ea22db780fc7d0d2dbf0655ebb0c7cb0958.
Automated review by the review-bot: it approves only when it found nothing red or yellow, and a human still merges. Request a review from review-bot again for a fresh one.
Summary
This PR routes 802.1X requests by their outer EAP identity. anonymous and anonymous@… in any case keep the existing path: EAP-TTLS ends in FreeRADIUS and inner PAP goes to authentik. Every other outer identity has its whole EAP conversation proxied untouched to a new udm home server at 10.2.47.1:1812. The routing, the post-auth/post-proxy bypasses and the new egress policy are consistent with the existing config and match what the PR describes. I found no verified defects. The PR is mergeable, and no other open PR touches these files.
⚪ Unverified
The new secret must exist in OpenBao before rollout.kubernetes/apps/authentik/freeradius.yaml now requires radius-secrets key UDM_RADIUS_SECRET; the reference is not optional. radius-externalsecret.yaml pulls every key from apps/radius with dataFrom.extract, so the key appears only once it has been added in OpenBao. I can't see OpenBao from here. If the key is missing, new FreeRADIUS pods fail with CreateContainerConfigError and the rollout stalls. A config-only rollout would also leave $ENV{UDM_RADIUS_SECRET} in proxy.conf:166 unresolved. Confirm the key is there before merging.
The UDM must accept the FreeRADIUS pod IPs as RADIUS clients. The PR body says pod traffic to the LAN is not masqueraded, so the UDM sees pod IPs as the source. If the UDM only trusts the APs or a fixed address, every Endpoint login is silently dropped, and after zombie_period the home server is marked dead. I did not read cilium-bgp.yaml and cannot see the UDM config.
require_message_authenticator = yes inside home_server udm (freeradius-config.yaml:168). I could not confirm that the FreeRADIUS 3.2 image in use accepts this option in a home_server block. The existing authentik home server already sets it (:133), and the author reports 55/55 tests passing on the same image, so it most likely works.
🟡 Suggestions
Document the UDM-side setup.docs/radius/dot1x.md (new "Routing by outer identity" section) covers the FreeRADIUS side of the UDM target. It should also say what the UDM needs: the pod CIDR (or pod IPs) as a RADIUS client using the UDM_RADIUS_SECRET value. Right now that is only in the PR body.
Unknown outer identities now reach the UDM.freeradius-config.yaml:267 sends any non-anonymous outer identity to the UDM, including typos and authentik clients missing the anonymous identity. Such a client should fail the server-certificate/domain check against the UDM before it sends a password, so the risk is low. Still, a short note in the doc that a misconfigured authentik client fails at the UDM's TLS certificate step, not at authentik, would help anyone debugging it later.
🟢 Nits
freeradius-config.yaml:297 and :306: noop inside an if with an else is fine, but a negated condition (if (&control:Proxy-To-Realm != "udm") { … }) would be shorter.
Tests
Coverage is good for this change:
mock_udm.py on a separate internal bridge checks that both EAP rounds reach it, with State on the second.
Tunnel-* VLAN 200 and the MS-MPPE keys reach the NAS unchanged, over IPv4 and IPv6.
The UDM's reject is passed through.
The Anonymous@Example.Org case still goes to authentik.
Neither anonymous logins nor accounting ever reach the mock UDM.
The mock_outpost.py change (reading MOCK_PASSWORD inside main()) is needed so mock_udm.py can import its helpers without that variable set. One gap: no test covers the UDM home server going dead and recovering. That is optional.
Recommendation
Looks reasonable — no blocking issues; comments above are optional. Before merging, confirm that UDM_RADIUS_SECRET exists in OpenBao apps/radius and that the UDM's client list covers the pod IPs.
🤖 **Automated review** — model `claude-opus-5-5`, commit `bc698ea22db780fc7d0d2dbf0655ebb0c7cb0958`.
> Automated review by the `review-bot`: it approves only when it found nothing red or yellow, and a human still merges. Request a review from `review-bot` again for a fresh one.
---
## Summary
This PR routes 802.1X requests by their outer EAP identity. `anonymous` and `anonymous@…` in any case keep the existing path: EAP-TTLS ends in FreeRADIUS and inner PAP goes to authentik. Every other outer identity has its whole EAP conversation proxied untouched to a new `udm` home server at 10.2.47.1:1812. The routing, the post-auth/post-proxy bypasses and the new egress policy are consistent with the existing config and match what the PR describes. I found no verified defects. The PR is mergeable, and no other open PR touches these files.
## ⚪ Unverified
- **The new secret must exist in OpenBao before rollout.** `kubernetes/apps/authentik/freeradius.yaml` now requires `radius-secrets` key `UDM_RADIUS_SECRET`; the reference is not optional. `radius-externalsecret.yaml` pulls every key from `apps/radius` with `dataFrom.extract`, so the key appears only once it has been added in OpenBao. I can't see OpenBao from here. If the key is missing, new FreeRADIUS pods fail with `CreateContainerConfigError` and the rollout stalls. A config-only rollout would also leave `$ENV{UDM_RADIUS_SECRET}` in `proxy.conf:166` unresolved. Confirm the key is there before merging.
- **The UDM must accept the FreeRADIUS pod IPs as RADIUS clients.** The PR body says pod traffic to the LAN is not masqueraded, so the UDM sees pod IPs as the source. If the UDM only trusts the APs or a fixed address, every Endpoint login is silently dropped, and after `zombie_period` the home server is marked dead. I did not read `cilium-bgp.yaml` and cannot see the UDM config.
- **`require_message_authenticator = yes` inside `home_server udm` (`freeradius-config.yaml:168`).** I could not confirm that the FreeRADIUS 3.2 image in use accepts this option in a `home_server` block. The existing `authentik` home server already sets it (`:133`), and the author reports 55/55 tests passing on the same image, so it most likely works.
## 🟡 Suggestions
- **Document the UDM-side setup.** `docs/radius/dot1x.md` (new "Routing by outer identity" section) covers the FreeRADIUS side of the UDM target. It should also say what the UDM needs: the pod CIDR (or pod IPs) as a RADIUS client using the `UDM_RADIUS_SECRET` value. Right now that is only in the PR body.
- **Unknown outer identities now reach the UDM.** `freeradius-config.yaml:267` sends any non-anonymous outer identity to the UDM, including typos and authentik clients missing the anonymous identity. Such a client should fail the server-certificate/domain check against the UDM before it sends a password, so the risk is low. Still, a short note in the doc that a misconfigured authentik client fails at the UDM's TLS certificate step, not at authentik, would help anyone debugging it later.
## 🟢 Nits
- `freeradius-config.yaml:297` and `:306`: `noop` inside an `if` with an `else` is fine, but a negated condition (`if (&control:Proxy-To-Realm != "udm") { … }`) would be shorter.
## Tests
Coverage is good for this change:
- `mock_udm.py` on a separate internal bridge checks that both EAP rounds reach it, with `State` on the second.
- Tunnel-* VLAN 200 and the MS-MPPE keys reach the NAS unchanged, over IPv4 and IPv6.
- The UDM's reject is passed through.
- The `Anonymous@Example.Org` case still goes to authentik.
- Neither anonymous logins nor accounting ever reach the mock UDM.
The `mock_outpost.py` change (reading `MOCK_PASSWORD` inside `main()`) is needed so `mock_udm.py` can import its helpers without that variable set. One gap: no test covers the UDM home server going dead and recovering. That is optional.
## Recommendation
Looks reasonable — no blocking issues; comments above are optional. Before merging, confirm that `UDM_RADIUS_SECRET` exists in OpenBao `apps/radius` and that the UDM's client list covers the pod IPs.
<!-- pr-review:sha=bc698ea22db780fc7d0d2dbf0655ebb0c7cb0958 -->
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
UniFi Endpoint devices authenticate against the UDM RADIUS and send their real account name as the outer EAP identity; authentik clients send
anonymous. FreeRADIUS now routes on that:anonymous/anonymous@…(any case): unchanged — EAP-TTLS terminated here, inner PAP to the authentik outpost, VLAN fromdot1x_vlan, reject without VLAN.udm(10.2.47.1:1812, the UDM on VLAN 47, secretUDM_RADIUS_SECRETfromradius-secrets). post-proxy / post-auth /Post-Auth-Type REJECTleave UDM replies untouched (Tunnel-* VLAN, MS-MPPE keys, EAP-Success/Failure). No EAP state is kept locally for these, so rounds can land on either replica. A wrong authentik password is never tried against the UDM.home_server udm:response_window 5(<proxy_dedup_window 10, the rule that avoids the 3.2.10 proxy-hash segfault),require_message_authenticator yes(RFC 3579),status_check requestwith userudm-status-probe.invalidwhile zombie/dead.freeradius.yaml: envUDM_RADIUS_SECRET(secretKeyRefradius-secrets; Reloader already watches the Secret).freeradius-cnp.yaml:freeradius-udm-egressallows only10.2.47.1/32UDP 1812 (the namespace baseline has no LAN egress). Pod traffic to the LAN is not masqueraded (cilium-bgp.yaml), so the UDM sees the FreeRADIUS pod IPs as RADIUS source.tests/radius:mock_udm.pyon a second internal bridge at theudmaddress fromproxy.conf. New checks: both EAP rounds (with State) of a non-anonymous login reach the mock; its Accept reaches the NAS with VLAN 200 and decryptable MS-MPPE keys; its Reject passes; IPv6 NAS works;Anonymous@Example.Orgstill goes to authentik; no anonymous login or accounting reaches the mock.run.sh: 55/55 pass.docs/radius/dot1x.md: routing section; the anonymous outer identity is mandatory for authentik clients (Android/Linux steps updated).🤖 Generated with Claude Code
🤖 Automated review — model
claude-opus-5-5, commitbc698ea22db780fc7d0d2dbf0655ebb0c7cb0958.Summary
This PR routes 802.1X requests by their outer EAP identity.
anonymousandanonymous@…in any case keep the existing path: EAP-TTLS ends in FreeRADIUS and inner PAP goes to authentik. Every other outer identity has its whole EAP conversation proxied untouched to a newudmhome server at 10.2.47.1:1812. The routing, the post-auth/post-proxy bypasses and the new egress policy are consistent with the existing config and match what the PR describes. I found no verified defects. The PR is mergeable, and no other open PR touches these files.⚪ Unverified
kubernetes/apps/authentik/freeradius.yamlnow requiresradius-secretskeyUDM_RADIUS_SECRET; the reference is not optional.radius-externalsecret.yamlpulls every key fromapps/radiuswithdataFrom.extract, so the key appears only once it has been added in OpenBao. I can't see OpenBao from here. If the key is missing, new FreeRADIUS pods fail withCreateContainerConfigErrorand the rollout stalls. A config-only rollout would also leave$ENV{UDM_RADIUS_SECRET}inproxy.conf:166unresolved. Confirm the key is there before merging.zombie_periodthe home server is marked dead. I did not readcilium-bgp.yamland cannot see the UDM config.require_message_authenticator = yesinsidehome_server udm(freeradius-config.yaml:168). I could not confirm that the FreeRADIUS 3.2 image in use accepts this option in ahome_serverblock. The existingauthentikhome server already sets it (:133), and the author reports 55/55 tests passing on the same image, so it most likely works.🟡 Suggestions
docs/radius/dot1x.md(new "Routing by outer identity" section) covers the FreeRADIUS side of the UDM target. It should also say what the UDM needs: the pod CIDR (or pod IPs) as a RADIUS client using theUDM_RADIUS_SECRETvalue. Right now that is only in the PR body.freeradius-config.yaml:267sends any non-anonymous outer identity to the UDM, including typos and authentik clients missing the anonymous identity. Such a client should fail the server-certificate/domain check against the UDM before it sends a password, so the risk is low. Still, a short note in the doc that a misconfigured authentik client fails at the UDM's TLS certificate step, not at authentik, would help anyone debugging it later.🟢 Nits
freeradius-config.yaml:297and:306:noopinside anifwith anelseis fine, but a negated condition (if (&control:Proxy-To-Realm != "udm") { … }) would be shorter.Tests
Coverage is good for this change:
mock_udm.pyon a separate internal bridge checks that both EAP rounds reach it, withStateon the second.Anonymous@Example.Orgcase still goes to authentik.The
mock_outpost.pychange (readingMOCK_PASSWORDinsidemain()) is needed somock_udm.pycan import its helpers without that variable set. One gap: no test covers the UDM home server going dead and recovering. That is optional.Recommendation
Looks reasonable — no blocking issues; comments above are optional. Before merging, confirm that
UDM_RADIUS_SECRETexists in OpenBaoapps/radiusand that the UDM's client list covers the pod IPs.