feat(wlan-setup): Go service for the WLAN setup page #1570

Merged
k.renner merged 1 commits from feat/wlan-setup-go into main 2026-11-05 13:46:29 +00:00
Member

wlan-setup/ is a Go service that replaces the nginx + njs backend of wlan.renner.uno. This PR adds the service, its tests, CI and image automation; the Deployment switches to the image in a follow-up once the first image exists.

  • Page, /whoami, /healthz; /{android,apple,windows}/install and /callback: authentik OAuth code flow (client wlan-setup, client_secret_basic), user check against X-authentik-username, per-device app password wlan-<platform>-… (29 days), profile per platform:
    • Android: Passpoint (application/x-wifi-config)
    • Apple: .mobileconfig with Passpoint
    • Windows: CarrierProvisioning with Passpoint, signed with XML-DSig in the form of Microsoft's samples (enveloped, C14N 1.0, rsa-sha1, chain in KeyInfo) by the key in /etc/wlan-signing; kept for 5 minutes under a single-use token; the browser is sent to ms-settings:wifi-provisioning?uri=…/windows/profile/<token>.
  • Containerfile as review-bot (static binary, FROM scratch, uid 65534).
  • Tests offline against a fake authentik (11 tests, go test -race); goxmldsig verifies the Windows signature in the tests only.
  • CI: .woodpecker/wlan-setup-test.yml (gofmt, vet, test) on PRs; build-ci-images.yml builds gitea.renner.uno/gitea_admin/wlan-setup:YYYY-MM-DD-HHMM on main.
  • Image automation wlan-setup (newest YYYY-MM-DD-HHMM tag).

🤖 Generated with Claude Code

`wlan-setup/` is a Go service that replaces the nginx + njs backend of wlan.renner.uno. This PR adds the service, its tests, CI and image automation; the Deployment switches to the image in a follow-up once the first image exists. - Page, `/whoami`, `/healthz`; `/{android,apple,windows}/install` and `/callback`: authentik OAuth code flow (client `wlan-setup`, client_secret_basic), user check against `X-authentik-username`, per-device app password `wlan-<platform>-…` (29 days), profile per platform: - Android: Passpoint (`application/x-wifi-config`) - Apple: `.mobileconfig` with Passpoint - Windows: CarrierProvisioning with Passpoint, signed with XML-DSig in the form of Microsoft's samples (enveloped, C14N 1.0, rsa-sha1, chain in KeyInfo) by the key in `/etc/wlan-signing`; kept for 5 minutes under a single-use token; the browser is sent to `ms-settings:wifi-provisioning?uri=…/windows/profile/<token>`. - Containerfile as review-bot (static binary, FROM scratch, uid 65534). - Tests offline against a fake authentik (11 tests, `go test -race`); goxmldsig verifies the Windows signature in the tests only. - CI: `.woodpecker/wlan-setup-test.yml` (gofmt, vet, test) on PRs; `build-ci-images.yml` builds `gitea.renner.uno/gitea_admin/wlan-setup:YYYY-MM-DD-HHMM` on main. - Image automation `wlan-setup` (newest `YYYY-MM-DD-HHMM` tag). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
claude-bot added 1 commit 2026-11-05 13:39:09 +00:00
feat(wlan-setup): Go service for the WLAN setup page
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/wlan-setup-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
a05f1431ca
wlan-setup/ is a Go service that will replace the nginx + njs backend of
wlan.renner.uno (switched over in a follow-up once the first image exists).

- Page, /whoami, /healthz; /{android,apple,windows}/install and /callback:
  authentik OAuth code flow (client wlan-setup, client_secret_basic), user
  check against X-authentik-username, per-device app password
  wlan-<platform>-... (29 days), profile per platform: Android Passpoint
  (application/x-wifi-config), Apple .mobileconfig with Passpoint, Windows
  CarrierProvisioning with Passpoint.
- Windows: the profile is signed with XML-DSig in the form of Microsoft's
  samples (enveloped, C14N 1.0, rsa-sha1, chain in KeyInfo) by the key in
  /etc/wlan-signing, kept for 5 minutes under a single-use token, and the
  browser is sent to ms-settings:wifi-provisioning?uri=.../windows/profile/<token>.
