feat(radius): Android Passpoint profile on the WLAN setup page #1565

Merged
k.renner merged 2 commits from feat/wlan-setup-passpoint into main 2026-11-05 10:37:22 +00:00
Member

Android section on wlan.renner.uno: one-tap Passpoint profile install.

Flow (Android installs a Passpoint R1 profile only from a user-triggered HTTP GET with Content-Type: application/x-wifi-config, Content-Transfer-Encoding: base64, no Content-Disposition — source.android.com, Passpoint):

  1. The user types the authentik password; the page POSTs it to /android/prepare.
  2. nginx (njs module of the house image, njs 1.0.2) builds the profile and stores it in a shared dict under a random 24-byte token: 120 s, bound to the caller's X-authentik-username.
  3. The page shows the link /android/profile/<token>; the tap GETs the profile once, then the token is gone. Other users get 404.

Profile: base64 of a MIME multipart with

  • PPS MO: HomeSP FQDN renner.uno, friendly name Renner; Credential realm renner.uno, UsernamePassword (username, base64 password), EAP-TTLS (21) with inner PAP; Extension/Android/AAAServerTrustedNames radius.renner.uno.
  • Trust anchor ISRG Root X1 (DER). The radius-tls chain is radius.renner.uno ← YR2 ← Root YR ← ISRG Root X1.

It matches WLANs that advertise Hotspot 2.0 with domain and NAI realm renner.uno.

Changes:

  • wlan-setup-config.yaml: nginx.conf (image default plus load_module njs, one worker), passpoint.js, locations /android/prepare (POST only, body ≤ 1k) and /android/profile/<token>, Android section of the page. The access log format has no request body.
  • wlan-setup.yaml: mounts for nginx.conf (subPath; the reloader annotation rolls the pod on ConfigMap changes) and the njs directory.
  • docs/radius/dot1x.md: Android path.

🤖 Generated with Claude Code

Android section on `wlan.renner.uno`: one-tap Passpoint profile install. Flow (Android installs a Passpoint R1 profile only from a user-triggered HTTP GET with `Content-Type: application/x-wifi-config`, `Content-Transfer-Encoding: base64`, no `Content-Disposition` — source.android.com, Passpoint): 1. The user types the authentik password; the page POSTs it to `/android/prepare`. 2. nginx (njs module of the house image, njs 1.0.2) builds the profile and stores it in a shared dict under a random 24-byte token: 120 s, bound to the caller's `X-authentik-username`. 3. The page shows the link `/android/profile/<token>`; the tap GETs the profile once, then the token is gone. Other users get 404. Profile: base64 of a MIME multipart with - PPS MO: HomeSP FQDN `renner.uno`, friendly name `Renner`; Credential realm `renner.uno`, UsernamePassword (username, base64 password), EAP-TTLS (21) with inner `PAP`; `Extension/Android/AAAServerTrustedNames` `radius.renner.uno`. - Trust anchor ISRG Root X1 (DER). The `radius-tls` chain is radius.renner.uno ← YR2 ← Root YR ← ISRG Root X1. It matches WLANs that advertise Hotspot 2.0 with domain and NAI realm `renner.uno`. Changes: - `wlan-setup-config.yaml`: `nginx.conf` (image default plus `load_module` njs, one worker), `passpoint.js`, locations `/android/prepare` (POST only, body ≤ 1k) and `/android/profile/<token>`, Android section of the page. The access log format has no request body. - `wlan-setup.yaml`: mounts for `nginx.conf` (subPath; the reloader annotation rolls the pod on ConfigMap changes) and the njs directory. - `docs/radius/dot1x.md`: Android path. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
claude-bot added 1 commit 2026-11-05 09:53:05 +00:00
feat(radius): Android Passpoint profile on the WLAN setup page
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
1b724ef114
The Android section takes the password, builds a Passpoint R1 profile
(application/x-wifi-config: PPS MO with HomeSP FQDN and realm renner.uno,
EAP-TTLS with inner PAP, AAA server name radius.renner.uno, ISRG Root X1 as
trust anchor) in the browser and hands it to Android as a blob. The password
is not sent to any server.

