diff --git a/docs/radius/dot1x.md b/docs/radius/dot1x.md index 8b9e1333..89ae9846 100644 --- a/docs/radius/dot1x.md +++ b/docs/radius/dot1x.md @@ -154,22 +154,31 @@ For IPv6 set `LIVE_SOURCE` to the workstation's `fd02:50::` address and use ## Client setup (SSID "Renner UniFi") Setup page: `https://wlan.renner.uno` (behind the Authentik gate, any -authentik user; `kubernetes/apps/authentik/wlan-setup*.yaml`). Android and -Apple get Passpoint profiles with their own app password; Windows gets a file -that imports the WLAN profile with `netsh`, Linux an `nmcli` command -(NetworkManager has no Passpoint support). The Windows and Linux setups carry -no password. +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 (Android, Apple): `//install` (`android` or `apple`) signs -the user in through the authentik OAuth2 application `wlan-setup` (scope -`goauthentik.io/api`). On `//callback`, nginx (njs) exchanges the +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 returns the profile in the response to -that GET: Android `application/x-wifi-config` (as its installer requires), -Apple a `.mobileconfig`. The page stores nothing. 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. +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 @@ -203,7 +212,9 @@ Passpoint profile from the setup page ("Auf Android installieren"). ### Windows 11 -Settings > Network & Internet > Wi-Fi > Manage known networks > Add network: +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 diff --git a/kubernetes/apps/authentik/blueprints-configmap.yaml b/kubernetes/apps/authentik/blueprints-configmap.yaml index d0490c13..6e8c065e 100644 --- a/kubernetes/apps/authentik/blueprints-configmap.yaml +++ b/kubernetes/apps/authentik/blueprints-configmap.yaml @@ -1700,6 +1700,8 @@ data: matching_mode: strict - url: https://wlan.renner.uno/apple/callback matching_mode: strict + - url: https://wlan.renner.uno/windows/callback + matching_mode: strict - model: authentik_core.application identifiers: diff --git a/kubernetes/apps/authentik/kustomization.yaml b/kubernetes/apps/authentik/kustomization.yaml index 93d498e9..9a2a7df0 100644 --- a/kubernetes/apps/authentik/kustomization.yaml +++ b/kubernetes/apps/authentik/kustomization.yaml @@ -17,7 +17,7 @@ resources: - freeradius.yaml - freeradius-cnp.yaml - wlan-setup-oauth.yaml - - wlan-setup-config.yaml + - wlan-setup-signing-cert.yaml - wlan-setup.yaml # ak-pg CNPG Postgres backend (moved here from the retired seed-authentik KS; # each carries prune: disabled so ownership transferred without a teardown). diff --git a/kubernetes/apps/authentik/wlan-setup-config.yaml b/kubernetes/apps/authentik/wlan-setup-config.yaml deleted file mode 100644 index c3fb58f0..00000000 --- a/kubernetes/apps/authentik/wlan-setup-config.yaml +++ /dev/null @@ -1,482 +0,0 @@ -# wlan.renner.uno: setup files for "Renner UniFi" (802.1X, EAP-TTLS/PAP -# against freeradius, outer identity anonymous, server name radius.renner.uno). -# Served by wlan-setup (wlan-setup.yaml) behind the Authentik gate; the -# username comes from X-authentik-username. The Android and Apple Passpoint -# profiles carry a credential: a per-device authentik app password that the -# page creates with the user's own OAuth access token. -apiVersion: v1 -kind: ConfigMap -metadata: - name: wlan-setup - namespace: authentik -data: - # The image's nginx.conf plus the njs module and the OAuth client secret - # passpoint.js reads from the environment. - nginx.conf: | - load_module modules/ngx_http_js_module.so; - - env WLAN_SETUP_CLIENT_SECRET; - - worker_processes 1; - error_log /var/log/nginx/error.log notice; - pid /tmp/nginx.pid; - - events { - worker_connections 1024; - } - - http { - proxy_temp_path /tmp/proxy_temp; - client_body_temp_path /tmp/client_temp; - fastcgi_temp_path /tmp/fastcgi_temp; - uwsgi_temp_path /tmp/uwsgi_temp; - scgi_temp_path /tmp/scgi_temp; - - include /etc/nginx/mime.types; - default_type application/octet-stream; - - log_format main '$remote_addr - [$time_local] "$request" $status $body_bytes_sent "$http_user_agent"'; - access_log /var/log/nginx/access.log main; - - sendfile on; - keepalive_timeout 65; - - include /etc/nginx/conf.d/*.conf; - } - - default.conf: | - # Only plain usernames reach a generated file; anything else leaves the - # field empty and the device asks for it. - map $http_x_authentik_username $wifi_user { - "~^[A-Za-z0-9._@+-]{1,64}$" $http_x_authentik_username; - default ""; - } - - js_path /etc/nginx/njs/; - js_import passpoint from passpoint.js; - - # authentik-server, resolved once at start; reached only by passpoint.js - # subrequests (/_ak/...). - upstream authentik { - server authentik-server.authentik.svc.cluster.local:80; - } - - server { - listen 8080; - listen [::]:8080; - server_tokens off; - root /usr/share/nginx/html; - add_header Cache-Control "no-store" always; - - location = / { - try_files /index.html =404; - } - - location = /whoami { - default_type text/plain; - return 200 $wifi_user; - } - - location = /renner-unifi-wlan.cmd { - default_type application/octet-stream; - add_header Cache-Control "no-store" always; - add_header Content-Disposition 'attachment; filename="renner-unifi-wlan.cmd"' always; - try_files /renner-unifi-wlan.cmd =404; - } - - # Android and Apple: a tap on //install starts an - # authorization-code sign-in at authentik (the user is already signed - # in there); the callback creates the app password as the user and - # answers the same navigation with the Passpoint profile. Android: - # application/x-wifi-config, base64, no Content-Disposition, as its - # installer requires. Apple: a .mobileconfig download. - location ~ ^/(android|apple)/install$ { - js_content passpoint.install; - } - - location ~ ^/(android|apple)/callback$ { - # The query carries the one-time authorization code. - access_log off; - js_content passpoint.callback; - } - - # Subrequests from passpoint.js only. Credential and content type - # arrive as arguments and leave as headers; proxy_pass with a variable - # URI does not forward the arguments. $arg_* only: subrequests share - # the parent's variable cache, so a map would keep its first value. - location ~ ^/_ak(/.*)$ { - internal; - proxy_pass http://authentik$1; - # njs reads the whole reply from this buffer (default one page). - subrequest_output_buffer_size 256k; - # Only the headers set here reach authentik. - proxy_pass_request_headers off; - proxy_set_header Host auth.renner.uno; - proxy_set_header Cookie ""; - proxy_set_header Authorization "$arg_scheme $arg_cred"; - proxy_set_header Content-Type $arg_ct; - proxy_set_header Accept "application/json"; - } - - location = /healthz { - access_log off; - default_type text/plain; - return 200 "ok\n"; - } - - location / { - return 404; - } - } - - # Windows: extracts the WLAN profile (everything after line 8 of itself) - # into %TEMP% and imports it for the current user with netsh. EAP-TTLS with - # PAPAuthentication, outer identity anonymous, server name - # radius.renner.uno, trust anchor ISRG Root X1 (SHA-1 thumbprint; the - # Let's Encrypt chain of radius-tls ends there). Windows asks for username - # and password on connect. CRLF line endings: cmd.exe needs them. - renner-unifi-wlan.cmd: "@echo off\r\nrem WLAN Renner UniFi einrichten (802.1X, Anmeldung mit dem authentik-Konto)\r\nset \"WLANXML=%TEMP%\\renner-unifi-wlan.xml\"\r\nmore +8 \"%~f0\" > \"%WLANXML%\"\r\nnetsh wlan add profile filename=\"%WLANXML%\" user=current\r\ndel \"%WLANXML%\"\r\nnetsh wlan connect name=\"Renner UniFi\"\r\npause & exit /b\r\nRenner UniFi52656E6E657220556E694669Renner UniFifalseESSautoWPA2AEStruetrueuser2100311radius.renner.unoca bd 2a 79 a1 07 6a 31 f2 1d 25 36 35 cb 03 9d 43 29 a5 e8falsetrueanonymous\r\n" - - # Android Passpoint R1 provisioning (source.android.com, Passpoint): the - # body is base64 of a MIME multipart with the PPS MO and the trust anchor. - # HomeSP FQDN and realm renner.uno match the Hotspot 2.0 domain and NAI realm - # the Passpoint WLAN announces; EAP-TTLS (21) with inner PAP. Trust anchor - # ISRG Root X1 (the radius-tls chain ends there), AAA server name - # radius.renner.uno (Extension/Android/AAAServerTrustedNames, as Android's - # PpsMoParser expects it). The credential is a new app password per install, - # created as the user with the access token of the "WLAN setup" OAuth client - # (authorization code, scope goauthentik.io/api). It starts with 29 days - # (below the 30-day token cap); each 802.1X login renews it to 60 days - # (policy dot1x-app-password-renew). - # Apple: com.apple.wifi.managed with IsHotspot, DomainName and NAIRealmNames - # renner.uno (no SSID: Passpoint matching), TTLS (21) with inner PAP, outer - # identity anonymous, trusted server name radius.renner.uno, username and - # app password. Fixed PayloadIdentifier/UUIDs so a reinstall replaces the - # previous profile. - passpoint.js: | - const ISRG_ROOT_X1 = "MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAwTzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2VhcmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygch77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6UA5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sWT8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyHB5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UCB5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUvKBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWnOlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTnjh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbwqHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CIrU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkqhkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZLubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KKNFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7UrTkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdCjNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVcoyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPAmRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57demyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc="; - const USER = /^[A-Za-z0-9._@+-]{1,64}$/; - - function b64(s) { return Buffer.from(s).toString("base64"); } - function node(name, inner) { return "" + name + "" + inner + ""; } - function leaf(name, value) { return node(name, "" + value + ""); } - - function profile(user, password) { - const pps = '1.2' + - node("PerProviderSubscription", - "urn:wfa:mo:hotspot2dot0-perprovidersubscription:1.0" + - node("i001", - node("HomeSP", leaf("FriendlyName", "Renner") + leaf("FQDN", "renner.uno")) + - node("Credential", - leaf("Realm", "renner.uno") + - node("UsernamePassword", - leaf("Username", user) + - leaf("Password", b64(password)) + - node("EAPMethod", leaf("EAPType", "21") + leaf("InnerMethod", "PAP")))) + - node("Extension", node("Android", - node("AAAServerTrustedNames", leaf("FQDN", "radius.renner.uno")))))) + - ""; - const mime = "Content-Type: multipart/mixed; boundary={boundary}\r\n" + - "Content-Transfer-Encoding: base64\r\n\r\n" + - "--{boundary}\r\n" + - "Content-Type: application/x-passpoint-profile\r\n" + - "Content-Transfer-Encoding: base64\r\n\r\n" + - b64(pps) + "\r\n" + - "--{boundary}\r\n" + - "Content-Type: application/x-x509-ca-cert\r\n" + - "Content-Transfer-Encoding: base64\r\n\r\n" + - ISRG_ROOT_X1 + "\r\n" + - "--{boundary}--\r\n"; - return b64(mime); - } - - function xml(s) { - return s.replace(/&/g, "&").replace(//g, ">"); - } - - function mobileconfig(user, password) { - return '\n' + - '\n' + - '' + - 'PayloadContent' + - 'PayloadTypecom.apple.wifi.managed' + - 'PayloadVersion1' + - 'PayloadIdentifieruno.renner.wifi.renner-unifi.network' + - 'PayloadUUID824959bf-c55d-4ed2-b569-c33f3759c2b6' + - 'PayloadDisplayNameRenner UniFi' + - 'PayloadOrganizationRenner' + - 'IsHotspot' + - 'DomainNamerenner.uno' + - 'NAIRealmNamesrenner.uno' + - 'DisplayedOperatorNameRenner' + - 'ServiceProviderRoamingEnabled' + - 'AutoJoin' + - 'EncryptionTypeAny' + - 'EAPClientConfiguration' + - 'UserName' + xml(user) + '' + - 'UserPassword' + xml(password) + '' + - 'OuterIdentityanonymous' + - 'AcceptEAPTypes21' + - 'TTLSInnerAuthenticationPAP' + - 'TLSTrustedServerNamesradius.renner.uno' + - '' + - 'PayloadTypeConfiguration' + - 'PayloadVersion1' + - 'PayloadIdentifieruno.renner.wifi.renner-unifi' + - 'PayloadUUID7175a2a3-ddd3-42ea-b4ad-b7e5501892f3' + - 'PayloadDisplayNameWLAN Renner UniFi' + - 'PayloadOrganizationRenner' + - '\n'; - } - - const PLATFORMS = { - android: { label: "Android" }, - apple: { label: "Apple" }, - }; - - // "android" or "apple" from //install or //callback. - function platform(r) { - return r.uri.split("/")[1]; - } - - function redirectUri(name) { - return "https://wlan.renner.uno/" + name + "/callback"; - } - - const CLIENT_ID = "wlan-setup"; - const AUTHORIZE = "https://auth.renner.uno/application/o/authorize/"; - const STATE_COOKIE = "wlan_oauth_state"; - const LIFETIME_DAYS = 29; - - function random() { - const bytes = new Uint8Array(24); - crypto.getRandomValues(bytes); - return Buffer.from(bytes).toString("base64url"); - } - - function cookie(r, name) { - const all = r.headersIn["Cookie"] || ""; - const parts = all.split(";"); - for (let n = 0; n < parts.length; n++) { - const part = parts[n]; - const i = part.indexOf("="); - if (i > 0 && part.substring(0, i).trim() === name) return part.substring(i + 1).trim(); - } - return ""; - } - - function form(fields) { - return Object.keys(fields).map(k => encodeURIComponent(k) + "=" + encodeURIComponent(fields[k])).join("&"); - } - - const FORM = "application/x-www-form-urlencoded"; - const JSON_CT = "application/json"; - const CRED = /^[A-Za-z0-9._~+\/=-]+$/; - - // authentik call through the internal /_ak location; the arguments - // become the Authorization and Content-Type headers there (raw, nginx - // does not decode $arg_*). - async function ak(r, method, path, scheme, cred, ct, body) { - if (!CRED.test(cred)) throw new Error(method + " " + path + ": no credential"); - const args = "scheme=" + scheme + "&cred=" + cred + "&ct=" + ct; - const opts = { method: method, args: args }; - if (body !== undefined) opts.body = body; - const reply = await r.subrequest("/_ak" + path, opts); - if (reply.status < 200 || reply.status > 299) { - throw new Error(method + " " + path + " -> " + reply.status + " " + reply.responseText.substring(0, 300)); - } - return JSON.parse(reply.responseText); - } - - function install(r) { - const name = platform(r); - const state = random(); - r.headersOut["Set-Cookie"] = STATE_COOKIE + "=" + state + - "; Path=/" + name + "/; Max-Age=300; HttpOnly; Secure; SameSite=Lax"; - r.return(302, AUTHORIZE + "?" + form({ - response_type: "code", - client_id: CLIENT_ID, - redirect_uri: redirectUri(name), - scope: "openid profile goauthentik.io/api", - state: state, - })); - } - - async function callback(r) { - const name = platform(r); - try { - const state = cookie(r, STATE_COOKIE); - if (!state || r.args.state !== state || !r.args.code) { r.return(400, "Bitte die Einrichtung neu starten.\n"); return; } - const client = Buffer.from(CLIENT_ID + ":" + process.env.WLAN_SETUP_CLIENT_SECRET).toString("base64"); - const grant = await ak(r, "POST", "/application/o/token/", "Basic", client, FORM, form({ - grant_type: "authorization_code", - code: r.args.code, - redirect_uri: redirectUri(name), - })); - const access = grant.access_token; - const me = await ak(r, "GET", "/api/v3/core/users/me/", "Bearer", access, ""); - const user = me.user.username; - if (!USER.test(user) || user !== r.headersIn["X-authentik-username"]) { r.return(403); return; } - - const now = new Date(); - const stamp = now.toISOString().replace(/[-:]/g, "").replace(/\..*$/, ""); - const identifier = "wlan-" + name + "-" + user.replace(/[^A-Za-z0-9-]/g, "-").toLowerCase() + "-" + stamp + "-" + random().substring(0, 6).replace(/[^A-Za-z0-9]/g, "x"); - const expires = new Date(now.getTime() + LIFETIME_DAYS * 86400000).toISOString(); - await ak(r, "POST", "/api/v3/core/tokens/", "Bearer", access, JSON_CT, JSON.stringify({ - identifier: identifier, - intent: "app_password", - description: "WLAN " + PLATFORMS[name].label + ", " + now.toISOString().substring(0, 10) + " (wlan.renner.uno)", - expiring: true, - expires: expires, - })); - const key = (await ak(r, "GET", "/api/v3/core/tokens/" + identifier + "/view_key/", "Bearer", access, "")).key; - - r.headersOut["Set-Cookie"] = STATE_COOKIE + "=; Path=/" + name + "/; Max-Age=0; HttpOnly; Secure; SameSite=Lax"; - if (name === "apple") { - r.headersOut["Content-Type"] = "application/x-apple-aspen-config"; - r.headersOut["Content-Disposition"] = 'attachment; filename="renner-unifi.mobileconfig"'; - r.return(200, mobileconfig(user, key)); - } else { - r.headersOut["Content-Type"] = "application/x-wifi-config"; - r.headersOut["Content-Transfer-Encoding"] = "base64"; - r.return(200, profile(user, key)); - } - } catch (e) { - r.error("wlan-setup " + name + ": " + e.message); - r.return(502, "Die Einrichtung ist fehlgeschlagen.\n"); - } - } - - export default { install, callback }; - - index.html: | - - - - - - WLAN einrichten - - - -
-