- Offline tests against a fake authentik; goxmldsig verifies the signature
  in the tests only.
- CI: wlan-setup-test (gofmt, vet, test -race) on PRs; build-ci-images
  builds gitea.renner.uno/gitea_admin/wlan-setup:YYYY-MM-DD-HHMM on main;
  Flux image automation follows the newest tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
k.renner requested review from review-bot 2026-11-05 13:41:56 +00:00
review-bot approved these changes 2026-11-05 13:45:44 +00:00
review-bot left a comment
Member

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

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 wlan-setup/, a Go service meant to replace the nginx + njs backend in kubernetes/apps/authentik/wlan-setup-config.yaml. It also adds an offline test suite, a PR test pipeline, a kaniko build step and Flux image automation. I compared the Android/Apple flow line by line with the current passpoint.js and found no deviation: same state cookie, the same four authentik calls, the same user check against X-authentik-username, the same identifier and expiry format, and the same profile bodies. The new Windows path (XML-DSig signing plus a single-use token store) looks correct, and a test verifies its signature with goxmldsig. The CI step, Containerfile and ImagePolicy match their siblings (build-review-bot, review-bot/Containerfile, kuma-sync.yaml). mergeable is true. Every changed file is new except three append-only lines in build-ci-images.yml, image-automation/kustomization.yaml and its README. I found no blocking defect.

⚪ Unverified

  • Gate exemption for /windows/profile/{token} (wlan-setup/server.go, windowsProfile handler at the end of the file). The handler expects Windows Settings to fetch the profile without an authentik session, so the follow-up Deployment/ingress change has to exempt exactly this path from the forward-auth gate. Two things matter:

    • The gate must stay in front of /{platform}/callback and /whoami.
    • The proxy in front must keep stripping or overwriting any X-authentik-username header sent by the client, because callback trusts that header as the signed-in user.

    That config is not in this PR, so I couldn't check it.

  • Missing redirect URI for Windows. The wlan-setup OAuth provider in the authentik blueprint has to allow https://wlan.renner.uno/windows/callback. The current config only serves android/apple. I didn't read the blueprint, and this belongs in the follow-up.

  • Windows Settings behaviour. I can't verify from the repo whether Windows Settings fetches the ms-settings:wifi-provisioning?uri=… target only once (HEAD is handled without consuming the token). I also can't verify whether it accepts the WLANProfile with Hotspot2 + WPA3ENT and a chain signed by the wlan-signing certificate. If it does a second GET, the single-use token returns 404.

  • Multiple replicas. ProfileStore (wlan-setup/store.go) is in-memory. If the follow-up Deployment runs more than one replica, the profile fetch can land on a different pod than the callback and get a 404. With one replica this is fine.

🟡 Suggestions

  • Orphan app passwords on the Windows path (wlan-setup/server.go, callback, case "windows"). signer.Sign and s.profiles.Put run after AppPassword has already created the app password. If either fails, the user keeps an unused wlan-windows-… token in authentik until it expires 29 days later. One fix is to check store capacity before creating the password; at minimum, log the identifier in the failure message so it can be found and deleted.
  • Unescaped token in the redirect (wlan-setup/server.go, the ms-settings: redirect). The URL is concatenated without escaping. That is safe today, because PublicURL comes from config and the token is raw-URL base64. Wrapping it in url.QueryEscape would make it robust if PublicURL ever has a path or query.

🟢 Nits

  • wlan-setup/index.html already says Windows gets its own app password and links to /windows/install. That's fine for an image that only goes live with the follow-up, but the ConfigMap's index.html and the image's copy will disagree until the switch.

Tests

Coverage is good for a new service. There are 11 offline tests against a fake authentik. They cover:

  • state mismatch, user mismatch, each upstream failure, and a missing signing key (no authentik call)
  • the exact call sequence and request body sent to authentik
  • Android, Apple and Windows output
  • signature verification, a tamper check, single use and TTL expiry
  • route 404s and config validation

.woodpecker/wlan-setup-test.yml runs gofmt, vet and test -race on PRs. Nothing is missing that would block.

Recommendation

Looks reasonable: no blocking issues; the comments above are optional. The unverified points need settling in the follow-up Deployment/gate change.

