Enhance Authentik role in noble cluster setup by adding support for resolving OAuth2 flow, signing key, and scope mapping UUIDs from the worker database, improving API access under 2026+ RBAC. Update README with troubleshooting steps for common OAuth2 provider issues and adjust default variables for better configuration management. Ensure seamless integration with oauth2-proxy by allowing unverified email handling in development environments.
This commit is contained in:
@@ -12,11 +12,11 @@ Installs **Authentik** (Helm `goauthentik/authentik`) as the cluster IdP, **oaut
|
|||||||
|
|
||||||
## Variables
|
## Variables
|
||||||
|
|
||||||
See **`defaults/main.yml`**. Hostnames default to **`auth.apps.noble.lab.pcenicni.dev`** and **`oauth2.apps.noble.lab.pcenicni.dev`**.
|
See **`defaults/main.yml`**. Hostnames default to **`auth.apps.noble.lab.pcenicni.dev`** and **`oauth2.apps.noble.lab.pcenicni.dev`**. **`noble_authentik_ensure_admin_ui_access`** (default **true**) re-applies **authentik Admins** superuser membership via the worker on each **`--tags authentik`** run so the admin UI keeps working under **2026+** RBAC.
|
||||||
|
|
||||||
## IdP configuration
|
## IdP configuration
|
||||||
|
|
||||||
When **`noble_authentik_configure_idp`** is true, Ansible runs **`files/configure_authentik.py`** (Python 3, stdlib only) with the bootstrap token to create/update OAuth2 providers and applications for **argocd**, **grafana**, **headlamp**, and **oauth2-proxy**, create **`noble-admins`** / **`noble-editors`**, and add the bootstrap user (by email) to those groups.
|
When **`noble_authentik_configure_idp`** is true, Ansible creates/updates OAuth2 providers and applications for **argocd**, **grafana**, **headlamp**, and **oauth2-proxy** using either the **worker ORM path** (default **`noble_authentik_oidc_provision_via: worker`**: **`kubectl exec`** + **`ak shell`** + **`files/worker_upsert_oauth_oidc.py`**, which avoids **2026+** REST **403** on **`GET …/providers/oauth2/**`) or the **REST-only path** (**`noble_authentik_oidc_provision_via: rest`**: **`files/configure_authentik.py`** needs a token that can list/patch OAuth2 providers). With the worker path and a bootstrap email, it also runs **`files/worker_add_bootstrap_user_groups.py`** so **`User.groups.add`** does not depend on **`GET …/core/users/**`. It then runs **`configure_authentik.py`** with **`AUTHENTIK_SKIP_OIDC_REST`** / **`AUTHENTIK_SKIP_USER_GROUP_REST`** when those worker steps ran, so the script only calls **`ensure_group`** over the API (skipped when **`AUTHENTIK_NOBLE_*_GROUP_PK`** are set).
|
||||||
|
|
||||||
## RBAC notes
|
## RBAC notes
|
||||||
|
|
||||||
@@ -25,17 +25,24 @@ When **`noble_authentik_configure_idp`** is true, Ansible runs **`files/configur
|
|||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **oauth2-proxy shows 500** on **`oauth2.apps…/oauth2/callback`** (logs: `email in id_token (...) isn't verified`): Authentik’s id_token often lacks **`email_verified: true`** for bootstrap users. **`clusters/noble/bootstrap/oauth2-proxy/values.yaml`** sets **`insecure-oidc-allow-unverified-email`** for the lab; otherwise verify the user’s email in Authentik, then **`helm upgrade oauth2-proxy`** (or **`--tags authentik`**).
|
||||||
- Re-run **`configure_authentik.py`** only by executing **`noble.yml`** with **`--tags authentik`** after fixing `.env`.
|
- Re-run **`configure_authentik.py`** only by executing **`noble.yml`** with **`--tags authentik`** after fixing `.env`.
|
||||||
- If Authentik API calls fail, check flows exist (slug **`default-provider-authorization-implicit-consent`**) and TLS reaches **`AUTHENTIK_API_BASE`**.
|
- If Authentik API calls fail, check flows exist (slug **`default-provider-authorization-implicit-consent`**) and TLS reaches **`AUTHENTIK_API_BASE`**.
|
||||||
- **`GET …/flows/instances/…` → HTTP 403** with **`Token invalid/expired`**: the bootstrap API token is not accepted yet (common right after install: worker still creating it) or **`NOBLE_AUTHENTIK_BOOTSTRAP_TOKEN`** in `.env` does not match the value Helm applied. Re-run **`--tags authentik`** (the role waits for **`GET …/core/applications/`** to return **200** with your token). If you rotated the token in `.env` only, run the play again so Helm picks up the new value, or mint a new API token for **`akadmin`** in the admin UI.
|
- **`GET …/flows/instances/…` → HTTP 403** with **`Token invalid/expired`**: the bootstrap API token is not accepted yet (common right after install: worker still creating it) or **`NOBLE_AUTHENTIK_BOOTSTRAP_TOKEN`** in `.env` does not match the value Helm applied. Re-run **`--tags authentik`** (the role waits for **`GET …/core/applications/`** to return **200** with your token). If you rotated the token in `.env` only, run the play again so Helm picks up the new value, or mint a new API token for **`akadmin`** in the admin UI.
|
||||||
- **`GET …/flows/instances/…` → HTTP 403** with **permission** errors (Authentik **2026+** RBAC): the bearer token’s user must be able to **view flows**. The Helm bootstrap token belongs to **`akadmin`**, which must be in the **`authentik Admins`** group. Add **`akadmin`** to **Directory → Groups → authentik Admins**, or create a new **API token** for `akadmin` after fixing groups, and put that token in **`NOBLE_AUTHENTIK_BOOTSTRAP_TOKEN`**. As a workaround, set **`noble_authentik_oauth_authorization_flow_pk`** and **`noble_authentik_oauth_invalidation_flow_pk`** (both required) to the flows’ **UUID** primary keys from **Admin → Flows** (or `-e` / `group_vars`); the configure script then skips flow list API calls.
|
- **`GET …/flows/instances/…` → HTTP 403** with **permission** errors (Authentik **2026+** RBAC): the bootstrap API token often cannot **view flows**. The role reads flow UUIDs from the **worker** database (`kubectl exec` + **`ak shell`**) when **`noble_authentik_oauth_authorization_flow_pk`** / **`noble_authentik_oauth_invalidation_flow_pk`** are unset. The same pattern applies to **`/crypto/certificatekeypairs/`**, **`/propertymappings/…`**, **`/core/groups/`**, and the matching **`noble_authentik_*`** inventory variables. If a lookup fails, fix **`akadmin`** / **authentik Admins** / token, or set the UUID variables manually (see below).
|
||||||
- **`/if/admin/` redirects to `/if/user/`** (even as **`akadmin`**): the admin UI only loads when **`canAccessAdmin`** is true. That comes from **`user.isSuperuser`** on **`GET /api/v3/core/users/me/`**, which is **not** the Django username — in Authentik **2026.x** it is derived from **membership in a group with the superuser flag** (bootstrap blueprint: **`authentik Admins`**). If **`isSuperuser`** is false in **`/me`**, `akadmin` is missing that membership or the group’s flag is off. Fix in **Directory → Groups** when you can, or run the worker shell below, then **log out** and sign in again.
|
- **`GET …/crypto/certificatekeypairs/…` → HTTP 403** (permission): same RBAC issue as flows. When **`noble_authentik_oauth_signing_key_pk`** is unset, the role resolves the first **CertificateKeyPair** UUID from the **worker** DB. You can also set **`noble_authentik_oauth_signing_key_pk`** manually (Admin → **System** → **Certificates**).
|
||||||
|
- **`GET …/propertymappings/…` → HTTP 403** (permission): when **`noble_authentik_oauth_scope_mapping_pks`** is unset, the role resolves **ScopeMapping** UUIDs from the **worker** DB: **openid**, **email**, **profile**, **offline_access**, and **groups** only if a separate **`groups`** mapping exists (Authentik **2026.x** defaults put **groups** inside **profile** only).
|
||||||
|
- **`GET …/core/groups/…` → HTTP 403** (permission): when **`noble_authentik_group_pk_noble_admins`** and **`noble_authentik_group_pk_noble_editors`** are unset, the role runs **`resolve_noble_group_pks.py`** in the worker (**`get_or_create`** for **noble-admins** / **noble-editors**), then passes **`AUTHENTIK_NOBLE_*_GROUP_PK`** into **`configure_authentik.py`** so it skips group list/create via REST.
|
||||||
|
- **`GET …/providers/oauth2/…` → HTTP 403** (permission): bootstrap tokens often cannot list OAuth2 providers. With the default **`noble_authentik_oidc_provision_via: worker`**, the role upserts providers and applications in **`authentik-worker`** via Django ORM (**`worker_upsert_oauth_oidc.py`**) instead of **`configure_authentik.py`** REST. Set **`noble_authentik_oidc_provision_via: rest`** only if your API token has **view_oauth2provider** / provider edit permissions (e.g. a full **akadmin** token from the UI).
|
||||||
|
- **`GET …/core/users/…` → HTTP 403** when adding the bootstrap user to **noble-admins** / **noble-editors**: with **`noble_authentik_oidc_provision_via: worker`** and a non-empty bootstrap email, the role runs **`worker_add_bootstrap_user_groups.py`** in the worker (ORM **`User.groups.add`**) and sets **`AUTHENTIK_SKIP_USER_GROUP_REST`** so **`configure_authentik.py`** does not call the users API for membership.
|
||||||
|
- **Manual flow / signing / scope / group UUIDs (optional):** set **`noble_authentik_oauth_authorization_flow_pk`** and **`noble_authentik_oauth_invalidation_flow_pk`** (both together), optionally **`noble_authentik_oauth_signing_key_pk`**, **`noble_authentik_oauth_scope_mapping_pks`**, **`noble_authentik_group_pk_noble_admins`**, and **`noble_authentik_group_pk_noble_editors`**, from the admin UI or `-e` / `group_vars`; **`configure_authentik.py`** then skips the matching REST discovery calls.
|
||||||
|
- **`/if/admin/` redirects to `/if/user/`** (lost admin panel): in **2026.x**, **`canAccessAdmin`** follows **`isSuperuser`**, which is true only when the user belongs to a group with the **superuser** flag (**`authentik Admins`** by default). **`noble_authentik_ensure_admin_ui_access`** (default **true**) makes **`--tags authentik`** run **`files/worker_ensure_authentik_admin_access.py`** in **authentik-worker** (adds **akadmin** or the **bootstrap email** user to **authentik Admins** and forces **`is_superuser`** on that group). **Log out** of Authentik (private window is fine) and sign in again. Set **`noble_authentik_ensure_admin_ui_access: false`** to skip. Without Ansible, you can fix it in **Directory → Groups → authentik Admins** (superuser flag + membership) or run **`ak shell`** with the same logic as that script.
|
||||||
|
|
||||||
### Fix `akadmin` superuser / admin redirect (worker shell)
|
### Fix admin access manually (worker shell, no Ansible)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
kubectl exec -it deploy/authentik-worker -n authentik -- ak shell -c "from authentik.core.models import User, Group; u=User.objects.get(username='akadmin'); adm,_=Group.objects.get_or_create(name='authentik Admins', defaults={'is_superuser': True}); adm.is_superuser=True; adm.save(update_fields=['is_superuser']); adm.users.add(u); u=User.objects.get(pk=u.pk); print('all_groups', list(u.all_groups().values_list('name', flat=True))); print('is_superuser', u.is_superuser)"
|
kubectl exec -it deploy/authentik-worker -n authentik -- ak shell -c "from authentik.core.models import User, Group; u=User.objects.get(username='akadmin'); adm,_=Group.objects.get_or_create(name='authentik Admins', defaults={'is_superuser': True}); adm.is_superuser=True; adm.save(update_fields=['is_superuser']); u.groups.add(adm); u=User.objects.get(pk=u.pk); print('is_superuser', u.is_superuser)"
|
||||||
```
|
```
|
||||||
|
|
||||||
Then **log out** of Authentik (or use a private window) and sign in again as **`akadmin`**.
|
Then **log out** of Authentik and sign in again.
|
||||||
- **Grafana / Headlamp / ForwardAuth “Unauthorized” or Authentik “Not found”** (Authentik **2026.x**): OAuth endpoints are no longer under **`/application/o/<app>/oauth2/...`**. Use **issuer discovery** (Grafana **`server_url`** at **`…/application/o/<slug>/`**; oauth2-proxy **`oidc-issuer-url`**; Headlamp **`-oidc-idp-issuer-url`**). Re-apply **Traefik** (**`allowCrossNamespace`** so Ingresses can use Middleware in **`oauth2-proxy`**), **kube-prometheus-stack**, and **Headlamp** after updating values (e.g. **`ansible-playbook playbooks/noble.yml --tags authentik`**).
|
- **Grafana / Headlamp / ForwardAuth “Unauthorized” or Authentik “Not found”** (Authentik **2026.x**): OAuth endpoints are no longer under **`/application/o/<app>/oauth2/...`**. Use **issuer discovery** (Grafana **`server_url`** at **`…/application/o/<slug>/`**; oauth2-proxy **`oidc-issuer-url`**; Headlamp **`-oidc-idp-issuer-url`**). Re-apply **Traefik** (**`allowCrossNamespace`** so Ingresses can use Middleware in **`oauth2-proxy`**), **kube-prometheus-stack**, and **Headlamp** after updating values (e.g. **`ansible-playbook playbooks/noble.yml --tags authentik`**).
|
||||||
|
|||||||
@@ -3,8 +3,16 @@
|
|||||||
noble_authentik_install: false
|
noble_authentik_install: false
|
||||||
# When true, run **configure_authentik.py** against the Authentik API (requires bootstrap token + client secrets).
|
# When true, run **configure_authentik.py** against the Authentik API (requires bootstrap token + client secrets).
|
||||||
noble_authentik_configure_idp: true
|
noble_authentik_configure_idp: true
|
||||||
|
# **worker** — upsert OAuth2 providers + applications via **ak shell** + Django ORM (avoids **GET …/providers/oauth2/** 403
|
||||||
|
# for bootstrap tokens). **rest** — use the Authentik API only (needs a token that can list/patch OAuth2 providers).
|
||||||
|
# When true (default), run **worker_ensure_authentik_admin_access.py** so **akadmin** / bootstrap email is in
|
||||||
|
# **authentik Admins** with **is_superuser** on the group (fixes **/if/admin/** redirecting to user UI in 2026+).
|
||||||
|
noble_authentik_ensure_admin_ui_access: true
|
||||||
|
|
||||||
noble_authentik_chart_version: "2026.2.3"
|
noble_authentik_chart_version: "2026.2.3"
|
||||||
|
noble_authentik_namespace: authentik
|
||||||
|
# Helm release name (deployments: **{release}-server**, **{release}-worker**).
|
||||||
|
noble_authentik_release_name: authentik
|
||||||
noble_authentik_oauth2_proxy_chart_version: "10.4.3"
|
noble_authentik_oauth2_proxy_chart_version: "10.4.3"
|
||||||
|
|
||||||
noble_authentik_host: auth.apps.noble.lab.pcenicni.dev
|
noble_authentik_host: auth.apps.noble.lab.pcenicni.dev
|
||||||
@@ -33,9 +41,21 @@ noble_authentik_client_secret_oauth2_proxy: ""
|
|||||||
noble_authentik_oauth2_proxy_cookie_secret: ""
|
noble_authentik_oauth2_proxy_cookie_secret: ""
|
||||||
|
|
||||||
# Optional: OAuth2 provider flow PKs (UUID strings). When **both** are set, **configure_authentik.py**
|
# Optional: OAuth2 provider flow PKs (UUID strings). When **both** are set, **configure_authentik.py**
|
||||||
# skips **GET /flows/instances/** (avoids 403 if the API token user is not a superuser). See role README.
|
# skips **GET /flows/instances/** (avoids 403 if the API token cannot view flows). If unset, the role
|
||||||
|
# tries **kubectl exec** into **authentik-worker** + **ak shell** to read the same slugs from the DB.
|
||||||
noble_authentik_oauth_authorization_flow_pk: ""
|
noble_authentik_oauth_authorization_flow_pk: ""
|
||||||
noble_authentik_oauth_invalidation_flow_pk: ""
|
noble_authentik_oauth_invalidation_flow_pk: ""
|
||||||
|
# Optional: OAuth2 signing key (**CertificateKeyPair** UUID). When set, **configure_authentik.py** skips
|
||||||
|
# **GET /crypto/certificatekeypairs/** (often 403 for bootstrap tokens). If unset, the role resolves it
|
||||||
|
# from the worker DB when possible (see **resolve_oauth_signing_key_pk.py**).
|
||||||
|
noble_authentik_oauth_signing_key_pk: ""
|
||||||
|
# Optional: comma-separated **ScopeMapping** UUIDs (openid, email, profile, offline_access; optional **groups**
|
||||||
|
# if you created a separate mapping — 2026.x defaults embed groups in **profile** only).
|
||||||
|
# When set, **configure_authentik.py** skips **GET /propertymappings/...** (often 403 for bootstrap tokens).
|
||||||
|
noble_authentik_oauth_scope_mapping_pks: ""
|
||||||
|
# Optional: **Group** UUIDs for **noble-admins** / **noble-editors** (skip **GET /core/groups/** when set).
|
||||||
|
noble_authentik_group_pk_noble_admins: ""
|
||||||
|
noble_authentik_group_pk_noble_editors: ""
|
||||||
|
|
||||||
noble_authentik_helm_wait_timeout: 25m
|
noble_authentik_helm_wait_timeout: 25m
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -6,9 +6,17 @@ Environment:
|
|||||||
AUTHENTIK_TOKEN bootstrap API token (Bearer) for user **akadmin** (see README if flows return 403)
|
AUTHENTIK_TOKEN bootstrap API token (Bearer) for user **akadmin** (see README if flows return 403)
|
||||||
BOOTSTRAP_EMAIL initial admin email (added to noble-admins)
|
BOOTSTRAP_EMAIL initial admin email (added to noble-admins)
|
||||||
CLIENT_JSON path to JSON file: { "argocd": {"client_id","client_secret","redirect_uri"}, ... }
|
CLIENT_JSON path to JSON file: { "argocd": {"client_id","client_secret","redirect_uri"}, ... }
|
||||||
Optional (skip flow API discovery; Authentik 2026+ flow **pk** UUID strings):
|
Optional (skip API discovery; Authentik 2026+ RBAC — use UUID strings from worker shell or Admin UI):
|
||||||
AUTHENTIK_OAUTH_AUTHORIZATION_FLOW_PK
|
AUTHENTIK_OAUTH_AUTHORIZATION_FLOW_PK
|
||||||
AUTHENTIK_OAUTH_INVALIDATION_FLOW_PK
|
AUTHENTIK_OAUTH_INVALIDATION_FLOW_PK
|
||||||
|
AUTHENTIK_OAUTH_SIGNING_KEY_PK
|
||||||
|
AUTHENTIK_OAUTH_SCOPE_MAPPING_PKS comma-separated ScopeMapping UUIDs (openid,email,profile,offline_access[,groups])
|
||||||
|
AUTHENTIK_NOBLE_ADMINS_GROUP_PK noble-admins Group UUID (skip GET /core/groups/)
|
||||||
|
AUTHENTIK_NOBLE_EDITORS_GROUP_PK noble-editors Group UUID
|
||||||
|
AUTHENTIK_SKIP_OIDC_REST if 1/true/yes, skip OAuth2 provider + application REST calls (Ansible
|
||||||
|
uses **worker_upsert_oauth_oidc.py** when bootstrap tokens cannot **GET …/providers/oauth2/**)
|
||||||
|
AUTHENTIK_SKIP_USER_GROUP_REST if 1/true/yes, skip **GET/PATCH …/core/users/** for bootstrap group membership (Ansible uses
|
||||||
|
**worker_add_bootstrap_user_groups.py** when the token cannot **view_user**)
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -173,10 +181,19 @@ def find_flow_pk_first(base: str, slugs: list[str]) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def signing_key_pk(base: str):
|
def signing_key_pk(base: str):
|
||||||
code, payload = req("GET", f"{base}/crypto/certificatekeypairs/?ordering=pk&page_size=1")
|
raw = (os.environ.get("AUTHENTIK_OAUTH_SIGNING_KEY_PK") or "").strip()
|
||||||
if code != 200 or not isinstance(payload, dict) or not payload.get("results"):
|
if raw:
|
||||||
|
return primary_key(raw)
|
||||||
|
code, payload = req_any("GET", f"{base}/crypto/certificatekeypairs/?ordering=pk&page_size=1")
|
||||||
|
if code != 200:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"GET certificatekeypairs -> HTTP {code}: {payload!r}. "
|
||||||
|
"Authentik 2026+ RBAC: set AUTHENTIK_OAUTH_SIGNING_KEY_PK (CertificateKeyPair UUID) or let "
|
||||||
|
"Ansible resolve it from the worker DB (noble_authentik_oauth_signing_key_pk)."
|
||||||
|
)
|
||||||
|
if not isinstance(payload, dict) or not payload.get("results"):
|
||||||
raise RuntimeError("no signing certificate keypairs returned")
|
raise RuntimeError("no signing certificate keypairs returned")
|
||||||
return payload["results"][0]["pk"]
|
return primary_key(payload["results"][0]["pk"])
|
||||||
|
|
||||||
|
|
||||||
def _collect_scope_mappings(base: str) -> list:
|
def _collect_scope_mappings(base: str) -> list:
|
||||||
@@ -187,7 +204,7 @@ def _collect_scope_mappings(base: str) -> list:
|
|||||||
return collect(base, path)
|
return collect(base, path)
|
||||||
except RuntimeError as exc:
|
except RuntimeError as exc:
|
||||||
err = str(exc)
|
err = str(exc)
|
||||||
if " 404" in err or "404:" in err:
|
if " 404" in err or "404:" in err or " 403" in err or "403:" in err:
|
||||||
last_err = exc
|
last_err = exc
|
||||||
continue
|
continue
|
||||||
raise
|
raise
|
||||||
@@ -195,6 +212,17 @@ def _collect_scope_mappings(base: str) -> list:
|
|||||||
|
|
||||||
|
|
||||||
def scope_mapping_pks(base: str) -> list[int | str]:
|
def scope_mapping_pks(base: str) -> list[int | str]:
|
||||||
|
raw = (os.environ.get("AUTHENTIK_OAUTH_SCOPE_MAPPING_PKS") or "").strip()
|
||||||
|
if raw:
|
||||||
|
parts = [p.strip() for p in raw.split(",") if p.strip()]
|
||||||
|
uniq = dedupe_pks_preserve_order([primary_key(p) for p in parts])
|
||||||
|
if len(uniq) < 3:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"AUTHENTIK_OAUTH_SCOPE_MAPPING_PKS must list at least 3 UUIDs (got {len(uniq)}); "
|
||||||
|
"use comma-separated ScopeMapping primary keys in order "
|
||||||
|
"openid,email,profile,offline_access,groups from the worker helper or Admin UI."
|
||||||
|
)
|
||||||
|
return uniq
|
||||||
mappings = _collect_scope_mappings(base)
|
mappings = _collect_scope_mappings(base)
|
||||||
want_scopes = {"openid", "email", "profile", "offline_access", "groups"}
|
want_scopes = {"openid", "email", "profile", "offline_access", "groups"}
|
||||||
pks: list[int | str] = []
|
pks: list[int | str] = []
|
||||||
@@ -225,7 +253,11 @@ def scope_mapping_pks(base: str) -> list[int | str]:
|
|||||||
hints = ", ".join(
|
hints = ", ".join(
|
||||||
f"GET {base}{p.split('?', 1)[0]}/" for p in _OAUTH_SCOPE_MAPPING_LIST_PATHS
|
f"GET {base}{p.split('?', 1)[0]}/" for p in _OAUTH_SCOPE_MAPPING_LIST_PATHS
|
||||||
)
|
)
|
||||||
raise RuntimeError(f"too few oauth scope mappings resolved ({uniq}); inspect one of: {hints}")
|
raise RuntimeError(
|
||||||
|
f"too few oauth scope mappings resolved ({uniq}); inspect one of: {hints}. "
|
||||||
|
"Authentik 2026+ RBAC: set AUTHENTIK_OAUTH_SCOPE_MAPPING_PKS (comma-separated UUIDs) or "
|
||||||
|
"let Ansible resolve them from the worker DB."
|
||||||
|
)
|
||||||
return uniq
|
return uniq
|
||||||
|
|
||||||
|
|
||||||
@@ -234,9 +266,13 @@ def find_oauth2_provider_by_client_id(base: str, client_id: str) -> dict | None:
|
|||||||
url = f"{base}/providers/oauth2/?page_size=100"
|
url = f"{base}/providers/oauth2/?page_size=100"
|
||||||
origin = base.split("/api/v3", 1)[0] if "/api/v3" in base else base
|
origin = base.split("/api/v3", 1)[0] if "/api/v3" in base else base
|
||||||
while url:
|
while url:
|
||||||
code, payload = req("GET", url)
|
code, payload = req_any("GET", url)
|
||||||
if code != 200 or not isinstance(payload, dict):
|
if code != 200 or not isinstance(payload, dict):
|
||||||
raise RuntimeError(f"provider list failed: GET {url} -> {code}")
|
raise RuntimeError(
|
||||||
|
f"provider list failed: GET {url} -> {code}: {payload!r}. "
|
||||||
|
"Authentik 2026+ RBAC: the token may not list OAuth2 providers; use an **akadmin** API token "
|
||||||
|
"with provider permissions, or grant **view_oauth2provider** (see Admin → Roles)."
|
||||||
|
)
|
||||||
for row in payload.get("results") or []:
|
for row in payload.get("results") or []:
|
||||||
if row.get("client_id") == client_id:
|
if row.get("client_id") == client_id:
|
||||||
return row
|
return row
|
||||||
@@ -319,22 +355,52 @@ def upsert_application(base: str, slug: str, name: str, provider_pk: int | str)
|
|||||||
raise RuntimeError(f"GET application {slug} -> {code}: {payload!r}")
|
raise RuntimeError(f"GET application {slug} -> {code}: {payload!r}")
|
||||||
|
|
||||||
|
|
||||||
|
def _group_pk_from_env(name: str) -> str | None:
|
||||||
|
env_keys = {
|
||||||
|
"noble-admins": "AUTHENTIK_NOBLE_ADMINS_GROUP_PK",
|
||||||
|
"noble-editors": "AUTHENTIK_NOBLE_EDITORS_GROUP_PK",
|
||||||
|
}
|
||||||
|
key = env_keys.get(name)
|
||||||
|
if not key:
|
||||||
|
return None
|
||||||
|
raw = (os.environ.get(key) or "").strip()
|
||||||
|
return raw or None
|
||||||
|
|
||||||
|
|
||||||
def ensure_group(base: str, name: str) -> int | str:
|
def ensure_group(base: str, name: str) -> int | str:
|
||||||
code, payload = req("GET", f"{base}/core/groups/?name={urllib.parse.quote(name)}")
|
env_pk = _group_pk_from_env(name)
|
||||||
if code != 200 or not isinstance(payload, dict):
|
if env_pk:
|
||||||
|
return primary_key(env_pk)
|
||||||
|
code, payload = req_any("GET", f"{base}/core/groups/?name={urllib.parse.quote(name)}")
|
||||||
|
if code != 200:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"GET groups name={name!r} -> HTTP {code}: {payload!r}. "
|
||||||
|
"Authentik 2026+ RBAC: set AUTHENTIK_NOBLE_ADMINS_GROUP_PK and "
|
||||||
|
"AUTHENTIK_NOBLE_EDITORS_GROUP_PK (Group UUIDs), or let Ansible resolve/create them via the worker DB."
|
||||||
|
)
|
||||||
|
if not isinstance(payload, dict):
|
||||||
raise RuntimeError("group list failed")
|
raise RuntimeError("group list failed")
|
||||||
results = payload.get("results") or []
|
results = payload.get("results") or []
|
||||||
if results:
|
if results:
|
||||||
return primary_key(results[0]["pk"])
|
return primary_key(results[0]["pk"])
|
||||||
code2, created = req("POST", f"{base}/core/groups/", {"name": name})
|
code2, created = req_any("POST", f"{base}/core/groups/", {"name": name})
|
||||||
if code2 not in (200, 201) or not isinstance(created, dict):
|
if code2 not in (200, 201) or not isinstance(created, dict):
|
||||||
raise RuntimeError(f"POST group {name} -> {code2}")
|
raise RuntimeError(
|
||||||
|
f"POST group {name} -> {code2}: {created!r}. "
|
||||||
|
"If the API user cannot create groups, run the noble_authentik worker helper or create the group in Admin."
|
||||||
|
)
|
||||||
return primary_key(created["pk"])
|
return primary_key(created["pk"])
|
||||||
|
|
||||||
|
|
||||||
def add_user_to_groups(base: str, email: str, group_pks: list[int | str]) -> None:
|
def add_user_to_groups(base: str, email: str, group_pks: list[int | str]) -> None:
|
||||||
code, payload = req("GET", f"{base}/core/users/?email={urllib.parse.quote(email)}")
|
code, payload = req_any("GET", f"{base}/core/users/?email={urllib.parse.quote(email)}")
|
||||||
if code != 200 or not isinstance(payload, dict):
|
if code != 200:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"GET users email={email!r} -> HTTP {code}: {payload!r}. "
|
||||||
|
"Authentik 2026+ RBAC: the token may not list users; add the bootstrap user to **noble-admins** / "
|
||||||
|
"**noble-editors** in Admin, or use a token with **view_user** permission."
|
||||||
|
)
|
||||||
|
if not isinstance(payload, dict):
|
||||||
raise RuntimeError("user list failed")
|
raise RuntimeError("user list failed")
|
||||||
results = payload.get("results") or []
|
results = payload.get("results") or []
|
||||||
if not results:
|
if not results:
|
||||||
@@ -350,9 +416,13 @@ def add_user_to_groups(base: str, email: str, group_pks: list[int | str]) -> Non
|
|||||||
merged = dedupe_pks_preserve_order([*existing, *group_pks])
|
merged = dedupe_pks_preserve_order([*existing, *group_pks])
|
||||||
if {str(x) for x in merged} == {str(x) for x in existing}:
|
if {str(x) for x in merged} == {str(x) for x in existing}:
|
||||||
return
|
return
|
||||||
code2, _ = req("PATCH", f"{base}/core/users/{upk}/", {"groups": merged})
|
code2, err = req_any("PATCH", f"{base}/core/users/{upk}/", {"groups": merged})
|
||||||
if code2 not in (200, 204):
|
if code2 not in (200, 204):
|
||||||
raise RuntimeError(f"PATCH user groups -> {code2}")
|
raise RuntimeError(f"PATCH user groups -> {code2}: {err!r}")
|
||||||
|
|
||||||
|
|
||||||
|
def _truthy_env(name: str) -> bool:
|
||||||
|
return (os.environ.get(name) or "").strip().lower() in ("1", "true", "yes")
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
@@ -360,15 +430,29 @@ def main() -> int:
|
|||||||
tok = os.environ.get("AUTHENTIK_TOKEN", "")
|
tok = os.environ.get("AUTHENTIK_TOKEN", "")
|
||||||
cfg_path = os.environ.get("CLIENT_JSON", "")
|
cfg_path = os.environ.get("CLIENT_JSON", "")
|
||||||
email = os.environ.get("BOOTSTRAP_EMAIL", "")
|
email = os.environ.get("BOOTSTRAP_EMAIL", "")
|
||||||
if not base or not tok or not cfg_path:
|
skip_oidc_rest = _truthy_env("AUTHENTIK_SKIP_OIDC_REST")
|
||||||
print("AUTHENTIK_API_BASE, AUTHENTIK_TOKEN, CLIENT_JSON required", file=sys.stderr)
|
skip_user_group_rest = _truthy_env("AUTHENTIK_SKIP_USER_GROUP_REST")
|
||||||
|
if not base or not tok:
|
||||||
|
print("AUTHENTIK_API_BASE and AUTHENTIK_TOKEN required", file=sys.stderr)
|
||||||
return 2
|
return 2
|
||||||
|
if not skip_oidc_rest and not cfg_path:
|
||||||
|
print("CLIENT_JSON required unless AUTHENTIK_SKIP_OIDC_REST is set", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
clients: dict = {}
|
||||||
|
if not skip_oidc_rest:
|
||||||
with open(cfg_path, encoding="utf-8") as f:
|
with open(cfg_path, encoding="utf-8") as f:
|
||||||
clients = json.load(f)
|
clients = json.load(f)
|
||||||
if not isinstance(clients, dict):
|
if not isinstance(clients, dict):
|
||||||
print("CLIENT_JSON must be an object keyed by provider slug", file=sys.stderr)
|
print("CLIENT_JSON must be an object keyed by provider slug", file=sys.stderr)
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
|
auth_flow: str | None = None
|
||||||
|
invalidation_flow: str | None = None
|
||||||
|
signing_key = None
|
||||||
|
pmap: list[int | str] = []
|
||||||
|
|
||||||
|
if not skip_oidc_rest:
|
||||||
auth_pk = (os.environ.get("AUTHENTIK_OAUTH_AUTHORIZATION_FLOW_PK") or "").strip()
|
auth_pk = (os.environ.get("AUTHENTIK_OAUTH_AUTHORIZATION_FLOW_PK") or "").strip()
|
||||||
inv_pk = (os.environ.get("AUTHENTIK_OAUTH_INVALIDATION_FLOW_PK") or "").strip()
|
inv_pk = (os.environ.get("AUTHENTIK_OAUTH_INVALIDATION_FLOW_PK") or "").strip()
|
||||||
if auth_pk and inv_pk:
|
if auth_pk and inv_pk:
|
||||||
@@ -394,6 +478,8 @@ def main() -> int:
|
|||||||
editors_pk = ensure_group(base, "noble-editors")
|
editors_pk = ensure_group(base, "noble-editors")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if not skip_oidc_rest:
|
||||||
|
assert auth_flow is not None and invalidation_flow is not None
|
||||||
for slug, meta in clients.items():
|
for slug, meta in clients.items():
|
||||||
name = meta.get("name") or slug
|
name = meta.get("name") or slug
|
||||||
cid = meta["client_id"]
|
cid = meta["client_id"]
|
||||||
@@ -413,8 +499,13 @@ def main() -> int:
|
|||||||
)
|
)
|
||||||
upsert_application(base, slug, name, ppk)
|
upsert_application(base, slug, name, ppk)
|
||||||
|
|
||||||
if email:
|
if email and not skip_user_group_rest:
|
||||||
add_user_to_groups(base, email, [admins_pk, editors_pk])
|
add_user_to_groups(base, email, [admins_pk, editors_pk])
|
||||||
|
if skip_oidc_rest and skip_user_group_rest:
|
||||||
|
print("authentik: noble groups verified; OAuth2 apps + bootstrap group membership via worker", flush=True)
|
||||||
|
elif skip_oidc_rest:
|
||||||
|
print("authentik: groups verified; OAuth2 apps via worker (user membership via API)", flush=True)
|
||||||
|
else:
|
||||||
print("authentik: providers + applications configured", flush=True)
|
print("authentik: providers + applications configured", flush=True)
|
||||||
return 0
|
return 0
|
||||||
except RuntimeError as exc:
|
except RuntimeError as exc:
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Run inside the Authentik worker image (see noble_authentik Ansible role).
|
||||||
|
# Ensures **noble-admins** and **noble-editors** exist, then prints their UUIDs (one per line).
|
||||||
|
# Order: noble-admins, noble-editors — matches **configure_authentik.py** usage.
|
||||||
|
from authentik.core.models import Group
|
||||||
|
|
||||||
|
_NAMES = ("noble-admins", "noble-editors")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
for name in _NAMES:
|
||||||
|
g, _ = Group.objects.get_or_create(name=name, defaults={"is_superuser": False})
|
||||||
|
print(str(g.pk))
|
||||||
|
|
||||||
|
|
||||||
|
main()
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Run inside the Authentik worker image: `ak shell -c "exec(open('/tmp/...').read())"`.
|
||||||
|
# Prints two lines: authorization flow UUID, invalidation flow UUID (for configure_authentik.py).
|
||||||
|
from authentik.flows.models import Flow
|
||||||
|
|
||||||
|
|
||||||
|
def _pk(slug: str) -> str:
|
||||||
|
return str(Flow.objects.get(slug=slug).pk)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
auth = _pk("default-provider-authorization-implicit-consent")
|
||||||
|
inv_slug = None
|
||||||
|
for candidate in ("default-invalidation-flow", "default-provider-invalidation-flow"):
|
||||||
|
if Flow.objects.filter(slug=candidate).exists():
|
||||||
|
inv_slug = candidate
|
||||||
|
break
|
||||||
|
if not inv_slug:
|
||||||
|
raise SystemExit("no default invalidation flow (expected one of: default-invalidation-flow, default-provider-invalidation-flow)")
|
||||||
|
print(auth)
|
||||||
|
print(_pk(inv_slug))
|
||||||
|
|
||||||
|
|
||||||
|
main()
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Run inside the Authentik worker image (see noble_authentik Ansible role).
|
||||||
|
# Prints one UUID per line for **ScopeMapping** rows used by configure_authentik.py.
|
||||||
|
#
|
||||||
|
# Authentik 2026.x default blueprint puts **groups** inside the **profile** mapping expression;
|
||||||
|
# there is often no separate scope_name **groups** — so **groups** is optional here.
|
||||||
|
from authentik.providers.oauth2.models import ScopeMapping
|
||||||
|
|
||||||
|
_REQUIRED = ("openid", "email", "profile", "offline_access")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
for scope in _REQUIRED:
|
||||||
|
m = ScopeMapping.objects.filter(scope_name=scope).order_by("name").first()
|
||||||
|
if not m:
|
||||||
|
raise SystemExit(f"no ScopeMapping for scope_name={scope!r}")
|
||||||
|
print(str(m.pk))
|
||||||
|
groups = ScopeMapping.objects.filter(scope_name="groups").order_by("name").first()
|
||||||
|
if groups:
|
||||||
|
print(str(groups.pk))
|
||||||
|
|
||||||
|
|
||||||
|
main()
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Run inside the Authentik worker image (see noble_authentik Ansible role).
|
||||||
|
# Prints one line: default **CertificateKeyPair** UUID (same ordering as REST: by pk).
|
||||||
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
ckp = CertificateKeyPair.objects.order_by("kp_uuid").first()
|
||||||
|
if not ckp:
|
||||||
|
raise SystemExit("no CertificateKeyPair in database")
|
||||||
|
print(str(ckp.pk))
|
||||||
|
|
||||||
|
|
||||||
|
main()
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Run inside the Authentik worker image (**ak shell**; see noble_authentik Ansible role).
|
||||||
|
# Adds the bootstrap user to the given **Group** rows by primary key (ORM; bypasses **GET /core/users/** RBAC).
|
||||||
|
#
|
||||||
|
# Env:
|
||||||
|
# AUTHENTIK_WORKER_USER_GROUPS_SPEC absolute path inside the container to JSON:
|
||||||
|
# {"email": "<bootstrap email>", "group_pks": ["<uuid>", ...]}
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from django.db import transaction
|
||||||
|
|
||||||
|
from authentik.core.models import Group, User
|
||||||
|
|
||||||
|
|
||||||
|
def _load_spec() -> dict[str, Any]:
|
||||||
|
path = (os.environ.get("AUTHENTIK_WORKER_USER_GROUPS_SPEC") or "").strip()
|
||||||
|
if not path:
|
||||||
|
print("AUTHENTIK_WORKER_USER_GROUPS_SPEC must point to the JSON spec file", file=sys.stderr)
|
||||||
|
raise SystemExit(2)
|
||||||
|
with open(path, encoding="utf-8") as f:
|
||||||
|
spec = json.load(f)
|
||||||
|
if not isinstance(spec, dict):
|
||||||
|
raise SystemExit("spec root must be an object")
|
||||||
|
return spec
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
spec = _load_spec()
|
||||||
|
email = (spec.get("email") or "").strip()
|
||||||
|
pks = spec.get("group_pks")
|
||||||
|
if not email:
|
||||||
|
print("spec.email is required", file=sys.stderr)
|
||||||
|
raise SystemExit(2)
|
||||||
|
if not isinstance(pks, list) or not pks:
|
||||||
|
print("spec.group_pks must be a non-empty list of UUIDs", file=sys.stderr)
|
||||||
|
raise SystemExit(2)
|
||||||
|
|
||||||
|
user = User.objects.filter(email__iexact=email).first()
|
||||||
|
if user is None:
|
||||||
|
print(f"WARN: no user with email {email!r}; skip group membership (ORM)", file=sys.stderr)
|
||||||
|
return
|
||||||
|
|
||||||
|
groups = [Group.objects.get(pk=pk) for pk in pks]
|
||||||
|
with transaction.atomic():
|
||||||
|
user.groups.add(*groups)
|
||||||
|
|
||||||
|
print("worker: bootstrap user group membership updated", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Run inside the Authentik worker image (**ak shell**; see noble_authentik Ansible role).
|
||||||
|
# Restores **Admin interface** access for the bootstrap account: ensures the **authentik Admins** group
|
||||||
|
# exists with **is_superuser** set and adds **akadmin** (or the bootstrap user by email) to that group.
|
||||||
|
# Authentik 2026+ derives **canAccessAdmin** from **User.is_superuser**, which requires membership in a
|
||||||
|
# group with **is_superuser=True** (see role README).
|
||||||
|
#
|
||||||
|
# Env:
|
||||||
|
# AUTHENTIK_WORKER_ADMIN_ACCESS_SPEC path inside the container to JSON: {"bootstrap_email": "..."}
|
||||||
|
# (email may be empty — then only **username=akadmin** is considered)
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from django.db import transaction
|
||||||
|
|
||||||
|
from authentik.core.models import Group, User
|
||||||
|
|
||||||
|
_ADMINS = "authentik Admins"
|
||||||
|
|
||||||
|
|
||||||
|
def _load_spec() -> dict[str, Any]:
|
||||||
|
path = (os.environ.get("AUTHENTIK_WORKER_ADMIN_ACCESS_SPEC") or "").strip()
|
||||||
|
if not path:
|
||||||
|
print("AUTHENTIK_WORKER_ADMIN_ACCESS_SPEC must point to the JSON spec file", file=sys.stderr)
|
||||||
|
raise SystemExit(2)
|
||||||
|
with open(path, encoding="utf-8") as f:
|
||||||
|
spec = json.load(f)
|
||||||
|
if not isinstance(spec, dict):
|
||||||
|
raise SystemExit("spec root must be an object")
|
||||||
|
return spec
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_user(spec: dict[str, Any]) -> User | None:
|
||||||
|
u = User.objects.filter(username="akadmin").first()
|
||||||
|
if u is not None:
|
||||||
|
return u
|
||||||
|
email = (spec.get("bootstrap_email") or "").strip()
|
||||||
|
if not email:
|
||||||
|
return None
|
||||||
|
return User.objects.filter(email__iexact=email).first()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
spec = _load_spec()
|
||||||
|
user = _resolve_user(spec)
|
||||||
|
if user is None:
|
||||||
|
print(
|
||||||
|
"WARN: no user with username=akadmin and no bootstrap_email match; skip authentik Admins repair",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
adm, _ = Group.objects.get_or_create(
|
||||||
|
name=_ADMINS,
|
||||||
|
defaults={"is_superuser": True},
|
||||||
|
)
|
||||||
|
if not adm.is_superuser:
|
||||||
|
adm.is_superuser = True
|
||||||
|
adm.save(update_fields=["is_superuser"])
|
||||||
|
user.groups.add(adm)
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"worker: {user.username!r} is in {_ADMINS!r} (superuser group); log out of Authentik and sign in again",
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
110
ansible/roles/noble_authentik/files/worker_upsert_oauth_oidc.py
Normal file
110
ansible/roles/noble_authentik/files/worker_upsert_oauth_oidc.py
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
# Run inside the Authentik worker image (**ak shell**; see noble_authentik Ansible role).
|
||||||
|
# Upserts **OAuth2Provider** + **Application** from a JSON spec (Django ORM; bypasses OAuth2 provider REST RBAC).
|
||||||
|
#
|
||||||
|
# Env:
|
||||||
|
# AUTHENTIK_WORKER_OIDC_SPEC absolute path inside the container to the JSON spec file
|
||||||
|
#
|
||||||
|
# Spec JSON:
|
||||||
|
# {
|
||||||
|
# "authorization_flow": "<uuid>",
|
||||||
|
# "invalidation_flow": "<uuid>",
|
||||||
|
# "signing_key": "<uuid>",
|
||||||
|
# "property_mappings": ["<uuid>", ...],
|
||||||
|
# "clients": { "<slug>": {"name","client_id","client_secret","redirect_uri"}, ... }
|
||||||
|
# }
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from django.db import transaction
|
||||||
|
|
||||||
|
from authentik.core.models import Application, PropertyMapping
|
||||||
|
from authentik.flows.models import Flow
|
||||||
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
|
from authentik.providers.oauth2.models import (
|
||||||
|
ClientTypes,
|
||||||
|
OAuth2Provider,
|
||||||
|
RedirectURI,
|
||||||
|
RedirectURIMatchingMode,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_spec() -> dict[str, Any]:
|
||||||
|
path = (os.environ.get("AUTHENTIK_WORKER_OIDC_SPEC") or "").strip()
|
||||||
|
if not path:
|
||||||
|
print("AUTHENTIK_WORKER_OIDC_SPEC must point to the JSON spec file", file=sys.stderr)
|
||||||
|
raise SystemExit(2)
|
||||||
|
with open(path, encoding="utf-8") as f:
|
||||||
|
spec = json.load(f)
|
||||||
|
if not isinstance(spec, dict):
|
||||||
|
raise SystemExit("spec root must be an object")
|
||||||
|
return spec
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
spec = _load_spec()
|
||||||
|
auth_id = spec.get("authorization_flow")
|
||||||
|
inv_id = spec.get("invalidation_flow")
|
||||||
|
sk_id = spec.get("signing_key")
|
||||||
|
pmap_ids = spec.get("property_mappings")
|
||||||
|
clients = spec.get("clients")
|
||||||
|
if not auth_id or not inv_id or not sk_id:
|
||||||
|
print("spec requires authorization_flow, invalidation_flow, signing_key", file=sys.stderr)
|
||||||
|
raise SystemExit(2)
|
||||||
|
if not isinstance(pmap_ids, list) or len(pmap_ids) < 1:
|
||||||
|
print("spec.property_mappings must be a non-empty list of UUIDs", file=sys.stderr)
|
||||||
|
raise SystemExit(2)
|
||||||
|
if not isinstance(clients, dict) or not clients:
|
||||||
|
print("spec.clients must be a non-empty object", file=sys.stderr)
|
||||||
|
raise SystemExit(2)
|
||||||
|
|
||||||
|
auth_flow = Flow.objects.get(pk=auth_id)
|
||||||
|
inv_flow = Flow.objects.get(pk=inv_id)
|
||||||
|
signing_key = CertificateKeyPair.objects.get(pk=sk_id)
|
||||||
|
mappings = list(PropertyMapping.objects.filter(pk__in=pmap_ids))
|
||||||
|
if len(mappings) != len(pmap_ids):
|
||||||
|
found = {str(m.pk) for m in mappings}
|
||||||
|
missing = [p for p in pmap_ids if str(p) not in found]
|
||||||
|
print(f"property_mappings: unknown pk(s): {missing}", file=sys.stderr)
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
for slug, meta in clients.items():
|
||||||
|
if not isinstance(meta, dict):
|
||||||
|
print(f"clients[{slug!r}] must be an object", file=sys.stderr)
|
||||||
|
raise SystemExit(2)
|
||||||
|
name = (meta.get("name") or slug).strip()
|
||||||
|
cid = meta["client_id"]
|
||||||
|
csec = meta["client_secret"]
|
||||||
|
redir = meta["redirect_uri"].strip()
|
||||||
|
redirect = [RedirectURI(matching_mode=RedirectURIMatchingMode.STRICT, url=redir)]
|
||||||
|
|
||||||
|
prov = OAuth2Provider.objects.filter(client_id=cid).first()
|
||||||
|
if prov is None:
|
||||||
|
prov = OAuth2Provider(client_id=cid)
|
||||||
|
prov.name = name
|
||||||
|
prov.client_secret = csec
|
||||||
|
prov.client_type = ClientTypes.CONFIDENTIAL
|
||||||
|
prov.authorization_flow = auth_flow
|
||||||
|
prov.invalidation_flow = inv_flow
|
||||||
|
prov.signing_key = signing_key
|
||||||
|
prov.redirect_uris = redirect
|
||||||
|
prov.save()
|
||||||
|
prov.property_mappings.set(mappings)
|
||||||
|
|
||||||
|
app = Application.objects.filter(slug=slug).first()
|
||||||
|
if app is None:
|
||||||
|
Application.objects.create(name=name, slug=slug, provider=prov)
|
||||||
|
else:
|
||||||
|
app.name = name
|
||||||
|
app.provider = prov
|
||||||
|
app.save()
|
||||||
|
|
||||||
|
print("worker: OAuth2 providers + applications upserted", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -129,6 +129,249 @@
|
|||||||
mode: "0600"
|
mode: "0600"
|
||||||
no_log: true
|
no_log: true
|
||||||
|
|
||||||
|
# Authentik 2026+ RBAC: bootstrap tokens often cannot **GET /flows/instances/** (403). Resolve UUIDs
|
||||||
|
# from the worker DB via **ak shell** when inventory PKs are unset (same slugs as configure_authentik.py).
|
||||||
|
- name: Resolve OAuth provider flow UUIDs from authentik-worker (DB; bypasses flows API RBAC)
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
set -euo pipefail
|
||||||
|
NS="{{ noble_authentik_namespace }}"
|
||||||
|
POD="$(kubectl get pods -n "$NS" \
|
||||||
|
-l "app.kubernetes.io/name=authentik,app.kubernetes.io/component=worker" \
|
||||||
|
-o jsonpath='{.items[0].metadata.name}')"
|
||||||
|
REM=/tmp/ansible_resolve_oauth_flow_pks.py
|
||||||
|
kubectl cp "{{ role_path }}/files/resolve_oauth_flow_pks.py" "${NS}/${POD}:${REM}"
|
||||||
|
kubectl exec -n "$NS" "$POD" -- ak shell -c "exec(compile(open('${REM}').read(), 'ansible_resolve_oauth_flow_pks.py', 'exec'))"
|
||||||
|
kubectl exec -n "$NS" "$POD" -- rm -f "$REM" || true
|
||||||
|
environment:
|
||||||
|
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||||
|
register: noble_authentik_oauth_flow_pk_resolve
|
||||||
|
changed_when: false
|
||||||
|
when:
|
||||||
|
- noble_authentik_configure_idp | default(true) | bool
|
||||||
|
- (noble_authentik_oauth_authorization_flow_pk | default('') | trim | length) == 0
|
||||||
|
- (noble_authentik_oauth_invalidation_flow_pk | default('') | trim | length) == 0
|
||||||
|
|
||||||
|
- name: Apply OAuth flow PKs from worker resolution for configure_authentik.py
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
noble_authentik_oauth_authorization_flow_pk: "{{ (noble_authentik_oauth_flow_pk_resolve.stdout_lines | select('match', '^[0-9a-fA-F-]{36}$') | list)[0] }}"
|
||||||
|
noble_authentik_oauth_invalidation_flow_pk: "{{ (noble_authentik_oauth_flow_pk_resolve.stdout_lines | select('match', '^[0-9a-fA-F-]{36}$') | list)[1] }}"
|
||||||
|
when:
|
||||||
|
- noble_authentik_oauth_flow_pk_resolve is defined
|
||||||
|
- not (noble_authentik_oauth_flow_pk_resolve.skipped | default(false))
|
||||||
|
- (noble_authentik_oauth_flow_pk_resolve.rc | default(-1)) == 0
|
||||||
|
- (noble_authentik_oauth_flow_pk_resolve.stdout_lines | default([]) | select('match', '^[0-9a-fA-F-]{36}$') | list | length) >= 2
|
||||||
|
|
||||||
|
# Bootstrap tokens often cannot list **/crypto/certificatekeypairs/** (403).
|
||||||
|
- name: Resolve OAuth signing key UUID from authentik-worker (DB; bypasses crypto API RBAC)
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
set -euo pipefail
|
||||||
|
NS="{{ noble_authentik_namespace }}"
|
||||||
|
POD="$(kubectl get pods -n "$NS" \
|
||||||
|
-l "app.kubernetes.io/name=authentik,app.kubernetes.io/component=worker" \
|
||||||
|
-o jsonpath='{.items[0].metadata.name}')"
|
||||||
|
REM=/tmp/ansible_resolve_oauth_signing_key_pk.py
|
||||||
|
kubectl cp "{{ role_path }}/files/resolve_oauth_signing_key_pk.py" "${NS}/${POD}:${REM}"
|
||||||
|
kubectl exec -n "$NS" "$POD" -- ak shell -c "exec(compile(open('${REM}').read(), 'ansible_resolve_oauth_signing_key_pk.py', 'exec'))"
|
||||||
|
kubectl exec -n "$NS" "$POD" -- rm -f "$REM" || true
|
||||||
|
environment:
|
||||||
|
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||||
|
register: noble_authentik_oauth_signing_key_resolve
|
||||||
|
changed_when: false
|
||||||
|
when:
|
||||||
|
- noble_authentik_configure_idp | default(true) | bool
|
||||||
|
- (noble_authentik_oauth_signing_key_pk | default('') | trim | length) == 0
|
||||||
|
|
||||||
|
- name: Apply OAuth signing key PK from worker resolution for configure_authentik.py
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
noble_authentik_oauth_signing_key_pk: "{{ (noble_authentik_oauth_signing_key_resolve.stdout_lines | select('match', '^[0-9a-fA-F-]{36}$') | list)[0] }}"
|
||||||
|
when:
|
||||||
|
- noble_authentik_oauth_signing_key_resolve is defined
|
||||||
|
- not (noble_authentik_oauth_signing_key_resolve.skipped | default(false))
|
||||||
|
- (noble_authentik_oauth_signing_key_resolve.rc | default(-1)) == 0
|
||||||
|
- (noble_authentik_oauth_signing_key_resolve.stdout_lines | default([]) | select('match', '^[0-9a-fA-F-]{36}$') | list | length) >= 1
|
||||||
|
|
||||||
|
# Bootstrap tokens often cannot list **/propertymappings/provider/scope/** (403).
|
||||||
|
- name: Resolve OAuth scope mapping UUIDs from authentik-worker (DB; bypasses propertymappings API RBAC)
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
set -euo pipefail
|
||||||
|
NS="{{ noble_authentik_namespace }}"
|
||||||
|
POD="$(kubectl get pods -n "$NS" \
|
||||||
|
-l "app.kubernetes.io/name=authentik,app.kubernetes.io/component=worker" \
|
||||||
|
-o jsonpath='{.items[0].metadata.name}')"
|
||||||
|
REM=/tmp/ansible_resolve_oauth_scope_mapping_pks.py
|
||||||
|
kubectl cp "{{ role_path }}/files/resolve_oauth_scope_mapping_pks.py" "${NS}/${POD}:${REM}"
|
||||||
|
kubectl exec -n "$NS" "$POD" -- ak shell -c "exec(compile(open('${REM}').read(), 'ansible_resolve_oauth_scope_mapping_pks.py', 'exec'))"
|
||||||
|
kubectl exec -n "$NS" "$POD" -- rm -f "$REM" || true
|
||||||
|
environment:
|
||||||
|
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||||
|
register: noble_authentik_oauth_scope_mapping_resolve
|
||||||
|
changed_when: false
|
||||||
|
when:
|
||||||
|
- noble_authentik_configure_idp | default(true) | bool
|
||||||
|
- (noble_authentik_oauth_scope_mapping_pks | default('') | trim | length) == 0
|
||||||
|
|
||||||
|
- name: Apply OAuth scope mapping PKs from worker resolution for configure_authentik.py
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
noble_authentik_oauth_scope_mapping_pks: "{{ (noble_authentik_oauth_scope_mapping_resolve.stdout_lines | default([]) | select('match', '^[0-9a-fA-F-]{36}$') | list) | join(',') }}"
|
||||||
|
when:
|
||||||
|
- noble_authentik_oauth_scope_mapping_resolve is defined
|
||||||
|
- not (noble_authentik_oauth_scope_mapping_resolve.skipped | default(false))
|
||||||
|
- (noble_authentik_oauth_scope_mapping_resolve.rc | default(-1)) == 0
|
||||||
|
- (noble_authentik_oauth_scope_mapping_resolve.stdout_lines | default([]) | select('match', '^[0-9a-fA-F-]{36}$') | list | length) >= 4
|
||||||
|
|
||||||
|
# Bootstrap tokens often cannot **GET /core/groups/** (403). Worker **get_or_create** ensures groups exist.
|
||||||
|
- name: Resolve noble-admins / noble-editors group UUIDs from authentik-worker (DB; bypasses groups API RBAC)
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
set -euo pipefail
|
||||||
|
NS="{{ noble_authentik_namespace }}"
|
||||||
|
POD="$(kubectl get pods -n "$NS" \
|
||||||
|
-l "app.kubernetes.io/name=authentik,app.kubernetes.io/component=worker" \
|
||||||
|
-o jsonpath='{.items[0].metadata.name}')"
|
||||||
|
REM=/tmp/ansible_resolve_noble_group_pks.py
|
||||||
|
kubectl cp "{{ role_path }}/files/resolve_noble_group_pks.py" "${NS}/${POD}:${REM}"
|
||||||
|
kubectl exec -n "$NS" "$POD" -- ak shell -c "exec(compile(open('${REM}').read(), 'ansible_resolve_noble_group_pks.py', 'exec'))"
|
||||||
|
kubectl exec -n "$NS" "$POD" -- rm -f "$REM" || true
|
||||||
|
environment:
|
||||||
|
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||||
|
register: noble_authentik_noble_group_resolve
|
||||||
|
changed_when: false
|
||||||
|
when:
|
||||||
|
- noble_authentik_configure_idp | default(true) | bool
|
||||||
|
- (noble_authentik_group_pk_noble_admins | default('') | trim | length) == 0
|
||||||
|
- (noble_authentik_group_pk_noble_editors | default('') | trim | length) == 0
|
||||||
|
|
||||||
|
- name: Apply noble group PKs from worker resolution for configure_authentik.py
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
noble_authentik_group_pk_noble_admins: "{{ (noble_authentik_noble_group_resolve.stdout_lines | default([]) | select('match', '^[0-9a-fA-F-]{36}$') | list)[0] }}"
|
||||||
|
noble_authentik_group_pk_noble_editors: "{{ (noble_authentik_noble_group_resolve.stdout_lines | default([]) | select('match', '^[0-9a-fA-F-]{36}$') | list)[1] }}"
|
||||||
|
when:
|
||||||
|
- noble_authentik_noble_group_resolve is defined
|
||||||
|
- not (noble_authentik_noble_group_resolve.skipped | default(false))
|
||||||
|
- (noble_authentik_noble_group_resolve.rc | default(-1)) == 0
|
||||||
|
- (noble_authentik_noble_group_resolve.stdout_lines | default([]) | select('match', '^[0-9a-fA-F-]{36}$') | list | length) >= 2
|
||||||
|
|
||||||
|
- name: Render Authentik worker admin-access spec (JSON for ak shell)
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: authentik-worker-admin-access-spec.json.j2
|
||||||
|
dest: "{{ noble_repo_root }}/ansible/.ansible-tmp/authentik-worker-admin-access-spec.json"
|
||||||
|
mode: "0600"
|
||||||
|
no_log: true
|
||||||
|
when:
|
||||||
|
- noble_authentik_configure_idp | default(true) | bool
|
||||||
|
- noble_authentik_ensure_admin_ui_access | default(true) | bool
|
||||||
|
|
||||||
|
- name: Ensure authentik Admins + superuser group flag (worker ORM; restores admin UI access)
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
set -euo pipefail
|
||||||
|
NS="{{ noble_authentik_namespace }}"
|
||||||
|
POD="$(kubectl get pods -n "$NS" \
|
||||||
|
-l "app.kubernetes.io/name=authentik,app.kubernetes.io/component=worker" \
|
||||||
|
-o jsonpath='{.items[0].metadata.name}')"
|
||||||
|
SPEC=/tmp/ansible_authentik_worker_admin_access_spec.json
|
||||||
|
REM=/tmp/ansible_worker_ensure_authentik_admin_access.py
|
||||||
|
kubectl cp "{{ noble_repo_root }}/ansible/.ansible-tmp/authentik-worker-admin-access-spec.json" "${NS}/${POD}:${SPEC}"
|
||||||
|
kubectl cp "{{ role_path }}/files/worker_ensure_authentik_admin_access.py" "${NS}/${POD}:${REM}"
|
||||||
|
kubectl exec -n "$NS" "$POD" -- env AUTHENTIK_WORKER_ADMIN_ACCESS_SPEC="${SPEC}" ak shell -c "exec(compile(open('${REM}').read(), 'ansible_worker_ensure_authentik_admin_access.py', 'exec'))"
|
||||||
|
kubectl exec -n "$NS" "$POD" -- rm -f "$SPEC" "$REM" || true
|
||||||
|
environment:
|
||||||
|
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||||
|
register: noble_authentik_worker_admin_access
|
||||||
|
changed_when: true
|
||||||
|
when:
|
||||||
|
- noble_authentik_configure_idp | default(true) | bool
|
||||||
|
- noble_authentik_ensure_admin_ui_access | default(true) | bool
|
||||||
|
|
||||||
|
- name: Require OAuth PKs for worker OIDC upsert (ORM path)
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- (noble_authentik_oauth_authorization_flow_pk | default('') | trim | length) > 0
|
||||||
|
- (noble_authentik_oauth_invalidation_flow_pk | default('') | trim | length) > 0
|
||||||
|
- (noble_authentik_oauth_signing_key_pk | default('') | trim | length) > 0
|
||||||
|
- (noble_authentik_oauth_scope_mapping_pks | default('') | trim | length) > 0
|
||||||
|
fail_msg: >-
|
||||||
|
Worker OIDC provisioning needs flow UUIDs, signing key UUID, and comma-separated scope-mapping UUIDs.
|
||||||
|
Ensure worker resolution tasks ran, or set noble_authentik_oauth_* inventory vars (see role README).
|
||||||
|
when:
|
||||||
|
- noble_authentik_configure_idp | default(true) | bool
|
||||||
|
- (noble_authentik_oidc_provision_via | default('worker') | lower) == 'worker'
|
||||||
|
|
||||||
|
- name: Render Authentik worker OIDC spec (JSON for ak shell upsert)
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: authentik-worker-oidc-spec.json.j2
|
||||||
|
dest: "{{ noble_repo_root }}/ansible/.ansible-tmp/authentik-worker-oidc-spec.json"
|
||||||
|
mode: "0600"
|
||||||
|
no_log: true
|
||||||
|
when:
|
||||||
|
- noble_authentik_configure_idp | default(true) | bool
|
||||||
|
- (noble_authentik_oidc_provision_via | default('worker') | lower) == 'worker'
|
||||||
|
|
||||||
|
- name: Upsert OAuth2 providers + applications in authentik-worker (ORM; bypasses provider REST RBAC)
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
set -euo pipefail
|
||||||
|
NS="{{ noble_authentik_namespace }}"
|
||||||
|
POD="$(kubectl get pods -n "$NS" \
|
||||||
|
-l "app.kubernetes.io/name=authentik,app.kubernetes.io/component=worker" \
|
||||||
|
-o jsonpath='{.items[0].metadata.name}')"
|
||||||
|
SPEC=/tmp/ansible_authentik_worker_oidc_spec.json
|
||||||
|
REM=/tmp/ansible_worker_upsert_oauth_oidc.py
|
||||||
|
kubectl cp "{{ noble_repo_root }}/ansible/.ansible-tmp/authentik-worker-oidc-spec.json" "${NS}/${POD}:${SPEC}"
|
||||||
|
kubectl cp "{{ role_path }}/files/worker_upsert_oauth_oidc.py" "${NS}/${POD}:${REM}"
|
||||||
|
kubectl exec -n "$NS" "$POD" -- env AUTHENTIK_WORKER_OIDC_SPEC="${SPEC}" ak shell -c "exec(compile(open('${REM}').read(), 'ansible_worker_upsert_oauth_oidc.py', 'exec'))"
|
||||||
|
kubectl exec -n "$NS" "$POD" -- rm -f "$SPEC" "$REM" || true
|
||||||
|
environment:
|
||||||
|
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||||
|
register: noble_authentik_worker_oidc_upsert
|
||||||
|
changed_when: true
|
||||||
|
when:
|
||||||
|
- noble_authentik_configure_idp | default(true) | bool
|
||||||
|
- (noble_authentik_oidc_provision_via | default('worker') | lower) == 'worker'
|
||||||
|
|
||||||
|
- name: Require noble group PKs for worker bootstrap group membership
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- (noble_authentik_group_pk_noble_admins | default('') | trim | length) > 0
|
||||||
|
- (noble_authentik_group_pk_noble_editors | default('') | trim | length) > 0
|
||||||
|
fail_msg: >-
|
||||||
|
Worker bootstrap group membership needs noble-admins / noble-editors UUIDs (worker DB resolve or inventory).
|
||||||
|
See noble_authentik_group_pk_noble_* in defaults/README.
|
||||||
|
when:
|
||||||
|
- noble_authentik_configure_idp | default(true) | bool
|
||||||
|
- (noble_authentik_oidc_provision_via | default('worker') | lower) == 'worker'
|
||||||
|
- (noble_authentik_bootstrap_email | default('') | trim | length) > 0
|
||||||
|
|
||||||
|
- name: Render Authentik worker user-groups spec (JSON for ak shell)
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: authentik-worker-user-groups-spec.json.j2
|
||||||
|
dest: "{{ noble_repo_root }}/ansible/.ansible-tmp/authentik-worker-user-groups-spec.json"
|
||||||
|
mode: "0600"
|
||||||
|
no_log: true
|
||||||
|
when:
|
||||||
|
- noble_authentik_configure_idp | default(true) | bool
|
||||||
|
- (noble_authentik_oidc_provision_via | default('worker') | lower) == 'worker'
|
||||||
|
- (noble_authentik_bootstrap_email | default('') | trim | length) > 0
|
||||||
|
|
||||||
|
- name: Add bootstrap user to noble groups in authentik-worker (ORM; bypasses users API RBAC)
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
set -euo pipefail
|
||||||
|
NS="{{ noble_authentik_namespace }}"
|
||||||
|
POD="$(kubectl get pods -n "$NS" \
|
||||||
|
-l "app.kubernetes.io/name=authentik,app.kubernetes.io/component=worker" \
|
||||||
|
-o jsonpath='{.items[0].metadata.name}')"
|
||||||
|
SPEC=/tmp/ansible_authentik_worker_user_groups_spec.json
|
||||||
|
REM=/tmp/ansible_worker_add_bootstrap_user_groups.py
|
||||||
|
kubectl cp "{{ noble_repo_root }}/ansible/.ansible-tmp/authentik-worker-user-groups-spec.json" "${NS}/${POD}:${SPEC}"
|
||||||
|
kubectl cp "{{ role_path }}/files/worker_add_bootstrap_user_groups.py" "${NS}/${POD}:${REM}"
|
||||||
|
kubectl exec -n "$NS" "$POD" -- env AUTHENTIK_WORKER_USER_GROUPS_SPEC="${SPEC}" ak shell -c "exec(compile(open('${REM}').read(), 'ansible_worker_add_bootstrap_user_groups.py', 'exec'))"
|
||||||
|
kubectl exec -n "$NS" "$POD" -- rm -f "$SPEC" "$REM" || true
|
||||||
|
environment:
|
||||||
|
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||||
|
register: noble_authentik_worker_user_groups
|
||||||
|
changed_when: true
|
||||||
|
when:
|
||||||
|
- noble_authentik_configure_idp | default(true) | bool
|
||||||
|
- (noble_authentik_oidc_provision_via | default('worker') | lower) == 'worker'
|
||||||
|
- (noble_authentik_bootstrap_email | default('') | trim | length) > 0
|
||||||
|
|
||||||
- name: Configure Authentik OAuth2/OIDC providers (API)
|
- name: Configure Authentik OAuth2/OIDC providers (API)
|
||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
argv:
|
argv:
|
||||||
@@ -141,6 +384,12 @@
|
|||||||
CLIENT_JSON: "{{ noble_repo_root }}/ansible/.ansible-tmp/authentik-clients.json"
|
CLIENT_JSON: "{{ noble_repo_root }}/ansible/.ansible-tmp/authentik-clients.json"
|
||||||
AUTHENTIK_OAUTH_AUTHORIZATION_FLOW_PK: "{{ noble_authentik_oauth_authorization_flow_pk | default('') }}"
|
AUTHENTIK_OAUTH_AUTHORIZATION_FLOW_PK: "{{ noble_authentik_oauth_authorization_flow_pk | default('') }}"
|
||||||
AUTHENTIK_OAUTH_INVALIDATION_FLOW_PK: "{{ noble_authentik_oauth_invalidation_flow_pk | default('') }}"
|
AUTHENTIK_OAUTH_INVALIDATION_FLOW_PK: "{{ noble_authentik_oauth_invalidation_flow_pk | default('') }}"
|
||||||
|
AUTHENTIK_OAUTH_SIGNING_KEY_PK: "{{ noble_authentik_oauth_signing_key_pk | default('') }}"
|
||||||
|
AUTHENTIK_OAUTH_SCOPE_MAPPING_PKS: "{{ noble_authentik_oauth_scope_mapping_pks | default('') }}"
|
||||||
|
AUTHENTIK_NOBLE_ADMINS_GROUP_PK: "{{ noble_authentik_group_pk_noble_admins | default('') }}"
|
||||||
|
AUTHENTIK_NOBLE_EDITORS_GROUP_PK: "{{ noble_authentik_group_pk_noble_editors | default('') }}"
|
||||||
|
AUTHENTIK_SKIP_OIDC_REST: "{{ '1' if (noble_authentik_oidc_provision_via | default('worker') | lower) == 'worker' else '' }}"
|
||||||
|
AUTHENTIK_SKIP_USER_GROUP_REST: "{{ '1' if ((noble_authentik_oidc_provision_via | default('worker') | lower) == 'worker' and (noble_authentik_bootstrap_email | default('') | trim | length) > 0) else '' }}"
|
||||||
when: noble_authentik_configure_idp | default(true) | bool
|
when: noble_authentik_configure_idp | default(true) | bool
|
||||||
changed_when: true
|
changed_when: true
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"bootstrap_email": {{ noble_authentik_bootstrap_email | default('') | trim | to_json }}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"authorization_flow": {{ noble_authentik_oauth_authorization_flow_pk | trim | to_json }},
|
||||||
|
"invalidation_flow": {{ noble_authentik_oauth_invalidation_flow_pk | trim | to_json }},
|
||||||
|
"signing_key": {{ noble_authentik_oauth_signing_key_pk | trim | to_json }},
|
||||||
|
"property_mappings": {{ (noble_authentik_oauth_scope_mapping_pks | default('')).split(',') | map('trim') | reject('equalto', '') | list | to_json }},
|
||||||
|
"clients": {
|
||||||
|
"argocd": {
|
||||||
|
"name": "Argo CD",
|
||||||
|
"client_id": {{ noble_authentik_client_id_argocd | to_json }},
|
||||||
|
"client_secret": {{ noble_authentik_client_secret_argocd | to_json }},
|
||||||
|
"redirect_uri": "https://argo.apps.noble.lab.pcenicni.dev/auth/callback"
|
||||||
|
},
|
||||||
|
"grafana": {
|
||||||
|
"name": "Grafana",
|
||||||
|
"client_id": {{ noble_authentik_client_id_grafana | to_json }},
|
||||||
|
"client_secret": {{ noble_authentik_client_secret_grafana | to_json }},
|
||||||
|
"redirect_uri": "https://grafana.apps.noble.lab.pcenicni.dev/login/generic_oauth"
|
||||||
|
},
|
||||||
|
"headlamp": {
|
||||||
|
"name": "Headlamp",
|
||||||
|
"client_id": {{ noble_authentik_client_id_headlamp | to_json }},
|
||||||
|
"client_secret": {{ noble_authentik_client_secret_headlamp | to_json }},
|
||||||
|
"redirect_uri": "https://headlamp.apps.noble.lab.pcenicni.dev/oidc-callback"
|
||||||
|
},
|
||||||
|
"oauth2-proxy": {
|
||||||
|
"name": "oauth2-proxy (ForwardAuth)",
|
||||||
|
"client_id": {{ noble_authentik_client_id_oauth2_proxy | to_json }},
|
||||||
|
"client_secret": {{ noble_authentik_client_secret_oauth2_proxy | to_json }},
|
||||||
|
"redirect_uri": "https://{{ noble_authentik_oauth2_proxy_host }}/oauth2/callback"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"email": {{ noble_authentik_bootstrap_email | trim | to_json }},
|
||||||
|
"group_pks": [
|
||||||
|
{{ noble_authentik_group_pk_noble_admins | trim | to_json }},
|
||||||
|
{{ noble_authentik_group_pk_noble_editors | trim | to_json }}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
# Included once per **talos_node** (see **main.yml**). When the cluster probe used **joined** mode,
|
||||||
|
# a straggler may still expose the maintenance API cert (**x509: certificate signed by unknown authority**);
|
||||||
|
# we retry that node with **--insecure** so apply-config can complete.
|
||||||
|
|
||||||
|
- name: Apply machine config (full cluster maintenance — insecure)
|
||||||
|
ansible.builtin.command:
|
||||||
|
argv:
|
||||||
|
- talosctl
|
||||||
|
- apply-config
|
||||||
|
- --insecure
|
||||||
|
- -n
|
||||||
|
- "{{ talos_node.ip }}"
|
||||||
|
- --file
|
||||||
|
- "{{ noble_talos_dir }}/out/{{ talos_node.machine }}"
|
||||||
|
when: noble_talos_apply_insecure | bool
|
||||||
|
changed_when: true
|
||||||
|
|
||||||
|
- name: Apply machine config (joined cluster — TLS with per-node maintenance fallback)
|
||||||
|
when: not (noble_talos_apply_insecure | bool)
|
||||||
|
block:
|
||||||
|
- name: Reset CA mismatch flag for this node
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
noble_talos_apply_node_ca_mismatch: false
|
||||||
|
|
||||||
|
- name: Try TLS (TALOSCONFIG / cluster CA)
|
||||||
|
ansible.builtin.command:
|
||||||
|
argv:
|
||||||
|
- talosctl
|
||||||
|
- apply-config
|
||||||
|
- -n
|
||||||
|
- "{{ talos_node.ip }}"
|
||||||
|
- --file
|
||||||
|
- "{{ noble_talos_dir }}/out/{{ talos_node.machine }}"
|
||||||
|
environment:
|
||||||
|
TALOSCONFIG: "{{ noble_talos_dir }}/out/talosconfig"
|
||||||
|
changed_when: true
|
||||||
|
|
||||||
|
rescue:
|
||||||
|
- name: Detect Talos API still on maintenance / unknown CA (straggler vs first node)
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
noble_talos_apply_node_ca_mismatch: >-
|
||||||
|
{{
|
||||||
|
('unknown authority' in (ansible_failed_result.stderr | default('')))
|
||||||
|
or ('certificate signed by unknown authority' in (ansible_failed_result.stderr | default('')))
|
||||||
|
or ('authentication handshake failed' in (ansible_failed_result.stderr | default('')))
|
||||||
|
}}
|
||||||
|
|
||||||
|
- name: Apply machine config with --insecure (node not yet trusting cluster CA)
|
||||||
|
ansible.builtin.command:
|
||||||
|
argv:
|
||||||
|
- talosctl
|
||||||
|
- apply-config
|
||||||
|
- --insecure
|
||||||
|
- -n
|
||||||
|
- "{{ talos_node.ip }}"
|
||||||
|
- --file
|
||||||
|
- "{{ noble_talos_dir }}/out/{{ talos_node.machine }}"
|
||||||
|
when: noble_talos_apply_node_ca_mismatch | bool
|
||||||
|
register: noble_talos_apply_node_insecure_cmd
|
||||||
|
changed_when: true
|
||||||
|
failed_when: noble_talos_apply_node_insecure_cmd.rc != 0
|
||||||
|
|
||||||
|
- name: Fail when apply-config failed for reasons other than unknown CA
|
||||||
|
ansible.builtin.fail:
|
||||||
|
msg: >-
|
||||||
|
talosctl apply-config failed on {{ talos_node.ip }} (TLS, no insecure fallback):
|
||||||
|
{{ ansible_failed_result.stderr | default('no stderr') }}
|
||||||
|
when: not (noble_talos_apply_node_ca_mismatch | bool)
|
||||||
@@ -114,38 +114,12 @@
|
|||||||
apply-config: {{ 'maintenance (--insecure)' if noble_talos_apply_insecure | bool else 'joined (TALOSCONFIG)' }}
|
apply-config: {{ 'maintenance (--insecure)' if noble_talos_apply_insecure | bool else 'joined (TALOSCONFIG)' }}
|
||||||
(noble_talos_apply_mode={{ noble_talos_apply_mode | default('auto') }})
|
(noble_talos_apply_mode={{ noble_talos_apply_mode | default('auto') }})
|
||||||
|
|
||||||
- name: Apply machine config to each node (first install — insecure)
|
- name: Apply machine config to each Talos node (TLS or insecure; per-node CA fallback when joined)
|
||||||
ansible.builtin.command:
|
ansible.builtin.include_tasks: apply_talos_node_config.yml
|
||||||
argv:
|
|
||||||
- talosctl
|
|
||||||
- apply-config
|
|
||||||
- --insecure
|
|
||||||
- -n
|
|
||||||
- "{{ item.ip }}"
|
|
||||||
- --file
|
|
||||||
- "{{ noble_talos_dir }}/out/{{ item.machine }}"
|
|
||||||
loop: "{{ noble_talos_nodes }}"
|
loop: "{{ noble_talos_nodes }}"
|
||||||
loop_control:
|
loop_control:
|
||||||
label: "{{ item.ip }}"
|
label: "{{ talos_node.ip }}"
|
||||||
when: noble_talos_apply_insecure | bool
|
loop_var: talos_node
|
||||||
changed_when: true
|
|
||||||
|
|
||||||
- name: Apply machine config to each node (cluster already has TLS — no insecure)
|
|
||||||
ansible.builtin.command:
|
|
||||||
argv:
|
|
||||||
- talosctl
|
|
||||||
- apply-config
|
|
||||||
- -n
|
|
||||||
- "{{ item.ip }}"
|
|
||||||
- --file
|
|
||||||
- "{{ noble_talos_dir }}/out/{{ item.machine }}"
|
|
||||||
environment:
|
|
||||||
TALOSCONFIG: "{{ noble_talos_dir }}/out/talosconfig"
|
|
||||||
loop: "{{ noble_talos_nodes }}"
|
|
||||||
loop_control:
|
|
||||||
label: "{{ item.ip }}"
|
|
||||||
when: not (noble_talos_apply_insecure | bool)
|
|
||||||
changed_when: true
|
|
||||||
|
|
||||||
# apply-config triggers reboots; apid on :50000 must accept connections before talosctl bootstrap / kubeconfig.
|
# apply-config triggers reboots; apid on :50000 must accept connections before talosctl bootstrap / kubeconfig.
|
||||||
- name: Wait for Talos machine API (apid) on bootstrap node
|
- name: Wait for Talos machine API (apid) on bootstrap node
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ ingress:
|
|||||||
extraArgs:
|
extraArgs:
|
||||||
provider: oidc
|
provider: oidc
|
||||||
skip-provider-button: "true"
|
skip-provider-button: "true"
|
||||||
|
# Authentik bootstrap / local users often omit **email_verified** in the id_token; without this,
|
||||||
|
# oauth2-proxy returns **500** on `/oauth2/callback` with: "email in id_token (...) isn't verified".
|
||||||
|
# Prefer marking the account verified in Authentik (Directory) in production.
|
||||||
|
insecure-oidc-allow-unverified-email: "true"
|
||||||
oidc-issuer-url: "https://auth.apps.noble.lab.pcenicni.dev/application/o/oauth2-proxy/"
|
oidc-issuer-url: "https://auth.apps.noble.lab.pcenicni.dev/application/o/oauth2-proxy/"
|
||||||
redirect-url: "https://oauth2.apps.noble.lab.pcenicni.dev/oauth2/callback"
|
redirect-url: "https://oauth2.apps.noble.lab.pcenicni.dev/oauth2/callback"
|
||||||
scope: "openid profile email groups"
|
scope: "openid profile email groups"
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ talosctl apply-config -n 192.168.50.20 --file out/noble-neon.yaml
|
|||||||
|
|
||||||
**Do not pass `--insecure` for (B).** With `--insecure`, `talosctl` does not use client certificates from `TALOSCONFIG`, so the node still responds with `tls: certificate required`. The flag means “maintenance API only,” not “skip server verification.”
|
**Do not pass `--insecure` for (B).** With `--insecure`, `talosctl` does not use client certificates from `TALOSCONFIG`, so the node still responds with `tls: certificate required`. The flag means “maintenance API only,” not “skip server verification.”
|
||||||
|
|
||||||
|
**Mixed state:** the first node may already present the **cluster CA** (TLS works with `TALOSCONFIG`) while another node is still on the **maintenance** cert (`x509: certificate signed by unknown authority`). **`ansible-playbook playbooks/deploy.yml`** (`talos_phase_a`) probes only the first node in **`noble_talos_nodes`**; for each remaining node it tries TLS first, then retries with **`--insecure`** when stderr indicates an unknown CA / handshake failure.
|
||||||
|
|
||||||
**Wrong (what triggers the error):**
|
**Wrong (what triggers the error):**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
Reference in New Issue
Block a user