Add Authentik and oauth2-proxy support to noble cluster setup, including environment variables, playbook tags, and landing URLs. Update README and kustomization.yaml to reflect new OIDC integration, enhancing security and user authentication capabilities.
This commit is contained in:
13
.env.sample
13
.env.sample
@@ -17,3 +17,16 @@ NOBLE_VELERO_S3_BUCKET=
|
||||
NOBLE_VELERO_S3_URL=
|
||||
NOBLE_VELERO_AWS_ACCESS_KEY_ID=
|
||||
NOBLE_VELERO_AWS_SECRET_ACCESS_KEY=
|
||||
|
||||
# Authentik + OIDC — when **noble_authentik_install=true**, Ansible installs Authentik and reconfigures Argo CD, Grafana, Headlamp (native OIDC) and Prometheus/Alertmanager/Longhorn via oauth2-proxy (OIDC to Authentik + Traefik ForwardAuth). See **ansible/roles/noble_authentik/README.md**.
|
||||
NOBLE_AUTHENTIK_SECRET_KEY=
|
||||
NOBLE_AUTHENTIK_POSTGRES_PASSWORD=
|
||||
NOBLE_AUTHENTIK_BOOTSTRAP_TOKEN=
|
||||
NOBLE_AUTHENTIK_BOOTSTRAP_EMAIL=
|
||||
NOBLE_AUTHENTIK_BOOTSTRAP_PASSWORD=
|
||||
NOBLE_AUTHENTIK_CLIENT_SECRET_ARGOCD=
|
||||
NOBLE_AUTHENTIK_CLIENT_SECRET_GRAFANA=
|
||||
NOBLE_AUTHENTIK_CLIENT_SECRET_HEADLAMP=
|
||||
NOBLE_AUTHENTIK_CLIENT_SECRET_OAUTH2_PROXY=
|
||||
# Random secret for oauth2-proxy session cookie (see oauth2-proxy Helm chart docs; e.g. openssl rand -base64 32 | head -c 32 | base64)
|
||||
NOBLE_AUTHENTIK_OAUTH2_PROXY_COOKIE_SECRET=
|
||||
|
||||
@@ -76,11 +76,12 @@ ansible-playbook playbooks/noble.yml --tags cilium,metallb
|
||||
ansible-playbook playbooks/noble.yml --tags trivy
|
||||
ansible-playbook playbooks/noble.yml --skip-tags newt
|
||||
ansible-playbook playbooks/noble.yml --tags velero -e noble_velero_install=true -e noble_velero_s3_bucket=... -e noble_velero_s3_url=...
|
||||
ansible-playbook playbooks/noble.yml --tags authentik -e noble_authentik_install=true
|
||||
```
|
||||
|
||||
### Variables — `group_vars/all.yml` and role defaults
|
||||
|
||||
- **`group_vars/all.yml`:** **`noble_newt_install`**, **`noble_velero_install`**, **`noble_cert_manager_require_cloudflare_secret`**, **`noble_argocd_apply_root_application`**, **`noble_argocd_apply_bootstrap_root_application`**, **`noble_k8s_api_server_override`**, **`noble_k8s_api_server_auto_fallback`**, **`noble_k8s_api_server_fallback`**, **`noble_skip_k8s_health_check`**
|
||||
- **`group_vars/all.yml`:** **`noble_newt_install`**, **`noble_velero_install`**, **`noble_authentik_install`**, **`noble_cert_manager_require_cloudflare_secret`**, **`noble_argocd_apply_root_application`**, **`noble_argocd_apply_bootstrap_root_application`**, **`noble_k8s_api_server_override`**, **`noble_k8s_api_server_auto_fallback`**, **`noble_k8s_api_server_fallback`**, **`noble_skip_k8s_health_check`**
|
||||
- **`roles/noble_platform/defaults/main.yml`:** **`noble_apply_sops_secrets`**, **`noble_sops_age_key_file`** (SOPS secrets under **`clusters/noble/secrets/`**)
|
||||
|
||||
## Roles
|
||||
@@ -89,7 +90,7 @@ ansible-playbook playbooks/noble.yml --tags velero -e noble_velero_install=true
|
||||
|------|----------|
|
||||
| `talos_phase_a` | Talos genconfig, apply-config, bootstrap, kubeconfig |
|
||||
| `helm_repos` | `helm repo add` / `update` |
|
||||
| `noble_*` | Cilium, CSI Volume Snapshot CRDs + controller, metrics-server, Longhorn, MetalLB (20m Helm wait), kube-vip, Traefik, cert-manager, Newt, Argo CD, Kyverno, platform stack, **Trivy Operator**, Velero (optional) |
|
||||
| `noble_*` | Cilium, CSI Volume Snapshot CRDs + controller, metrics-server, Longhorn, MetalLB (20m Helm wait), kube-vip, Traefik, cert-manager, Newt, Argo CD, Kyverno, platform stack, **Authentik** (optional OIDC), **Trivy Operator**, Velero (optional) |
|
||||
| `noble_landing_urls` | Writes **`ansible/output/noble-lab-ui-urls.md`** — URLs, service names, and (optional) Argo/Grafana passwords from Secrets |
|
||||
| `noble_post_deploy` | Post-install reminders |
|
||||
| `talos_bootstrap` | Genconfig-only (used by older playbook) |
|
||||
|
||||
@@ -26,3 +26,6 @@ noble_velero_install: false
|
||||
noble_argocd_apply_root_application: true
|
||||
# Bootstrap kustomize in Argo (**noble-bootstrap-root** → **clusters/noble/bootstrap**). Applied with manual sync; enable automation after **noble.yml** (see **clusters/noble/bootstrap/argocd/README.md** §5).
|
||||
noble_argocd_apply_bootstrap_root_application: true
|
||||
|
||||
# Authentik (OIDC IdP) + oauth2-proxy ForwardAuth — set **true** after **.env** has NOBLE_AUTHENTIK_* (see ansible/roles/noble_authentik/README.md).
|
||||
noble_authentik_install: false
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# Run from repo **ansible/** directory: ansible-playbook playbooks/noble.yml
|
||||
#
|
||||
# Tags: repos, cilium, csi_snapshot, metrics, longhorn, metallb, kube_vip, traefik, cert_manager, newt,
|
||||
# argocd, kyverno, kyverno_policies, platform, trivy, velero, all (default)
|
||||
# argocd, kyverno, kyverno_policies, platform, authentik, trivy, velero, all (default)
|
||||
- name: Noble cluster — platform stack (Ansible-managed)
|
||||
hosts: localhost
|
||||
connection: local
|
||||
@@ -228,6 +228,8 @@
|
||||
tags: [argocd, gitops]
|
||||
- role: noble_platform
|
||||
tags: [platform, observability, apps]
|
||||
- role: noble_authentik
|
||||
tags: [authentik, sso, oauth, oidc]
|
||||
- role: noble_trivy
|
||||
tags: [trivy, security, scanning]
|
||||
- role: noble_velero
|
||||
|
||||
@@ -15,3 +15,5 @@ noble_helm_repos:
|
||||
- { name: kyverno, url: "https://kyverno.github.io/kyverno/" }
|
||||
- { name: vmware-tanzu, url: "https://vmware-tanzu.github.io/helm-charts" }
|
||||
- { name: aqua, url: "https://aquasecurity.github.io/helm-charts/" }
|
||||
- { name: goauthentik, url: "https://charts.goauthentik.io" }
|
||||
- { name: oauth2-proxy, url: "https://oauth2-proxy.github.io/manifests" }
|
||||
|
||||
29
ansible/roles/noble_authentik/README.md
Normal file
29
ansible/roles/noble_authentik/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# noble_authentik — Authentik + OIDC for the noble stack
|
||||
|
||||
Installs **Authentik** (Helm `goauthentik/authentik`) as the cluster IdP, **oauth2-proxy** as an **OIDC** client to Authentik for Traefik **ForwardAuth** (Prometheus, Alertmanager, Longhorn UI), and re-applies Helm values so **Argo CD**, **Grafana**, and **Headlamp** use **native OIDC** to Authentik (not HTTP BasicAuth).
|
||||
|
||||
## Enable
|
||||
|
||||
1. Copy repository **`.env.sample`** to **`.env`** and set every **`NOBLE_AUTHENTIK_*`** variable (see comments there).
|
||||
2. Set **`noble_authentik_install: true`** in **`ansible/group_vars/all.yml`** (or pass **`-e noble_authentik_install=true`**).
|
||||
3. Run **`ansible-playbook playbooks/noble.yml --tags authentik`** (or a full **`noble.yml`**) from **`ansible/`** with a working **`KUBECONFIG`**.
|
||||
|
||||
`noble_authentik` runs **after** **`noble_platform`** so Grafana / Headlamp / Prometheus exist before SSO Helm upgrades.
|
||||
|
||||
## Variables
|
||||
|
||||
See **`defaults/main.yml`**. Hostnames default to **`auth.apps.noble.lab.pcenicni.dev`** and **`oauth2.apps.noble.lab.pcenicni.dev`**.
|
||||
|
||||
## 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.
|
||||
|
||||
## RBAC notes
|
||||
|
||||
- **Argo CD:** `noble-admins` group → `role:admin` (see **`clusters/noble/bootstrap/argocd/values-authentik-oidc.yaml`**).
|
||||
- **Grafana:** `noble-admins` → Admin, `noble-editors` → Editor (see **`values-authentik-oidc.yaml`**).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- 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`**.
|
||||
42
ansible/roles/noble_authentik/defaults/main.yml
Normal file
42
ansible/roles/noble_authentik/defaults/main.yml
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
# Set **noble_authentik_install: true** after filling **.env** (see role README and repository **.env.sample**).
|
||||
noble_authentik_install: false
|
||||
# When true, run **configure_authentik.py** against the Authentik API (requires bootstrap token + client secrets).
|
||||
noble_authentik_configure_idp: true
|
||||
|
||||
noble_authentik_chart_version: "2026.2.3"
|
||||
noble_authentik_oauth2_proxy_chart_version: "10.4.3"
|
||||
|
||||
noble_authentik_host: auth.apps.noble.lab.pcenicni.dev
|
||||
noble_authentik_public_url: "https://{{ noble_authentik_host }}"
|
||||
noble_authentik_api_base: "{{ noble_authentik_public_url }}/api/v3"
|
||||
|
||||
noble_authentik_oauth2_proxy_host: oauth2.apps.noble.lab.pcenicni.dev
|
||||
|
||||
# OIDC client ids (must match Authentik providers created by configure script)
|
||||
noble_authentik_client_id_argocd: argocd
|
||||
noble_authentik_client_id_grafana: grafana
|
||||
noble_authentik_client_id_headlamp: headlamp
|
||||
noble_authentik_client_id_oauth2_proxy: oauth2-proxy
|
||||
|
||||
# Secrets / bootstrap — prefer **lookup('env', ...)** set via repository **.env** (see from_env.yml).
|
||||
noble_authentik_secret_key: ""
|
||||
noble_authentik_postgresql_password: ""
|
||||
noble_authentik_bootstrap_token: ""
|
||||
noble_authentik_bootstrap_email: ""
|
||||
noble_authentik_bootstrap_password: ""
|
||||
|
||||
noble_authentik_client_secret_argocd: ""
|
||||
noble_authentik_client_secret_grafana: ""
|
||||
noble_authentik_client_secret_headlamp: ""
|
||||
noble_authentik_client_secret_oauth2_proxy: ""
|
||||
noble_authentik_oauth2_proxy_cookie_secret: ""
|
||||
|
||||
noble_authentik_helm_wait_timeout: 25m
|
||||
|
||||
# Re-apply the same chart versions as the rest of noble.yml when flipping SSO on.
|
||||
noble_authentik_argocd_chart_version: "9.4.17"
|
||||
noble_authentik_kube_prometheus_chart_version: "82.15.1"
|
||||
noble_authentik_headlamp_chart_version: "0.40.1"
|
||||
noble_authentik_longhorn_chart_version: "1.11.2"
|
||||
noble_authentik_kube_prometheus_helm_wait_timeout: 60m
|
||||
Binary file not shown.
259
ansible/roles/noble_authentik/files/configure_authentik.py
Normal file
259
ansible/roles/noble_authentik/files/configure_authentik.py
Normal file
@@ -0,0 +1,259 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Create / update Authentik OAuth2/OpenID providers + applications + groups (stdlib only).
|
||||
Environment:
|
||||
AUTHENTIK_API_BASE e.g. https://auth.apps.example.com/api/v3
|
||||
AUTHENTIK_TOKEN bootstrap token (Bearer)
|
||||
BOOTSTRAP_EMAIL initial admin email (added to noble-admins)
|
||||
CLIENT_JSON path to JSON file: { "argocd": {"client_id","client_secret","redirect_uri"}, ... }
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
|
||||
def req(method: str, url: str, body: dict | None = None) -> tuple[int, dict | list]:
|
||||
data = None
|
||||
headers = {"Authorization": f"Bearer {os.environ['AUTHENTIK_TOKEN']}", "Accept": "application/json"}
|
||||
if body is not None:
|
||||
data = json.dumps(body).encode("utf-8")
|
||||
headers["Content-Type"] = "application/json"
|
||||
r = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(r, timeout=120) as resp:
|
||||
raw = resp.read().decode("utf-8")
|
||||
code = resp.getcode()
|
||||
if not raw:
|
||||
return code, {}
|
||||
return code, json.loads(raw)
|
||||
except urllib.error.HTTPError as e:
|
||||
err = e.read().decode("utf-8", errors="replace")
|
||||
raise RuntimeError(f"{method} {url} -> HTTP {e.code}: {err}") from e
|
||||
|
||||
|
||||
def collect(base: str, path: str) -> list:
|
||||
out: list = []
|
||||
url = base + path
|
||||
origin = base.split("/api/v3", 1)[0] if "/api/v3" in base else base
|
||||
while url:
|
||||
code, payload = req("GET", url)
|
||||
if code != 200:
|
||||
raise RuntimeError(f"GET {url} unexpected {code}")
|
||||
if isinstance(payload, dict) and "results" in payload:
|
||||
out.extend(payload["results"])
|
||||
nxt = payload.get("next") or ""
|
||||
if not nxt:
|
||||
url = ""
|
||||
elif nxt.startswith("http"):
|
||||
url = nxt
|
||||
else:
|
||||
url = origin + nxt
|
||||
else:
|
||||
break
|
||||
return out
|
||||
|
||||
|
||||
def find_flow_pk(base: str, slug: str) -> str:
|
||||
code, payload = req("GET", f"{base}/flows/instances/?slug={urllib.parse.quote(slug)}")
|
||||
if code != 200 or not isinstance(payload, dict):
|
||||
raise RuntimeError(f"flow lookup failed for {slug}")
|
||||
results = payload.get("results") or []
|
||||
if not results:
|
||||
raise RuntimeError(f"flow slug not found: {slug}")
|
||||
return str(results[0]["pk"])
|
||||
|
||||
|
||||
def find_flow_pk_first(base: str, slugs: list[str]) -> str:
|
||||
last_err: Exception | None = None
|
||||
for s in slugs:
|
||||
try:
|
||||
return find_flow_pk(base, s)
|
||||
except RuntimeError as e:
|
||||
last_err = e
|
||||
raise RuntimeError(f"no flow matched {slugs}: {last_err}")
|
||||
|
||||
|
||||
def signing_key_pk(base: str) -> int:
|
||||
code, payload = req("GET", f"{base}/crypto/certificatekeypairs/?ordering=pk&page_size=1")
|
||||
if code != 200 or not isinstance(payload, dict) or not payload.get("results"):
|
||||
raise RuntimeError("no signing certificate keypairs returned")
|
||||
return int(payload["results"][0]["pk"])
|
||||
|
||||
|
||||
def scope_mapping_pks(base: str) -> list[int]:
|
||||
mappings = collect(base, "/propertymappings/oauthscope/?page_size=500")
|
||||
want_scopes = {"openid", "email", "profile", "offline_access", "groups"}
|
||||
pks: list[int] = []
|
||||
for m in mappings:
|
||||
sn = (m.get("scope_name") or "").strip()
|
||||
if sn in want_scopes:
|
||||
pks.append(int(m["pk"]))
|
||||
# de-dupe preserve order
|
||||
seen: set[int] = set()
|
||||
uniq: list[int] = []
|
||||
for p in pks:
|
||||
if p not in seen:
|
||||
seen.add(p)
|
||||
uniq.append(p)
|
||||
if len(uniq) >= 3:
|
||||
return uniq
|
||||
# Fallback: include managed OpenID mappings by name heuristics
|
||||
for m in mappings:
|
||||
name = (m.get("name") or "").lower()
|
||||
if "openid" in name and any(x in name for x in ("openid", "email", "profile", "groups", "offline")):
|
||||
pk = int(m["pk"])
|
||||
if pk not in seen:
|
||||
seen.add(pk)
|
||||
uniq.append(pk)
|
||||
if len(uniq) < 3:
|
||||
raise RuntimeError(f"too few oauth scope mappings resolved ({uniq}); inspect /propertymappings/oauthscope/")
|
||||
return uniq
|
||||
|
||||
|
||||
def upsert_oauth_provider(
|
||||
base: str,
|
||||
slug: str,
|
||||
name: str,
|
||||
client_id: str,
|
||||
client_secret: str,
|
||||
redirect_uri: str,
|
||||
auth_flow: str,
|
||||
invalidation_flow: str,
|
||||
signing_key: int,
|
||||
property_mappings: list[int],
|
||||
) -> int:
|
||||
code, payload = req("GET", f"{base}/providers/oauth2/?slug={urllib.parse.quote(slug)}")
|
||||
if code != 200 or not isinstance(payload, dict):
|
||||
raise RuntimeError("provider list failed")
|
||||
results = payload.get("results") or []
|
||||
body = {
|
||||
"name": name,
|
||||
"slug": slug,
|
||||
"authorization_flow": auth_flow,
|
||||
"invalidation_flow": invalidation_flow,
|
||||
"property_mappings": property_mappings,
|
||||
"client_type": "confidential",
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
"redirect_uris": redirect_uri.strip() + "\n",
|
||||
"signing_key": signing_key,
|
||||
}
|
||||
if results:
|
||||
pk = int(results[0]["pk"])
|
||||
code2, _ = req("PATCH", f"{base}/providers/oauth2/{pk}/", body)
|
||||
if code2 not in (200, 204):
|
||||
raise RuntimeError(f"PATCH provider {slug} -> {code2}")
|
||||
return pk
|
||||
code3, created = req("POST", f"{base}/providers/oauth2/", body)
|
||||
if code3 not in (200, 201) or not isinstance(created, dict):
|
||||
raise RuntimeError(f"POST provider {slug} -> {code3} {created}")
|
||||
return int(created["pk"])
|
||||
|
||||
|
||||
def upsert_application(base: str, slug: str, name: str, provider_pk: int) -> None:
|
||||
code, payload = req("GET", f"{base}/core/applications/?slug={urllib.parse.quote(slug)}")
|
||||
if code != 200 or not isinstance(payload, dict):
|
||||
raise RuntimeError("application list failed")
|
||||
results = payload.get("results") or []
|
||||
body = {"name": name, "slug": slug, "provider": provider_pk}
|
||||
if results:
|
||||
pk = int(results[0]["pk"])
|
||||
code2, _ = req("PATCH", f"{base}/core/applications/{pk}/", body)
|
||||
if code2 not in (200, 204):
|
||||
raise RuntimeError(f"PATCH application {slug} -> {code2}")
|
||||
return
|
||||
code3, _ = req("POST", f"{base}/core/applications/", body)
|
||||
if code3 not in (200, 201):
|
||||
raise RuntimeError(f"POST application {slug} -> {code3}")
|
||||
|
||||
|
||||
def ensure_group(base: str, name: str) -> int:
|
||||
code, payload = req("GET", f"{base}/core/groups/?name={urllib.parse.quote(name)}")
|
||||
if code != 200 or not isinstance(payload, dict):
|
||||
raise RuntimeError("group list failed")
|
||||
results = payload.get("results") or []
|
||||
if results:
|
||||
return int(results[0]["pk"])
|
||||
code2, created = req("POST", f"{base}/core/groups/", {"name": name})
|
||||
if code2 not in (200, 201) or not isinstance(created, dict):
|
||||
raise RuntimeError(f"POST group {name} -> {code2}")
|
||||
return int(created["pk"])
|
||||
|
||||
|
||||
def add_user_to_groups(base: str, email: str, group_pks: list[int]) -> None:
|
||||
code, payload = req("GET", f"{base}/core/users/?email={urllib.parse.quote(email)}")
|
||||
if code != 200 or not isinstance(payload, dict):
|
||||
raise RuntimeError("user list failed")
|
||||
results = payload.get("results") or []
|
||||
if not results:
|
||||
print(f"WARN: no user with email {email}; skip group membership", file=sys.stderr)
|
||||
return
|
||||
user = results[0]
|
||||
upk = int(user["pk"])
|
||||
existing = [int(g["pk"]) for g in user.get("groups", []) if isinstance(g, dict) and "pk" in g]
|
||||
merged = sorted(set(existing + group_pks))
|
||||
if merged == sorted(set(existing)):
|
||||
return
|
||||
code2, _ = req("PATCH", f"{base}/core/users/{upk}/", {"groups": merged})
|
||||
if code2 not in (200, 204):
|
||||
raise RuntimeError(f"PATCH user groups -> {code2}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
base = os.environ.get("AUTHENTIK_API_BASE", "").rstrip("/")
|
||||
tok = os.environ.get("AUTHENTIK_TOKEN", "")
|
||||
cfg_path = os.environ.get("CLIENT_JSON", "")
|
||||
email = os.environ.get("BOOTSTRAP_EMAIL", "")
|
||||
if not base or not tok or not cfg_path:
|
||||
print("AUTHENTIK_API_BASE, AUTHENTIK_TOKEN, CLIENT_JSON required", file=sys.stderr)
|
||||
return 2
|
||||
with open(cfg_path, encoding="utf-8") as f:
|
||||
clients = json.load(f)
|
||||
if not isinstance(clients, dict):
|
||||
print("CLIENT_JSON must be an object keyed by provider slug", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
auth_flow = find_flow_pk(base, "default-provider-authorization-implicit-consent")
|
||||
invalidation_flow = find_flow_pk_first(
|
||||
base,
|
||||
["default-invalidation-flow", "default-provider-invalidation-flow"],
|
||||
)
|
||||
|
||||
signing_key = signing_key_pk(base)
|
||||
pmap = scope_mapping_pks(base)
|
||||
|
||||
admins_pk = ensure_group(base, "noble-admins")
|
||||
editors_pk = ensure_group(base, "noble-editors")
|
||||
|
||||
for slug, meta in clients.items():
|
||||
name = meta.get("name") or slug
|
||||
cid = meta["client_id"]
|
||||
csec = meta["client_secret"]
|
||||
redir = meta["redirect_uri"]
|
||||
ppk = upsert_oauth_provider(
|
||||
base,
|
||||
slug,
|
||||
name,
|
||||
cid,
|
||||
csec,
|
||||
redir,
|
||||
auth_flow,
|
||||
invalidation_flow,
|
||||
signing_key,
|
||||
pmap,
|
||||
)
|
||||
upsert_application(base, slug, name, ppk)
|
||||
|
||||
if email:
|
||||
add_user_to_groups(base, email, [admins_pk, editors_pk])
|
||||
print("authentik: providers + applications configured", flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
217
ansible/roles/noble_authentik/tasks/from_env.yml
Normal file
217
ansible/roles/noble_authentik/tasks/from_env.yml
Normal file
@@ -0,0 +1,217 @@
|
||||
---
|
||||
# **.env** is shell `KEY=value` syntax (not YAML). Source it like **noble_velero** does.
|
||||
- name: Stat repository .env for Authentik
|
||||
ansible.builtin.stat:
|
||||
path: "{{ noble_repo_root }}/.env"
|
||||
register: noble_authentik_dotenv_stat
|
||||
changed_when: false
|
||||
|
||||
- name: Load NOBLE_AUTHENTIK_SECRET_KEY from .env when unset
|
||||
ansible.builtin.shell: |
|
||||
set -a
|
||||
. "{{ noble_repo_root }}/.env"
|
||||
set +a
|
||||
printf '%s' "${NOBLE_AUTHENTIK_SECRET_KEY:-}"
|
||||
register: noble_authentik_secret_key_from_env
|
||||
when:
|
||||
- noble_authentik_dotenv_stat.stat.exists | default(false)
|
||||
- noble_authentik_secret_key | default('') | length == 0
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Apply NOBLE_AUTHENTIK_SECRET_KEY from .env
|
||||
ansible.builtin.set_fact:
|
||||
noble_authentik_secret_key: "{{ noble_authentik_secret_key_from_env.stdout | trim }}"
|
||||
when:
|
||||
- noble_authentik_secret_key_from_env is defined
|
||||
- (noble_authentik_secret_key_from_env.stdout | default('') | trim | length) > 0
|
||||
no_log: true
|
||||
|
||||
- name: Load NOBLE_AUTHENTIK_POSTGRES_PASSWORD from .env when unset
|
||||
ansible.builtin.shell: |
|
||||
set -a
|
||||
. "{{ noble_repo_root }}/.env"
|
||||
set +a
|
||||
printf '%s' "${NOBLE_AUTHENTIK_POSTGRES_PASSWORD:-}"
|
||||
register: noble_authentik_pg_from_env
|
||||
when:
|
||||
- noble_authentik_dotenv_stat.stat.exists | default(false)
|
||||
- noble_authentik_postgresql_password | default('') | length == 0
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Apply NOBLE_AUTHENTIK_POSTGRES_PASSWORD from .env
|
||||
ansible.builtin.set_fact:
|
||||
noble_authentik_postgresql_password: "{{ noble_authentik_pg_from_env.stdout | trim }}"
|
||||
when:
|
||||
- noble_authentik_pg_from_env is defined
|
||||
- (noble_authentik_pg_from_env.stdout | default('') | trim | length) > 0
|
||||
no_log: true
|
||||
|
||||
- name: Load NOBLE_AUTHENTIK_BOOTSTRAP_TOKEN from .env when unset
|
||||
ansible.builtin.shell: |
|
||||
set -a
|
||||
. "{{ noble_repo_root }}/.env"
|
||||
set +a
|
||||
printf '%s' "${NOBLE_AUTHENTIK_BOOTSTRAP_TOKEN:-}"
|
||||
register: noble_authentik_bt_from_env
|
||||
when:
|
||||
- noble_authentik_dotenv_stat.stat.exists | default(false)
|
||||
- noble_authentik_bootstrap_token | default('') | length == 0
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Apply NOBLE_AUTHENTIK_BOOTSTRAP_TOKEN from .env
|
||||
ansible.builtin.set_fact:
|
||||
noble_authentik_bootstrap_token: "{{ noble_authentik_bt_from_env.stdout | trim }}"
|
||||
when:
|
||||
- noble_authentik_bt_from_env is defined
|
||||
- (noble_authentik_bt_from_env.stdout | default('') | trim | length) > 0
|
||||
no_log: true
|
||||
|
||||
- name: Load NOBLE_AUTHENTIK_BOOTSTRAP_EMAIL from .env when unset
|
||||
ansible.builtin.shell: |
|
||||
set -a
|
||||
. "{{ noble_repo_root }}/.env"
|
||||
set +a
|
||||
printf '%s' "${NOBLE_AUTHENTIK_BOOTSTRAP_EMAIL:-}"
|
||||
register: noble_authentik_be_from_env
|
||||
when:
|
||||
- noble_authentik_dotenv_stat.stat.exists | default(false)
|
||||
- noble_authentik_bootstrap_email | default('') | length == 0
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Apply NOBLE_AUTHENTIK_BOOTSTRAP_EMAIL from .env
|
||||
ansible.builtin.set_fact:
|
||||
noble_authentik_bootstrap_email: "{{ noble_authentik_be_from_env.stdout | trim }}"
|
||||
when:
|
||||
- noble_authentik_be_from_env is defined
|
||||
- (noble_authentik_be_from_env.stdout | default('') | trim | length) > 0
|
||||
no_log: true
|
||||
|
||||
- name: Load NOBLE_AUTHENTIK_BOOTSTRAP_PASSWORD from .env when unset
|
||||
ansible.builtin.shell: |
|
||||
set -a
|
||||
. "{{ noble_repo_root }}/.env"
|
||||
set +a
|
||||
printf '%s' "${NOBLE_AUTHENTIK_BOOTSTRAP_PASSWORD:-}"
|
||||
register: noble_authentik_bp_from_env
|
||||
when:
|
||||
- noble_authentik_dotenv_stat.stat.exists | default(false)
|
||||
- noble_authentik_bootstrap_password | default('') | length == 0
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Apply NOBLE_AUTHENTIK_BOOTSTRAP_PASSWORD from .env
|
||||
ansible.builtin.set_fact:
|
||||
noble_authentik_bootstrap_password: "{{ noble_authentik_bp_from_env.stdout | trim }}"
|
||||
when:
|
||||
- noble_authentik_bp_from_env is defined
|
||||
- (noble_authentik_bp_from_env.stdout | default('') | trim | length) > 0
|
||||
no_log: true
|
||||
|
||||
- name: Load NOBLE_AUTHENTIK_CLIENT_SECRET_ARGOCD from .env when unset
|
||||
ansible.builtin.shell: |
|
||||
set -a
|
||||
. "{{ noble_repo_root }}/.env"
|
||||
set +a
|
||||
printf '%s' "${NOBLE_AUTHENTIK_CLIENT_SECRET_ARGOCD:-}"
|
||||
register: noble_authentik_cs_argo_from_env
|
||||
when:
|
||||
- noble_authentik_dotenv_stat.stat.exists | default(false)
|
||||
- noble_authentik_client_secret_argocd | default('') | length == 0
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Apply NOBLE_AUTHENTIK_CLIENT_SECRET_ARGOCD from .env
|
||||
ansible.builtin.set_fact:
|
||||
noble_authentik_client_secret_argocd: "{{ noble_authentik_cs_argo_from_env.stdout | trim }}"
|
||||
when:
|
||||
- noble_authentik_cs_argo_from_env is defined
|
||||
- (noble_authentik_cs_argo_from_env.stdout | default('') | trim | length) > 0
|
||||
no_log: true
|
||||
|
||||
- name: Load NOBLE_AUTHENTIK_CLIENT_SECRET_GRAFANA from .env when unset
|
||||
ansible.builtin.shell: |
|
||||
set -a
|
||||
. "{{ noble_repo_root }}/.env"
|
||||
set +a
|
||||
printf '%s' "${NOBLE_AUTHENTIK_CLIENT_SECRET_GRAFANA:-}"
|
||||
register: noble_authentik_cs_graf_from_env
|
||||
when:
|
||||
- noble_authentik_dotenv_stat.stat.exists | default(false)
|
||||
- noble_authentik_client_secret_grafana | default('') | length == 0
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Apply NOBLE_AUTHENTIK_CLIENT_SECRET_GRAFANA from .env
|
||||
ansible.builtin.set_fact:
|
||||
noble_authentik_client_secret_grafana: "{{ noble_authentik_cs_graf_from_env.stdout | trim }}"
|
||||
when:
|
||||
- noble_authentik_cs_graf_from_env is defined
|
||||
- (noble_authentik_cs_graf_from_env.stdout | default('') | trim | length) > 0
|
||||
no_log: true
|
||||
|
||||
- name: Load NOBLE_AUTHENTIK_CLIENT_SECRET_HEADLAMP from .env when unset
|
||||
ansible.builtin.shell: |
|
||||
set -a
|
||||
. "{{ noble_repo_root }}/.env"
|
||||
set +a
|
||||
printf '%s' "${NOBLE_AUTHENTIK_CLIENT_SECRET_HEADLAMP:-}"
|
||||
register: noble_authentik_cs_hl_from_env
|
||||
when:
|
||||
- noble_authentik_dotenv_stat.stat.exists | default(false)
|
||||
- noble_authentik_client_secret_headlamp | default('') | length == 0
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Apply NOBLE_AUTHENTIK_CLIENT_SECRET_HEADLAMP from .env
|
||||
ansible.builtin.set_fact:
|
||||
noble_authentik_client_secret_headlamp: "{{ noble_authentik_cs_hl_from_env.stdout | trim }}"
|
||||
when:
|
||||
- noble_authentik_cs_hl_from_env is defined
|
||||
- (noble_authentik_cs_hl_from_env.stdout | default('') | trim | length) > 0
|
||||
no_log: true
|
||||
|
||||
- name: Load NOBLE_AUTHENTIK_CLIENT_SECRET_OAUTH2_PROXY from .env when unset
|
||||
ansible.builtin.shell: |
|
||||
set -a
|
||||
. "{{ noble_repo_root }}/.env"
|
||||
set +a
|
||||
printf '%s' "${NOBLE_AUTHENTIK_CLIENT_SECRET_OAUTH2_PROXY:-}"
|
||||
register: noble_authentik_cs_o2_from_env
|
||||
when:
|
||||
- noble_authentik_dotenv_stat.stat.exists | default(false)
|
||||
- noble_authentik_client_secret_oauth2_proxy | default('') | length == 0
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Apply NOBLE_AUTHENTIK_CLIENT_SECRET_OAUTH2_PROXY from .env
|
||||
ansible.builtin.set_fact:
|
||||
noble_authentik_client_secret_oauth2_proxy: "{{ noble_authentik_cs_o2_from_env.stdout | trim }}"
|
||||
when:
|
||||
- noble_authentik_cs_o2_from_env is defined
|
||||
- (noble_authentik_cs_o2_from_env.stdout | default('') | trim | length) > 0
|
||||
no_log: true
|
||||
|
||||
- name: Load NOBLE_AUTHENTIK_OAUTH2_PROXY_COOKIE_SECRET from .env when unset
|
||||
ansible.builtin.shell: |
|
||||
set -a
|
||||
. "{{ noble_repo_root }}/.env"
|
||||
set +a
|
||||
printf '%s' "${NOBLE_AUTHENTIK_OAUTH2_PROXY_COOKIE_SECRET:-}"
|
||||
register: noble_authentik_cs_cookie_from_env
|
||||
when:
|
||||
- noble_authentik_dotenv_stat.stat.exists | default(false)
|
||||
- noble_authentik_oauth2_proxy_cookie_secret | default('') | length == 0
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Apply NOBLE_AUTHENTIK_OAUTH2_PROXY_COOKIE_SECRET from .env
|
||||
ansible.builtin.set_fact:
|
||||
noble_authentik_oauth2_proxy_cookie_secret: "{{ noble_authentik_cs_cookie_from_env.stdout | trim }}"
|
||||
when:
|
||||
- noble_authentik_cs_cookie_from_env is defined
|
||||
- (noble_authentik_cs_cookie_from_env.stdout | default('') | trim | length) > 0
|
||||
no_log: true
|
||||
302
ansible/roles/noble_authentik/tasks/main.yml
Normal file
302
ansible/roles/noble_authentik/tasks/main.yml
Normal file
@@ -0,0 +1,302 @@
|
||||
---
|
||||
- name: Authentik disabled (set noble_authentik_install=true and .env — see role README)
|
||||
ansible.builtin.debug:
|
||||
msg: "Skipping noble_authentik (noble_authentik_install is false)."
|
||||
when: not (noble_authentik_install | default(false) | bool)
|
||||
|
||||
- name: Authentik + OIDC stack
|
||||
when: noble_authentik_install | default(false) | bool
|
||||
block:
|
||||
- name: Include Authentik secrets from .env
|
||||
ansible.builtin.include_tasks: from_env.yml
|
||||
|
||||
- name: Require Authentik secrets and bootstrap settings
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- noble_authentik_secret_key | default('') | length > 0
|
||||
- noble_authentik_postgresql_password | default('') | length > 0
|
||||
- noble_authentik_bootstrap_token | default('') | length > 0
|
||||
- noble_authentik_bootstrap_email | default('') | length > 0
|
||||
- noble_authentik_bootstrap_password | default('') | length > 0
|
||||
- noble_authentik_client_secret_argocd | default('') | length > 0
|
||||
- noble_authentik_client_secret_grafana | default('') | length > 0
|
||||
- noble_authentik_client_secret_headlamp | default('') | length > 0
|
||||
- noble_authentik_client_secret_oauth2_proxy | default('') | length > 0
|
||||
- noble_authentik_oauth2_proxy_cookie_secret | default('') | length > 0
|
||||
fail_msg: >-
|
||||
Authentik requires secrets in .env (see ansible/roles/noble_authentik/README.md) or matching -e extra-vars.
|
||||
|
||||
- name: Ensure Ansible temp dir for rendered Helm values
|
||||
ansible.builtin.file:
|
||||
path: "{{ noble_repo_root }}/ansible/.ansible-tmp"
|
||||
state: directory
|
||||
mode: "0700"
|
||||
|
||||
- name: Render Authentik Helm extra values (secrets)
|
||||
ansible.builtin.template:
|
||||
src: authentik-extra-values.yaml.j2
|
||||
dest: "{{ noble_repo_root }}/ansible/.ansible-tmp/authentik-extra-values.yaml"
|
||||
mode: "0600"
|
||||
no_log: true
|
||||
|
||||
- name: Apply Authentik and oauth2-proxy namespaces
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- apply
|
||||
- -f
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/authentik/namespace.yaml"
|
||||
- -f
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/oauth2-proxy/namespace.yaml"
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
changed_when: true
|
||||
|
||||
- name: Install Authentik (Helm)
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- helm
|
||||
- upgrade
|
||||
- --install
|
||||
- authentik
|
||||
- goauthentik/authentik
|
||||
- --namespace
|
||||
- authentik
|
||||
- --version
|
||||
- "{{ noble_authentik_chart_version }}"
|
||||
- -f
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/authentik/values.yaml"
|
||||
- -f
|
||||
- "{{ noble_repo_root }}/ansible/.ansible-tmp/authentik-extra-values.yaml"
|
||||
- --force-conflicts
|
||||
- --wait
|
||||
- --timeout
|
||||
- "{{ noble_authentik_helm_wait_timeout }}"
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
changed_when: true
|
||||
|
||||
- name: Wait for authentik server rollout
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- rollout
|
||||
- status
|
||||
- deployment/authentik-server
|
||||
- -n
|
||||
- authentik
|
||||
- --timeout=15m
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
changed_when: false
|
||||
|
||||
- name: Render Authentik API client descriptor (JSON)
|
||||
ansible.builtin.template:
|
||||
src: authentik-clients.json.j2
|
||||
dest: "{{ noble_repo_root }}/ansible/.ansible-tmp/authentik-clients.json"
|
||||
mode: "0600"
|
||||
no_log: true
|
||||
|
||||
- name: Configure Authentik OAuth2/OIDC providers (API)
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- python3
|
||||
- "{{ role_path }}/files/configure_authentik.py"
|
||||
environment:
|
||||
AUTHENTIK_API_BASE: "{{ noble_authentik_api_base }}"
|
||||
AUTHENTIK_TOKEN: "{{ noble_authentik_bootstrap_token }}"
|
||||
BOOTSTRAP_EMAIL: "{{ noble_authentik_bootstrap_email }}"
|
||||
CLIENT_JSON: "{{ noble_repo_root }}/ansible/.ansible-tmp/authentik-clients.json"
|
||||
when: noble_authentik_configure_idp | default(true) | bool
|
||||
changed_when: true
|
||||
no_log: true
|
||||
|
||||
- name: Create argocd namespace Secret for OIDC client (Argo CD $authentik-oidc:clientSecret)
|
||||
ansible.builtin.shell: |
|
||||
set -euo pipefail
|
||||
kubectl -n argocd create secret generic authentik-oidc \
|
||||
--from-literal=clientSecret="${ARGOCD_OIDC_SECRET}" \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
kubectl -n argocd label secret authentik-oidc app.kubernetes.io/part-of=argocd --overwrite
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
ARGOCD_OIDC_SECRET: "{{ noble_authentik_client_secret_argocd }}"
|
||||
no_log: true
|
||||
changed_when: true
|
||||
|
||||
- name: Create Grafana OIDC client secret (GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET)
|
||||
ansible.builtin.shell: |
|
||||
set -euo pipefail
|
||||
kubectl -n monitoring create secret generic authentik-grafana-oauth \
|
||||
--from-literal=GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET="${GRAFANA_OIDC_SECRET}" \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
GRAFANA_OIDC_SECRET: "{{ noble_authentik_client_secret_grafana }}"
|
||||
no_log: true
|
||||
changed_when: true
|
||||
|
||||
- name: Create Headlamp OIDC env secret (OIDC_* env vars)
|
||||
ansible.builtin.shell: |
|
||||
set -euo pipefail
|
||||
kubectl -n headlamp create secret generic headlamp-oidc \
|
||||
--from-literal=OIDC_CLIENT_ID="{{ noble_authentik_client_id_headlamp }}" \
|
||||
--from-literal=OIDC_CLIENT_SECRET="${HEADLAMP_OIDC_SECRET}" \
|
||||
--from-literal=OIDC_ISSUER_URL="{{ noble_authentik_public_url }}/application/o/headlamp/" \
|
||||
--from-literal=OIDC_SCOPES="openid profile email groups offline_access" \
|
||||
--from-literal=OIDC_CALLBACK_URL="https://headlamp.apps.noble.lab.pcenicni.dev/oidc-callback" \
|
||||
--from-literal=OIDC_USE_PKCE="true" \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
HEADLAMP_OIDC_SECRET: "{{ noble_authentik_client_secret_headlamp }}"
|
||||
no_log: true
|
||||
changed_when: true
|
||||
|
||||
- name: Create oauth2-proxy credentials Secret (OIDC to Authentik; not BasicAuth)
|
||||
ansible.builtin.shell: |
|
||||
set -euo pipefail
|
||||
kubectl -n oauth2-proxy create secret generic oauth2-proxy-credentials \
|
||||
--from-literal=client-id="{{ noble_authentik_client_id_oauth2_proxy }}" \
|
||||
--from-literal=client-secret="${O2_CLIENT_SECRET}" \
|
||||
--from-literal=cookie-secret="${O2_COOKIE_SECRET}" \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
O2_CLIENT_SECRET: "{{ noble_authentik_client_secret_oauth2_proxy }}"
|
||||
O2_COOKIE_SECRET: "{{ noble_authentik_oauth2_proxy_cookie_secret }}"
|
||||
no_log: true
|
||||
changed_when: true
|
||||
|
||||
- name: Install oauth2-proxy (Helm) — OIDC provider Authentik
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- helm
|
||||
- upgrade
|
||||
- --install
|
||||
- oauth2-proxy
|
||||
- oauth2-proxy/oauth2-proxy
|
||||
- --namespace
|
||||
- oauth2-proxy
|
||||
- --version
|
||||
- "{{ noble_authentik_oauth2_proxy_chart_version }}"
|
||||
- -f
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/oauth2-proxy/values.yaml"
|
||||
- --force-conflicts
|
||||
- --wait
|
||||
- --timeout
|
||||
- 10m
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
changed_when: true
|
||||
|
||||
- name: Apply Traefik ForwardAuth Middleware (references oauth2-proxy OIDC session)
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- apply
|
||||
- -f
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/oauth2-proxy/middleware-forwardauth.yaml"
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
changed_when: true
|
||||
|
||||
- name: Helm upgrade Argo CD with Authentik OIDC values
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- helm
|
||||
- upgrade
|
||||
- --install
|
||||
- argocd
|
||||
- argo/argo-cd
|
||||
- --namespace
|
||||
- argocd
|
||||
- --version
|
||||
- "{{ noble_authentik_argocd_chart_version }}"
|
||||
- -f
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/argocd/values.yaml"
|
||||
- -f
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/argocd/values-authentik-oidc.yaml"
|
||||
- --force-conflicts
|
||||
- --wait
|
||||
- --timeout
|
||||
- 15m
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
changed_when: true
|
||||
|
||||
- name: Helm upgrade kube-prometheus-stack (Grafana OIDC + ForwardAuth on Prom/AM)
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- helm
|
||||
- upgrade
|
||||
- --install
|
||||
- kube-prometheus
|
||||
- prometheus-community/kube-prometheus-stack
|
||||
- -n
|
||||
- monitoring
|
||||
- --version
|
||||
- "{{ noble_authentik_kube_prometheus_chart_version }}"
|
||||
- -f
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/kube-prometheus-stack/values.yaml"
|
||||
- -f
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/kube-prometheus-stack/values-authentik-oidc.yaml"
|
||||
- --force-conflicts
|
||||
- --wait
|
||||
- --timeout
|
||||
- "{{ noble_authentik_kube_prometheus_helm_wait_timeout }}"
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
changed_when: true
|
||||
|
||||
- name: Helm upgrade Headlamp with Authentik OIDC values
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- helm
|
||||
- upgrade
|
||||
- --install
|
||||
- headlamp
|
||||
- headlamp/headlamp
|
||||
- --version
|
||||
- "{{ noble_authentik_headlamp_chart_version }}"
|
||||
- -n
|
||||
- headlamp
|
||||
- -f
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/headlamp/values.yaml"
|
||||
- -f
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/headlamp/values-authentik-oidc.yaml"
|
||||
- --force-conflicts
|
||||
- --wait
|
||||
- --timeout
|
||||
- 10m
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
changed_when: true
|
||||
|
||||
- name: Helm upgrade Longhorn with ForwardAuth (oauth2-proxy OIDC)
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- helm
|
||||
- upgrade
|
||||
- --install
|
||||
- longhorn
|
||||
- longhorn/longhorn
|
||||
- -n
|
||||
- longhorn-system
|
||||
- --version
|
||||
- "{{ noble_authentik_longhorn_chart_version }}"
|
||||
- -f
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/longhorn/values.yaml"
|
||||
- -f
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/longhorn/values-authentik-forwardauth.yaml"
|
||||
- --force-conflicts
|
||||
- --wait
|
||||
- --timeout
|
||||
- "{{ noble_helm_longhorn_wait_timeout | default('20m') }}"
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
register: noble_authentik_longhorn_helm
|
||||
retries: "{{ noble_helm_longhorn_retries | default(8) | int }}"
|
||||
delay: "{{ noble_helm_longhorn_retry_delay | default(25) | int }}"
|
||||
until: noble_authentik_longhorn_helm.rc == 0
|
||||
changed_when: true
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"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,16 @@
|
||||
---
|
||||
authentik:
|
||||
secret_key: "{{ noble_authentik_secret_key }}"
|
||||
postgresql:
|
||||
password: "{{ noble_authentik_postgresql_password }}"
|
||||
global:
|
||||
env:
|
||||
- name: AUTHENTIK_BOOTSTRAP_TOKEN
|
||||
value: "{{ noble_authentik_bootstrap_token }}"
|
||||
- name: AUTHENTIK_BOOTSTRAP_EMAIL
|
||||
value: "{{ noble_authentik_bootstrap_email }}"
|
||||
- name: AUTHENTIK_BOOTSTRAP_PASSWORD
|
||||
value: "{{ noble_authentik_bootstrap_password }}"
|
||||
postgresql:
|
||||
auth:
|
||||
password: "{{ noble_authentik_postgresql_password }}"
|
||||
@@ -9,6 +9,16 @@ noble_landing_urls_fetch_credentials: true
|
||||
noble_landing_urls_headlamp_token_duration: 48h
|
||||
|
||||
noble_lab_ui_entries:
|
||||
- name: Authentik
|
||||
description: OIDC IdP (admin UI, OAuth2/OIDC for cluster apps)
|
||||
namespace: authentik
|
||||
service: authentik-server
|
||||
url: https://auth.apps.noble.lab.pcenicni.dev
|
||||
- name: oauth2-proxy
|
||||
description: OIDC to Authentik + Traefik ForwardAuth (Prometheus, Alertmanager, Longhorn)
|
||||
namespace: oauth2-proxy
|
||||
service: oauth2-proxy
|
||||
url: https://oauth2.apps.noble.lab.pcenicni.dev
|
||||
- name: Argo CD
|
||||
description: GitOps UI (sync, apps, repos)
|
||||
namespace: argocd
|
||||
|
||||
@@ -21,9 +21,9 @@ This file is **generated** by Ansible (`noble_landing_urls` role). Use it as a t
|
||||
| **Argo CD** | `admin` | {% if (noble_fetch_argocd_pw_b64 is defined) and (noble_fetch_argocd_pw_b64.rc | default(1) == 0) and (noble_fetch_argocd_pw_b64.stdout | default('') | length > 0) %}`{{ noble_fetch_argocd_pw_b64.stdout | b64decode }}`{% else %}*(not fetched — use commands below)*{% endif %} |
|
||||
| **Grafana** | {% if (noble_fetch_grafana_user_b64 is defined) and (noble_fetch_grafana_user_b64.rc | default(1) == 0) and (noble_fetch_grafana_user_b64.stdout | default('') | length > 0) %}`{{ noble_fetch_grafana_user_b64.stdout | b64decode }}`{% else %}*(from Secret — use commands below)*{% endif %} | {% if (noble_fetch_grafana_pw_b64 is defined) and (noble_fetch_grafana_pw_b64.rc | default(1) == 0) and (noble_fetch_grafana_pw_b64.stdout | default('') | length > 0) %}`{{ noble_fetch_grafana_pw_b64.stdout | b64decode }}`{% else %}*(not fetched — use commands below)*{% endif %} |
|
||||
| **Headlamp** | ServiceAccount **`headlamp`** | {% if (noble_fetch_headlamp_token is defined) and (noble_fetch_headlamp_token.rc | default(1) == 0) and (noble_fetch_headlamp_token.stdout | default('') | trim | length > 0) %}Token ({{ noble_landing_urls_headlamp_token_duration | default('48h') }}): `{{ noble_fetch_headlamp_token.stdout | trim }}`{% else %}*(not generated — use command below)*{% endif %} |
|
||||
| **Prometheus** | — | No auth in default install (lab). |
|
||||
| **Alertmanager** | — | No auth in default install (lab). |
|
||||
| **Longhorn** | — | No default login unless you enable access control in the UI settings. |
|
||||
| **Prometheus** | — | Browser login via **oauth2-proxy** → **Authentik** (OIDC). |
|
||||
| **Alertmanager** | — | Browser login via **oauth2-proxy** → **Authentik** (OIDC). |
|
||||
| **Longhorn** | — | Browser login via **oauth2-proxy** → **Authentik** (OIDC). |
|
||||
|
||||
### Commands to retrieve passwords (if not filled above)
|
||||
|
||||
@@ -45,7 +45,8 @@ To generate this file **without** calling kubectl, run Ansible with **`-e noble_
|
||||
|
||||
- **Argo CD** `argocd-initial-admin-secret` disappears after you change the admin password.
|
||||
- **Grafana** password is random unless you set `grafana.adminPassword` in chart values.
|
||||
- **Prometheus / Alertmanager** UIs are unauthenticated by default — restrict when hardening (`talos/CLUSTER-BUILD.md` Phase G).
|
||||
- **Argo CD / Grafana / Headlamp** use **native OIDC** to **Authentik** when **`noble_authentik_install`** ran with **`ansible/roles/noble_authentik`** (see **`clusters/noble/bootstrap/**/values-authentik*.yaml`**).
|
||||
- **Prometheus / Alertmanager / Longhorn** UIs use **oauth2-proxy** as an **OIDC RP** to Authentik (Traefik ForwardAuth), not HTTP BasicAuth.
|
||||
- **SOPS:** cluster secrets in git under **`clusters/noble/secrets/`** are encrypted; decrypt with **`age-key.txt`** (not in git). See **`clusters/noble/secrets/README.md`**.
|
||||
- **Headlamp** token above expires after the configured duration; re-run Ansible or `kubectl create token` to refresh.
|
||||
- **Velero** has **no web UI** — use **`velero`** CLI or **`kubectl -n velero get backup,schedule,backupstoragelocation`**. Metrics: **`velero`** Service in **`velero`** (Prometheus scrape). See `clusters/noble/bootstrap/velero/README.md`.
|
||||
|
||||
20
clusters/noble/bootstrap/argocd/values-authentik-oidc.yaml
Normal file
20
clusters/noble/bootstrap/argocd/values-authentik-oidc.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
# OIDC with Authentik (merged on `helm upgrade` after **noble_authentik** provisions providers + Secret **authentik-oidc**).
|
||||
# Issuer path uses provider slug **argocd** (see noble_authentik/configure_authentik.py).
|
||||
|
||||
configs:
|
||||
cm:
|
||||
oidc.config: |
|
||||
name: Authentik
|
||||
issuer: https://auth.apps.noble.lab.pcenicni.dev/application/o/argocd/
|
||||
clientID: argocd
|
||||
clientSecret: $authentik-oidc:clientSecret
|
||||
requestedScopes:
|
||||
- openid
|
||||
- profile
|
||||
- email
|
||||
- groups
|
||||
rbac:
|
||||
policy.default: role:readonly
|
||||
policy.csv: |
|
||||
g, admin, role:admin
|
||||
g, noble-admins, role:admin
|
||||
8
clusters/noble/bootstrap/authentik/namespace.yaml
Normal file
8
clusters/noble/bootstrap/authentik/namespace.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: authentik
|
||||
labels:
|
||||
pod-security.kubernetes.io/enforce: privileged
|
||||
pod-security.kubernetes.io/audit: privileged
|
||||
pod-security.kubernetes.io/warn: privileged
|
||||
51
clusters/noble/bootstrap/authentik/values.yaml
Normal file
51
clusters/noble/bootstrap/authentik/values.yaml
Normal file
@@ -0,0 +1,51 @@
|
||||
# Authentik — noble lab (Helm: goauthentik/authentik)
|
||||
#
|
||||
# Secrets (secret_key, postgres password, bootstrap) are supplied at install time by Ansible
|
||||
# (-f authentik-extra-values.yaml from noble_authentik role). Do not commit real secrets here.
|
||||
#
|
||||
# DNS: auth.apps.noble.lab.pcenicni.dev → Traefik LB (see traefik/values.yaml).
|
||||
#
|
||||
# helm repo add goauthentik https://charts.goauthentik.io && helm repo update
|
||||
# kubectl apply -f clusters/noble/bootstrap/authentik/namespace.yaml
|
||||
# helm upgrade --install authentik goauthentik/authentik -n authentik --create-namespace \
|
||||
# --version 2026.2.3 -f clusters/noble/bootstrap/authentik/values.yaml -f /path/to/extra.yaml --wait
|
||||
|
||||
postgresql:
|
||||
enabled: true
|
||||
auth:
|
||||
username: authentik
|
||||
database: authentik
|
||||
password: ""
|
||||
primary:
|
||||
persistence:
|
||||
enabled: true
|
||||
storageClassName: longhorn
|
||||
size: 10Gi
|
||||
|
||||
authentik:
|
||||
secret_key: ""
|
||||
postgresql:
|
||||
name: authentik
|
||||
user: authentik
|
||||
password: ""
|
||||
port: 5432
|
||||
|
||||
server:
|
||||
replicas: 1
|
||||
ingress:
|
||||
enabled: true
|
||||
ingressClassName: traefik
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
hosts:
|
||||
- host: auth.apps.noble.lab.pcenicni.dev
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- secretName: authentik-apps-noble-tls
|
||||
hosts:
|
||||
- auth.apps.noble.lab.pcenicni.dev
|
||||
|
||||
worker:
|
||||
replicas: 1
|
||||
@@ -0,0 +1,9 @@
|
||||
# OIDC with Authentik — credentials live in Secret **headlamp-oidc** (envFrom), created by **noble_authentik**.
|
||||
|
||||
config:
|
||||
oidc:
|
||||
secret:
|
||||
create: false
|
||||
externalSecret:
|
||||
enabled: true
|
||||
name: headlamp-oidc
|
||||
@@ -0,0 +1,33 @@
|
||||
# Authentik OIDC for Grafana; ForwardAuth to **oauth2-proxy** (OIDC to Authentik) for Prometheus / Alertmanager UIs.
|
||||
|
||||
prometheus:
|
||||
ingress:
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.middlewares: oauth2-proxy-forward-auth@kubernetescrd
|
||||
|
||||
alertmanager:
|
||||
ingress:
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.middlewares: oauth2-proxy-forward-auth@kubernetescrd
|
||||
|
||||
grafana:
|
||||
env:
|
||||
GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: authentik-grafana-oauth
|
||||
key: GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET
|
||||
grafana.ini:
|
||||
auth:
|
||||
disable_login_form: "false"
|
||||
auth.generic_oauth:
|
||||
enabled: true
|
||||
name: Authentik
|
||||
allow_sign_up: true
|
||||
client_id: grafana
|
||||
scopes: openid profile email groups
|
||||
use_pkce: true
|
||||
auth_url: https://auth.apps.noble.lab.pcenicni.dev/application/o/grafana/oauth2/authorize/
|
||||
token_url: https://auth.apps.noble.lab.pcenicni.dev/application/o/grafana/oauth2/token/
|
||||
api_url: https://auth.apps.noble.lab.pcenicni.dev/application/o/grafana/userinfo/
|
||||
role_attribute_path: "contains(groups[*], 'noble-admins') && 'Admin' || contains(groups[*], 'noble-editors') && 'Editor' || 'Viewer'"
|
||||
@@ -9,6 +9,8 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- authentik/namespace.yaml
|
||||
- oauth2-proxy/namespace.yaml
|
||||
- kube-prometheus-stack/namespace.yaml
|
||||
- loki/namespace.yaml
|
||||
- fluent-bit/namespace.yaml
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# ForwardAuth to **oauth2-proxy** (OIDC with Authentik) for the Longhorn UI Ingress.
|
||||
|
||||
ingress:
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.middlewares: oauth2-proxy-forward-auth@kubernetescrd
|
||||
@@ -0,0 +1,16 @@
|
||||
# Traefik ForwardAuth → oauth2-proxy (OIDC with Authentik). Reference from Ingress:
|
||||
# traefik.ingress.kubernetes.io/router.middlewares: oauth2-proxy-forward-auth@kubernetescrd
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: forward-auth
|
||||
namespace: oauth2-proxy
|
||||
spec:
|
||||
forwardAuth:
|
||||
address: http://oauth2-proxy.oauth2-proxy.svc.cluster.local:4180/oauth2/auth
|
||||
trustForwardHeader: true
|
||||
authResponseHeaders:
|
||||
- X-Forwarded-User
|
||||
- X-Forwarded-Email
|
||||
- X-Forwarded-Preferred-Username
|
||||
- X-Forwarded-Groups
|
||||
4
clusters/noble/bootstrap/oauth2-proxy/namespace.yaml
Normal file
4
clusters/noble/bootstrap/oauth2-proxy/namespace.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: oauth2-proxy
|
||||
47
clusters/noble/bootstrap/oauth2-proxy/values.yaml
Normal file
47
clusters/noble/bootstrap/oauth2-proxy/values.yaml
Normal file
@@ -0,0 +1,47 @@
|
||||
# oauth2-proxy — OIDC client to **Authentik** (not BasicAuth). Used with Traefik ForwardAuth
|
||||
# so apps without native OIDC (Prometheus, Alertmanager, Longhorn UI) still get a full OAuth code flow.
|
||||
#
|
||||
# Client id/secret/cookie-secret are created by Ansible (Kubernetes Secret + Helm values).
|
||||
#
|
||||
# helm repo add oauth2-proxy https://oauth2-proxy.github.io/manifests && helm repo update
|
||||
# kubectl apply -f clusters/noble/bootstrap/oauth2-proxy/namespace.yaml
|
||||
# helm upgrade --install oauth2-proxy oauth2-proxy/oauth2-proxy -n oauth2-proxy \
|
||||
# --version 10.4.3 -f clusters/noble/bootstrap/oauth2-proxy/values.yaml -f /path/to/extra.yaml --wait
|
||||
|
||||
config:
|
||||
# Populated by Ansible: Secret **oauth2-proxy-credentials** (keys client-id, client-secret, cookie-secret).
|
||||
existingSecret: oauth2-proxy-credentials
|
||||
clientID: oauth2-proxy
|
||||
clientSecret: ""
|
||||
cookieSecret: ""
|
||||
cookieName: _oauth2_proxy
|
||||
emailDomains: ["*"]
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
className: traefik
|
||||
path: /
|
||||
hosts:
|
||||
- oauth2.apps.noble.lab.pcenicni.dev
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
tls:
|
||||
- secretName: oauth2-apps-noble-tls
|
||||
hosts:
|
||||
- oauth2.apps.noble.lab.pcenicni.dev
|
||||
|
||||
extraArgs:
|
||||
provider: oidc
|
||||
skip-provider-button: "true"
|
||||
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"
|
||||
scope: "openid profile email groups"
|
||||
cookie-domain: ".apps.noble.lab.pcenicni.dev"
|
||||
whitelist-domain: ".apps.noble.lab.pcenicni.dev"
|
||||
set-authorization-header: "true"
|
||||
pass-access-token: "false"
|
||||
reverse-proxy: "true"
|
||||
upstream: static://200
|
||||
|
||||
service:
|
||||
portNumber: 4180
|
||||
Reference in New Issue
Block a user