Matches WLANs that advertise Hotspot 2.0 with domain and NAI realm
renner.uno.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude-bot added 1 commit 2026-11-05 10:32:24 +00:00
feat(radius): serve the Android Passpoint profile from nginx
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
8610853a6b
Android starts its Passpoint installer only for a profile fetched with a
user-triggered GET and Content-Type application/x-wifi-config; a
browser-generated blob does not qualify. The page now POSTs the password to
/android/prepare, where njs builds the profile and stores it in a shared
dict (120 s, bound to the caller's username) under a random token; the
link /android/profile/<token> delivers it once.

AAAServerTrustedNames moves under Extension/Android, the only place
Android's PpsMoParser accepts it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
k.renner requested review from review-bot 2026-11-05 10:33:23 +00:00
review-bot approved these changes 2026-11-05 10:35:48 +00:00
review-bot left a comment
Member

🤖 Automated review — model claude-opus-5-5, commit 8610853a6bdf54000958ab5b459e2b0ff3126933.

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 adds an Android Passpoint install path to wlan.renner.uno. An njs handler builds a Passpoint profile (application/x-wifi-config) from the password the user types in, stores it in a shared dict for 120 s under a 24-byte random token bound to the caller's username, and serves it once. The code is careful: usernames are checked against a regex before they reach the XML, the password goes in base64, the POST body is capped at 1k, and a token held by another user returns 404 and is not consumed. Nothing I could verify is broken. The real open question is whether the profile can connect at all with the current WLAN and RADIUS setup (see Unverified). mergeable is true. #1564 (the original setup page) is closed, and I found nothing else on the base that supersedes this change.

⚪ Unverified

  • No Hotspot 2.0 network is documented for the profile to match. A Passpoint profile only joins networks that advertise Hotspot 2.0 with domain/NAI realm renner.uno. The UniFi objects table in docs/radius/dot1x.md lists only the firewall policy, the RADIUS profile and the WLAN "Renner Test", with no Hotspot 2.0 / ANQP settings. This PR does not add any either. If "Renner UniFi" does not advertise them, Android users install a profile that never connects. Meanwhile the page now shows them only the Android install section, and the manual values are still documented only in the docs. I can't check the live UniFi config from the repo.

  • The outer identity Android sends may not match the RADIUS routing rule. docs/radius/dot1x.md says FreeRADIUS sends the login to authentik only when the outer identity is anonymous or anonymous@…. Anything else goes to the UDM and fails, and the doc calls anonymous mandatory. The PPS MO in wlan-setup-config.yaml (profile(), from about line 199) cannot set an anonymous identity. Whether Android's Passpoint stack uses something like anonymous@renner.uno for TTLS user credentials, or the real username, is Android platform behaviour I can't check here. If it uses the username, every Passpoint login fails. Before merging, run one real device login, or check the Android source for the version you target.

  • Runtime support in the pinned image is assumed, not checked. Specifically:

    • modules/ngx_http_js_module.so exists in nginxinc/nginx-unprivileged:1.31-alpine.
    • The bundled njs has js_shared_dict_zone … evict, the global crypto.getRandomValues and Buffer#toString("base64url").
    • r.requestText is filled for js_content.

    The PR body claims njs 1.0.2; I can't verify that from the repo. Any of these missing would show up as a pod that fails to start or a 500 on /android/prepare.

  • The Android parser may not accept the PPS layout. The profile relies on the Extension/Android/AAAServerTrustedNames/FQDN layout and the ISRG Root X1 DER part. This follows the PR's description of Android's PpsMoParser, which I could not check against source.

🟡 Suggestions

  • kubernetes/apps/authentik/wlan-setup.yaml:3-5: the header comment still says "the page holds no secrets". It now handles the authentik password (POST body plus 120 s in nginx memory). Update it so the trust assumption is accurate. This also matters for anyone loosening the gate later.
  • kubernetes/apps/authentik/wlan-setup-config.yaml:302-314: when Android is detected, add a short pointer to the manual values as a fallback (or link to docs/radius/dot1x.md#android) until Passpoint is confirmed working on the WLAN.
  • wlan-setup-config.yaml (deliver): dict.pop(token) would make read-and-delete a single step. With worker_processes 1 and synchronous njs this is not a real race today.

🟢 Nits

  • wlan-setup-config.yaml:12-13: the comment explains worker_processes 1 as keeping tokens in one process. js_shared_dict_zone is shared memory across workers anyway, so the reason given is not what makes it work.
  • currentUser (wlan-setup-config.yaml:365,368) is assigned but never read.

Tests

There are no automated tests. That is in line with the rest of this setup page, which also has none. What matters here is a manual end-to-end check on a real Android device: the profile installs, the device associates with the Hotspot 2.0 WLAN, and FreeRADIUS logs show the login taking the authentik path rather than the UDM path. A quick curl check would also help: POST to /android/prepare, GET the token as a different user (expect 404), GET it as the right user (expect 200), then GET it again (expect 404).

Recommendation

Looks reasonable — no blocking issues; comments above are optional. Before relying on this, confirm Hotspot 2.0 advertisement and the outer identity on a real device.

🤖 **Automated review** — model `claude-opus-5-5`, commit `8610853a6bdf54000958ab5b459e2b0ff3126933`. > 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 adds an Android Passpoint install path to `wlan.renner.uno`. An njs handler builds a Passpoint profile (`application/x-wifi-config`) from the password the user types in, stores it in a shared dict for 120 s under a 24-byte random token bound to the caller's username, and serves it once. The code is careful: usernames are checked against a regex before they reach the XML, the password goes in base64, the POST body is capped at 1k, and a token held by another user returns 404 and is not consumed. Nothing I could verify is broken. The real open question is whether the profile can connect at all with the current WLAN and RADIUS setup (see Unverified). `mergeable` is true. #1564 (the original setup page) is closed, and I found nothing else on the base that supersedes this change. ## ⚪ Unverified - **No Hotspot 2.0 network is documented for the profile to match.** A Passpoint profile only joins networks that advertise Hotspot 2.0 with domain/NAI realm `renner.uno`. The UniFi objects table in `docs/radius/dot1x.md` lists only the firewall policy, the RADIUS profile and the WLAN "Renner Test", with no Hotspot 2.0 / ANQP settings. This PR does not add any either. If "Renner UniFi" does not advertise them, Android users install a profile that never connects. Meanwhile the page now shows them only the Android install section, and the manual values are still documented only in the docs. I can't check the live UniFi config from the repo. - **The outer identity Android sends may not match the RADIUS routing rule.** `docs/radius/dot1x.md` says FreeRADIUS sends the login to authentik only when the outer identity is `anonymous` or `anonymous@…`. Anything else goes to the UDM and fails, and the doc calls `anonymous` mandatory. The PPS MO in `wlan-setup-config.yaml` (`profile()`, from about line 199) cannot set an anonymous identity. Whether Android's Passpoint stack uses something like `anonymous@renner.uno` for TTLS user credentials, or the real username, is Android platform behaviour I can't check here. If it uses the username, every Passpoint login fails. Before merging, run one real device login, or check the Android source for the version you target. - **Runtime support in the pinned image is assumed, not checked.** Specifically: - `modules/ngx_http_js_module.so` exists in `nginxinc/nginx-unprivileged:1.31-alpine`. - The bundled njs has `js_shared_dict_zone … evict`, the global `crypto.getRandomValues` and `Buffer#toString("base64url")`. - `r.requestText` is filled for `js_content`. The PR body claims njs 1.0.2; I can't verify that from the repo. Any of these missing would show up as a pod that fails to start or a 500 on `/android/prepare`. - **The Android parser may not accept the PPS layout.** The profile relies on the `Extension/Android/AAAServerTrustedNames/FQDN` layout and the ISRG Root X1 DER part. This follows the PR's description of Android's `PpsMoParser`, which I could not check against source. ## 🟡 Suggestions - `kubernetes/apps/authentik/wlan-setup.yaml:3-5`: the header comment still says "the page holds no secrets". It now handles the authentik password (POST body plus 120 s in nginx memory). Update it so the trust assumption is accurate. This also matters for anyone loosening the gate later. - `kubernetes/apps/authentik/wlan-setup-config.yaml:302-314`: when Android is detected, add a short pointer to the manual values as a fallback (or link to `docs/radius/dot1x.md#android`) until Passpoint is confirmed working on the WLAN. - `wlan-setup-config.yaml` (`deliver`): `dict.pop(token)` would make read-and-delete a single step. With `worker_processes 1` and synchronous njs this is not a real race today. ## 🟢 Nits - `wlan-setup-config.yaml:12-13`: the comment explains `worker_processes 1` as keeping tokens in one process. `js_shared_dict_zone` is shared memory across workers anyway, so the reason given is not what makes it work. - `currentUser` (`wlan-setup-config.yaml:365,368`) is assigned but never read. ## Tests There are no automated tests. That is in line with the rest of this setup page, which also has none. What matters here is a manual end-to-end check on a real Android device: the profile installs, the device associates with the Hotspot 2.0 WLAN, and FreeRADIUS logs show the login taking the authentik path rather than the UDM path. A quick `curl` check would also help: POST to `/android/prepare`, GET the token as a different user (expect 404), GET it as the right user (expect 200), then GET it again (expect 404). ## Recommendation Looks reasonable — no blocking issues; comments above are optional. Before relying on this, confirm Hotspot 2.0 advertisement and the outer identity on a real device. <!-- pr-review:sha=8610853a6bdf54000958ab5b459e2b0ff3126933 -->
k.renner merged commit 08b087337e into main 2026-11-05 10:37:22 +00:00
Sign in to join this conversation.