diff --git a/docs/radius/dot1x.md b/docs/radius/dot1x.md index dd6ddb5b..8b9e1333 100644 --- a/docs/radius/dot1x.md +++ b/docs/radius/dot1x.md @@ -154,21 +154,22 @@ 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`). It serves the -Apple profile (caller's username filled in), a Windows file that imports the -WLAN profile with `netsh`, and the Linux `nmcli` command. None of these carries a password. +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. -Android gets a Passpoint profile (`application/x-wifi-config`) with its own -app password. "Auf Android installieren" (`/android/install`) signs the user in -through the authentik OAuth2 application `wlan-setup` (scope -`goauthentik.io/api`). On `/android/callback`, nginx (njs) exchanges the code, -creates an app password (`wlan-android---`) with the -user's own access token, and returns the profile in the response to that GET, -as the Android installer requires. The page stores nothing. Deleting the app -password in authentik removes that device from the WLAN. The profile matches -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. +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 +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. App passwords expire when unused. The page creates them with 29 days (the API caps new tokens at 30 days unless the group attribute @@ -188,14 +189,7 @@ is never sent in clear over the air. ### Android -- EAP method: TTLS -- Phase 2 authentication: PAP -- CA certificate: Use system certificates -- Domain: `radius.renner.uno` -- Identity: authentik username -- Anonymous identity: `anonymous` (mandatory; empty sends the login to the - UDM, where it fails) -- Password: authentik password +Passpoint profile from the setup page ("Auf Android installieren"). ### Linux (NetworkManager) @@ -227,13 +221,14 @@ wireless network" instead. ### Apple (iOS, iPadOS, macOS) -Apple devices need a configuration profile; the setup page serves it with the -username filled in. Install it via Settings (Profile Downloaded). The device -asks for the password on the first join. The profile sets security type Any (WPA2/WPA3), outer identity -`anonymous` and trusted server name `radius.renner.uno`; it contains no -password and no certificate. Apple -devices may show a one-time "trust certificate" prompt for `radius.renner.uno` -on the first join. +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 diff --git a/kubernetes/apps/authentik/blueprints-configmap.yaml b/kubernetes/apps/authentik/blueprints-configmap.yaml index 19b20935..d0490c13 100644 --- a/kubernetes/apps/authentik/blueprints-configmap.yaml +++ b/kubernetes/apps/authentik/blueprints-configmap.yaml @@ -1698,6 +1698,8 @@ data: redirect_uris: - url: https://wlan.renner.uno/android/callback matching_mode: strict + - url: https://wlan.renner.uno/apple/callback + matching_mode: strict - model: authentik_core.application identifiers: diff --git a/kubernetes/apps/authentik/wlan-setup-config.yaml b/kubernetes/apps/authentik/wlan-setup-config.yaml index 61d2bd86..c3fb58f0 100644 --- a/kubernetes/apps/authentik/wlan-setup-config.yaml +++ b/kubernetes/apps/authentik/wlan-setup-config.yaml @@ -1,9 +1,9 @@ # 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. Only the Android Passpoint profile -# carries a credential (the format requires it): a per-device authentik app -# password that the page creates with the user's own OAuth access token. +# 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: @@ -77,75 +77,6 @@ data: return 200 $wifi_user; } - # Apple: com.apple.wifi.managed, TTLS (21) with inner PAP. Fixed - # PayloadIdentifier/UUIDs so a reinstall replaces the old profile. - location = /renner-unifi.mobileconfig { - default_type application/x-apple-aspen-config; - add_header Cache-Control "no-store" always; - add_header Content-Disposition 'attachment; filename="renner-unifi.mobileconfig"' always; - return 200 ' - - - - PayloadContent - - - PayloadType - com.apple.wifi.managed - PayloadVersion - 1 - PayloadIdentifier - uno.renner.wifi.renner-unifi.network - PayloadUUID - 824959bf-c55d-4ed2-b569-c33f3759c2b6 - PayloadDisplayName - Renner UniFi - PayloadOrganization - Renner - SSID_STR - Renner UniFi - HIDDEN_NETWORK - - AutoJoin - - EncryptionType - Any - EAPClientConfiguration - - UserName - $wifi_user - OuterIdentity - anonymous - AcceptEAPTypes - - 21 - - TTLSInnerAuthentication - PAP - TLSTrustedServerNames - - radius.renner.uno - - - - - PayloadType - Configuration - PayloadVersion - 1 - PayloadIdentifier - uno.renner.wifi.renner-unifi - PayloadUUID - 7175a2a3-ddd3-42ea-b4ad-b7e5501892f3 - PayloadDisplayName - WLAN Renner UniFi - PayloadOrganization - Renner - - - '; - } - location = /renner-unifi-wlan.cmd { default_type application/octet-stream; add_header Cache-Control "no-store" always; @@ -153,16 +84,17 @@ data: try_files /renner-unifi-wlan.cmd =404; } - # Android: a tap on /android/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 (application/x-wifi-config, - # base64, no Content-Disposition), as the Android installer requires. - location = /android/install { + # 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/callback { + location ~ ^/(android|apple)/callback$ { # The query carries the one-time authorization code. access_log off; js_content passpoint.callback; @@ -216,6 +148,11 @@ data: # (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}$/; @@ -253,8 +190,60 @@ data: 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 REDIRECT_URI = "https://wlan.renner.uno/android/callback"; const AUTHORIZE = "https://auth.renner.uno/application/o/authorize/"; const STATE_COOKIE = "wlan_oauth_state"; const LIFETIME_DAYS = 29; @@ -300,19 +289,21 @@ data: } function install(r) { + const name = platform(r); const state = random(); r.headersOut["Set-Cookie"] = STATE_COOKIE + "=" + state + - "; Path=/android/; Max-Age=300; HttpOnly; Secure; SameSite=Lax"; + "; Path=/" + name + "/; Max-Age=300; HttpOnly; Secure; SameSite=Lax"; r.return(302, AUTHORIZE + "?" + form({ response_type: "code", client_id: CLIENT_ID, - redirect_uri: REDIRECT_URI, + 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; } @@ -320,7 +311,7 @@ data: const grant = await ak(r, "POST", "/application/o/token/", "Basic", client, FORM, form({ grant_type: "authorization_code", code: r.args.code, - redirect_uri: REDIRECT_URI, + redirect_uri: redirectUri(name), })); const access = grant.access_token; const me = await ak(r, "GET", "/api/v3/core/users/me/", "Bearer", access, ""); @@ -329,23 +320,29 @@ data: const now = new Date(); const stamp = now.toISOString().replace(/[-:]/g, "").replace(/\..*$/, ""); - const identifier = "wlan-android-" + user.replace(/[^A-Za-z0-9-]/g, "-").toLowerCase() + "-" + stamp + "-" + random().substring(0, 6).replace(/[^A-Za-z0-9]/g, "x"); + 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 Android, " + now.toISOString().substring(0, 10) + " (wlan.renner.uno)", + 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=/android/; Max-Age=0; HttpOnly; Secure; SameSite=Lax"; - r.headersOut["Content-Type"] = "application/x-wifi-config"; - r.headersOut["Content-Transfer-Encoding"] = "base64"; - r.return(200, profile(user, 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 android: " + e.message); + r.error("wlan-setup " + name + ": " + e.message); r.return(502, "Die Einrichtung ist fehlgeschlagen.\n"); } } @@ -383,7 +380,7 @@ data:

WLAN einrichten

-

WLAN Renner UniFi. Anmeldung mit deinem Konto … und deinem authentik-Passwort.

+

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.

@@ -403,11 +400,12 @@ data: