feat(radius): Android WLAN profile with a per-device app password
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful

wlan.renner.uno signs the user in through the new authentik OAuth2
application wlan-setup (confidential, scope goauthentik.io/api). The
callback exchanges the code (client_secret_basic), creates an app password
with the user's own access token (10 years) and returns the Passpoint
profile carrying it in the response to the user's GET. The page asks for
no password and stores nothing; deleting the app password in authentik
removes the device.

- dot1x-password stage also accepts app passwords (TokenBackend).
- Client secret: ESO Password generator wlan-setup-oauth, read by the
  authentik worker (blueprint) and by wlan-setup.
- authentik calls go through an internal nginx location; headers come
  from $arg_* because subrequests share the parent's variable cache.
- Reloader annotations on the wlan-setup Deployment metadata.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-05 12:52:09 +01:00
co-authored by Claude Opus 5.5
parent 08b087337e
commit b1be3181ef
7 changed files with 256 additions and 89 deletions
+20 -8
View File
@@ -148,18 +148,30 @@ For IPv6 set `LIVE_SOURCE` to the workstation's `fd02:50::` address and use
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. Android
gets a Passpoint profile (`application/x-wifi-config`): the page POSTs the
password the user types there, nginx (njs) builds the profile and keeps it in
memory for at most 120 s under a one-time token bound to the user, and a tap on
the resulting link GETs it, as the Android installer requires. It matches WLANs
that advertise Hotspot 2.0 with domain and NAI realm `renner.uno`
WLAN profile with `netsh`, and the Linux `nmcli` command. None of these carries a 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-<user>-<timestamp>`, 10 years) 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).
App password lifetimes are capped by the group attribute
`goauthentik.io/user/token-maximum-lifetime` (default: the token default of
30 minutes). WLAN users need a group with
`goauthentik.io/user/token-maximum-lifetime: days=3650`, or the creation fails
(502 on the callback, reason in the wlan-setup log).
All clients use EAP-TTLS with inner PAP. The server certificate is a public
Let's Encrypt certificate for `radius.renner.uno`, so no CA needs to be
installed. Username and password are the authentik credentials. PAP runs
inside the TLS tunnel to FreeRADIUS and is never sent in clear over the air.
installed. The 802.1X password stage (`dot1x-password`) accepts the authentik
password and app passwords. PAP runs inside the TLS tunnel to FreeRADIUS and
is never sent in clear over the air.
### Android
@@ -1483,8 +1483,11 @@ data:
name: dot1x-password
state: present
attrs:
# App passwords: the WLAN credential that devices store (Android
# Passpoint profiles carry it), created by the user in authentik.
backends:
- authentik.core.auth.InbuiltBackend
- authentik.core.auth.TokenBackend
- id: dot1x-login
model: authentik_stages_user_login.userloginstage
@@ -1624,3 +1627,43 @@ data:
kubernetes_httproute_annotations: {}
kubernetes_httproute_parent_refs: []
kubernetes_json_patches: null
# wlan.renner.uno (wlan-setup.yaml): signs the user in and, with the
# user's own access token (scope goauthentik.io/api), creates the WLAN
# app password for the Android Passpoint profile. Confidential client;
# the secret comes from wlan-setup-oauth (wlan-setup-oauth.yaml). No
# policy binding: every authentik user may set up the WLAN.
- id: wlan-setup-provider
model: authentik_providers_oauth2.oauth2provider
identifiers:
name: WLAN setup
state: present
attrs:
authentication_flow: null
authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]]
invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]]
client_type: confidential
client_id: wlan-setup
client_secret: !Env WLAN_SETUP_CLIENT_SECRET
grant_types:
- authorization_code
access_token_validity: minutes=5
signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]]
property_mappings:
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-openid]]
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-profile]]
- !Find [authentik_providers_oauth2.scopemapping, [managed, goauthentik.io/providers/oauth2/scope-authentik_api]]
redirect_uris:
- url: https://wlan.renner.uno/android/callback
matching_mode: strict
- model: authentik_core.application
identifiers:
slug: wlan-setup
state: present
attrs:
name: WLAN setup
slug: wlan-setup
provider: !KeyOf wlan-setup-provider
meta_launch_url: https://wlan.renner.uno
meta_description: "WLAN setup page; creates WLAN app passwords"
+11 -4
View File
@@ -171,10 +171,11 @@ spec:
worker:
# Overrides the global key; keep authentik-secrets in the list.
podAnnotations:
secret.reloader.stakater.com/reload: authentik-secrets,radius-secrets
# Shared secret of the 802.1X Radius provider, set via `!Env` in
# dot1x-radius.yaml. Worker only (blueprint applier); optional so a
# missing Secret fails only the dot1x blueprint entry.
secret.reloader.stakater.com/reload: authentik-secrets,radius-secrets,wlan-setup-oauth
# Shared secret of the 802.1X Radius provider and client secret of the
# WLAN setup provider, set via `!Env` in dot1x-radius.yaml. Worker only
# (blueprint applier); optional so a missing Secret fails only the
# blueprint entries that use it.
env:
- name: DOT1X_OUTPOST_SECRET
valueFrom:
@@ -182,6 +183,12 @@ spec:
name: radius-secrets
key: DOT1X_OUTPOST_SECRET
optional: true
- name: WLAN_SETUP_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: wlan-setup-oauth
key: client_secret
optional: true
resources:
requests:
# Same reasoning as server above, and the worker is burstier still:
@@ -16,6 +16,7 @@ resources:
- freeradius-network-attachment.yaml
- freeradius.yaml
- freeradius-cnp.yaml
- wlan-setup-oauth.yaml
- wlan-setup-config.yaml
- wlan-setup.yaml
# ak-pg CNPG Postgres backend (moved here from the retired seed-authentik KS;
+134 -71
View File
@@ -2,19 +2,21 @@
# 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 the password (the format requires it); it exists in nginx memory for
# at most 120 s and is handed out once.
# 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.
apiVersion: v1
kind: ConfigMap
metadata:
name: wlan-setup
namespace: authentik
data:
# The image's nginx.conf plus the njs module and a single worker, so the
# one-time tokens of passpoint.js live in one process.
# 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;
@@ -52,8 +54,12 @@ data:
js_path /etc/nginx/njs/;
js_import passpoint from passpoint.js;
# token -> {user, profile}; entries expire after 120 s.
js_shared_dict_zone zone=passpoint:1m timeout=120s evict;
# 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;
@@ -147,19 +153,33 @@ data:
try_files /renner-unifi-wlan.cmd =404;
}
# Android: the page POSTs the password and gets a one-time token; a tap
# on /android/profile/<token> then GETs the Passpoint profile, as the
# Android installer requires (application/x-wifi-config, base64,
# no Content-Disposition).
location = /android/prepare {
limit_except POST { deny all; }
client_max_body_size 1k;
client_body_buffer_size 1k;
js_content passpoint.prepare;
# 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 {
js_content passpoint.install;
}
location ~ "^/android/profile/[A-Za-z0-9_-]{32}$" {
js_content passpoint.deliver;
location = /android/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;
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 {
@@ -187,8 +207,10 @@ data:
# 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). A token is bound to the user who created it and
# can be fetched once.
# 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); its lifetime needs the
# group attribute goauthentik.io/user/token-maximum-lifetime >= days=3650.
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}$/;
@@ -226,38 +248,104 @@ data:
return b64(mime);
}
function caller(r) {
const user = r.headersIn["X-authentik-username"] || "";
return USER.test(user) ? user : "";
}
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 = 3650;
function prepare(r) {
const user = caller(r);
const password = r.requestText || "";
if (!user) { r.return(403); return; }
if (!password || password.length > 256) { r.return(400); return; }
function random() {
const bytes = new Uint8Array(24);
crypto.getRandomValues(bytes);
const token = Buffer.from(bytes).toString("base64url");
ngx.shared.passpoint.set(token, JSON.stringify({ user: user, body: profile(user, password) }));
r.headersOut["Content-Type"] = "text/plain";
r.return(200, token);
return Buffer.from(bytes).toString("base64url");
}
function deliver(r) {
const token = r.uri.substring(r.uri.lastIndexOf("/") + 1);
const dict = ngx.shared.passpoint;
const entry = dict.get(token);
if (!entry) { r.return(404); return; }
const data = JSON.parse(entry);
if (data.user !== caller(r)) { r.return(404); return; }
dict.delete(token);
r.headersOut["Content-Type"] = "application/x-wifi-config";
r.headersOut["Content-Transfer-Encoding"] = "base64";
r.return(200, data.body);
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 "";
}
export default { prepare, deliver };
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 state = random();
r.headersOut["Set-Cookie"] = STATE_COOKIE + "=" + state +
"; Path=/android/; Max-Age=300; HttpOnly; Secure; SameSite=Lax";
r.return(302, AUTHORIZE + "?" + form({
response_type: "code",
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
scope: "openid profile goauthentik.io/api",
state: state,
}));
}
async function callback(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: REDIRECT_URI,
}));
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-android-" + user.replace(/[^A-Za-z0-9-]/g, "-").toLowerCase() + "-" + stamp;
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)",
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));
} catch (e) {
r.error("wlan-setup android: " + e.message);
r.return(502, "Die Einrichtung ist fehlgeschlagen.\n");
}
}
export default { install, callback };
index.html: |
<!doctype html>
@@ -302,14 +390,8 @@ data:
<template id="t-android">
<section data-os="android">
<h2>Android</h2>
<form id="android-form">
<p><label>Passwort für <strong class="u">…</strong><br>
<input type="password" id="android-password" autocomplete="current-password" required></label></p>
<p><button type="submit">Profil vorbereiten</button></p>
</form>
<p><a class="button" id="android-install" href="#" hidden>Auf Android installieren</a></p>
<p class="muted" id="android-status"></p>
<p class="muted">Ändert sich dein Passwort, hier einfach neu installieren.</p>
<p><a class="button" href="/android/install">Auf Android installieren</a></p>
<p class="muted">Das Gerät bekommt ein eigenes WLAN-App-Passwort in deinem authentik-Konto; dort gelöscht, ist das Gerät aus dem WLAN.</p>
</section>
</template>
@@ -382,25 +464,6 @@ data:
.then(function (t) { render(t.trim()); })
.catch(function () {});
// Android: the server builds the Passpoint profile from the password and
// keeps it under a one-time token; the installer only starts on a GET
// that the user triggers by tapping, so the token becomes a link.
document.getElementById("android-form").addEventListener("submit", function (ev) {
ev.preventDefault();
var field = document.getElementById("android-password");
var install = document.getElementById("android-install");
var status = document.getElementById("android-status");
if (!field.value) return;
fetch("/android/prepare", { method: "POST", credentials: "same-origin", body: field.value })
.then(function (r) { if (!r.ok) throw new Error(r.status); return r.text(); })
.then(function (token) {
field.value = "";
install.href = "/android/profile/" + token.trim();
install.hidden = false;
status.textContent = "Bereit. Gültig für 2 Minuten.";
})
.catch(function () { status.textContent = "Das hat nicht geklappt. Bitte neu laden und erneut versuchen."; });
});
document.getElementById("copy-nmcli").addEventListener("click", function () {
var text = document.getElementById("nmcli").textContent;
@@ -0,0 +1,32 @@
# Client secret of the "WLAN setup" OAuth2 provider (dot1x-radius blueprint).
# Generated once; the authentik worker reads it as WLAN_SETUP_CLIENT_SECRET
# for the blueprint, wlan-setup reads it for the code exchange.
apiVersion: generators.external-secrets.io/v1alpha1
kind: Password
metadata:
name: wlan-setup-oauth
namespace: authentik
spec:
length: 64
digits: 16
symbols: 0
allowRepeat: true
noUpper: false
secretKeys:
- client_secret
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: wlan-setup-oauth
namespace: authentik
spec:
refreshPolicy: CreatedOnce
target:
name: wlan-setup-oauth
dataFrom:
- sourceRef:
generatorRef:
apiVersion: generators.external-secrets.io/v1alpha1
kind: Password
name: wlan-setup-oauth
+15 -6
View File
@@ -1,8 +1,9 @@
# wlan.renner.uno: setup page for the 802.1X WLAN "Renner UniFi" (Apple
# profile, Windows import file, Linux nmcli command; config and files in
# wlan-setup-config.yaml). 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 holds no secrets, only the caller's own username.
# profile, Windows import file, Linux nmcli command, Android Passpoint profile
# with its own app password; config and files in wlan-setup-config.yaml).
# 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.
apiVersion: apps/v1
kind: Deployment
metadata:
@@ -10,6 +11,9 @@ metadata:
namespace: authentik
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
selector:
@@ -19,8 +23,6 @@ spec:
metadata:
labels:
app.kubernetes.io/name: wlan-setup
annotations:
configmap.reloader.stakater.com/reload: wlan-setup
spec:
automountServiceAccountToken: false
securityContext:
@@ -32,6 +34,13 @@ spec:
containers:
- name: nginx
image: nginxinc/nginx-unprivileged:1.31-alpine@sha256:b5047b9b76845eac124358373c6852377c52e4b05834a47647e955dd91cb8f1e
env:
# Client secret of the "WLAN setup" OAuth2 provider (passpoint.js).
- name: WLAN_SETUP_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: wlan-setup-oauth
key: client_secret
ports:
- name: http
containerPort: 8080