🤖 **Automated review** — model `claude-opus-5-5`, commit `a05f1431caea881b81f808b0017a7ea923524ca6`. > 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 `wlan-setup/`, a Go service meant to replace the nginx + njs backend in `kubernetes/apps/authentik/wlan-setup-config.yaml`. It also adds an offline test suite, a PR test pipeline, a kaniko build step and Flux image automation. I compared the Android/Apple flow line by line with the current `passpoint.js` and found no deviation: same state cookie, the same four authentik calls, the same user check against `X-authentik-username`, the same identifier and expiry format, and the same profile bodies. The new Windows path (XML-DSig signing plus a single-use token store) looks correct, and a test verifies its signature with goxmldsig. The CI step, Containerfile and ImagePolicy match their siblings (`build-review-bot`, `review-bot/Containerfile`, `kuma-sync.yaml`). `mergeable` is true. Every changed file is new except three append-only lines in `build-ci-images.yml`, `image-automation/kustomization.yaml` and its README. I found no blocking defect. ## ⚪ Unverified - **Gate exemption for `/windows/profile/{token}`** (`wlan-setup/server.go`, `windowsProfile` handler at the end of the file). The handler expects Windows Settings to fetch the profile without an authentik session, so the follow-up Deployment/ingress change has to exempt exactly this path from the forward-auth gate. Two things matter: - The gate must stay in front of `/{platform}/callback` and `/whoami`. - The proxy in front must keep stripping or overwriting any `X-authentik-username` header sent by the client, because `callback` trusts that header as the signed-in user. That config is not in this PR, so I couldn't check it. - **Missing redirect URI for Windows.** The `wlan-setup` OAuth provider in the authentik blueprint has to allow `https://wlan.renner.uno/windows/callback`. The current config only serves android/apple. I didn't read the blueprint, and this belongs in the follow-up. - **Windows Settings behaviour.** I can't verify from the repo whether Windows Settings fetches the `ms-settings:wifi-provisioning?uri=…` target only once (HEAD is handled without consuming the token). I also can't verify whether it accepts the WLANProfile with `Hotspot2` + `WPA3ENT` and a chain signed by the `wlan-signing` certificate. If it does a second GET, the single-use token returns 404. - **Multiple replicas.** `ProfileStore` (`wlan-setup/store.go`) is in-memory. If the follow-up Deployment runs more than one replica, the profile fetch can land on a different pod than the callback and get a 404. With one replica this is fine. ## 🟡 Suggestions - **Orphan app passwords on the Windows path** (`wlan-setup/server.go`, `callback`, `case "windows"`). `signer.Sign` and `s.profiles.Put` run after `AppPassword` has already created the app password. If either fails, the user keeps an unused `wlan-windows-…` token in authentik until it expires 29 days later. One fix is to check store capacity before creating the password; at minimum, log the identifier in the failure message so it can be found and deleted. - **Unescaped token in the redirect** (`wlan-setup/server.go`, the `ms-settings:` redirect). The URL is concatenated without escaping. That is safe today, because `PublicURL` comes from config and the token is raw-URL base64. Wrapping it in `url.QueryEscape` would make it robust if `PublicURL` ever has a path or query. ## 🟢 Nits - `wlan-setup/index.html` already says Windows gets its own app password and links to `/windows/install`. That's fine for an image that only goes live with the follow-up, but the ConfigMap's `index.html` and the image's copy will disagree until the switch. ## Tests Coverage is good for a new service. There are 11 offline tests against a fake authentik. They cover: - state mismatch, user mismatch, each upstream failure, and a missing signing key (no authentik call) - the exact call sequence and request body sent to authentik - Android, Apple and Windows output - signature verification, a tamper check, single use and TTL expiry - route 404s and config validation `.woodpecker/wlan-setup-test.yml` runs `gofmt`, `vet` and `test -race` on PRs. Nothing is missing that would block. ## Recommendation Looks reasonable: no blocking issues; the comments above are optional. The unverified points need settling in the follow-up Deployment/gate change. <!-- pr-review:sha=a05f1431caea881b81f808b0017a7ea923524ca6 -->
k.renner merged commit 8a41b80217 into main 2026-11-05 13:46:29 +00:00
Sign in to join this conversation.