# 802.1X via authentik FreeRADIUS terminates EAP-TTLS and proxies the inner PAP request to the authentik RADIUS outpost `802.1X Radius`. authentik checks the password and the access policy and returns the VLAN. ## Routing by outer identity FreeRADIUS decides on the outer EAP identity (`User-Name`) of each request, before any local EAP processing: | Outer identity | Path | |---|---| | `anonymous` or `anonymous@` (any case) | EAP-TTLS terminated in FreeRADIUS, inner PAP to authentik | | anything else | whole EAP conversation (all rounds, `State` included) proxied to the UDM RADIUS for UniFi Endpoint accounts | UniFi Endpoint clients send their real account name as outer identity; their names look like ordinary usernames, so the rule cannot match on name patterns. The UDM's Access-Accept/Reject/Challenge goes to the NAS unchanged, including its Tunnel-* VLAN and MS-MPPE keys. The "Accept without VLAN is rejected" rule applies to the authentik path only. Accounting is answered locally on both paths. The anonymous outer identity is mandatory for authentik clients: without it the login goes to the UDM and fails. UDM target: home server `udm`, `10.2.11.1` UDP 1812 (the UDM on VLAN 11, UniFi network "(11) RADIUS"), shared secret `UDM_RADIUS_SECRET` from OpenBao `apps/radius`. The UDM RADIUS accepts clients from `10.2.11.0/24` only, not from the pod CIDR. ### Path to the UDM Each FreeRADIUS pod has a second interface `net1` on VLAN 11 (NetworkAttachmentDefinition `freeradius-udm`, `kubernetes/apps/authentik/freeradius-network-attachment.yaml`): - macvlan on the node link `radius0` (VLAN 11 on `uplink`, no host addresses; VLANConfig in `kubernetes/talos/patches/k8s-{1,2,3}.yaml`). VLAN 11 is in Cilium's `bpf.vlanBypass`, otherwise the tagged frames are dropped on `uplink`. - IPv4 leased from the UDM DHCP server (cni-dhcp-daemon), requested without the routers option: `net1` holds only the connected `10.2.11.0/24`, no default route. The proxy socket is not bound to an address; the kernel's route lookup for `10.2.11.1` picks `net1` and its leased address as source. - IPv6 disabled on `net1` (VLAN 11 sends router advertisements). - The NAS listeners are bound to `eth0` (`interface = eth0`); nothing listens on `net1`. - Traffic on `net1` bypasses Cilium; there is no CNP for the UDM hop. `radius0` must exist on every node before FreeRADIUS pods are scheduled there: without it the macvlan ADD fails and the pod stays in `ContainerCreating`. ## authentik objects Defined in `kubernetes/apps/authentik/blueprints-configmap.yaml`, key `dot1x-radius.yaml`: | Object | Name | Purpose | |---|---|---| | Flow | `dot1x-authentication-flow` | identification, password, login; no MFA, no enrollment | | Expression policy | `dot1x-has-vlan` | admits users with a valid `dot1x_vlan` group | | RADIUS property mapping | `dot1x-vlan` | puts the VLAN into the Access-Accept | | RADIUS provider | `802.1X Radius` | shared secret `DOT1X_OUTPOST_SECRET`, clients = cluster pod CIDRs | | Application | `dot1x` | bound to `dot1x-has-vlan` | | Outpost | `802.1X Radius` | radius, 2 replicas, ClusterIP, namespace `authentik` | The outpost's Kubernetes objects are named from the slugified outpost name: Service/Deployment `ak-outpost-8021x-radius`, pod label `goauthentik.io/outpost-name: 8021x-radius`. The shared secret is key `DOT1X_OUTPOST_SECRET` of Secret `radius-secrets` (ExternalSecret from OpenBao `apps/radius`), passed to the authentik worker only (it applies the blueprints) as env `DOT1X_OUTPOST_SECRET`, with `optional: true`: a missing Secret fails only the dot1x blueprint, not authentik. Reloader restarts the worker when `radius-secrets` changes. On the FreeRADIUS → outpost hop the shared secret is the only guard: the authentik namespace baseline admits all same-namespace pods, and the provider's client_networks is the pod CIDR. FreeRADIUS resolves the outpost Service name at startup; a re-created outpost Service needs the FreeRADIUS pods restarted. ## VLAN convention - A group carries the VLAN as attribute `dot1x_vlan`, an integer VLAN id (1–4094), e.g. `dot1x_vlan: 50`. It must be an unquoted integer: a string such as `"50"` is ignored and the user is denied. - Membership counts through group inheritance (`User.all_groups()`). - A user without any such group is rejected (policy `dot1x-has-vlan`). - A user in several such groups gets the lowest `dot1x_vlan`. - The same attribute applies to WiFi and wired 802.1X. Groups and memberships are managed by the owner in authentik, not in blueprints. ## Reply format The mapping sets both: - RFC 3580 `Tunnel-Type = VLAN`, `Tunnel-Medium-Type = IEEE-802`, `Tunnel-Private-Group-Id = ""` (raw attribute codes 64, 65, 81; the dictionary bundled with authentik does not define them). - `Cisco-AV-Pair = "dot1x-vlan="`. Contract for FreeRADIUS: if the outpost reply carries the standard Tunnel-* attributes, use them; otherwise translate `Cisco-AV-Pair = "dot1x-vlan="` into `Tunnel-Type = VLAN`, `Tunnel-Medium-Type = IEEE-802`, `Tunnel-Private-Group-Id = ""`. An Access-Accept without a VLAN is rejected. ## Test-phase entries The FreeRADIUS clients `test` (10.2.50.0/24) and `test6` (fd02:50::/64) and their CIDRs in `freeradius-cnp.yaml` are for the test phase only and must be removed after the test. ## UniFi objects Created through the UniFi Network API (site `default`): | Object | ID | Settings | |---|---|---| | Firewall policy "Allow APs RADIUS" | `62c21e63-8c15-4720-adf8-b2333c090e2f` | zone Isolated `10.2.10.0/24` → zone External `10.2.99.91`, UDP `1812-1813`, ALLOW, IPv4 | | RADIUS profile "Cluster 802.1X" | `3e5fcfed-63dc-4e7d-ad51-17cbc074e4bb` | auth `10.2.99.91:1812`, acct `10.2.99.91:1813`, secret `DOT1X_NAS_SECRET`, VLAN assignment on (`vlan_wlan_mode: optional`) | | WLAN "Renner UniFi" | `ef4122f2-4a83-4dca-9ef3-468d29f39a6b` | WPA3 Enterprise (no transition), PMF required, fast roaming, BSS transition, client isolation off, RADIUS profile "Cluster 802.1X"; Application Hotspot, type Passpoint (set in the UniFi UI: the API format differs from the UI's) | Passpoint on "Renner UniFi" (UniFi offers Passpoint only with WPA3 Enterprise): venue "Renner", Residential / Private Residence, network type Private with Guest Access, IPv4 single NATed private, IPv6 available, NAI realm `renner.uno` with EAP-TTLS, sub-methods PAP and username/password, domain `renner.uno`, operator friendly name "Renner", connection capability ICMP allow. The AKMs on air are `FT/802.1X` and `802.1X/SHA-256`; plain `802.1X` is not offered. ## Live login check `tests/radius/live.sh` runs EAP-TTLS/PAP logins with `eapol_test` against the deployed FreeRADIUS from a test client range and prints result, duration and the VLAN attributes of the Access-Accept (hex) per login; secrets are read from files and never printed: ``` LIVE_PASSWORD_VAR=CLAUDE_BOT_PASSWORD tests/radius/live.sh 10.2.99.91 claude-bot \ ~/.config/claude-bot/authentik ~/.config/claude-bot/radius-secret 20 ``` For IPv6 set `LIVE_SOURCE` to the workstation's `fd02:50::` address and use `fd99::91`. `group-id=[3530]` is VLAN `50`, `tunnel-type=[0000000d]` VLAN, `medium=[00000006]` IEEE-802. ## Client setup (SSID "Renner UniFi") Setup page: `https://wlan.renner.uno` (behind the Authentik gate, any authentik user), served by the Go service `wlan-setup/` (`kubernetes/apps/authentik/wlan-setup*.yaml`). Android, Apple and Windows get Passpoint profiles with their own app password; Linux an `nmcli` command (NetworkManager has no Passpoint support) that carries no password. Passpoint: `//install` (`android`, `apple` or `windows`) signs the user in through the authentik OAuth2 application `wlan-setup` (scope `goauthentik.io/api`). On `//callback`, the service exchanges the code, creates an app password (`wlan----`) with the user's own access token, and hands out the profile: - Android: `application/x-wifi-config` in the response to that GET, as its installer requires. - Apple: a `.mobileconfig` download. - Windows: a CarrierProvisioning XML signed with XML-DSig (enveloped, C14N 1.0, rsa-sha1, chain in KeyInfo) by the Let's Encrypt certificate `wlan-setup-signing`; kept for 5 minutes under a single-use token. The browser is sent to `ms-settings:wifi-provisioning?uri=https://wlan.renner.uno/windows/profile/`; Windows Settings fetches it (Ingress `wlan-setup-provisioning`, without the gate) and offers "Add". The service stores nothing else. Deleting the app password in authentik removes that device from the WLAN. The profiles match WLANs that advertise Hotspot 2.0 with domain and NAI realm `renner.uno` (EAP-TTLS/PAP), i.e. "Renner UniFi". Creating the app password needs no admin rights. App passwords expire when unused. The page creates them with 29 days (the API caps new tokens at 30 days unless the group attribute `goauthentik.io/user/token-maximum-lifetime` allows more). Every 802.1X login with an app password renews it to 60 days: the policy `dot1x-app-password-renew` on the `dot1x-login` binding (at most one write per day). authentik rejects expired app passwords and deletes them in its `clean_expired_models` task. A device that has not connected for 60 days, or a profile that was never installed, loses its app password and needs a new profile from the page. All clients use EAP-TTLS with inner PAP. The server certificate is a public Let's Encrypt certificate for `radius.renner.uno`, so no CA needs to be installed. The 802.1X password stage (`dot1x-password`) accepts the authentik password and app passwords. PAP runs inside the TLS tunnel to FreeRADIUS and is never sent in clear over the air. ### Android Passpoint profile from the setup page ("Auf Android installieren"). ### Linux (NetworkManager) - Security: WPA & WPA2/3 Enterprise - Authentication: Tunneled TLS (TTLS) - Inner authentication: PAP - CA certificate: system bundle (e.g. `/etc/ssl/certs/ca-certificates.crt`) - Domain: `radius.renner.uno` - Anonymous identity: `anonymous` (mandatory) - Username / password: authentik credentials ### Windows 11 Passpoint profile from the setup page ("Auf Windows installieren"). Manual setup without the page: Settings > Network & Internet > Wi-Fi > Manage known networks > Add network: - Network name: `Renner UniFi` - Security type: WPA3-Enterprise AES - EAP method: EAP-TTLS - Authentication method: Unencrypted password (PAP) - Server validation: where offered ("Verify server identity" / trusted server name), set `radius.renner.uno` - Do not use Windows sign-in credentials; enter the authentik credentials On the first join, check the certificate details before connecting: it must be issued to `radius.renner.uno` by Let's Encrypt. Otherwise cancel. If EAP-TTLS is not offered, use Control Panel > "Manually connect to a wireless network" instead. ### Apple (iOS, iPadOS, macOS) Passpoint configuration profile from the setup page ("Profil laden"), installed via Settings (Profile Downloaded) or, on a Mac, System Settings > General > Device Management. It sets `IsHotspot` with domain and NAI realm `renner.uno` (no SSID), security type Any, outer identity `anonymous`, trusted server name `radius.renner.uno`, username and app password; it contains no certificate. Its PayloadIdentifier is fixed, so installing it replaces an earlier profile. Apple devices may show a one-time "trust certificate" prompt for `radius.renner.uno` on the first join. ## Measurement: authentication latency per SSID The UniFi access points log station events (`stahtd`) to Loki. The field `radius_auth_delta` (microseconds) is in a JSON payload after `stahtd_dump_event(): `. VAP names per SSID are looked up through the UniFi API: `stat/device`, `vap_table`, map `essid` to `name`. The JSON mixes `"key":"v"` and `"key": "v"`, so the filters tolerate an optional space. Values are microseconds. Run once per VAP of "Renner Test" and of "Renner UniFi" and compare avg and max: ``` logcli instant-query --quiet \ 'avg_over_time({source="unifi-syslog",app="stahtd"} |~ "\"vap\": ?\"\"" | regexp "radius_auth_delta\": ?\"?(?P[0-9]+)" | unwrap d [24h])' logcli instant-query --quiet \ 'max_over_time({source="unifi-syslog",app="stahtd"} |~ "\"vap\": ?\"\"" | regexp "radius_auth_delta\": ?\"?(?P[0-9]+)" | unwrap d [24h])' ``` | SSID | VAP(s) | Events | avg radius_auth_delta (us) | max radius_auth_delta (us) | |---|---|---|---|---| | Renner Test | | | | | | Renner UniFi | | | | |