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):
The user types the authentik password; the page POSTs it to /android/prepare.
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.
The page shows the link /android/profile/<token>; the tap GETs the profile once, then the token is gone. Other users get 404.
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.
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)
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>
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
🤖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 -->
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.
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, noContent-Disposition— source.android.com, Passpoint):/android/prepare.X-authentik-username./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
renner.uno, friendly nameRenner; Credential realmrenner.uno, UsernamePassword (username, base64 password), EAP-TTLS (21) with innerPAP;Extension/Android/AAAServerTrustedNamesradius.renner.uno.radius-tlschain 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 plusload_modulenjs, 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 fornginx.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
🤖 Automated review — model
claude-opus-5-5, commit8610853a6bdf54000958ab5b459e2b0ff3126933.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).mergeableis 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 indocs/radius/dot1x.mdlists 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.mdsays FreeRADIUS sends the login to authentik only when the outer identity isanonymousoranonymous@…. Anything else goes to the UDM and fails, and the doc callsanonymousmandatory. The PPS MO inwlan-setup-config.yaml(profile(), from about line 199) cannot set an anonymous identity. Whether Android's Passpoint stack uses something likeanonymous@renner.unofor 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.soexists innginxinc/nginx-unprivileged:1.31-alpine.js_shared_dict_zone … evict, the globalcrypto.getRandomValuesandBuffer#toString("base64url").r.requestTextis filled forjs_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/FQDNlayout and the ISRG Root X1 DER part. This follows the PR's description of Android'sPpsMoParser, 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 todocs/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. Withworker_processes 1and synchronous njs this is not a real race today.🟢 Nits
wlan-setup-config.yaml:12-13: the comment explainsworker_processes 1as keeping tokens in one process.js_shared_dict_zoneis 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
curlcheck 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.