WLAN einrichten

-

WLAN Renner UniFi für dein Konto …. Android, iPhone, iPad und Mac bekommen ein eigenes WLAN-App-Passwort; Windows und Linux melden sich mit deinem authentik-Passwort an.

- -
- -
- Andere Geräte -
-
- - - - - - - - -
- - - diff --git a/kubernetes/apps/authentik/wlan-setup-signing-cert.yaml b/kubernetes/apps/authentik/wlan-setup-signing-cert.yaml new file mode 100644 index 00000000..0760012e --- /dev/null +++ b/kubernetes/apps/authentik/wlan-setup-signing-cert.yaml @@ -0,0 +1,21 @@ +--- +# Signs the Windows Wi-Fi provisioning XML of wlan.renner.uno (XML-DSig in the +# wlan-setup service). Issued via the cluster-wide Cloudflare DNS-01 +# ClusterIssuer. +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: wlan-setup-signing + namespace: authentik +spec: + secretName: wlan-setup-signing + issuerRef: + name: letsencrypt-cluster + kind: ClusterIssuer + dnsNames: + - wlan.renner.uno + privateKey: + algorithm: RSA + size: 2048 + encoding: PKCS8 + rotationPolicy: Always diff --git a/kubernetes/apps/authentik/wlan-setup.yaml b/kubernetes/apps/authentik/wlan-setup.yaml index fb89627b..97fad643 100644 --- a/kubernetes/apps/authentik/wlan-setup.yaml +++ b/kubernetes/apps/authentik/wlan-setup.yaml @@ -1,9 +1,9 @@ -# wlan.renner.uno: setup page for the 802.1X WLAN "Renner UniFi" (Apple -# profile, Windows import file, Linux nmcli command, Android Passpoint profile -# with its own app password; config and files in wlan-setup-config.yaml). +# wlan.renner.uno: setup page for the 802.1X WLAN "Renner UniFi", served by the +# wlan-setup Go service (wlan-setup/). Android, Apple and Windows get Passpoint +# profiles with a per-device authentik app password; Linux an nmcli command. # Public, so a device can reach it before it is in the WLAN (mobile data, -# Renner Gast). Any Authentik user passes the gate (no group); the page stores -# no user credentials. +# Renner Gast). Any Authentik user passes the gate (no group); the service +# stores no user credentials beyond the 5-minute single-use Windows profile. apiVersion: apps/v1 kind: Deployment metadata: @@ -12,7 +12,6 @@ metadata: labels: app.kubernetes.io/name: wlan-setup annotations: - configmap.reloader.stakater.com/reload: wlan-setup secret.reloader.stakater.com/reload: wlan-setup-oauth spec: replicas: 1 @@ -27,15 +26,17 @@ spec: automountServiceAccountToken: false securityContext: runAsNonRoot: true - runAsUser: 101 - runAsGroup: 101 + runAsUser: 65534 + runAsGroup: 65534 + # Group of the signing key files (defaultMode 0440). + fsGroup: 65534 seccompProfile: type: RuntimeDefault containers: - - name: nginx - image: nginxinc/nginx-unprivileged:1.31-alpine@sha256:b5047b9b76845eac124358373c6852377c52e4b05834a47647e955dd91cb8f1e + - name: wlan-setup + image: gitea.renner.uno/gitea_admin/wlan-setup:2026-11-05-1352 # {"$imagepolicy": "flux-system:wlan-setup"} env: - # Client secret of the "WLAN setup" OAuth2 provider (passpoint.js). + # Client secret of the "WLAN setup" OAuth2 provider. - name: WLAN_SETUP_CLIENT_SECRET valueFrom: secretKeyRef: @@ -65,55 +66,21 @@ spec: limits: memory: 64Mi volumeMounts: - # subPath: replaced on rollout, which the reloader annotation - # triggers on every ConfigMap change. - - name: main - mountPath: /etc/nginx/nginx.conf - subPath: nginx.conf + # Key and chain that sign the Windows provisioning XML; read per + # request, so cert-manager renewals need no restart. + - name: signing + mountPath: /etc/wlan-signing readOnly: true - - name: config - mountPath: /etc/nginx/conf.d - readOnly: true - - name: njs - mountPath: /etc/nginx/njs - readOnly: true - - name: html - mountPath: /usr/share/nginx/html - readOnly: true - # pid file and temp paths of the unprivileged image. - - name: tmp - mountPath: /tmp volumes: - - name: main - configMap: - name: wlan-setup + - name: signing + secret: + secretName: wlan-setup-signing + defaultMode: 0440 items: - - key: nginx.conf - path: nginx.conf - - name: config - configMap: - name: wlan-setup - items: - - key: default.conf - path: default.conf - - name: njs - configMap: - name: wlan-setup - items: - - key: passpoint.js - path: passpoint.js - - name: html - configMap: - name: wlan-setup - items: - - key: index.html - path: index.html - - key: renner-unifi-wlan.cmd - path: renner-unifi-wlan.cmd - - name: tmp - emptyDir: - medium: Memory - sizeLimit: 16Mi + - key: tls.key + path: tls.key + - key: tls.crt + path: tls.crt --- apiVersion: v1 kind: Service @@ -161,3 +128,30 @@ spec: name: wlan-setup port: number: 80 +--- +# Without the gate: Windows Settings fetches the signed provisioning XML +# without the browser's session; the single-use token in the path is the only +# gate. Traefik reaches the pods through the CNP wlan-setup-gate that +# gate-sync creates for the Ingress above. +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: wlan-setup-provisioning + namespace: authentik +spec: + ingressClassName: traefik + tls: + - hosts: + - wlan.renner.uno + secretName: wlan-setup-tls + rules: + - host: wlan.renner.uno + http: + paths: + - path: /windows/profile/ + pathType: Prefix + backend: + service: + name: wlan-setup + port: + number: 80