Compare commits
2 Commits
main
...
ed2df96d10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed2df96d10 | ||
|
|
f6c44024a2 |
67
.env.sample
67
.env.sample
@@ -1,67 +0,0 @@
|
||||
# Copy to **.env** in this repository root (`.env` is gitignored).
|
||||
# Ansible **noble_cert_manager** role sources `.env` after cert-manager Helm install and creates
|
||||
# **cert-manager/cloudflare-dns-api-token** when **CLOUDFLARE_DNS_API_TOKEN** is set.
|
||||
#
|
||||
# Cloudflare: Zone → DNS → Edit + Zone → Read for **pcenicni.dev** (see clusters/noble/bootstrap/cert-manager/README.md).
|
||||
CLOUDFLARE_DNS_API_TOKEN=
|
||||
|
||||
# --- Optional: other deploy-time values (documented for manual use or future automation) ---
|
||||
|
||||
# Pangolin / Newt — with **noble_newt_install=true**, Ansible creates **newt/newt-pangolin-auth** when all are set (see clusters/noble/bootstrap/newt/README.md).
|
||||
PANGOLIN_ENDPOINT=
|
||||
NEWT_ID=
|
||||
NEWT_SECRET=
|
||||
|
||||
# Optional: Pangolin Integration API — automate public HTTP resources + Traefik targets (**noble_pangolin_sync_http_resources=true** in **group_vars**; see **clusters/noble/bootstrap/newt/README.md** §4).
|
||||
# NOBLE_PANGOLIN_API_BASE=https://api.example.com/v1 # Integration API — separate host from the main Pangolin UI; see clusters/noble/bootstrap/newt/README.md §4
|
||||
# NOBLE_PANGOLIN_ORG_ID=
|
||||
# NOBLE_PANGOLIN_API_TOKEN= # **apiKeyId.apiKeySecret** (one value, dot in the middle) from Organization → API keys — **not** login password; browser cookies do not apply. Alternatively: secret only here + **NOBLE_PANGOLIN_API_KEY_ID** below.
|
||||
# NOBLE_PANGOLIN_API_KEY_ID= # optional; if set, **NOBLE_PANGOLIN_API_TOKEN** may be the secret half only
|
||||
# NOBLE_PANGOLIN_SITE_ID= # numeric siteId, or Pangolin **niceId** (Sites UI slug, e.g. unruly-asian-badger)
|
||||
# NOBLE_PANGOLIN_TRAEFIK_IP=192.168.50.211
|
||||
# NOBLE_PANGOLIN_TRAEFIK_PORT=443
|
||||
# Self-signed Integration API TLS: either trust your CA (preferred) or homelab-only skip verify:
|
||||
# NOBLE_PANGOLIN_CA_BUNDLE=/path/to/ca.pem
|
||||
# NOBLE_PANGOLIN_INSECURE_SKIP_TLS_VERIFY=true
|
||||
|
||||
# Velero — when **noble_velero_install=true**, set bucket + S3 API URL and credentials (see clusters/noble/bootstrap/velero/README.md).
|
||||
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, **Open WebUI** (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=
|
||||
# Open WebUI (Argo app **clusters/noble/apps/open-webui**) — OIDC client secret + app secrets (see **clusters/noble/apps/open-webui/values.yaml**).
|
||||
NOBLE_AUTHENTIK_CLIENT_SECRET_OPEN_WEBUI=
|
||||
NOBLE_OPEN_WEBUI_OPENAI_API_KEY=
|
||||
# e.g. openssl rand -hex 32
|
||||
NOBLE_OPEN_WEBUI_WEBUI_SECRET_KEY=
|
||||
# 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=
|
||||
# S3 media — **separate** bucket from Velero backups (**NOBLE_VELERO_S3_BUCKET**). Endpoint and keys default to the Velero vars above unless you set the Authentik-specific overrides.
|
||||
NOBLE_AUTHENTIK_MEDIA_S3_BUCKET=
|
||||
# Optional overrides (otherwise **NOBLE_VELERO_S3_URL** and Velero AWS keys are used):
|
||||
# NOBLE_AUTHENTIK_S3_URL=
|
||||
# NOBLE_AUTHENTIK_S3_ACCESS_KEY=
|
||||
# NOBLE_AUTHENTIK_S3_SECRET_KEY=
|
||||
# NOBLE_AUTHENTIK_S3_REGION=
|
||||
# NOBLE_AUTHENTIK_S3_ADDRESSING_STYLE=
|
||||
#
|
||||
# Optional outbound email (password recovery, invites, etc.) — maps to Authentik **AUTHENTIK_EMAIL__*** (see https://docs.goauthentik.io/install-config/configuration/#email-settings ). Omit **NOBLE_AUTHENTIK_SMTP_HOST** to leave email unset in Helm.
|
||||
# NOBLE_AUTHENTIK_SMTP_HOST=
|
||||
# NOBLE_AUTHENTIK_SMTP_FROM=
|
||||
# NOBLE_AUTHENTIK_SMTP_PORT=587
|
||||
# NOBLE_AUTHENTIK_SMTP_USERNAME=
|
||||
# NOBLE_AUTHENTIK_SMTP_PASSWORD=
|
||||
# NOBLE_AUTHENTIK_SMTP_USE_TLS=true
|
||||
# NOBLE_AUTHENTIK_SMTP_USE_SSL=false
|
||||
# NOBLE_AUTHENTIK_SMTP_TIMEOUT=30
|
||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -1,12 +0,0 @@
|
||||
ansible/inventory/hosts.ini
|
||||
# Talos generated
|
||||
talos/out/
|
||||
talos/kubeconfig
|
||||
|
||||
# Local secrets
|
||||
age-key.txt
|
||||
.env
|
||||
.tmp*
|
||||
|
||||
# Generated by ansible noble_landing_urls
|
||||
ansible/output/noble-lab-ui-urls.md
|
||||
@@ -1,7 +0,0 @@
|
||||
# Mozilla SOPS — encrypt/decrypt Kubernetes Secret manifests under clusters/noble/secrets/
|
||||
# Generate a key: age-keygen -o age-key.txt (age-key.txt is gitignored)
|
||||
# Add the printed public key below (one recipient per line is supported).
|
||||
creation_rules:
|
||||
- path_regex: clusters/noble/secrets/.*\.yaml$
|
||||
age: >-
|
||||
age1juym5p3ez3dkt0dxlznydgfgqvaujfnyk9hpdsssf50hsxeh3p4sjpf3gn
|
||||
@@ -180,12 +180,6 @@ Shared services used across multiple applications.
|
||||
|
||||
**Configuration:** Requires Pangolin endpoint URL, Newt ID, and Newt secret.
|
||||
|
||||
### versitygw/ (`komodo/s3/versitygw/`)
|
||||
|
||||
- **[Versity S3 Gateway](https://github.com/versity/versitygw)** — S3 API on port **10000** by default; optional **WebUI** on **8080** (not the same listener—enable `VERSITYGW_WEBUI_PORT` / `VGW_WEBUI_GATEWAYS` per `.env.sample`). Behind **Pangolin**, expose the API and WebUI separately (or you will see **404** browsing the API URL).
|
||||
|
||||
**Configuration:** Set either `ROOT_ACCESS_KEY` / `ROOT_SECRET_KEY` or `ROOT_ACCESS_KEY_ID` / `ROOT_SECRET_ACCESS_KEY`. Optional `VERSITYGW_PORT`. For a browser UI on another host (for example `https://s3-ui.pcenicni.dev` calling the API through Pangolin), set **`VGW_CORS_ALLOW_ORIGIN`** to that UI origin; VersityGW reflects requested headers on preflight when bucket CORS is unset (see `.env.sample`). If that is already set and CORS still fails, **Pangolin** may be handling **OPTIONS** before Versity (for example auth on the public resource blocking preflight; see `komodo/s3/versitygw/.env.sample` § Pangolin). Compose uses `${VAR}` interpolation so credentials work with Komodo’s `docker compose --env-file <run_directory>/.env` (avoid `env_file:` in the service when `run_directory` is not the same folder as `compose.yaml`, or the written `.env` will not be found).
|
||||
|
||||
---
|
||||
|
||||
## 📊 Monitoring (`komodo/monitor/`)
|
||||
|
||||
1
ansible/.gitignore
vendored
1
ansible/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
.ansible-tmp/
|
||||
@@ -1,163 +1,84 @@
|
||||
# Ansible — noble cluster
|
||||
# Home Server Ansible Configuration
|
||||
|
||||
**Narrative walkthrough (Proxmox → Talos → platform → Argo):** [`docs/ansible-getting-started.md`](../docs/ansible-getting-started.md).
|
||||
This directory contains Ansible playbooks for managing the Proxmox home server environment.
|
||||
|
||||
Automates [`talos/CLUSTER-BUILD.md`](../talos/CLUSTER-BUILD.md): optional **Talos Phase A** (genconfig → apply → bootstrap → kubeconfig), then **Phase B+** (CNI → add-ons → ingress → Argo CD → Kyverno → observability, etc.). **Trivy Operator** is installed via Argo (**`noble-trivy-operator`** app-of-apps), not **`noble.yml`**. **Argo CD** does not reconcile core charts first — optional add-on **`Application`** manifests live under [`clusters/noble/apps/`](../clusters/noble/apps/) and are included when **`noble_platform`** runs **`kubectl apply -k clusters/noble/bootstrap`** (see [`clusters/noble/apps/README.md`](../clusters/noble/apps/README.md)).
|
||||
## Directory Structure
|
||||
|
||||
## Order of operations
|
||||
- `inventory/`: Contains the inventory file `hosts.ini` where you define your servers.
|
||||
- `playbooks/`: Contains the actual Ansible playbooks.
|
||||
- `ansible.cfg`: Local Ansible configuration.
|
||||
- `requirements.yml`: List of Ansible collections required.
|
||||
|
||||
1. **From `talos/`:** `talhelper gensecret` / `talsecret` as in [`talos/README.md`](../talos/README.md) §1 (if not already done).
|
||||
2. **Talos Phase A (automated):** run [`playbooks/talos_phase_a.yml`](playbooks/talos_phase_a.yml) **or** the full pipeline [`playbooks/deploy.yml`](playbooks/deploy.yml). This runs **`talhelper genconfig -o out`**, **`talosctl apply-config`** on each node, **`talosctl bootstrap`**, and **`talosctl kubeconfig`** → **`talos/kubeconfig`**.
|
||||
3. **Platform stack:** [`playbooks/noble.yml`](playbooks/noble.yml) (included at the end of **`deploy.yml`**).
|
||||
## Setup
|
||||
|
||||
Your workstation must be able to reach **node IPs on the lab LAN** (Talos API **:50000** for `talosctl`, Kubernetes **:6443** for `kubectl` / Helm). If `kubectl` cannot reach the VIP (`192.168.50.230`), use `-e 'noble_k8s_api_server_override=https://<control-plane-ip>:6443'` on **`noble.yml`** (see `inventory/group_vars/all.yml`).
|
||||
1. **Install Requirements**:
|
||||
```bash
|
||||
ansible-galaxy install -r requirements.yml
|
||||
```
|
||||
|
||||
**One-shot full deploy** (after nodes are booted and reachable):
|
||||
2. **Configure Inventory**:
|
||||
Edit `inventory/hosts.ini` and update the following:
|
||||
- `ansible_host`: The IP address of your Proxmox node.
|
||||
- `ansible_user`: The SSH user (usually root).
|
||||
- `proxmox_api_*`: Variables if you plan to use API-based modules in the future.
|
||||
|
||||
*Note: Ensure you have SSH key access to your Proxmox node for passwordless login, or uncomment `ansible_ssh_pass`.*
|
||||
|
||||
## Available Playbooks
|
||||
|
||||
### Create Ubuntu Cloud Template (`playbooks/create_ubuntu_template.yml`)
|
||||
|
||||
This playbook downloads a generic Ubuntu 22.04 Cloud Image and converts it into a Proxmox VM Template.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```bash
|
||||
cd ansible
|
||||
ansible-playbook playbooks/deploy.yml
|
||||
# Run the playbook
|
||||
ansible-playbook playbooks/create_ubuntu_template.yml
|
||||
```
|
||||
|
||||
## Deploy secrets (`.env`)
|
||||
**Variables:**
|
||||
You can override variables at runtime or by editing the playbook:
|
||||
|
||||
Copy **`.env.sample`** to **`.env`** at the repository root (`.env` is gitignored). At minimum set **`CLOUDFLARE_DNS_API_TOKEN`** for cert-manager DNS-01. The **cert-manager** role applies it automatically during **`noble.yml`**. See **`.env.sample`** for optional placeholders (e.g. Newt/Pangolin).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `talosctl` (matches node Talos version), `talhelper`, `helm`, `kubectl`.
|
||||
- **SOPS secrets:** `sops` and `age` on the control host if you use **`clusters/noble/secrets/`** with **`age-key.txt`** (see **`clusters/noble/secrets/README.md`**).
|
||||
- **Phase A:** same LAN/VPN as nodes so **Talos :50000** and **Kubernetes :6443** are reachable (see [`talos/README.md`](../talos/README.md) §3).
|
||||
- **noble.yml:** bootstrapped cluster and **`talos/kubeconfig`** (or `KUBECONFIG`).
|
||||
|
||||
## Playbooks
|
||||
|
||||
| Playbook | Purpose |
|
||||
|----------|---------|
|
||||
| [`playbooks/deploy.yml`](playbooks/deploy.yml) | **Talos Phase A** then **`noble.yml`** (full automation). |
|
||||
| [`playbooks/talos_phase_a.yml`](playbooks/talos_phase_a.yml) | `genconfig` → `apply-config` → `bootstrap` → `kubeconfig` only. |
|
||||
| [`playbooks/noble.yml`](playbooks/noble.yml) | Helm + `kubectl` platform (after Phase A). |
|
||||
| [`playbooks/post_deploy.yml`](playbooks/post_deploy.yml) | SOPS reminders and optional Argo root Application note. |
|
||||
| [`playbooks/talos_bootstrap.yml`](playbooks/talos_bootstrap.yml) | **`talhelper genconfig` only** (legacy shortcut; prefer **`talos_phase_a.yml`**). |
|
||||
| [`playbooks/debian_harden.yml`](playbooks/debian_harden.yml) | Baseline hardening for Debian servers (SSH/sysctl/fail2ban/unattended-upgrades). |
|
||||
| [`playbooks/debian_maintenance.yml`](playbooks/debian_maintenance.yml) | Debian maintenance run (apt upgrades, autoremove/autoclean, reboot when required). |
|
||||
| [`playbooks/debian_rotate_ssh_keys.yml`](playbooks/debian_rotate_ssh_keys.yml) | Rotate managed users' `authorized_keys`. |
|
||||
| [`playbooks/debian_ops.yml`](playbooks/debian_ops.yml) | Convenience pipeline: harden then maintenance for Debian servers. |
|
||||
| [`playbooks/proxmox_prepare.yml`](playbooks/proxmox_prepare.yml) | Configure Proxmox community repos and disable no-subscription UI warning. |
|
||||
| [`playbooks/proxmox_upgrade.yml`](playbooks/proxmox_upgrade.yml) | Proxmox maintenance run (apt dist-upgrade, cleanup, reboot when required). |
|
||||
| [`playbooks/proxmox_cluster.yml`](playbooks/proxmox_cluster.yml) | Create a Proxmox cluster on the master and join additional hosts. |
|
||||
| [`playbooks/proxmox_ops.yml`](playbooks/proxmox_ops.yml) | Convenience pipeline: prepare, upgrade, then cluster Proxmox hosts. |
|
||||
- `template_id`: Default `9000`
|
||||
- `template_name`: Default `ubuntu-2204-cloud`
|
||||
- `storage_pool`: Default `local-lvm`
|
||||
|
||||
Example overriding variables:
|
||||
```bash
|
||||
cd ansible
|
||||
export KUBECONFIG=/absolute/path/to/home-server/talos/kubeconfig
|
||||
|
||||
# noble.yml only — if VIP is unreachable from this host:
|
||||
# ansible-playbook playbooks/noble.yml -e 'noble_k8s_api_server_override=https://192.168.50.20:6443'
|
||||
|
||||
ansible-playbook playbooks/noble.yml
|
||||
ansible-playbook playbooks/post_deploy.yml
|
||||
ansible-playbook playbooks/create_ubuntu_template.yml -e "template_id=9001 template_name=my-custom-template"
|
||||
```
|
||||
|
||||
### Talos Phase A variables (role `talos_phase_a` defaults)
|
||||
### Manage VM Playbook (`playbooks/manage_vm.yml`)
|
||||
|
||||
Override with `-e` when needed, e.g. **`-e noble_talos_skip_bootstrap=true`** if etcd is already initialized.
|
||||
This unified playbook allows you to manage VMs (create from template, delete, backup, create template) across your Proxmox hosts.
|
||||
|
||||
| Variable | Default | Meaning |
|
||||
|----------|---------|---------|
|
||||
| `noble_talos_genconfig` | `true` | Run **`talhelper genconfig -o out`** first. |
|
||||
| `noble_talos_apply_mode` | `auto` | **`auto`** — **`talosctl apply-config --dry-run`** on the first node picks maintenance (**`--insecure`**) vs joined (**`TALOSCONFIG`**). **`insecure`** / **`secure`** force talos/README §2 A or B. |
|
||||
| `noble_talos_skip_bootstrap` | `false` | Skip **`talosctl bootstrap`**. If etcd is **already** initialized, bootstrap is treated as a no-op (same as **`talosctl`** “etcd data directory is not empty”). |
|
||||
| `noble_talos_apid_wait_delay` / `noble_talos_apid_wait_timeout` | `20` / `900` | Seconds to wait for **apid :50000** on the bootstrap node after **apply-config** (nodes reboot). Increase if bootstrap hits **connection refused** to `:50000`. |
|
||||
| `noble_talos_nodes` | neon/argon/krypton/helium | IP + **`out/*.yaml`** filename — align with **`talos/talconfig.yaml`**. |
|
||||
**Usage:**
|
||||
|
||||
### Tags (partial runs)
|
||||
The playbook target defaults to the `proxmox` group, but you should usually specify a specific host using `target_host` variable or `-l` limit.
|
||||
|
||||
```bash
|
||||
ansible-playbook playbooks/noble.yml --tags cilium,metallb
|
||||
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
|
||||
```
|
||||
1. **Create a New Template**:
|
||||
```bash
|
||||
ansible-playbook playbooks/manage_vm.yml -e "proxmox_action=create_template vmid=9003 template_name=my-ubuntu-template"
|
||||
```
|
||||
|
||||
### Variables — `inventory/group_vars/` and role defaults
|
||||
2. **Create a VM from Template**:
|
||||
```bash
|
||||
ansible-playbook playbooks/manage_vm.yml -e "proxmox_action=create_vm vmid=9002 new_vmid=105 new_vm_name=my-new-vm"
|
||||
```
|
||||
|
||||
- **`inventory/group_vars/all.yml`:** **`noble_newt_install`**, **`noble_velero_install`**, **`noble_authentik_install`**, **`noble_cert_manager_require_cloudflare_secret`**, **`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`**, **`noble_platform_loki_helm_wait_timeout`**, **`noble_platform_wait_longhorn_csi_before_loki`**, **`noble_platform_longhorn_csi_rollout_timeout`**
|
||||
3. **Delete a VM**:
|
||||
```bash
|
||||
ansible-playbook playbooks/manage_vm.yml -e "proxmox_action=delete_vm vmid=105"
|
||||
```
|
||||
|
||||
## Roles
|
||||
4. **Backup a VM**:
|
||||
```bash
|
||||
ansible-playbook playbooks/manage_vm.yml -e "proxmox_action=backup_vm vmid=105"
|
||||
```
|
||||
|
||||
| Role | Contents |
|
||||
|------|----------|
|
||||
| `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, **Authentik** (optional OIDC), Velero (optional). **Trivy Operator:** Argo leaf **`noble-trivy-operator`** (see `clusters/noble/bootstrap/argocd/app-of-apps/`); role **`noble_trivy`** is not invoked by **`noble.yml`**. |
|
||||
| `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) |
|
||||
| `debian_baseline_hardening` | Baseline Debian hardening (SSH policy, sysctl profile, fail2ban, unattended upgrades) |
|
||||
| `debian_maintenance` | Routine Debian maintenance tasks (updates, cleanup, reboot-on-required) |
|
||||
| `debian_ssh_key_rotation` | Declarative `authorized_keys` rotation for server users |
|
||||
| `proxmox_baseline` | Proxmox repo prep (community repos) and no-subscription warning suppression |
|
||||
| `proxmox_maintenance` | Proxmox package maintenance (dist-upgrade, cleanup, reboot-on-required) |
|
||||
| `proxmox_cluster` | Proxmox cluster bootstrap/join automation using `pvecm` |
|
||||
**Variables:**
|
||||
- `proxmox_action`: One of `create_template`, `create_vm`, `delete_vm`, `backup_vm` (Default: `create_vm`)
|
||||
- `target_host`: The host to run on (Default: `proxmox` group). Example: `-e "target_host=mercury"`
|
||||
|
||||
## Debian server ops quick start
|
||||
|
||||
These playbooks are separate from the Talos/noble flow and target hosts in `debian_servers`.
|
||||
|
||||
1. Copy `inventory/debian.example.yml` to `inventory/debian.yml` and update hosts/users.
|
||||
2. Update `inventory/group_vars/debian_servers.yml` with your allowed SSH users and real public keys.
|
||||
3. Run with the Debian inventory:
|
||||
|
||||
```bash
|
||||
cd ansible
|
||||
ansible-playbook -i inventory/debian.yml playbooks/debian_harden.yml
|
||||
ansible-playbook -i inventory/debian.yml playbooks/debian_rotate_ssh_keys.yml
|
||||
ansible-playbook -i inventory/debian.yml playbooks/debian_maintenance.yml
|
||||
```
|
||||
|
||||
Or run the combined maintenance pipeline:
|
||||
|
||||
```bash
|
||||
cd ansible
|
||||
ansible-playbook -i inventory/debian.yml playbooks/debian_ops.yml
|
||||
```
|
||||
|
||||
## Proxmox host + cluster quick start
|
||||
|
||||
These playbooks are separate from the Talos/noble flow and target hosts in `proxmox_hosts`.
|
||||
|
||||
1. Copy `inventory/proxmox.example.yml` to `inventory/proxmox.yml` and update hosts/users.
|
||||
2. Update `inventory/group_vars/proxmox_hosts.yml` with your cluster name (`proxmox_cluster_name`), chosen cluster master, and root public key file paths to install.
|
||||
3. First run (no SSH keys yet): use `--ask-pass` **or** set `ansible_password` (prefer Ansible Vault). Keep `ansible_ssh_common_args: "-o StrictHostKeyChecking=accept-new"` in inventory for first-contact hosts.
|
||||
4. Run prepare first to install your public keys on each host, then continue:
|
||||
|
||||
```bash
|
||||
cd ansible
|
||||
ansible-playbook -i inventory/proxmox.yml playbooks/proxmox_prepare.yml --ask-pass
|
||||
ansible-playbook -i inventory/proxmox.yml playbooks/proxmox_upgrade.yml
|
||||
ansible-playbook -i inventory/proxmox.yml playbooks/proxmox_cluster.yml
|
||||
```
|
||||
|
||||
After `proxmox_prepare.yml` finishes, SSH key auth should work for root (keys from `proxmox_root_authorized_key_files`), so `--ask-pass` is usually no longer needed.
|
||||
|
||||
If `pvecm add` still prompts for the master root password during join, set `proxmox_cluster_master_root_password` (prefer Vault) to run join non-interactively.
|
||||
|
||||
Changing `proxmox_cluster_name` only affects new cluster creation; it does not rename an already-created cluster.
|
||||
|
||||
Or run the full Proxmox pipeline:
|
||||
|
||||
```bash
|
||||
cd ansible
|
||||
ansible-playbook -i inventory/proxmox.yml playbooks/proxmox_ops.yml
|
||||
```
|
||||
|
||||
## Migrating from Argo-managed `noble-platform`
|
||||
|
||||
```bash
|
||||
kubectl delete application -n argocd noble-platform noble-kyverno noble-kyverno-policies --ignore-not-found
|
||||
kubectl apply -f clusters/noble/bootstrap/argocd/bootstrap-root-application.yaml
|
||||
```
|
||||
|
||||
Then run `playbooks/noble.yml` so Helm state matches git values.
|
||||
*See `roles/proxmox_vm/defaults/main.yml` for all available configuration options.*
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
[defaults]
|
||||
# Inventory lives under **inventory/** — place **group_vars/** next to the inventory file
|
||||
# (e.g. **inventory/group_vars/all.yml**) so variables apply to playbooks under **playbooks/**.
|
||||
inventory = inventory/localhost.yml
|
||||
roles_path = roles
|
||||
inventory = inventory/hosts.ini
|
||||
host_key_checking = False
|
||||
retry_files_enabled = False
|
||||
stdout_callback = default
|
||||
callback_result_format = yaml
|
||||
local_tmp = .ansible-tmp
|
||||
|
||||
[privilege_escalation]
|
||||
become = False
|
||||
interpreter_python = auto_silent
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
---
|
||||
all:
|
||||
children:
|
||||
debian_servers:
|
||||
hosts:
|
||||
debian-01:
|
||||
ansible_host: 192.168.50.101
|
||||
ansible_user: admin
|
||||
debian-02:
|
||||
ansible_host: 192.168.50.102
|
||||
ansible_user: admin
|
||||
@@ -1,36 +0,0 @@
|
||||
---
|
||||
# noble_repo_root / noble_kubeconfig are set in playbooks (use **playbook_dir** magic var).
|
||||
|
||||
# When kubeconfig points at the API VIP but this workstation cannot reach the lab LAN (VPN off, etc.),
|
||||
# set a reachable control-plane URL — same as: kubectl config set-cluster noble --server=https://<cp-ip>:6443
|
||||
# Example: ansible-playbook playbooks/noble.yml -e 'noble_k8s_api_server_override=https://192.168.50.20:6443'
|
||||
noble_k8s_api_server_override: ""
|
||||
|
||||
# When /healthz fails with **network unreachable** to the VIP and **override** is empty, retry using this URL (neon).
|
||||
noble_k8s_api_server_auto_fallback: true
|
||||
noble_k8s_api_server_fallback: "https://192.168.50.20:6443"
|
||||
|
||||
# Only if you must skip the kubectl /healthz preflight (not recommended).
|
||||
noble_skip_k8s_health_check: false
|
||||
|
||||
# Pangolin / Newt — set true only after newt-pangolin-auth Secret exists (SOPS: clusters/noble/secrets/ or imperative — see clusters/noble/bootstrap/newt/README.md)
|
||||
noble_newt_install: true
|
||||
noble_pangolin_sync_http_resources: true
|
||||
|
||||
# cert-manager needs Secret cloudflare-dns-api-token in cert-manager namespace before ClusterIssuers work
|
||||
noble_cert_manager_require_cloudflare_secret: true
|
||||
|
||||
# Velero — set **noble_velero_install: true** plus S3 bucket/URL (and credentials — see clusters/noble/bootstrap/velero/README.md)
|
||||
noble_velero_install: true
|
||||
|
||||
# Bootstrap kustomize in Argo (**noble-bootstrap-root** → **clusters/noble/bootstrap**, includes **clusters/noble/apps**). 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: true
|
||||
# Optional: public (or extra) Authentik hostnames on the same IdP — list of FQDNs. Pangolin: CNAME + resource → Newt → Traefik (see noble_authentik README).
|
||||
noble_authentik_ingress_extra_hosts:
|
||||
- auth.nikflix.ca
|
||||
# Open WebUI — public Ingress host (Pangolin → Newt → Traefik). Must match **clusters/noble/apps/open-webui/values.yaml** **ingress.host** and Authentik redirect URI (Ansible uses this var).
|
||||
noble_open_webui_public_host: webui.nikflix.ca
|
||||
noble_authentik_blueprints_enabled: true
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
# Hardened SSH settings
|
||||
debian_baseline_ssh_allow_users:
|
||||
- admin
|
||||
|
||||
# Example key rotation entries. Replace with your real users and keys.
|
||||
debian_ssh_rotation_users:
|
||||
- name: admin
|
||||
home: /home/admin
|
||||
state: present
|
||||
keys:
|
||||
- "ssh-ed25519 AAAAEXAMPLE_REPLACE_ME admin@workstation"
|
||||
@@ -1,37 +0,0 @@
|
||||
---
|
||||
# Proxmox repositories
|
||||
proxmox_repo_debian_codename: trixie
|
||||
proxmox_repo_disable_enterprise: true
|
||||
proxmox_repo_disable_ceph_enterprise: true
|
||||
proxmox_repo_enable_pve_no_subscription: true
|
||||
proxmox_repo_enable_ceph_no_subscription: true
|
||||
|
||||
# Suppress "No valid subscription" warning in UI
|
||||
proxmox_no_subscription_notice_disable: true
|
||||
|
||||
# Public keys to install for root on each Proxmox host.
|
||||
proxmox_root_authorized_key_files:
|
||||
- "{{ lookup('env', 'HOME') }}/.ssh/id_ed25519.pub"
|
||||
- "{{ lookup('env', 'HOME') }}/.ssh/ansible.pub"
|
||||
|
||||
# Package upgrade/reboot policy
|
||||
proxmox_upgrade_apt_cache_valid_time: 3600
|
||||
proxmox_upgrade_autoremove: true
|
||||
proxmox_upgrade_autoclean: true
|
||||
proxmox_upgrade_reboot_if_required: true
|
||||
proxmox_upgrade_reboot_timeout: 1800
|
||||
|
||||
# Cluster settings
|
||||
proxmox_cluster_enabled: true
|
||||
proxmox_cluster_name: atomic-hub
|
||||
|
||||
# Bootstrap host name from inventory (first host by default if empty)
|
||||
proxmox_cluster_master: ""
|
||||
|
||||
# Optional explicit IP/FQDN for joining; leave empty to use ansible_host of master
|
||||
proxmox_cluster_master_ip: ""
|
||||
proxmox_cluster_force: false
|
||||
|
||||
# Optional: use only for first cluster joins when inter-node SSH trust is not established.
|
||||
# Prefer storing with Ansible Vault if you set this.
|
||||
proxmox_cluster_master_root_password: "Hemroid8"
|
||||
14
ansible/inventory/hosts.ini
Normal file
14
ansible/inventory/hosts.ini
Normal file
@@ -0,0 +1,14 @@
|
||||
[proxmox]
|
||||
# Replace pve1 with your proxmox node hostname or IP
|
||||
mercury ansible_host=192.168.50.100 ansible_user=root
|
||||
|
||||
[proxmox:vars]
|
||||
# If using password auth (ssh key recommended though):
|
||||
# ansible_ssh_pass=yourpassword
|
||||
|
||||
# Connection variables for the proxmox modules (api)
|
||||
proxmox_api_user=root@pam
|
||||
proxmox_api_password=Hemroid8
|
||||
proxmox_api_host=192.168.50.100
|
||||
# proxmox_api_token_id=
|
||||
# proxmox_api_token_secret=
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
all:
|
||||
hosts:
|
||||
localhost:
|
||||
ansible_connection: local
|
||||
ansible_python_interpreter: "{{ ansible_playbook_python }}"
|
||||
@@ -1,24 +0,0 @@
|
||||
---
|
||||
all:
|
||||
children:
|
||||
proxmox_hosts:
|
||||
vars:
|
||||
ansible_ssh_common_args: "-o StrictHostKeyChecking=accept-new"
|
||||
hosts:
|
||||
helium:
|
||||
ansible_host: 192.168.1.100
|
||||
ansible_user: root
|
||||
# First run without SSH keys:
|
||||
# ansible_password: "{{ vault_proxmox_root_password }}"
|
||||
neon:
|
||||
ansible_host: 192.168.1.90
|
||||
ansible_user: root
|
||||
# ansible_password: "{{ vault_proxmox_root_password }}"
|
||||
argon:
|
||||
ansible_host: 192.168.1.80
|
||||
ansible_user: root
|
||||
# ansible_password: "{{ vault_proxmox_root_password }}"
|
||||
krypton:
|
||||
ansible_host: 192.168.1.70
|
||||
ansible_user: root
|
||||
# ansible_password: "{{ vault_proxmox_root_password }}"
|
||||
@@ -1,24 +0,0 @@
|
||||
---
|
||||
all:
|
||||
children:
|
||||
proxmox_hosts:
|
||||
vars:
|
||||
ansible_ssh_common_args: "-o StrictHostKeyChecking=accept-new"
|
||||
hosts:
|
||||
helium:
|
||||
ansible_host: 192.168.1.100
|
||||
ansible_user: root
|
||||
# First run without SSH keys:
|
||||
# ansible_password: "{{ vault_proxmox_root_password }}"
|
||||
neon:
|
||||
ansible_host: 192.168.1.90
|
||||
ansible_user: root
|
||||
# ansible_password: "{{ vault_proxmox_root_password }}"
|
||||
argon:
|
||||
ansible_host: 192.168.1.80
|
||||
ansible_user: root
|
||||
# ansible_password: "{{ vault_proxmox_root_password }}"
|
||||
krypton:
|
||||
ansible_host: 192.168.1.70
|
||||
ansible_user: root
|
||||
# ansible_password: "{{ vault_proxmox_root_password }}"
|
||||
72
ansible/playbooks/create_ubuntu_template.yml
Normal file
72
ansible/playbooks/create_ubuntu_template.yml
Normal file
@@ -0,0 +1,72 @@
|
||||
---
|
||||
- name: Create Ubuntu Cloud-Init Template
|
||||
hosts: proxmox
|
||||
become: yes
|
||||
vars:
|
||||
template_id: 9000
|
||||
template_name: ubuntu-2204-cloud
|
||||
# URL for Ubuntu 22.04 Cloud Image (Jammy)
|
||||
image_url: "https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img"
|
||||
image_name: "ubuntu-22.04-server-cloudimg-amd64.img"
|
||||
storage_pool: "local-lvm"
|
||||
memory: 2048
|
||||
cores: 2
|
||||
|
||||
tasks:
|
||||
- name: Check if template already exists
|
||||
command: "qm status {{ template_id }}"
|
||||
register: vm_status
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
|
||||
- name: Fail if template ID exists
|
||||
fail:
|
||||
msg: "VM ID {{ template_id }} already exists. Please choose a different ID or delete the existing VM."
|
||||
when: vm_status.rc == 0
|
||||
|
||||
- name: Download Ubuntu Cloud Image
|
||||
get_url:
|
||||
url: "{{ image_url }}"
|
||||
dest: "/tmp/{{ image_name }}"
|
||||
mode: '0644'
|
||||
|
||||
- name: Install libguestfs-tools (required for virt-customize if needed, optional)
|
||||
apt:
|
||||
name: libguestfs-tools
|
||||
state: present
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Create VM with hardware config
|
||||
command: >
|
||||
qm create {{ template_id }}
|
||||
--name "{{ template_name }}"
|
||||
--memory {{ memory }}
|
||||
--core {{ cores }}
|
||||
--net0 virtio,bridge=vmbr0
|
||||
--scsihw virtio-scsi-pci
|
||||
--ostype l26
|
||||
--serial0 socket --vga serial0
|
||||
|
||||
- name: Import Disk
|
||||
command: "qm importdisk {{ template_id }} /tmp/{{ image_name }} {{ storage_pool }}"
|
||||
|
||||
- name: Attach Disk to SCSI
|
||||
command: "qm set {{ template_id }} --scsi0 {{ storage_pool }}:vm-{{ template_id }}-disk-0"
|
||||
|
||||
- name: Add Cloud-Init Drive
|
||||
command: "qm set {{ template_id }} --ide2 {{ storage_pool }}:cloudinit"
|
||||
|
||||
- name: Set Boot Order
|
||||
command: "qm set {{ template_id }} --boot c --bootdisk scsi0"
|
||||
|
||||
- name: Resize Disk (Optional, e.g. 10G)
|
||||
command: "qm resize {{ template_id }} scsi0 10G"
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Convert to Template
|
||||
command: "qm template {{ template_id }}"
|
||||
|
||||
- name: Remove Downloaded Image
|
||||
file:
|
||||
path: "/tmp/{{ image_name }}"
|
||||
state: absent
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
- name: Debian server baseline hardening
|
||||
hosts: debian_servers
|
||||
become: true
|
||||
gather_facts: true
|
||||
roles:
|
||||
- role: debian_baseline_hardening
|
||||
tags: [hardening, baseline]
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
- name: Debian maintenance (updates + reboot handling)
|
||||
hosts: debian_servers
|
||||
become: true
|
||||
gather_facts: true
|
||||
roles:
|
||||
- role: debian_maintenance
|
||||
tags: [maintenance, updates]
|
||||
@@ -1,3 +0,0 @@
|
||||
---
|
||||
- import_playbook: debian_harden.yml
|
||||
- import_playbook: debian_maintenance.yml
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
- name: Debian SSH key rotation
|
||||
hosts: debian_servers
|
||||
become: true
|
||||
gather_facts: false
|
||||
roles:
|
||||
- role: debian_ssh_key_rotation
|
||||
tags: [ssh, ssh_keys, rotation]
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
# Full bring-up: Talos Phase A then platform stack.
|
||||
# Run from **ansible/**: ansible-playbook playbooks/deploy.yml
|
||||
- import_playbook: talos_phase_a.yml
|
||||
- import_playbook: noble.yml
|
||||
6
ansible/playbooks/manage_vm.yml
Normal file
6
ansible/playbooks/manage_vm.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
- name: Manage Proxmox VMs
|
||||
hosts: "{{ target_host | default('proxmox') }}"
|
||||
become: yes
|
||||
roles:
|
||||
- proxmox_vm
|
||||
@@ -1,250 +0,0 @@
|
||||
---
|
||||
# Full platform install — **after** Talos bootstrap (`talosctl bootstrap` + working kubeconfig).
|
||||
# Do not run until `kubectl get --raw /healthz` returns ok (see talos/README.md §3, CLUSTER-BUILD Phase A).
|
||||
# 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, authentik, velero, landing, all (default)
|
||||
# Argo leaf **Application** CRs are applied in play **tasks:** after **noble_velero** (Ansible Helm first, then GitOps).
|
||||
# Trivy Operator is **not** installed here — sync **noble-trivy-operator** from Argo (app-of-apps) after deploy.
|
||||
- name: Noble cluster — platform stack (Ansible-managed)
|
||||
hosts: localhost
|
||||
connection: local
|
||||
gather_facts: false
|
||||
vars:
|
||||
noble_repo_root: "{{ playbook_dir | dirname | dirname }}"
|
||||
noble_kubeconfig: "{{ lookup('env', 'KUBECONFIG') | default(noble_repo_root + '/talos/kubeconfig', true) }}"
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
pre_tasks:
|
||||
# Helm/kubectl use $KUBECONFIG; a missing file yields "connection refused" to localhost:8080.
|
||||
- name: Stat kubeconfig path from KUBECONFIG or default
|
||||
ansible.builtin.stat:
|
||||
path: "{{ noble_kubeconfig }}"
|
||||
register: noble_kubeconfig_stat
|
||||
tags: [always]
|
||||
|
||||
- name: Fall back to repo talos/kubeconfig when $KUBECONFIG is unset or not a file
|
||||
ansible.builtin.set_fact:
|
||||
noble_kubeconfig: "{{ noble_repo_root }}/talos/kubeconfig"
|
||||
when: not noble_kubeconfig_stat.stat.exists | default(false)
|
||||
tags: [always]
|
||||
|
||||
- name: Stat kubeconfig after fallback
|
||||
ansible.builtin.stat:
|
||||
path: "{{ noble_kubeconfig }}"
|
||||
register: noble_kubeconfig_stat2
|
||||
tags: [always]
|
||||
|
||||
- name: Require a real kubeconfig file
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- noble_kubeconfig_stat2.stat.exists | default(false)
|
||||
- noble_kubeconfig_stat2.stat.isreg | default(false)
|
||||
fail_msg: >-
|
||||
No kubeconfig file at {{ noble_kubeconfig }}.
|
||||
Fix: export KUBECONFIG=/actual/path/from/talosctl-kubeconfig (see talos/README.md),
|
||||
or copy the admin kubeconfig to {{ noble_repo_root }}/talos/kubeconfig.
|
||||
Do not use documentation placeholders as the path.
|
||||
tags: [always]
|
||||
|
||||
- name: Ensure temp dir for kubeconfig API override
|
||||
ansible.builtin.file:
|
||||
path: "{{ noble_repo_root }}/ansible/.ansible-tmp"
|
||||
state: directory
|
||||
mode: "0700"
|
||||
when: noble_k8s_api_server_override | default('') | length > 0
|
||||
tags: [always]
|
||||
|
||||
- name: Copy kubeconfig for API server override (original file unchanged)
|
||||
ansible.builtin.copy:
|
||||
src: "{{ noble_kubeconfig }}"
|
||||
dest: "{{ noble_repo_root }}/ansible/.ansible-tmp/kubeconfig.patched"
|
||||
mode: "0600"
|
||||
when: noble_k8s_api_server_override | default('') | length > 0
|
||||
tags: [always]
|
||||
|
||||
- name: Resolve current cluster name (for set-cluster)
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- config
|
||||
- view
|
||||
- --minify
|
||||
- -o
|
||||
- jsonpath={.clusters[0].name}
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_repo_root }}/ansible/.ansible-tmp/kubeconfig.patched"
|
||||
register: noble_k8s_cluster_name
|
||||
changed_when: false
|
||||
when: noble_k8s_api_server_override | default('') | length > 0
|
||||
tags: [always]
|
||||
|
||||
- name: Point patched kubeconfig at reachable apiserver
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- config
|
||||
- set-cluster
|
||||
- "{{ noble_k8s_cluster_name.stdout }}"
|
||||
- --server={{ noble_k8s_api_server_override }}
|
||||
- --kubeconfig={{ noble_repo_root }}/ansible/.ansible-tmp/kubeconfig.patched
|
||||
when: noble_k8s_api_server_override | default('') | length > 0
|
||||
changed_when: true
|
||||
tags: [always]
|
||||
|
||||
- name: Use patched kubeconfig for this play
|
||||
ansible.builtin.set_fact:
|
||||
noble_kubeconfig: "{{ noble_repo_root }}/ansible/.ansible-tmp/kubeconfig.patched"
|
||||
when: noble_k8s_api_server_override | default('') | length > 0
|
||||
tags: [always]
|
||||
|
||||
- name: Verify Kubernetes API is reachable from this host
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- get
|
||||
- --raw
|
||||
- /healthz
|
||||
- --request-timeout=15s
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
register: noble_k8s_health_first
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
tags: [always]
|
||||
|
||||
# talosctl kubeconfig often sets server to the VIP; off-LAN you can reach a control-plane IP but not 192.168.50.230.
|
||||
# kubectl stderr is often "The connection to the server ... was refused" (no substring "connection refused").
|
||||
- name: Auto-fallback API server when VIP is unreachable (temp kubeconfig)
|
||||
tags: [always]
|
||||
when:
|
||||
- noble_k8s_api_server_auto_fallback | default(true) | bool
|
||||
- noble_k8s_api_server_override | default('') | length == 0
|
||||
- not (noble_skip_k8s_health_check | default(false) | bool)
|
||||
- (noble_k8s_health_first.rc | default(1)) != 0 or (noble_k8s_health_first.stdout | default('') | trim) != 'ok'
|
||||
- (((noble_k8s_health_first.stderr | default('')) ~ (noble_k8s_health_first.stdout | default(''))) | lower) is search('network is unreachable|no route to host|connection refused|was refused', multiline=False)
|
||||
block:
|
||||
- name: Ensure temp dir for kubeconfig auto-fallback
|
||||
ansible.builtin.file:
|
||||
path: "{{ noble_repo_root }}/ansible/.ansible-tmp"
|
||||
state: directory
|
||||
mode: "0700"
|
||||
|
||||
- name: Copy kubeconfig for API auto-fallback
|
||||
ansible.builtin.copy:
|
||||
src: "{{ noble_kubeconfig }}"
|
||||
dest: "{{ noble_repo_root }}/ansible/.ansible-tmp/kubeconfig.auto-fallback"
|
||||
mode: "0600"
|
||||
|
||||
- name: Resolve cluster name for kubectl set-cluster
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- config
|
||||
- view
|
||||
- --minify
|
||||
- -o
|
||||
- jsonpath={.clusters[0].name}
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_repo_root }}/ansible/.ansible-tmp/kubeconfig.auto-fallback"
|
||||
register: noble_k8s_cluster_fb
|
||||
changed_when: false
|
||||
|
||||
- name: Point temp kubeconfig at fallback apiserver
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- config
|
||||
- set-cluster
|
||||
- "{{ noble_k8s_cluster_fb.stdout }}"
|
||||
- --server={{ noble_k8s_api_server_fallback | default('https://192.168.50.20:6443', true) }}
|
||||
- --kubeconfig={{ noble_repo_root }}/ansible/.ansible-tmp/kubeconfig.auto-fallback
|
||||
changed_when: true
|
||||
|
||||
- name: Use kubeconfig with fallback API server for this play
|
||||
ansible.builtin.set_fact:
|
||||
noble_kubeconfig: "{{ noble_repo_root }}/ansible/.ansible-tmp/kubeconfig.auto-fallback"
|
||||
|
||||
- name: Re-verify Kubernetes API after auto-fallback
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- get
|
||||
- --raw
|
||||
- /healthz
|
||||
- --request-timeout=15s
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
register: noble_k8s_health_after_fallback
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
|
||||
- name: Mark that API was re-checked after kubeconfig fallback
|
||||
ansible.builtin.set_fact:
|
||||
noble_k8s_api_fallback_used: true
|
||||
|
||||
- name: Normalize API health result for preflight (scalars; avoids dict merge / set_fact stringification)
|
||||
ansible.builtin.set_fact:
|
||||
noble_k8s_health_rc: "{{ noble_k8s_health_after_fallback.rc | default(1) if (noble_k8s_api_fallback_used | default(false) | bool) else (noble_k8s_health_first.rc | default(1)) }}"
|
||||
noble_k8s_health_stdout: "{{ noble_k8s_health_after_fallback.stdout | default('') if (noble_k8s_api_fallback_used | default(false) | bool) else (noble_k8s_health_first.stdout | default('')) }}"
|
||||
noble_k8s_health_stderr: "{{ noble_k8s_health_after_fallback.stderr | default('') if (noble_k8s_api_fallback_used | default(false) | bool) else (noble_k8s_health_first.stderr | default('')) }}"
|
||||
tags: [always]
|
||||
|
||||
- name: Fail when API check did not return ok
|
||||
ansible.builtin.fail:
|
||||
msg: "{{ lookup('template', 'templates/api_health_hint.j2') }}"
|
||||
when:
|
||||
- not (noble_skip_k8s_health_check | default(false) | bool)
|
||||
- (noble_k8s_health_rc | int) != 0 or (noble_k8s_health_stdout | default('') | trim) != 'ok'
|
||||
tags: [always]
|
||||
|
||||
roles:
|
||||
- role: helm_repos
|
||||
tags: [repos, helm]
|
||||
- role: noble_cilium
|
||||
tags: [cilium, cni]
|
||||
- role: noble_csi_snapshot_controller
|
||||
tags: [csi_snapshot, snapshot, storage]
|
||||
- role: noble_metrics_server
|
||||
tags: [metrics, metrics_server]
|
||||
# Kyverno before Longhorn: Longhorn post-upgrade Job is admitted through Kyverno; policies use
|
||||
# failurePolicy Ignore so webhook transport timeouts do not fail Helm (see policies-values.yaml).
|
||||
- role: noble_kyverno
|
||||
tags: [kyverno, policy]
|
||||
- role: noble_kyverno_policies
|
||||
tags: [kyverno_policies, policy]
|
||||
- role: noble_longhorn
|
||||
tags: [longhorn, storage]
|
||||
- role: noble_metallb
|
||||
tags: [metallb, lb]
|
||||
- role: noble_kube_vip
|
||||
tags: [kube_vip, vip]
|
||||
- role: noble_traefik
|
||||
tags: [traefik, ingress]
|
||||
- role: noble_cert_manager
|
||||
tags: [cert_manager, certs]
|
||||
- role: noble_newt
|
||||
tags: [newt, pangolin]
|
||||
- role: noble_argocd
|
||||
tags: [argocd, gitops]
|
||||
- role: noble_platform
|
||||
tags: [platform, observability, apps]
|
||||
- role: noble_authentik
|
||||
tags: [authentik, sso, oauth, oidc]
|
||||
- role: noble_velero
|
||||
tags: [velero, backups]
|
||||
|
||||
tasks:
|
||||
# Leaf Application CRs must exist only after all Ansible Helm in this play (platform, authentik, velero, …)
|
||||
# so argocd-controller does not SSA resources before Helm owns them; then Argo can take over (manual → auto).
|
||||
- name: Apply Argo CD root / bootstrap / leaf Application manifests (post–Ansible Helm)
|
||||
ansible.builtin.include_role:
|
||||
name: noble_argocd
|
||||
tasks_from: applications_post_platform
|
||||
tags: [argocd, gitops, platform, apps, observability, all]
|
||||
|
||||
- name: Noble landing URLs (+ optional token fetch)
|
||||
ansible.builtin.include_role:
|
||||
name: noble_landing_urls
|
||||
tags: [landing, platform, observability, apps, all]
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
# Manual follow-ups after **noble.yml**: SOPS key backup, optional Argo root Application.
|
||||
- hosts: localhost
|
||||
connection: local
|
||||
gather_facts: false
|
||||
roles:
|
||||
- noble_post_deploy
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
- name: Proxmox cluster bootstrap/join
|
||||
hosts: proxmox_hosts
|
||||
become: true
|
||||
gather_facts: false
|
||||
serial: 1
|
||||
roles:
|
||||
- role: proxmox_cluster
|
||||
tags: [proxmox, cluster]
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
- import_playbook: proxmox_prepare.yml
|
||||
- import_playbook: proxmox_upgrade.yml
|
||||
- import_playbook: proxmox_cluster.yml
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
- name: Proxmox host preparation (community repos + no-subscription notice)
|
||||
hosts: proxmox_hosts
|
||||
become: true
|
||||
gather_facts: true
|
||||
roles:
|
||||
- role: proxmox_baseline
|
||||
tags: [proxmox, prepare, repos, ui]
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
- name: Proxmox host maintenance (upgrade to latest)
|
||||
hosts: proxmox_hosts
|
||||
become: true
|
||||
gather_facts: true
|
||||
serial: 1
|
||||
roles:
|
||||
- role: proxmox_maintenance
|
||||
tags: [proxmox, maintenance, updates]
|
||||
@@ -1,11 +0,0 @@
|
||||
---
|
||||
# Genconfig only — for full Talos Phase A (apply, bootstrap, kubeconfig) use **playbooks/talos_phase_a.yml**
|
||||
# or **playbooks/deploy.yml**. Run: ansible-playbook playbooks/talos_bootstrap.yml -e noble_talos_genconfig=true
|
||||
- name: Talos — optional genconfig helper
|
||||
hosts: localhost
|
||||
connection: local
|
||||
gather_facts: false
|
||||
vars:
|
||||
noble_repo_root: "{{ playbook_dir | dirname | dirname }}"
|
||||
roles:
|
||||
- role: talos_bootstrap
|
||||
@@ -1,15 +0,0 @@
|
||||
---
|
||||
# Talos Phase A — **talhelper genconfig** → **apply-config** (all nodes) → **bootstrap** → **kubeconfig**.
|
||||
# Requires: **talosctl**, **talhelper**, reachable node IPs (same LAN as nodes for Talos API :50000).
|
||||
# See **talos/README.md** §1–§3. Then run **playbooks/noble.yml** or **deploy.yml**.
|
||||
- name: Talos — genconfig, apply, bootstrap, kubeconfig
|
||||
hosts: localhost
|
||||
connection: local
|
||||
gather_facts: false
|
||||
vars:
|
||||
noble_repo_root: "{{ playbook_dir | dirname | dirname }}"
|
||||
noble_talos_dir: "{{ noble_repo_root }}/talos"
|
||||
noble_talos_kubeconfig_out: "{{ noble_repo_root }}/talos/kubeconfig"
|
||||
roles:
|
||||
- role: talos_phase_a
|
||||
tags: [talos, phase_a]
|
||||
@@ -1,22 +0,0 @@
|
||||
{# Error output for noble.yml API preflight when kubectl /healthz fails #}
|
||||
Cannot use the Kubernetes API from this host (kubectl get --raw /healthz).
|
||||
rc={{ noble_k8s_health_rc | default('n/a') }}
|
||||
stderr: {{ noble_k8s_health_stderr | default('') | trim }}
|
||||
|
||||
{% set err = (noble_k8s_health_stderr | default('')) | lower %}
|
||||
{% if 'connection refused' in err %}
|
||||
Connection refused: the TCP path to that host works, but nothing is accepting HTTPS on port 6443 there.
|
||||
• **Not bootstrapped yet?** Finish Talos first: `talosctl bootstrap` (once on a control plane), then `talosctl kubeconfig`, then confirm `kubectl get nodes`. See talos/README.md §2–§3 and CLUSTER-BUILD.md Phase A. **Do not run this playbook before the Kubernetes API exists.**
|
||||
• If bootstrap is done: try another control-plane IP (CLUSTER-BUILD inventory: neon 192.168.50.20, argon .30, krypton .40), or the VIP if kube-vip is up and you are on the LAN:
|
||||
-e 'noble_k8s_api_server_override=https://192.168.50.230:6443'
|
||||
• Do not point the API URL at a worker-only node.
|
||||
• `talosctl health` / `kubectl get nodes` from a working client.
|
||||
{% elif 'network is unreachable' in err or 'no route to host' in err %}
|
||||
Network unreachable / no route: this machine cannot route to the API IP. Join the lab LAN or VPN, or set a reachable API server URL (talos/README.md §3).
|
||||
{% else %}
|
||||
If kubeconfig used the VIP from off-LAN, try a reachable control-plane IP, e.g.:
|
||||
-e 'noble_k8s_api_server_override=https://192.168.50.20:6443'
|
||||
See talos/README.md §3.
|
||||
{% endif %}
|
||||
|
||||
To skip this check (not recommended): -e noble_skip_k8s_health_check=true
|
||||
2
ansible/requirements.yml
Normal file
2
ansible/requirements.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
collections:
|
||||
- name: community.general
|
||||
@@ -1,39 +0,0 @@
|
||||
---
|
||||
# Update apt metadata only when stale (seconds)
|
||||
debian_baseline_apt_cache_valid_time: 3600
|
||||
|
||||
# Core host hardening packages
|
||||
debian_baseline_packages:
|
||||
- unattended-upgrades
|
||||
- apt-listchanges
|
||||
- fail2ban
|
||||
- needrestart
|
||||
- sudo
|
||||
- ca-certificates
|
||||
|
||||
# SSH hardening controls
|
||||
debian_baseline_ssh_permit_root_login: "no"
|
||||
debian_baseline_ssh_password_authentication: "no"
|
||||
debian_baseline_ssh_pubkey_authentication: "yes"
|
||||
debian_baseline_ssh_x11_forwarding: "no"
|
||||
debian_baseline_ssh_max_auth_tries: 3
|
||||
debian_baseline_ssh_client_alive_interval: 300
|
||||
debian_baseline_ssh_client_alive_count_max: 2
|
||||
debian_baseline_ssh_allow_users: []
|
||||
|
||||
# unattended-upgrades controls
|
||||
debian_baseline_enable_unattended_upgrades: true
|
||||
debian_baseline_unattended_auto_upgrade: "1"
|
||||
debian_baseline_unattended_update_lists: "1"
|
||||
|
||||
# Kernel and network hardening sysctls
|
||||
debian_baseline_sysctl_settings:
|
||||
net.ipv4.conf.all.accept_redirects: "0"
|
||||
net.ipv4.conf.default.accept_redirects: "0"
|
||||
net.ipv4.conf.all.send_redirects: "0"
|
||||
net.ipv4.conf.default.send_redirects: "0"
|
||||
net.ipv4.conf.all.log_martians: "1"
|
||||
net.ipv4.conf.default.log_martians: "1"
|
||||
net.ipv4.tcp_syncookies: "1"
|
||||
net.ipv6.conf.all.accept_redirects: "0"
|
||||
net.ipv6.conf.default.accept_redirects: "0"
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
- name: Restart ssh
|
||||
ansible.builtin.service:
|
||||
name: ssh
|
||||
state: restarted
|
||||
|
||||
- name: Reload sysctl
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- sysctl
|
||||
- --system
|
||||
changed_when: true
|
||||
@@ -1,52 +0,0 @@
|
||||
---
|
||||
- name: Refresh apt cache
|
||||
ansible.builtin.apt:
|
||||
update_cache: true
|
||||
cache_valid_time: "{{ debian_baseline_apt_cache_valid_time }}"
|
||||
|
||||
- name: Install baseline hardening packages
|
||||
ansible.builtin.apt:
|
||||
name: "{{ debian_baseline_packages }}"
|
||||
state: present
|
||||
|
||||
- name: Configure unattended-upgrades auto settings
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/apt/apt.conf.d/20auto-upgrades
|
||||
mode: "0644"
|
||||
content: |
|
||||
APT::Periodic::Update-Package-Lists "{{ debian_baseline_unattended_update_lists }}";
|
||||
APT::Periodic::Unattended-Upgrade "{{ debian_baseline_unattended_auto_upgrade }}";
|
||||
when: debian_baseline_enable_unattended_upgrades | bool
|
||||
|
||||
- name: Configure SSH hardening options
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/ssh/sshd_config.d/99-hardening.conf
|
||||
mode: "0644"
|
||||
content: |
|
||||
PermitRootLogin {{ debian_baseline_ssh_permit_root_login }}
|
||||
PasswordAuthentication {{ debian_baseline_ssh_password_authentication }}
|
||||
PubkeyAuthentication {{ debian_baseline_ssh_pubkey_authentication }}
|
||||
X11Forwarding {{ debian_baseline_ssh_x11_forwarding }}
|
||||
MaxAuthTries {{ debian_baseline_ssh_max_auth_tries }}
|
||||
ClientAliveInterval {{ debian_baseline_ssh_client_alive_interval }}
|
||||
ClientAliveCountMax {{ debian_baseline_ssh_client_alive_count_max }}
|
||||
{% if debian_baseline_ssh_allow_users | length > 0 %}
|
||||
AllowUsers {{ debian_baseline_ssh_allow_users | join(' ') }}
|
||||
{% endif %}
|
||||
notify: Restart ssh
|
||||
|
||||
- name: Configure baseline sysctls
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/sysctl.d/99-hardening.conf
|
||||
mode: "0644"
|
||||
content: |
|
||||
{% for key, value in debian_baseline_sysctl_settings.items() %}
|
||||
{{ key }} = {{ value }}
|
||||
{% endfor %}
|
||||
notify: Reload sysctl
|
||||
|
||||
- name: Ensure fail2ban service is enabled
|
||||
ansible.builtin.service:
|
||||
name: fail2ban
|
||||
enabled: true
|
||||
state: started
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
debian_maintenance_apt_cache_valid_time: 3600
|
||||
debian_maintenance_upgrade_type: dist
|
||||
debian_maintenance_autoremove: true
|
||||
debian_maintenance_autoclean: true
|
||||
debian_maintenance_reboot_if_required: true
|
||||
debian_maintenance_reboot_timeout: 1800
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
- name: Refresh apt cache
|
||||
ansible.builtin.apt:
|
||||
update_cache: true
|
||||
cache_valid_time: "{{ debian_maintenance_apt_cache_valid_time }}"
|
||||
|
||||
- name: Upgrade Debian packages
|
||||
ansible.builtin.apt:
|
||||
upgrade: "{{ debian_maintenance_upgrade_type }}"
|
||||
|
||||
- name: Remove orphaned packages
|
||||
ansible.builtin.apt:
|
||||
autoremove: "{{ debian_maintenance_autoremove }}"
|
||||
|
||||
- name: Clean apt package cache
|
||||
ansible.builtin.apt:
|
||||
autoclean: "{{ debian_maintenance_autoclean }}"
|
||||
|
||||
- name: Check if reboot is required
|
||||
ansible.builtin.stat:
|
||||
path: /var/run/reboot-required
|
||||
register: debian_maintenance_reboot_required_file
|
||||
|
||||
- name: Reboot when required by package updates
|
||||
ansible.builtin.reboot:
|
||||
reboot_timeout: "{{ debian_maintenance_reboot_timeout }}"
|
||||
msg: "Reboot initiated by Ansible maintenance playbook"
|
||||
when:
|
||||
- debian_maintenance_reboot_if_required | bool
|
||||
- debian_maintenance_reboot_required_file.stat.exists | default(false)
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
# List of users to manage keys for.
|
||||
# Example:
|
||||
# debian_ssh_rotation_users:
|
||||
# - name: deploy
|
||||
# home: /home/deploy
|
||||
# state: present
|
||||
# keys:
|
||||
# - "ssh-ed25519 AAAA... deploy@laptop"
|
||||
debian_ssh_rotation_users: []
|
||||
@@ -1,50 +0,0 @@
|
||||
---
|
||||
- name: Validate SSH key rotation inputs
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- item.name is defined
|
||||
- item.home is defined
|
||||
- (item.state | default('present')) in ['present', 'absent']
|
||||
- (item.state | default('present')) == 'absent' or (item.keys is defined and item.keys | length > 0)
|
||||
fail_msg: >-
|
||||
Each entry in debian_ssh_rotation_users must include name, home, and either:
|
||||
state=absent, or keys with at least one SSH public key.
|
||||
loop: "{{ debian_ssh_rotation_users }}"
|
||||
loop_control:
|
||||
label: "{{ item.name | default('unknown') }}"
|
||||
|
||||
- name: Ensure ~/.ssh exists for managed users
|
||||
ansible.builtin.file:
|
||||
path: "{{ item.home }}/.ssh"
|
||||
state: directory
|
||||
owner: "{{ item.name }}"
|
||||
group: "{{ item.name }}"
|
||||
mode: "0700"
|
||||
loop: "{{ debian_ssh_rotation_users }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
when: (item.state | default('present')) == 'present'
|
||||
|
||||
- name: Rotate authorized_keys for managed users
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ item.home }}/.ssh/authorized_keys"
|
||||
owner: "{{ item.name }}"
|
||||
group: "{{ item.name }}"
|
||||
mode: "0600"
|
||||
content: |
|
||||
{% for key in item.keys %}
|
||||
{{ key }}
|
||||
{% endfor %}
|
||||
loop: "{{ debian_ssh_rotation_users }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
when: (item.state | default('present')) == 'present'
|
||||
|
||||
- name: Remove authorized_keys for users marked absent
|
||||
ansible.builtin.file:
|
||||
path: "{{ item.home }}/.ssh/authorized_keys"
|
||||
state: absent
|
||||
loop: "{{ debian_ssh_rotation_users }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
when: (item.state | default('present')) == 'absent'
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
noble_helm_repos:
|
||||
- { name: cilium, url: "https://helm.cilium.io/" }
|
||||
- { name: metallb, url: "https://metallb.github.io/metallb" }
|
||||
- { name: longhorn, url: "https://charts.longhorn.io" }
|
||||
- { name: traefik, url: "https://traefik.github.io/charts" }
|
||||
- { name: jetstack, url: "https://charts.jetstack.io" }
|
||||
- { name: fossorial, url: "https://charts.fossorial.io" }
|
||||
- { name: argo, url: "https://argoproj.github.io/argo-helm" }
|
||||
- { name: metrics-server, url: "https://kubernetes-sigs.github.io/metrics-server/" }
|
||||
- { name: prometheus-community, url: "https://prometheus-community.github.io/helm-charts" }
|
||||
- { name: grafana, url: "https://grafana.github.io/helm-charts" }
|
||||
- { name: fluent, url: "https://fluent.github.io/helm-charts" }
|
||||
- { name: headlamp, url: "https://kubernetes-sigs.github.io/headlamp/" }
|
||||
- { name: kyverno, url: "https://kyverno.github.io/kyverno/" }
|
||||
- { name: vmware-tanzu, url: "https://vmware-tanzu.github.io/helm-charts" }
|
||||
- { name: goauthentik, url: "https://charts.goauthentik.io" }
|
||||
- { name: oauth2-proxy, url: "https://oauth2-proxy.github.io/manifests" }
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
- name: Add Helm repositories
|
||||
ansible.builtin.command:
|
||||
cmd: "helm repo add {{ item.name }} {{ item.url }}"
|
||||
loop: "{{ noble_helm_repos }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
register: helm_repo_add
|
||||
changed_when: helm_repo_add.rc == 0
|
||||
failed_when: >-
|
||||
helm_repo_add.rc != 0 and
|
||||
('already exists' not in (helm_repo_add.stderr | default('')))
|
||||
|
||||
- name: helm repo update
|
||||
ansible.builtin.command: helm repo update
|
||||
changed_when: true
|
||||
@@ -1,3 +0,0 @@
|
||||
---
|
||||
# When true, applies clusters/noble/bootstrap/argocd/bootstrap-root-application.yaml (noble-bootstrap-root; manual sync until README §5).
|
||||
noble_argocd_apply_bootstrap_root_application: true
|
||||
@@ -1,27 +0,0 @@
|
||||
---
|
||||
# Run from **ansible/playbooks/noble.yml** *after* roles **noble_platform**, **noble_authentik**, **noble_velero**
|
||||
# (see play **tasks:**). Leaf **Application** CRs must not be reconciled before Ansible Helm finishes, or
|
||||
# **argocd-controller** can SSA resources without Helm release metadata (e.g. chart-owned ServiceAccounts).
|
||||
- name: Apply Argo CD bootstrap root Application
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- apply
|
||||
- -f
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/argocd/bootstrap-root-application.yaml"
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
when: noble_argocd_apply_bootstrap_root_application | default(false) | bool
|
||||
changed_when: true
|
||||
|
||||
- name: Apply Argo CD leaf Application definitions (argocd/app-of-apps — post-Helm)
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- apply
|
||||
- -k
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/argocd/app-of-apps"
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
when: noble_argocd_apply_bootstrap_root_application | default(false) | bool
|
||||
changed_when: true
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
- name: Install Argo CD
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- helm
|
||||
- upgrade
|
||||
- --install
|
||||
- argocd
|
||||
- argo/argo-cd
|
||||
- --namespace
|
||||
- argocd
|
||||
- --create-namespace
|
||||
- --version
|
||||
- "9.5.14"
|
||||
- -f
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/argocd/values.yaml"
|
||||
- --force-conflicts
|
||||
- --wait
|
||||
- --timeout
|
||||
- 15m
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
changed_when: true
|
||||
@@ -1,149 +0,0 @@
|
||||
# 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 all **required** **`NOBLE_AUTHENTIK_*`** values (see comments there; SMTP keys are optional).
|
||||
2. Set **`noble_authentik_install: true`** in **`ansible/inventory/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`**. **`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.
|
||||
|
||||
### S3 media (avatars, flows, uploads)
|
||||
|
||||
Authentik stores file-backed data in **S3** (not a shared PVC on **`authentik-worker`**). Set **`NOBLE_AUTHENTIK_MEDIA_S3_BUCKET`** in **`.env`** to a **dedicated** bucket name (do **not** reuse the Velero backup bucket). **`NOBLE_VELERO_S3_URL`**, **`NOBLE_VELERO_AWS_ACCESS_KEY_ID`**, and **`NOBLE_VELERO_AWS_SECRET_ACCESS_KEY`** are reused automatically when the Authentik-specific S3 variables are unset; override with **`NOBLE_AUTHENTIK_S3_URL`** / **`NOBLE_AUTHENTIK_S3_ACCESS_KEY`** / **`NOBLE_AUTHENTIK_S3_SECRET_KEY`** if needed. Optional: **`NOBLE_AUTHENTIK_S3_REGION`** (defaults to **`us-east-1`** in Ansible), **`NOBLE_AUTHENTIK_S3_ADDRESSING_STYLE`** (**`path`** vs **`virtual`** for some gateways). Create the bucket and grant the same credentials **read/write** to that bucket only. For browser uploads and public assets, follow [Authentik — S3 storage](https://docs.goauthentik.io/sys-mgmt/ops/storage-s3/) (CORS and policies). If you previously used a PVC for **`/data`**, sync into the new bucket (for example **`aws s3 sync`** from a volume snapshot or old mount) before relying on S3-only.
|
||||
|
||||
### Outbound email (SMTP)
|
||||
|
||||
Optional. Set **`NOBLE_AUTHENTIK_SMTP_HOST`** and **`NOBLE_AUTHENTIK_SMTP_FROM`** in repository **`.env`**; Ansible adds **`AUTHENTIK_EMAIL__HOST`**, **`AUTHENTIK_EMAIL__FROM`**, and related variables to Helm **`global.env`** (see [Authentik configuration — email](https://docs.goauthentik.io/install-config/configuration/#email-settings)). Omit **`NOBLE_AUTHENTIK_SMTP_HOST`** to skip SMTP env vars entirely. Optional overrides: **`NOBLE_AUTHENTIK_SMTP_PORT`** (default **587** in **`defaults/main.yml`**), **`NOBLE_AUTHENTIK_SMTP_USERNAME`**, **`NOBLE_AUTHENTIK_SMTP_PASSWORD`**, **`NOBLE_AUTHENTIK_SMTP_USE_TLS`** / **`USE_SSL`** / **`TIMEOUT`**. Re-run **`ansible-playbook playbooks/noble.yml --tags authentik`** after changes.
|
||||
|
||||
### Extra public hostname (Pangolin + Newt, same Authentik)
|
||||
|
||||
To expose the **same** Authentik instance on an **internet-facing** FQDN (while keeping the lab name on Traefik), set **`noble_authentik_ingress_extra_hosts`** in **`ansible/inventory/group_vars/all.yml`** (or **`-e`**) to a list of extra FQDNs, for example **`auth.example.com`**. Re-run **`ansible-playbook playbooks/noble.yml --tags authentik`**. Ansible extends **`server.ingress.hosts`** and **`tls[0].hosts`** so **cert-manager** issues one certificate with SANs for the primary **`noble_authentik_host`** plus those names (DNS must resolve for your issuer — often **Cloudflare** for public names, split horizon for lab).
|
||||
|
||||
Then in **Pangolin**: link the domain, create an **HTTP** resource for that hostname, and set the **target** to your **Newt** site with **`ip:port`** pointing at the cluster **Traefik** HTTPS entry (same pattern as **`clusters/noble/bootstrap/newt/README.md`** — typically the MetalLB / LAN VIP and **443**). One Newt tunnel can front many hostnames.
|
||||
|
||||
**Open WebUI (public hostname only):** Set **`noble_open_webui_public_host`** in **`group_vars`** to the same FQDN as **`clusters/noble/apps/open-webui/values.yaml`** **`ingress.host`** (for example **`webui.example.com`**). Add a **Pangolin** HTTP resource for that hostname to the same Newt target. Point **`OPENID_PROVIDER_URL`** in **`values.yaml`** at your **public** Authentik host (usually the first **`noble_authentik_ingress_extra_hosts`** entry, e.g. **`https://auth.example.com/application/o/open-webui/.well-known/openid-configuration`**), and set **`OPENID_REDIRECT_URI`** / **`WEBUI_URL`** to **`https://<noble_open_webui_public_host>/…`**. After changing **`noble_open_webui_public_host`**, re-run **`ansible-playbook playbooks/noble.yml --tags authentik`** so Authentik’s OAuth2 redirect URI matches.
|
||||
|
||||
### Split routing, two Brands, and optional blueprints
|
||||
|
||||
This role supports a **single Authentik deployment** with **two hostnames** (lab + public) and **different Brands** per **`Host`**, without Authentik’s separate-database **Tenancy** feature (see [Tenancy](https://docs.goauthentik.io/sys-mgmt/tenancy) — alpha / licensing). [Brands](https://docs.goauthentik.io/brands/) choose default **authentication** (and related) **flows** and branding for each FQDN.
|
||||
|
||||
**Split routing (recommended):**
|
||||
|
||||
- **Lab / operator URL** — **`noble_authentik_host`** (default **`auth.apps.noble.lab.pcenicni.dev`**): keep DNS **internal-only** (split horizon, VPN, or LAN DNS). Do **not** publish this hostname as a Pangolin HTTP resource toward the internet unless you intentionally want it reachable off-LAN.
|
||||
- **Public URL** — entries in **`noble_authentik_ingress_extra_hosts`**: use Pangolin (or another edge) only for these names so casual users never need the lab FQDN.
|
||||
|
||||
Network isolation is enforced at **DNS and the tunnel**, not inside Authentik. Optionally add firewall / Traefik entrypoint rules for defense in depth.
|
||||
|
||||
**Two-Brand model:**
|
||||
|
||||
- **Lab Brand** — domain equals **`noble_authentik_host`**: restricted authentication flow (**`noble_authentik_blueprint_lab_flow_slug`**) so only operator groups (defaults: **`noble-admins`**, **`authentik Admins`**) can continue past identification; dedicated password stage with **`noble_authentik_blueprint_lab_password_failed_attempts`**, expression-based password strength (**length / character classes / optional zxcvbn**), and MFA stage **`noble-lab-authenticator-validate-strict`** with **`noble_authentik_blueprint_lab_mfa_not_configured_action`** (**`configure`** injects default TOTP setup when no device; **`deny`** blocks; **`skip`** matches stock). WebAuthn passwordless does **not** skip MFA on this flow. Your bootstrap / break-glass account must remain in one of those groups (see **`noble_authentik_ensure_admin_ui_access`** and **`configure_authentik.py`** group membership).
|
||||
- **Nikflix Brand(s)** — one Brand per FQDN in **`noble_authentik_ingress_extra_hosts`**: authentication flow **`noble_authentik_blueprint_public_auth_flow_slug`** (default **`noble-public-authentication-flow`**) mirrors stock **`default-authentication-flow`** (optional password / MFA skip for WebAuthn passwordless). Directory groups default to **`nikflix-users`** / **`nikflix-admins`** only; add optional groups via **`noble_authentik_blueprint_extra_directory_groups`** or the optional **`noble_authentik_blueprint_public_groups`** list. **`noble-admins`** / **`noble-editors`** remain for cluster / Argo / Grafana as today.
|
||||
|
||||
**OAuth note:** Redirect URIs and **`iss`** must stay consistent with the hostname clients use (internal issuer for in-cluster apps vs public issuer is a deliberate choice — avoid mixing both for the same app).
|
||||
|
||||
**Mounted blueprints (optional):** set **`noble_authentik_blueprints_enabled: true`** in **`group_vars`** (or **`-e`**). On each **`--tags authentik`** run, Ansible renders Jinja templates under **`templates/blueprints/`** into a ConfigMap **`noble_authentik_blueprints_configmap_name`** (default **`authentik-noble-blueprints`**) and sets Helm **`blueprints.configMaps`** so **authentik-worker** loads them from **`/blueprints/mounted/cm-authentik-noble-blueprints/`** (see [Blueprints](https://docs.goauthentik.io/customize/blueprints/)). Files (apply in lexical order):
|
||||
|
||||
| Key | Purpose |
|
||||
| --- | --- |
|
||||
| **`10-noble-public-groups.yaml.j2`** | **`noble_authentik_blueprint_public_groups`** (optional, default empty) ∪ **`noble_authentik_blueprint_extra_directory_groups`** ∪ **`noble_authentik_blueprint_nikflix_groups`** → **Group** objects (defaults: **Nikflix** only — see **Blueprint: directory groups**). |
|
||||
| **`20-noble-lab-operator-authentication-flow.yaml.j2`** | Flow **`noble_authentik_blueprint_lab_flow_slug`**: operator policy **`noble_authentik_blueprint_operator_policy_name`**, lab password/MFA tunables (see **`defaults/main.yml`**). |
|
||||
| **`21-noble-public-authentication-flow.yaml.j2`** | Flow **`noble_authentik_blueprint_public_auth_flow_slug`** — public sign-in (same optional policies as stock default authentication). |
|
||||
| **`22-noble-invitation-enrollment-flows.yaml.j2`** | Two **enrollment** flows + **Invitation** stages: Nikflix / extra_hosts (**`noble_authentik_blueprint_public_invitation_enrollment_flow_slug`**) vs lab (**`noble_authentik_blueprint_lab_invitation_enrollment_flow_slug`**); see **Invitations** below. |
|
||||
| **`30-noble-brands-domain-split.yaml.j2`** | Brand for **`noble_authentik_host`** → lab flow; one Brand per **`noble_authentik_ingress_extra_hosts`** → public flow slug above. |
|
||||
|
||||
Tune titles via **`noble_authentik_blueprint_lab_brand_title`** and **`noble_authentik_blueprint_public_brand_title_prefix`**. After the worker applies blueprints, confirm **System → Brands** and **Flows** in the admin UI; fix any **`!Find`** failures if upstream default stage **names** change between Authentik versions.
|
||||
|
||||
#### Invitations (Nikflix vs lab)
|
||||
|
||||
[Brands](https://docs.goauthentik.io/brands/) do **not** expose a default “enrollment” or “invitation” flow: onboarding is driven by **Directory → Invitations**, where each invitation row selects an **enrollment** flow. The link Authentik shows is:
|
||||
|
||||
`https://<host>/if/flow/<enrollment-flow-slug>/?itoken=<invitation-uuid>`
|
||||
|
||||
Use **`<host>`** that matches the experience you want:
|
||||
|
||||
- **Nikflix / internet hostname** — an FQDN from **`noble_authentik_ingress_extra_hosts`**: use flow slug **`noble_authentik_blueprint_public_invitation_enrollment_flow_slug`** (default **`nikflix-invitation-enrollment`**). New users are added to **`noble_authentik_blueprint_public_invitation_user_group`** (default **`nikflix-users`**). **`noble_authentik_blueprint_public_invitation_user_type`** defaults to **`internal`**. If you previously used **`noble-public-invitation-enrollment`** or **`noble-public-users`**, update **Directory → Invitations** to the new flow slug and re-run **`--tags authentik`**; remove obsolete **`noble-public-*`** groups in the admin UI if they are empty (Ansible no longer defines them).
|
||||
- **Lab** — **`noble_authentik_host`** only when you intend to onboard someone who will later get **`noble_authentik_blueprint_lab_operator_groups`** access: use **`noble_authentik_blueprint_lab_invitation_enrollment_flow_slug`** (default **`noble-lab-invitation-enrollment`**). The blueprint creates **`noble_authentik_blueprint_lab_invitee_group_name`** (default **`noble-lab-invited`**) and assigns new enrollments there; **promote** people to **`noble-admins`** / **`authentik Admins`** (or your configured operator groups) in the admin UI when they should sign in on the lab URL.
|
||||
|
||||
Blueprint **22** does **not** create sample **Invitation** rows (no placeholder emails). Create invitations in the UI after blueprints apply. For richer patterns (prefilled attributes, extra policies), see [Invitations](https://docs.goauthentik.io/users-sources/user/invitations/) and the upstream example blueprint **`flows-invitation-enrollment.yaml`** ([download](https://goauthentik.io/blueprints/example/flows-invitation-enrollment.yaml)). Password strength for enrollment prompts is **not** duplicated from the lab **authentication** flow here; add **Prompt** validation policies or a dedicated policy if you need parity.
|
||||
|
||||
**Users already created as `external`:** change **User type** to **Internal** under **Directory → Users** (or edit the **User write** stage in **Flows** and re-run the playbook so future invitees use **`noble_authentik_blueprint_public_invitation_user_type: internal`** in **`group_vars`**).
|
||||
|
||||
#### Blueprint: directory groups
|
||||
|
||||
Three inventory lists are concatenated **in this order** into **`10-noble-public-groups.yaml.j2`**:
|
||||
|
||||
1. **`noble_authentik_blueprint_public_groups`** — optional extra groups merged first (default **empty**; use for legacy names or shared groups that must exist before **`extra`** / Nikflix).
|
||||
2. **`noble_authentik_blueprint_extra_directory_groups`** — any other groups (empty by default).
|
||||
3. **`noble_authentik_blueprint_nikflix_groups`** — Nikflix-facing groups (defaults **`nikflix-users`** / **`nikflix-admins`** with **`noble.ak/brand: nikflix`**). Listed last so **`parents`** can reference groups from (1) or (2) if you choose.
|
||||
|
||||
Each item may be:
|
||||
|
||||
| Form | Example |
|
||||
| --- | --- |
|
||||
| **String** | **`my-app-operators`** — creates a group with that **name** only. |
|
||||
| **Mapping** | **`name`** (required), optional **`is_superuser`**: **`true`** (use sparingly), **`attributes`**: dict (JSON on the group; useful in expression policies), **`parents`**: list of **existing** group **names** (resolved with **`!Find`**). |
|
||||
|
||||
Order matters for **`parents`**: every parent must already exist when the child row is applied — list parents **above** children in the merged list, or reference groups Authentik already created (for example **`nikflix-users`** before **`nikflix-admins`** with **`parents: [nikflix-users]`**). See [Group properties and attributes](https://docs.goauthentik.io/users-sources/groups/group_ref/).
|
||||
|
||||
##### Audience groups vs per-service groups
|
||||
|
||||
For Nikflix (and similar brands), prefer **one broad “users” group and a small “admins” group** (`nikflix-users` / `nikflix-admins`), then bind **OAuth providers**, **policies**, and **app access** to those groups. Add **per-service** groups (for example **`nikflix-media-readonly`**) only when a service truly needs a **different** membership set than the rest of the brand; every extra group is another object to keep in sync with enrollment and IdP claims. Optional pattern: make a service group a **child** of **`nikflix-users`** via **`parents`** so members inherit the parent for generic “logged in to Nikflix” checks.
|
||||
|
||||
**Confirming blueprints on the cluster:** the Ansible task **Install Authentik (Helm)** uses **`changed_when: true`**, so a **“changed”** line there does **not** prove Helm mutated the release. When **`noble_authentik_blueprints_enabled`** is true, the role asserts the **worker** Deployment has a volumeMount named **`blueprints-cm-<noble_authentik_blueprints_configmap_name>`** (default **`blueprints-cm-authentik-noble-blueprints`**). You can also run:
|
||||
|
||||
```bash
|
||||
kubectl -n authentik get configmap authentik-noble-blueprints -o yaml
|
||||
helm get values authentik -n authentik -o yaml | grep -A2 blueprints
|
||||
kubectl -n authentik get deploy -l app.kubernetes.io/component=worker -o yaml | grep blueprints-cm
|
||||
```
|
||||
|
||||
Mounted files are applied asynchronously by **authentik-worker**; check **System → Blueprints** (or **Customization → Blueprints** depending on version) for instances sourced from **`/blueprints/mounted/cm-authentik-noble-blueprints/`**, and **`kubectl logs -n authentik deploy/authentik-worker`** if a blueprint shows **Error** / failed apply.
|
||||
|
||||
### “Secondary tenant” (separate PostgreSQL schema — alpha)
|
||||
|
||||
Authentik **tenancy** (multiple isolated tenants in one deployment, **`AUTHENTIK_TENANTS__ENABLED`**) is **alpha**, requires **per-tenant Enterprise licensing**, **`AUTHENTIK_TENANTS__API_KEY`**, and **`AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true`** (embedded outposts are unsupported with tenancy). It is **not** wired in this repo by default. See [Tenancy](https://docs.goauthentik.io/sys-mgmt/tenancy). For most homelabs, **one tenant** plus **`noble_authentik_ingress_extra_hosts`** is the right split.
|
||||
|
||||
## IdP configuration
|
||||
|
||||
When **`noble_authentik_configure_idp`** is true, Ansible creates/updates OAuth2 providers and applications for **argocd**, **grafana**, **headlamp**, **open-webui**, 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
|
||||
|
||||
- **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
|
||||
|
||||
- **Blueprints from Ansible fail to apply** (worker logs / **System → Blueprints**): confirm the ConfigMap exists (**`kubectl -n authentik get cm authentik-noble-blueprints`** unless you changed **`noble_authentik_blueprints_configmap_name`**), that Helm mounts it (**`blueprints.configMaps`** in the rendered extra values), and that every **`!Find`** in **`templates/blueprints/20-*.j2`** still matches your Authentik version’s default stage **names**. For **`10-noble-public-groups.yaml.j2`**, **`parents`** must reference groups that appear **earlier** in the merged list (or already exist in Authentik). Re-run **`--tags authentik`** after editing templates.
|
||||
- **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`.
|
||||
- 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 **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).
|
||||
- **`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.
|
||||
- **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`**).
|
||||
- **Headlamp OIDC authorize fails / `invalid_scope`**: Authentik often has no separate **`groups`** ScopeMapping (groups live under **`profile`**). Default **`noble_authentik_headlamp_oidc_scopes`** omits **`groups`**; add a **`groups`** mapping to the provider in Authentik and set **`noble_authentik_headlamp_oidc_scopes`** to include **`groups`** if you need that scope by name.
|
||||
- **Headlamp OIDC: Authentik flashes then back at login / page refresh**: Headlamp **does** support normal browser OAuth (redirect to Authentik and return to **`/oidc-callback`**). If the callback fails, the UI looks like it “drops” auth. Common causes: **`X-Forwarded-Proto`** not reaching Headlamp (callback built as **`http`** — see [Headlamp OIDC docs](https://headlamp.dev/docs/latest/installation/in-cluster/oidc)); **Traefik ForwardAuth** on the same Ingress (do not combine with native OIDC); **PKCE** state issues — this role defaults **`noble_authentik_headlamp_oidc_use_pkce: false`** for Authentik confidential clients (set **`true`** in **`group_vars`** if you need PKCE).
|
||||
- **Headlamp UI: `/me` works but `/clusters/main/version` (and other K8s calls) return 401**: Headlamp forwards your **OIDC id_token** to **kube-apiserver**. The API server must be configured with **OIDC** flags for the same **issuer** and **`oidc-client-id`** as Headlamp (see **`talos/talconfig.yaml`** patch and [Kubernetes OIDC authentication](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#openid-connect-tokens)). Apply regenerated Talos configs to control plane nodes, then **`kubectl apply -k clusters/noble/bootstrap/headlamp`** (or **`--tags authentik`**) for **`oidc-noble-admins-clusterrolebinding.yaml`**. Ensure your user is in Authentik group **`noble-admins`** and the id_token includes a **`groups`** claim if you rely on that binding.
|
||||
- **Headlamp + Traefik ForwardAuth (`oauth2-proxy-forward-auth`)**: Do **not** put ForwardAuth on the **Headlamp** Ingress while using **native Headlamp OIDC**. Auth runs on **`/oidc-callback`** before Headlamp can finish the code exchange; ForwardAuth returns **401** and breaks login. Use **either** native OIDC (this repo’s **`values-authentik-oidc.yaml`**) **or** terminate auth at oauth2-proxy only (no **`config.oidc`**), not both. The same applies to **Open WebUI** native OIDC (**`/oauth/oidc/callback`** in **`clusters/noble/apps/open-webui/values.yaml`**).
|
||||
|
||||
### Fix admin access manually (worker shell, no Ansible)
|
||||
|
||||
```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']); u.groups.add(adm); u=User.objects.get(pk=u.pk); print('is_superuser', u.is_superuser)"
|
||||
```
|
||||
|
||||
Then **log out** of Authentik and sign in again.
|
||||
@@ -1,184 +0,0 @@
|
||||
---
|
||||
# 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
|
||||
# **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_namespace: authentik
|
||||
# Helm release name (deployments: **{release}-server**, **{release}-worker**).
|
||||
noble_authentik_release_name: authentik
|
||||
noble_authentik_oauth2_proxy_chart_version: "10.4.3"
|
||||
# Helm **--wait** timeout for **oauth2-proxy** (first pull / API checks can exceed 10m).
|
||||
noble_authentik_oauth2_proxy_helm_wait_timeout: 10m
|
||||
|
||||
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"
|
||||
|
||||
# Optional extra Ingress hostnames (FQDN strings) for the **same** Authentik release — e.g. a **public** name
|
||||
# (Pangolin HTTP resource → Newt site → Traefik) while **`noble_authentik_host`** stays the in-lab name.
|
||||
# Ansible merges these into **server.ingress.hosts** / **tls** (one cert Secret with multiple SANs).
|
||||
noble_authentik_ingress_extra_hosts: []
|
||||
|
||||
# Mounted **blueprints** (ConfigMap → worker `/blueprints/mounted/cm-*`). See README § split routing / two-Brand.
|
||||
noble_authentik_blueprints_enabled: false
|
||||
noble_authentik_blueprints_configmap_name: authentik-noble-blueprints
|
||||
# Directory groups blueprint (**`10-noble-public-groups.yaml.j2`**): merges **`noble_authentik_blueprint_public_groups`**
|
||||
# (optional — often empty), **`noble_authentik_blueprint_extra_directory_groups`**, and **`noble_authentik_blueprint_nikflix_groups`**
|
||||
# (defaults: **`nikflix-users`** / **`nikflix-admins`**). Lab onboarding uses **`noble_authentik_blueprint_lab_invitee_group_name`**
|
||||
# from blueprint **22**, not this list. Each item may be:
|
||||
# - a **string** (group name only), or
|
||||
# - a **dict** with **`name`** (required) and optional **`is_superuser`** (bool), **`attributes`** (dict → JSON in blueprint),
|
||||
# **`parents`** (list of **existing** group names — list parents *before* children in these lists, or use built-in groups).
|
||||
noble_authentik_blueprint_public_groups: []
|
||||
# Additional directory groups (same entry shape as **`noble_authentik_blueprint_public_groups`**); merged into blueprint **10** after **`public_groups`**.
|
||||
noble_authentik_blueprint_extra_directory_groups: []
|
||||
# Nikflix (e.g. **auth.nikflix.ca**) directory groups — merged **after** optional **`public_groups`** + **`extra_directory_groups`**
|
||||
# so **`parents`** can reference those. Prefer **`nikflix-users`** / **`nikflix-admins`** for the internet-facing Brand.
|
||||
noble_authentik_blueprint_nikflix_groups:
|
||||
- name: nikflix-users
|
||||
attributes:
|
||||
"noble.ak/brand": nikflix
|
||||
"noble.ak/audience": public
|
||||
- name: nikflix-admins
|
||||
parents:
|
||||
- nikflix-users
|
||||
attributes:
|
||||
"noble.ak/brand": nikflix
|
||||
"noble.ak/audience": public
|
||||
# Lab-only authentication flow slug (Brand for **`noble_authentik_host`** points here).
|
||||
noble_authentik_blueprint_lab_flow_slug: noble-lab-operator-authentication-flow
|
||||
noble_authentik_blueprint_operator_policy_name: noble-lab-operators-only
|
||||
# Who may sign in on the **lab** hostname (`noble_authentik_host`). Bootstrap user should be in **noble-admins**
|
||||
# and/or **authentik Admins** (see **`noble_authentik_ensure_admin_ui_access`**).
|
||||
noble_authentik_blueprint_lab_operator_groups:
|
||||
- noble-admins
|
||||
- authentik Admins
|
||||
noble_authentik_blueprint_lab_brand_title: Noble lab (operators)
|
||||
noble_authentik_blueprint_public_brand_title_prefix: Nikflix
|
||||
# Public hostname Brand(s) → dedicated authentication flow (**21-noble-public-…** blueprint).
|
||||
noble_authentik_blueprint_public_auth_flow_slug: noble-public-authentication-flow
|
||||
# Lab flow: password stage (**failed_attempts_before_cancel**) and strength checks (expression policy; skips when **password** not yet in request context).
|
||||
noble_authentik_blueprint_lab_password_failed_attempts: 3
|
||||
noble_authentik_blueprint_lab_password_policy_length_min: 16
|
||||
noble_authentik_blueprint_lab_password_policy_amount_uppercase: 1
|
||||
noble_authentik_blueprint_lab_password_policy_amount_lowercase: 1
|
||||
noble_authentik_blueprint_lab_password_policy_amount_digits: 1
|
||||
noble_authentik_blueprint_lab_password_policy_amount_symbols: 1
|
||||
noble_authentik_blueprint_lab_password_policy_check_zxcvbn: true
|
||||
noble_authentik_blueprint_lab_password_policy_zxcvbn_score_threshold: 3
|
||||
noble_authentik_blueprint_lab_password_policy_error_message: >-
|
||||
Lab password policy: at least 16 characters with upper, lower, digit, symbol, and sufficient strength.
|
||||
# Lab MFA when user has no compatible device: **skip** (like stock), **deny** (block), **configure** (TOTP setup via default stage).
|
||||
noble_authentik_blueprint_lab_mfa_not_configured_action: configure
|
||||
# Invitation-based **enrollment** flows (blueprint **22**). Brands do not select enrollment; each **Invitation** picks a flow.
|
||||
# Link shape: **`https://<host>/if/flow/<slug>/?itoken=<uuid>`** — use your **Nikflix / extra_hosts** FQDN for this flow’s invites.
|
||||
noble_authentik_blueprint_public_invitation_enrollment_flow_slug: nikflix-invitation-enrollment
|
||||
noble_authentik_blueprint_lab_invitation_enrollment_flow_slug: noble-lab-invitation-enrollment
|
||||
noble_authentik_blueprint_public_invitation_flow_name: Nikflix invitation enrollment
|
||||
noble_authentik_blueprint_public_invitation_flow_title: Complete your signup
|
||||
noble_authentik_blueprint_lab_invitation_flow_name: Noble lab invitation enrollment
|
||||
noble_authentik_blueprint_lab_invitation_flow_title: Lab access — complete enrollment
|
||||
# **User write** for Nikflix (internet) invites: must match a **Group** created in blueprint **10** (default **`nikflix-users`**).
|
||||
noble_authentik_blueprint_public_invitation_user_group: nikflix-users
|
||||
# **`internal`** — normal directory users (default). Use **`external`** only when you intentionally isolate invitees from admin / “internal-only” surfaces (see [Invitations troubleshooting](https://docs.goauthentik.io/users-sources/user/invitations/)).
|
||||
noble_authentik_blueprint_public_invitation_user_type: internal
|
||||
noble_authentik_blueprint_public_invitation_user_path: users/noble/nikflix
|
||||
# Lab invites: blueprint creates **`noble_authentik_blueprint_lab_invitee_group_name`**; add members to **`noble_authentik_blueprint_lab_operator_groups`** manually when they should use the lab URL.
|
||||
noble_authentik_blueprint_lab_invitee_group_name: noble-lab-invited
|
||||
noble_authentik_blueprint_lab_invitation_user_type: internal
|
||||
noble_authentik_blueprint_lab_invitation_user_path: users/noble/lab
|
||||
|
||||
noble_authentik_oauth2_proxy_host: oauth2.apps.noble.lab.pcenicni.dev
|
||||
|
||||
# Media: **S3** via Ansible **`global.env`** (same S3 **URL** + **access keys** as **Velero** when you omit Authentik-specific overrides).
|
||||
# Set **`NOBLE_AUTHENTIK_MEDIA_S3_BUCKET`** to a **dedicated** bucket (do not use the Velero backup bucket).
|
||||
noble_authentik_media_s3_bucket: ""
|
||||
noble_authentik_s3_endpoint: ""
|
||||
noble_authentik_s3_access_key: ""
|
||||
noble_authentik_s3_secret_key: ""
|
||||
noble_authentik_s3_region: "us-east-1"
|
||||
noble_authentik_s3_addressing_style: "path"
|
||||
|
||||
# Optional outbound SMTP (maps to **AUTHENTIK_EMAIL__*** in Helm **global.env**). Leave **noble_authentik_smtp_host**
|
||||
# empty to omit email env vars; set **NOBLE_AUTHENTIK_SMTP_HOST** (and **NOBLE_AUTHENTIK_SMTP_FROM**) in **.env** to enable.
|
||||
noble_authentik_smtp_host: ""
|
||||
noble_authentik_smtp_port: "587"
|
||||
noble_authentik_smtp_username: ""
|
||||
noble_authentik_smtp_password: ""
|
||||
noble_authentik_smtp_use_tls: "true"
|
||||
noble_authentik_smtp_use_ssl: "false"
|
||||
noble_authentik_smtp_timeout: "30"
|
||||
noble_authentik_smtp_from: ""
|
||||
|
||||
# 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
|
||||
noble_authentik_client_id_open_webui: open-webui
|
||||
|
||||
# Headlamp **OIDC_SCOPES** for Secret **headlamp-oidc**. Omit **groups** unless the Authentik OAuth2 provider
|
||||
# includes a separate **groups** ScopeMapping (2026.x defaults often embed groups in **profile** only; requesting
|
||||
# **groups** then yields **invalid_scope** on authorize). Override if your IdP exposes **groups** explicitly.
|
||||
noble_authentik_headlamp_oidc_scopes: "openid profile email offline_access"
|
||||
# PKCE for Headlamp OIDC. **false** is the default for Authentik **confidential** clients: auth still uses the
|
||||
# standard browser OAuth code flow; PKCE is optional and some users see the callback “flash” then login reset
|
||||
# when PKCE state/cookies do not survive the redirect. Set **true** if you require PKCE.
|
||||
noble_authentik_headlamp_oidc_use_pkce: false
|
||||
|
||||
# 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_client_secret_open_webui: ""
|
||||
noble_authentik_oauth2_proxy_cookie_secret: ""
|
||||
|
||||
# **open-webui** namespace — Secret **open-webui-secrets** (Ansible **--tags authentik**). See **clusters/noble/apps/open-webui/values.yaml**.
|
||||
noble_open_webui_openai_api_key: ""
|
||||
noble_open_webui_webui_secret_key: ""
|
||||
# Public FQDN for Open WebUI (Ingress + OIDC **redirect_uri**). Set in **group_vars** (e.g. **webui.example.com**); must match GitOps **values.yaml** **ingress.host** and **OPENID_REDIRECT_URI** / **WEBUI_URL**.
|
||||
noble_open_webui_public_host: ""
|
||||
|
||||
# Optional: OAuth2 provider flow PKs (UUID strings). When **both** are set, **configure_authentik.py**
|
||||
# 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_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
|
||||
|
||||
# After Helm --wait, the worker still creates the bootstrap API token; poll the public API before configure_authentik.py.
|
||||
noble_authentik_bootstrap_api_wait_retries: 36
|
||||
noble_authentik_bootstrap_api_wait_delay: 5
|
||||
|
||||
# Re-apply the same chart versions as the rest of noble.yml when flipping SSO on.
|
||||
noble_authentik_argocd_chart_version: "9.5.14"
|
||||
noble_authentik_kube_prometheus_chart_version: "85.0.3"
|
||||
noble_authentik_headlamp_chart_version: "0.42.0"
|
||||
noble_authentik_longhorn_chart_version: "1.11.2"
|
||||
noble_authentik_kube_prometheus_helm_wait_timeout: 60m
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,520 +0,0 @@
|
||||
#!/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 API token (Bearer) for user **akadmin** (see README if flows return 403)
|
||||
BOOTSTRAP_EMAIL initial admin email (added to noble-admins)
|
||||
CLIENT_JSON path to JSON file: { "argocd": {"client_id","client_secret","redirect_uri"}, ... }
|
||||
Optional (skip API discovery; Authentik 2026+ RBAC — use UUID strings from worker shell or Admin UI):
|
||||
AUTHENTIK_OAUTH_AUTHORIZATION_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
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
|
||||
# List endpoints for OAuth2 scope PropertyMapping (Authentik version-dependent).
|
||||
_OAUTH_SCOPE_MAPPING_LIST_PATHS = (
|
||||
"/propertymappings/provider/scope/?page_size=500",
|
||||
"/propertymappings/scope/?page_size=500",
|
||||
"/propertymappings/oauthscope/?page_size=500",
|
||||
)
|
||||
|
||||
|
||||
def primary_key(value) -> int | str:
|
||||
"""Authentik API pk: integer (legacy) or UUID string (2026+)."""
|
||||
if isinstance(value, bool):
|
||||
raise ValueError(f"invalid pk (bool): {value!r}")
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
raise ValueError(f"invalid pk type {type(value).__name__}: {value!r}")
|
||||
|
||||
|
||||
def dedupe_pks_preserve_order(items: list[int | str]) -> list[int | str]:
|
||||
out: list[int | str] = []
|
||||
seen: set[str] = set()
|
||||
for item in items:
|
||||
key = str(item)
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
out.append(item)
|
||||
return out
|
||||
|
||||
|
||||
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 req_any(method: str, url: str, body: dict | None = None) -> tuple[int, dict | list | str]:
|
||||
"""Like req() but returns HTTP status and body (or error text) instead of raising on HTTP errors."""
|
||||
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")
|
||||
try:
|
||||
return e.code, json.loads(err)
|
||||
except json.JSONDecodeError:
|
||||
return e.code, err
|
||||
|
||||
|
||||
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:
|
||||
"""Resolve a Flow primary key. Prefer detail-by-slug (REST); fall back to list filter."""
|
||||
slug_path = urllib.parse.quote(slug, safe="")
|
||||
attempts = (
|
||||
f"{base}/flows/instances/{slug_path}/",
|
||||
f"{base}/flows/instances/?slug={urllib.parse.quote(slug)}",
|
||||
)
|
||||
errors: list[str] = []
|
||||
for url in attempts:
|
||||
code, payload = req_any("GET", url)
|
||||
if code != 200:
|
||||
errors.append(f"{url} -> HTTP {code}: {payload!r}")
|
||||
continue
|
||||
if not isinstance(payload, dict):
|
||||
errors.append(f"{url} -> unexpected body type")
|
||||
continue
|
||||
if "results" in payload:
|
||||
results = payload.get("results") or []
|
||||
if results:
|
||||
return str(primary_key(results[0]["pk"]))
|
||||
elif payload.get("pk") is not None:
|
||||
return str(primary_key(payload["pk"]))
|
||||
errors.append(f"{url} -> empty")
|
||||
joined = "; ".join(errors)
|
||||
extra = ""
|
||||
low = joined.lower()
|
||||
if (
|
||||
"token invalid" in low
|
||||
or "invalid/expired" in low
|
||||
or ("invalid" in low and "expired" in low)
|
||||
):
|
||||
extra = (
|
||||
" The API rejected the bearer token (invalid/expired). After a fresh install, wait until "
|
||||
"the worker has finished bootstrap (re-run this playbook; Ansible now waits for HTTP 200). "
|
||||
"Confirm NOBLE_AUTHENTIK_BOOTSTRAP_TOKEN in .env matches what Helm was given. "
|
||||
"If you changed the token in .env without a matching helm upgrade, run the authentik tag again "
|
||||
"or create a new API token for akadmin."
|
||||
)
|
||||
else:
|
||||
extra = (
|
||||
" Authentik 2026+ RBAC: the API user must be a superuser (member of **authentik Admins**) "
|
||||
"or use a token for **akadmin** created from that account. "
|
||||
"Alternatively set AUTHENTIK_OAUTH_AUTHORIZATION_FLOW_PK and "
|
||||
"AUTHENTIK_OAUTH_INVALIDATION_FLOW_PK (flow UUIDs from Admin → Flows → open flow → URL / API)."
|
||||
)
|
||||
raise RuntimeError(f"cannot resolve flow slug {slug!r} ({joined}).{extra}")
|
||||
|
||||
|
||||
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):
|
||||
raw = (os.environ.get("AUTHENTIK_OAUTH_SIGNING_KEY_PK") or "").strip()
|
||||
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")
|
||||
return primary_key(payload["results"][0]["pk"])
|
||||
|
||||
|
||||
def _collect_scope_mappings(base: str) -> list:
|
||||
"""Try known Authentik API paths for OAuth2 scope property mappings (version-dependent)."""
|
||||
last_err: Exception | None = None
|
||||
for path in _OAUTH_SCOPE_MAPPING_LIST_PATHS:
|
||||
try:
|
||||
return collect(base, path)
|
||||
except RuntimeError as exc:
|
||||
err = str(exc)
|
||||
if " 404" in err or "404:" in err or " 403" in err or "403:" in err:
|
||||
last_err = exc
|
||||
continue
|
||||
raise
|
||||
raise RuntimeError(f"could not list OAuth scope property mappings; last error: {last_err}")
|
||||
|
||||
|
||||
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)
|
||||
want_scopes = {"openid", "email", "profile", "offline_access", "groups"}
|
||||
pks: list[int | str] = []
|
||||
for m in mappings:
|
||||
sn = (m.get("scope_name") or "").strip()
|
||||
if sn in want_scopes:
|
||||
pks.append(primary_key(m["pk"]))
|
||||
# de-dupe preserve order (str key: int 1 vs str "1" unlikely in one payload)
|
||||
seen: set[str] = set()
|
||||
uniq: list[int | str] = []
|
||||
for p in pks:
|
||||
key = str(p)
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
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_val = primary_key(m["pk"])
|
||||
key = str(pk_val)
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
uniq.append(pk_val)
|
||||
if len(uniq) < 3:
|
||||
hints = ", ".join(
|
||||
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}. "
|
||||
"Authentik 2026+ RBAC: set AUTHENTIK_OAUTH_SCOPE_MAPPING_PKS (comma-separated UUIDs) or "
|
||||
"let Ansible resolve them from the worker DB."
|
||||
)
|
||||
return uniq
|
||||
|
||||
|
||||
def find_oauth2_provider_by_client_id(base: str, client_id: str) -> dict | None:
|
||||
"""OAuth2 provider list has no reliable **slug** filter; match on **client_id**."""
|
||||
url = f"{base}/providers/oauth2/?page_size=100"
|
||||
origin = base.split("/api/v3", 1)[0] if "/api/v3" in base else base
|
||||
while url:
|
||||
code, payload = req_any("GET", url)
|
||||
if code != 200 or not isinstance(payload, dict):
|
||||
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 []:
|
||||
if row.get("client_id") == client_id:
|
||||
return row
|
||||
nxt = payload.get("next") or ""
|
||||
if not nxt:
|
||||
return None
|
||||
if nxt.startswith("http"):
|
||||
url = nxt
|
||||
else:
|
||||
url = origin + nxt
|
||||
return None
|
||||
|
||||
|
||||
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,
|
||||
property_mappings: list[int | str],
|
||||
) -> int | str:
|
||||
# Authentik 2025+ expects redirect_uris as structured JSON (not newline-separated text).
|
||||
redirect_uris = [
|
||||
{
|
||||
"matching_mode": "strict",
|
||||
"url": redirect_uri.strip(),
|
||||
"redirect_uri_type": "authorization",
|
||||
}
|
||||
]
|
||||
existing = find_oauth2_provider_by_client_id(base, client_id)
|
||||
body = {
|
||||
"name": name,
|
||||
"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_uris,
|
||||
"signing_key": signing_key,
|
||||
"grant_types": ["authorization_code"],
|
||||
}
|
||||
if existing:
|
||||
pk = primary_key(existing["pk"])
|
||||
code2, _ = req("PATCH", f"{base}/providers/oauth2/{pk}/", body)
|
||||
if code2 not in (200, 204):
|
||||
raise RuntimeError(f"PATCH provider client_id={client_id} -> {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 client_id={client_id} -> {code3} {created}")
|
||||
return primary_key(created["pk"])
|
||||
|
||||
|
||||
def upsert_application(base: str, slug: str, name: str, provider_pk: int | str) -> None:
|
||||
# Detail URL is /core/applications/{slug}/ (lookup_field slug, 2026+). Do not trust list
|
||||
# ordering: ?slug= filters can still return unrelated rows first → PATCH wrong app + 400.
|
||||
slug = str(slug).strip()
|
||||
seg = urllib.parse.quote(slug, safe="")
|
||||
detail = f"{base}/core/applications/{seg}/"
|
||||
code, payload = req_any("GET", detail)
|
||||
if code == 200:
|
||||
if not isinstance(payload, dict):
|
||||
raise RuntimeError(f"GET application {slug}: unexpected response")
|
||||
# Omit slug on update — sending the same slug/provider can trip UniqueValidator on PATCH.
|
||||
code2, err = req_any("PATCH", detail, {"name": name, "provider": provider_pk})
|
||||
if code2 not in (200, 204):
|
||||
raise RuntimeError(f"PATCH application {slug} -> {code2}: {err!r}")
|
||||
return
|
||||
if code == 404:
|
||||
body = {"name": name, "slug": slug, "provider": provider_pk}
|
||||
code3, err = req_any("POST", f"{base}/core/applications/", body)
|
||||
if code3 not in (200, 201):
|
||||
raise RuntimeError(f"POST application {slug} -> {code3}: {err!r}")
|
||||
return
|
||||
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:
|
||||
env_pk = _group_pk_from_env(name)
|
||||
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")
|
||||
results = payload.get("results") or []
|
||||
if results:
|
||||
return primary_key(results[0]["pk"])
|
||||
code2, created = req_any("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}: {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"])
|
||||
|
||||
|
||||
def add_user_to_groups(base: str, email: str, group_pks: list[int | str]) -> None:
|
||||
code, payload = req_any("GET", f"{base}/core/users/?email={urllib.parse.quote(email)}")
|
||||
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")
|
||||
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 = primary_key(user["pk"])
|
||||
existing = [
|
||||
primary_key(g["pk"])
|
||||
for g in user.get("groups", [])
|
||||
if isinstance(g, dict) and "pk" in g
|
||||
]
|
||||
merged = dedupe_pks_preserve_order([*existing, *group_pks])
|
||||
if {str(x) for x in merged} == {str(x) for x in existing}:
|
||||
return
|
||||
code2, err = req_any("PATCH", f"{base}/core/users/{upk}/", {"groups": merged})
|
||||
if code2 not in (200, 204):
|
||||
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:
|
||||
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", "")
|
||||
skip_oidc_rest = _truthy_env("AUTHENTIK_SKIP_OIDC_REST")
|
||||
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
|
||||
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:
|
||||
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: 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()
|
||||
inv_pk = (os.environ.get("AUTHENTIK_OAUTH_INVALIDATION_FLOW_PK") or "").strip()
|
||||
if auth_pk and inv_pk:
|
||||
auth_flow, invalidation_flow = auth_pk, inv_pk
|
||||
elif auth_pk or inv_pk:
|
||||
print(
|
||||
"Set both AUTHENTIK_OAUTH_AUTHORIZATION_FLOW_PK and AUTHENTIK_OAUTH_INVALIDATION_FLOW_PK "
|
||||
"or neither",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 2
|
||||
else:
|
||||
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")
|
||||
|
||||
try:
|
||||
if not skip_oidc_rest:
|
||||
assert auth_flow is not None and invalidation_flow is not None
|
||||
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 and not skip_user_group_rest:
|
||||
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)
|
||||
return 0
|
||||
except RuntimeError as exc:
|
||||
print(str(exc), file=sys.stderr)
|
||||
return 1
|
||||
except Exception as exc:
|
||||
print(f"{type(exc).__name__}: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,15 +0,0 @@
|
||||
# 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()
|
||||
@@ -1,23 +0,0 @@
|
||||
# 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()
|
||||
@@ -1,22 +0,0 @@
|
||||
# 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()
|
||||
@@ -1,13 +0,0 @@
|
||||
# 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()
|
||||
@@ -1,54 +0,0 @@
|
||||
# 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)
|
||||
|
||||
|
||||
main()
|
||||
@@ -1,72 +0,0 @@
|
||||
# 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,
|
||||
)
|
||||
|
||||
|
||||
main()
|
||||
@@ -1,109 +0,0 @@
|
||||
# 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)
|
||||
|
||||
|
||||
main()
|
||||
@@ -1,579 +0,0 @@
|
||||
---
|
||||
# **.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_CLIENT_SECRET_OPEN_WEBUI from .env when unset
|
||||
ansible.builtin.shell: |
|
||||
set -a
|
||||
. "{{ noble_repo_root }}/.env"
|
||||
set +a
|
||||
printf '%s' "${NOBLE_AUTHENTIK_CLIENT_SECRET_OPEN_WEBUI:-}"
|
||||
register: noble_authentik_cs_ow_from_env
|
||||
when:
|
||||
- noble_authentik_dotenv_stat.stat.exists | default(false)
|
||||
- noble_authentik_client_secret_open_webui | default('') | length == 0
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Apply NOBLE_AUTHENTIK_CLIENT_SECRET_OPEN_WEBUI from .env
|
||||
ansible.builtin.set_fact:
|
||||
noble_authentik_client_secret_open_webui: "{{ noble_authentik_cs_ow_from_env.stdout | trim }}"
|
||||
when:
|
||||
- noble_authentik_cs_ow_from_env is defined
|
||||
- (noble_authentik_cs_ow_from_env.stdout | default('') | trim | length) > 0
|
||||
no_log: true
|
||||
|
||||
- name: Load NOBLE_OPEN_WEBUI_OPENAI_API_KEY from .env when unset
|
||||
ansible.builtin.shell: |
|
||||
set -a
|
||||
. "{{ noble_repo_root }}/.env"
|
||||
set +a
|
||||
printf '%s' "${NOBLE_OPEN_WEBUI_OPENAI_API_KEY:-}"
|
||||
register: noble_open_webui_openai_from_env
|
||||
when:
|
||||
- noble_authentik_dotenv_stat.stat.exists | default(false)
|
||||
- noble_open_webui_openai_api_key | default('') | length == 0
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Apply NOBLE_OPEN_WEBUI_OPENAI_API_KEY from .env
|
||||
ansible.builtin.set_fact:
|
||||
noble_open_webui_openai_api_key: "{{ noble_open_webui_openai_from_env.stdout | trim }}"
|
||||
when:
|
||||
- noble_open_webui_openai_from_env is defined
|
||||
- (noble_open_webui_openai_from_env.stdout | default('') | trim | length) > 0
|
||||
no_log: true
|
||||
|
||||
- name: Load NOBLE_OPEN_WEBUI_WEBUI_SECRET_KEY from .env when unset
|
||||
ansible.builtin.shell: |
|
||||
set -a
|
||||
. "{{ noble_repo_root }}/.env"
|
||||
set +a
|
||||
printf '%s' "${NOBLE_OPEN_WEBUI_WEBUI_SECRET_KEY:-}"
|
||||
register: noble_open_webui_webui_secret_from_env
|
||||
when:
|
||||
- noble_authentik_dotenv_stat.stat.exists | default(false)
|
||||
- noble_open_webui_webui_secret_key | default('') | length == 0
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Apply NOBLE_OPEN_WEBUI_WEBUI_SECRET_KEY from .env
|
||||
ansible.builtin.set_fact:
|
||||
noble_open_webui_webui_secret_key: "{{ noble_open_webui_webui_secret_from_env.stdout | trim }}"
|
||||
when:
|
||||
- noble_open_webui_webui_secret_from_env is defined
|
||||
- (noble_open_webui_webui_secret_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
|
||||
|
||||
# --- S3 media (reuse Velero endpoint + AWS keys from .env unless Authentik-specific vars are set) ---
|
||||
- name: Load NOBLE_AUTHENTIK_MEDIA_S3_BUCKET from .env when unset
|
||||
ansible.builtin.shell: |
|
||||
set -a
|
||||
. "{{ noble_repo_root }}/.env"
|
||||
set +a
|
||||
printf '%s' "${NOBLE_AUTHENTIK_MEDIA_S3_BUCKET:-}"
|
||||
register: noble_authentik_media_s3_bucket_from_env
|
||||
when:
|
||||
- noble_authentik_dotenv_stat.stat.exists | default(false)
|
||||
- noble_authentik_media_s3_bucket | default('') | length == 0
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Apply NOBLE_AUTHENTIK_MEDIA_S3_BUCKET from .env
|
||||
ansible.builtin.set_fact:
|
||||
noble_authentik_media_s3_bucket: "{{ noble_authentik_media_s3_bucket_from_env.stdout | trim }}"
|
||||
when:
|
||||
- noble_authentik_media_s3_bucket_from_env is defined
|
||||
- (noble_authentik_media_s3_bucket_from_env.stdout | default('') | trim | length) > 0
|
||||
no_log: true
|
||||
|
||||
- name: Resolve Authentik S3 endpoint from .env (Authentik-specific URL or Velero S3 URL)
|
||||
ansible.builtin.shell: |
|
||||
set -a
|
||||
. "{{ noble_repo_root }}/.env"
|
||||
set +a
|
||||
if [ -n "${NOBLE_AUTHENTIK_S3_URL:-}" ]; then printf '%s' "${NOBLE_AUTHENTIK_S3_URL}"
|
||||
elif [ -n "${NOBLE_VELERO_S3_URL:-}" ]; then printf '%s' "${NOBLE_VELERO_S3_URL}"
|
||||
else printf ''
|
||||
fi
|
||||
register: noble_authentik_s3_endpoint_from_env
|
||||
when:
|
||||
- noble_authentik_dotenv_stat.stat.exists | default(false)
|
||||
- noble_authentik_s3_endpoint | default('') | length == 0
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Apply resolved Authentik S3 endpoint from .env
|
||||
ansible.builtin.set_fact:
|
||||
noble_authentik_s3_endpoint: "{{ noble_authentik_s3_endpoint_from_env.stdout | trim }}"
|
||||
when:
|
||||
- noble_authentik_s3_endpoint_from_env is defined
|
||||
- (noble_authentik_s3_endpoint_from_env.stdout | default('') | trim | length) > 0
|
||||
no_log: true
|
||||
|
||||
- name: Resolve Authentik S3 access key from .env (override or Velero AWS key)
|
||||
ansible.builtin.shell: |
|
||||
set -a
|
||||
. "{{ noble_repo_root }}/.env"
|
||||
set +a
|
||||
if [ -n "${NOBLE_AUTHENTIK_S3_ACCESS_KEY:-}" ]; then printf '%s' "${NOBLE_AUTHENTIK_S3_ACCESS_KEY}"
|
||||
elif [ -n "${NOBLE_VELERO_AWS_ACCESS_KEY_ID:-}" ]; then printf '%s' "${NOBLE_VELERO_AWS_ACCESS_KEY_ID}"
|
||||
else printf ''
|
||||
fi
|
||||
register: noble_authentik_s3_access_from_env
|
||||
when:
|
||||
- noble_authentik_dotenv_stat.stat.exists | default(false)
|
||||
- noble_authentik_s3_access_key | default('') | length == 0
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Apply resolved Authentik S3 access key from .env
|
||||
ansible.builtin.set_fact:
|
||||
noble_authentik_s3_access_key: "{{ noble_authentik_s3_access_from_env.stdout | trim }}"
|
||||
when:
|
||||
- noble_authentik_s3_access_from_env is defined
|
||||
- (noble_authentik_s3_access_from_env.stdout | default('') | trim | length) > 0
|
||||
no_log: true
|
||||
|
||||
- name: Resolve Authentik S3 secret key from .env (override or Velero AWS secret)
|
||||
ansible.builtin.shell: |
|
||||
set -a
|
||||
. "{{ noble_repo_root }}/.env"
|
||||
set +a
|
||||
if [ -n "${NOBLE_AUTHENTIK_S3_SECRET_KEY:-}" ]; then printf '%s' "${NOBLE_AUTHENTIK_S3_SECRET_KEY}"
|
||||
elif [ -n "${NOBLE_VELERO_AWS_SECRET_ACCESS_KEY:-}" ]; then printf '%s' "${NOBLE_VELERO_AWS_SECRET_ACCESS_KEY}"
|
||||
else printf ''
|
||||
fi
|
||||
register: noble_authentik_s3_secret_from_env
|
||||
when:
|
||||
- noble_authentik_dotenv_stat.stat.exists | default(false)
|
||||
- noble_authentik_s3_secret_key | default('') | length == 0
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Apply resolved Authentik S3 secret key from .env
|
||||
ansible.builtin.set_fact:
|
||||
noble_authentik_s3_secret_key: "{{ noble_authentik_s3_secret_from_env.stdout | trim }}"
|
||||
when:
|
||||
- noble_authentik_s3_secret_from_env is defined
|
||||
- (noble_authentik_s3_secret_from_env.stdout | default('') | trim | length) > 0
|
||||
no_log: true
|
||||
|
||||
- name: Load NOBLE_AUTHENTIK_S3_REGION from .env when set
|
||||
ansible.builtin.shell: |
|
||||
set -a
|
||||
. "{{ noble_repo_root }}/.env"
|
||||
set +a
|
||||
printf '%s' "${NOBLE_AUTHENTIK_S3_REGION:-}"
|
||||
register: noble_authentik_s3_region_from_env
|
||||
when:
|
||||
- noble_authentik_dotenv_stat.stat.exists | default(false)
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Apply NOBLE_AUTHENTIK_S3_REGION from .env
|
||||
ansible.builtin.set_fact:
|
||||
noble_authentik_s3_region: "{{ noble_authentik_s3_region_from_env.stdout | trim }}"
|
||||
when:
|
||||
- noble_authentik_s3_region_from_env is defined
|
||||
- (noble_authentik_s3_region_from_env.stdout | default('') | trim | length) > 0
|
||||
no_log: true
|
||||
|
||||
- name: Load NOBLE_AUTHENTIK_S3_ADDRESSING_STYLE from .env when set
|
||||
ansible.builtin.shell: |
|
||||
set -a
|
||||
. "{{ noble_repo_root }}/.env"
|
||||
set +a
|
||||
printf '%s' "${NOBLE_AUTHENTIK_S3_ADDRESSING_STYLE:-}"
|
||||
register: noble_authentik_s3_addr_from_env
|
||||
when:
|
||||
- noble_authentik_dotenv_stat.stat.exists | default(false)
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Apply NOBLE_AUTHENTIK_S3_ADDRESSING_STYLE from .env
|
||||
ansible.builtin.set_fact:
|
||||
noble_authentik_s3_addressing_style: "{{ noble_authentik_s3_addr_from_env.stdout | trim }}"
|
||||
when:
|
||||
- noble_authentik_s3_addr_from_env is defined
|
||||
- (noble_authentik_s3_addr_from_env.stdout | default('') | trim | length) > 0
|
||||
no_log: true
|
||||
|
||||
# --- Optional SMTP (AUTHENTIK_EMAIL__* via Helm global.env) ---
|
||||
- name: Load NOBLE_AUTHENTIK_SMTP_HOST from .env when unset
|
||||
ansible.builtin.shell: |
|
||||
set -a
|
||||
. "{{ noble_repo_root }}/.env"
|
||||
set +a
|
||||
printf '%s' "${NOBLE_AUTHENTIK_SMTP_HOST:-}"
|
||||
register: noble_authentik_smtp_host_from_env
|
||||
when:
|
||||
- noble_authentik_dotenv_stat.stat.exists | default(false)
|
||||
- noble_authentik_smtp_host | default('') | length == 0
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Apply NOBLE_AUTHENTIK_SMTP_HOST from .env
|
||||
ansible.builtin.set_fact:
|
||||
noble_authentik_smtp_host: "{{ noble_authentik_smtp_host_from_env.stdout | trim }}"
|
||||
when:
|
||||
- noble_authentik_smtp_host_from_env is defined
|
||||
- (noble_authentik_smtp_host_from_env.stdout | default('') | trim | length) > 0
|
||||
no_log: true
|
||||
|
||||
- name: Load NOBLE_AUTHENTIK_SMTP_FROM from .env when unset
|
||||
ansible.builtin.shell: |
|
||||
set -a
|
||||
. "{{ noble_repo_root }}/.env"
|
||||
set +a
|
||||
printf '%s' "${NOBLE_AUTHENTIK_SMTP_FROM:-}"
|
||||
register: noble_authentik_smtp_from_from_env
|
||||
when:
|
||||
- noble_authentik_dotenv_stat.stat.exists | default(false)
|
||||
- noble_authentik_smtp_from | default('') | length == 0
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Apply NOBLE_AUTHENTIK_SMTP_FROM from .env
|
||||
ansible.builtin.set_fact:
|
||||
noble_authentik_smtp_from: "{{ noble_authentik_smtp_from_from_env.stdout | trim }}"
|
||||
when:
|
||||
- noble_authentik_smtp_from_from_env is defined
|
||||
- (noble_authentik_smtp_from_from_env.stdout | default('') | trim | length) > 0
|
||||
no_log: true
|
||||
|
||||
- name: Load NOBLE_AUTHENTIK_SMTP_USERNAME from .env when unset
|
||||
ansible.builtin.shell: |
|
||||
set -a
|
||||
. "{{ noble_repo_root }}/.env"
|
||||
set +a
|
||||
printf '%s' "${NOBLE_AUTHENTIK_SMTP_USERNAME:-}"
|
||||
register: noble_authentik_smtp_username_from_env
|
||||
when:
|
||||
- noble_authentik_dotenv_stat.stat.exists | default(false)
|
||||
- noble_authentik_smtp_username | default('') | length == 0
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Apply NOBLE_AUTHENTIK_SMTP_USERNAME from .env
|
||||
ansible.builtin.set_fact:
|
||||
noble_authentik_smtp_username: "{{ noble_authentik_smtp_username_from_env.stdout | trim }}"
|
||||
when:
|
||||
- noble_authentik_smtp_username_from_env is defined
|
||||
- (noble_authentik_smtp_username_from_env.stdout | default('') | trim | length) > 0
|
||||
no_log: true
|
||||
|
||||
- name: Load NOBLE_AUTHENTIK_SMTP_PASSWORD from .env when unset
|
||||
ansible.builtin.shell: |
|
||||
set -a
|
||||
. "{{ noble_repo_root }}/.env"
|
||||
set +a
|
||||
printf '%s' "${NOBLE_AUTHENTIK_SMTP_PASSWORD:-}"
|
||||
register: noble_authentik_smtp_password_from_env
|
||||
when:
|
||||
- noble_authentik_dotenv_stat.stat.exists | default(false)
|
||||
- noble_authentik_smtp_password | default('') | length == 0
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Apply NOBLE_AUTHENTIK_SMTP_PASSWORD from .env
|
||||
ansible.builtin.set_fact:
|
||||
noble_authentik_smtp_password: "{{ noble_authentik_smtp_password_from_env.stdout | trim }}"
|
||||
when:
|
||||
- noble_authentik_smtp_password_from_env is defined
|
||||
- (noble_authentik_smtp_password_from_env.stdout | default('') | trim | length) > 0
|
||||
no_log: true
|
||||
|
||||
- name: Load NOBLE_AUTHENTIK_SMTP_PORT from .env
|
||||
ansible.builtin.shell: |
|
||||
set -a
|
||||
. "{{ noble_repo_root }}/.env"
|
||||
set +a
|
||||
printf '%s' "${NOBLE_AUTHENTIK_SMTP_PORT:-}"
|
||||
register: noble_authentik_smtp_port_from_env
|
||||
when:
|
||||
- noble_authentik_dotenv_stat.stat.exists | default(false)
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Apply NOBLE_AUTHENTIK_SMTP_PORT from .env
|
||||
ansible.builtin.set_fact:
|
||||
noble_authentik_smtp_port: "{{ noble_authentik_smtp_port_from_env.stdout | trim }}"
|
||||
when:
|
||||
- noble_authentik_smtp_port_from_env is defined
|
||||
- (noble_authentik_smtp_port_from_env.stdout | default('') | trim | length) > 0
|
||||
no_log: true
|
||||
|
||||
- name: Load NOBLE_AUTHENTIK_SMTP_USE_TLS from .env
|
||||
ansible.builtin.shell: |
|
||||
set -a
|
||||
. "{{ noble_repo_root }}/.env"
|
||||
set +a
|
||||
printf '%s' "${NOBLE_AUTHENTIK_SMTP_USE_TLS:-}"
|
||||
register: noble_authentik_smtp_use_tls_from_env
|
||||
when:
|
||||
- noble_authentik_dotenv_stat.stat.exists | default(false)
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Apply NOBLE_AUTHENTIK_SMTP_USE_TLS from .env
|
||||
ansible.builtin.set_fact:
|
||||
noble_authentik_smtp_use_tls: "{{ noble_authentik_smtp_use_tls_from_env.stdout | trim }}"
|
||||
when:
|
||||
- noble_authentik_smtp_use_tls_from_env is defined
|
||||
- (noble_authentik_smtp_use_tls_from_env.stdout | default('') | trim | length) > 0
|
||||
no_log: true
|
||||
|
||||
- name: Load NOBLE_AUTHENTIK_SMTP_USE_SSL from .env
|
||||
ansible.builtin.shell: |
|
||||
set -a
|
||||
. "{{ noble_repo_root }}/.env"
|
||||
set +a
|
||||
printf '%s' "${NOBLE_AUTHENTIK_SMTP_USE_SSL:-}"
|
||||
register: noble_authentik_smtp_use_ssl_from_env
|
||||
when:
|
||||
- noble_authentik_dotenv_stat.stat.exists | default(false)
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Apply NOBLE_AUTHENTIK_SMTP_USE_SSL from .env
|
||||
ansible.builtin.set_fact:
|
||||
noble_authentik_smtp_use_ssl: "{{ noble_authentik_smtp_use_ssl_from_env.stdout | trim }}"
|
||||
when:
|
||||
- noble_authentik_smtp_use_ssl_from_env is defined
|
||||
- (noble_authentik_smtp_use_ssl_from_env.stdout | default('') | trim | length) > 0
|
||||
no_log: true
|
||||
|
||||
- name: Load NOBLE_AUTHENTIK_SMTP_TIMEOUT from .env
|
||||
ansible.builtin.shell: |
|
||||
set -a
|
||||
. "{{ noble_repo_root }}/.env"
|
||||
set +a
|
||||
printf '%s' "${NOBLE_AUTHENTIK_SMTP_TIMEOUT:-}"
|
||||
register: noble_authentik_smtp_timeout_from_env
|
||||
when:
|
||||
- noble_authentik_dotenv_stat.stat.exists | default(false)
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Apply NOBLE_AUTHENTIK_SMTP_TIMEOUT from .env
|
||||
ansible.builtin.set_fact:
|
||||
noble_authentik_smtp_timeout: "{{ noble_authentik_smtp_timeout_from_env.stdout | trim }}"
|
||||
when:
|
||||
- noble_authentik_smtp_timeout_from_env is defined
|
||||
- (noble_authentik_smtp_timeout_from_env.stdout | default('') | trim | length) > 0
|
||||
no_log: true
|
||||
@@ -1,760 +0,0 @@
|
||||
---
|
||||
- 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_client_secret_open_webui | default('') | length > 0
|
||||
- noble_authentik_oauth2_proxy_cookie_secret | default('') | length > 0
|
||||
- noble_open_webui_openai_api_key | default('') | length > 0
|
||||
- noble_open_webui_webui_secret_key | default('') | length > 0
|
||||
- noble_open_webui_public_host | default('') | trim | length > 0
|
||||
fail_msg: >-
|
||||
Authentik requires secrets in .env (see ansible/roles/noble_authentik/README.md) or matching -e extra-vars.
|
||||
Includes Open WebUI: NOBLE_AUTHENTIK_CLIENT_SECRET_OPEN_WEBUI, NOBLE_OPEN_WEBUI_OPENAI_API_KEY,
|
||||
NOBLE_OPEN_WEBUI_WEBUI_SECRET_KEY (see .env.sample). Set **noble_open_webui_public_host** (must match
|
||||
**clusters/noble/apps/open-webui/values.yaml** ingress host; see README Pangolin section).
|
||||
|
||||
- name: Require Authentik S3 media settings (same endpoint/keys as Velero; dedicated bucket)
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- noble_authentik_media_s3_bucket | default('') | length > 0
|
||||
- noble_authentik_s3_endpoint | default('') | length > 0
|
||||
- noble_authentik_s3_access_key | default('') | length > 0
|
||||
- noble_authentik_s3_secret_key | default('') | length > 0
|
||||
fail_msg: >-
|
||||
Set NOBLE_AUTHENTIK_MEDIA_S3_BUCKET (dedicated bucket for media, not the Velero backup bucket).
|
||||
For S3 URL and keys, set NOBLE_AUTHENTIK_S3_URL / NOBLE_AUTHENTIK_S3_ACCESS_KEY / NOBLE_AUTHENTIK_S3_SECRET_KEY,
|
||||
or reuse Velero's NOBLE_VELERO_S3_URL and NOBLE_VELERO_AWS_ACCESS_KEY_ID / NOBLE_VELERO_AWS_SECRET_ACCESS_KEY
|
||||
in .env (see .env.sample and clusters/noble/bootstrap/velero/README.md).
|
||||
|
||||
- name: Require Authentik SMTP From when SMTP host is set
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- noble_authentik_smtp_from | default('') | trim | length > 0
|
||||
fail_msg: >-
|
||||
When NOBLE_AUTHENTIK_SMTP_HOST is set, set NOBLE_AUTHENTIK_SMTP_FROM (sender address).
|
||||
See repository .env.sample and https://docs.goauthentik.io/install-config/configuration/#email-settings
|
||||
when: noble_authentik_smtp_host | default('') | trim | length > 0
|
||||
|
||||
- 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: Ensure dir for rendered Authentik blueprints
|
||||
ansible.builtin.file:
|
||||
path: "{{ noble_repo_root }}/ansible/.ansible-tmp/authentik-blueprints"
|
||||
state: directory
|
||||
mode: "0700"
|
||||
when: noble_authentik_blueprints_enabled | default(false) | bool
|
||||
|
||||
- name: Assert noble Authentik blueprint variables (when blueprints enabled)
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- >-
|
||||
((noble_authentik_blueprint_public_groups | default([])) | length
|
||||
+ (noble_authentik_blueprint_extra_directory_groups | default([])) | length
|
||||
+ (noble_authentik_blueprint_nikflix_groups | default([])) | length) > 0
|
||||
- noble_authentik_blueprint_lab_operator_groups | default([]) | length > 0
|
||||
- noble_authentik_blueprint_lab_flow_slug | default('') | trim | length > 0
|
||||
- noble_authentik_blueprint_public_auth_flow_slug | default('') | trim | length > 0
|
||||
- (noble_authentik_blueprint_lab_mfa_not_configured_action | default('configure') | trim | lower)
|
||||
in ['skip', 'deny', 'configure']
|
||||
- noble_authentik_blueprint_public_invitation_enrollment_flow_slug | default('') | trim | length > 0
|
||||
- noble_authentik_blueprint_lab_invitation_enrollment_flow_slug | default('') | trim | length > 0
|
||||
- noble_authentik_blueprint_public_invitation_user_group | default('') | trim | length > 0
|
||||
- noble_authentik_blueprint_lab_invitee_group_name | default('') | trim | length > 0
|
||||
- (noble_authentik_blueprint_public_invitation_user_type | default('internal') | trim | lower) in ['external', 'internal']
|
||||
- (noble_authentik_blueprint_lab_invitation_user_type | default('internal') | trim | lower) in ['external', 'internal']
|
||||
fail_msg: >-
|
||||
When noble_authentik_blueprints_enabled is true, set at least one entry across
|
||||
noble_authentik_blueprint_public_groups, noble_authentik_blueprint_extra_directory_groups,
|
||||
and/or noble_authentik_blueprint_nikflix_groups,
|
||||
plus noble_authentik_blueprint_lab_operator_groups (non-empty), noble_authentik_blueprint_lab_flow_slug,
|
||||
noble_authentik_blueprint_public_auth_flow_slug, noble_authentik_blueprint_lab_mfa_not_configured_action
|
||||
(skip, deny, or configure), invitation enrollment flow slugs, noble_authentik_blueprint_public_invitation_user_group,
|
||||
noble_authentik_blueprint_lab_invitee_group_name, and invitation user_type values (external or internal).
|
||||
See ansible/roles/noble_authentik/defaults/main.yml and README.
|
||||
when: noble_authentik_blueprints_enabled | default(false) | bool
|
||||
|
||||
- name: Validate noble Authentik blueprint directory group entries (when blueprints enabled)
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (item is string and (item | trim | length) > 0) or (item is mapping and (item.name | default('') | trim | length) > 0)
|
||||
fail_msg: >-
|
||||
Each noble_authentik_blueprint_*_groups entry must be a non-empty string or a dict with key **name** (string).
|
||||
Invalid entry: {{ item }}
|
||||
loop: "{{ (noble_authentik_blueprint_public_groups | default([])) + (noble_authentik_blueprint_extra_directory_groups | default([])) + (noble_authentik_blueprint_nikflix_groups | default([])) }}"
|
||||
loop_control:
|
||||
label: "{{ item if item is string else item.name }}"
|
||||
when: noble_authentik_blueprints_enabled | default(false) | bool
|
||||
|
||||
- name: Render Authentik noble blueprint YAML files
|
||||
ansible.builtin.template:
|
||||
src: "blueprints/{{ item }}.j2"
|
||||
dest: "{{ noble_repo_root }}/ansible/.ansible-tmp/authentik-blueprints/{{ item }}"
|
||||
mode: "0600"
|
||||
loop:
|
||||
- 10-noble-public-groups.yaml
|
||||
- 20-noble-lab-operator-authentication-flow.yaml
|
||||
- 21-noble-public-authentication-flow.yaml
|
||||
- 22-noble-invitation-enrollment-flows.yaml
|
||||
- 30-noble-brands-domain-split.yaml
|
||||
when: noble_authentik_blueprints_enabled | default(false) | bool
|
||||
|
||||
- name: Apply Authentik noble blueprints ConfigMap (worker mounts under /blueprints/mounted/cm-*)
|
||||
ansible.builtin.shell: |
|
||||
set -euo pipefail
|
||||
kubectl -n "{{ noble_authentik_namespace }}" create configmap "{{ noble_authentik_blueprints_configmap_name }}" \
|
||||
--from-file="{{ noble_repo_root }}/ansible/.ansible-tmp/authentik-blueprints" \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
when: noble_authentik_blueprints_enabled | default(false) | bool
|
||||
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: Verify authentik-worker mounts noble blueprints volume (Helm blueprints.configMaps)
|
||||
ansible.builtin.shell: |
|
||||
set -euo pipefail
|
||||
WANT="blueprints-cm-{{ noble_authentik_blueprints_configmap_name }}"
|
||||
D="$(kubectl get deploy -n "{{ noble_authentik_namespace }}" \
|
||||
-l app.kubernetes.io/name=authentik,app.kubernetes.io/component=worker \
|
||||
-o jsonpath='{.items[0].metadata.name}')"
|
||||
MOUNTS="$(kubectl get deploy -n "{{ noble_authentik_namespace }}" "$D" \
|
||||
-o jsonpath='{.spec.template.spec.containers[0].volumeMounts[*].name}')"
|
||||
if ! echo "$MOUNTS" | tr ' ' '\n' | grep -Fxq "$WANT"; then
|
||||
echo "Expected volumeMount ${WANT} on ${D}; got: ${MOUNTS}" >&2
|
||||
exit 1
|
||||
fi
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
when: noble_authentik_blueprints_enabled | default(false) | bool
|
||||
changed_when: false
|
||||
|
||||
- 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: Wait for authentik worker rollout
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- rollout
|
||||
- status
|
||||
- deployment/authentik-worker
|
||||
- -n
|
||||
- authentik
|
||||
- --timeout=15m
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
changed_when: false
|
||||
when: noble_authentik_configure_idp | default(true) | bool
|
||||
|
||||
- name: Wait until Authentik API accepts bootstrap token (worker finished bootstrap)
|
||||
ansible.builtin.uri:
|
||||
url: "{{ noble_authentik_api_base }}/core/applications/?page_size=1"
|
||||
method: GET
|
||||
headers:
|
||||
Authorization: "Bearer {{ noble_authentik_bootstrap_token }}"
|
||||
Accept: application/json
|
||||
status_code: [200, 401, 403, 500, 502, 503]
|
||||
timeout: 30
|
||||
register: noble_authentik_api_bootstrap_ready
|
||||
until: noble_authentik_api_bootstrap_ready.status == 200
|
||||
retries: "{{ noble_authentik_bootstrap_api_wait_retries }}"
|
||||
delay: "{{ noble_authentik_bootstrap_api_wait_delay }}"
|
||||
when: noble_authentik_configure_idp | default(true) | bool
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- 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
|
||||
|
||||
# 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: >-
|
||||
"worker:" in (noble_authentik_worker_admin_access.stdout | default(""))
|
||||
and "authentik Admins" in (noble_authentik_worker_admin_access.stdout | default(""))
|
||||
failed_when: >-
|
||||
(noble_authentik_worker_admin_access.rc | default(-1)) != 0
|
||||
or (
|
||||
"worker:" not in (noble_authentik_worker_admin_access.stdout | default(""))
|
||||
or "authentik Admins" not in (noble_authentik_worker_admin_access.stdout | default(""))
|
||||
)
|
||||
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: >-
|
||||
"worker: OAuth2 providers + applications upserted"
|
||||
in (noble_authentik_worker_oidc_upsert.stdout | default(""))
|
||||
failed_when: >-
|
||||
(noble_authentik_worker_oidc_upsert.rc | default(-1)) != 0
|
||||
or (
|
||||
"worker: OAuth2 providers + applications upserted"
|
||||
not in (noble_authentik_worker_oidc_upsert.stdout | default(""))
|
||||
)
|
||||
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: >-
|
||||
"worker: bootstrap user group membership updated"
|
||||
in (noble_authentik_worker_user_groups.stdout | default(""))
|
||||
failed_when: (noble_authentik_worker_user_groups.rc | default(-1)) != 0
|
||||
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)
|
||||
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"
|
||||
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_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
|
||||
changed_when: 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="{{ noble_authentik_headlamp_oidc_scopes }}" \
|
||||
--from-literal=OIDC_CALLBACK_URL="https://headlamp.apps.noble.lab.pcenicni.dev/oidc-callback" \
|
||||
--from-literal=OIDC_USE_PKCE="{{ 'true' if (noble_authentik_headlamp_oidc_use_pkce | bool) else 'false' }}" \
|
||||
--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: Ensure open-webui namespace exists (Secret before Argo first sync)
|
||||
ansible.builtin.shell: |
|
||||
set -euo pipefail
|
||||
kubectl create namespace open-webui --dry-run=client -o yaml | kubectl apply -f -
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
when: noble_authentik_configure_idp | default(true) | bool
|
||||
changed_when: true
|
||||
|
||||
- name: Create Open WebUI secrets (OpenAI + WEBUI + OIDC client secret)
|
||||
ansible.builtin.shell: |
|
||||
set -euo pipefail
|
||||
kubectl -n open-webui create secret generic open-webui-secrets \
|
||||
--from-literal=OPENAI_API_KEY="${OPENAI_API_KEY}" \
|
||||
--from-literal=WEBUI_SECRET_KEY="${WEBUI_SECRET_KEY}" \
|
||||
--from-literal=OAUTH_CLIENT_SECRET="${OAUTH_CLIENT_SECRET}" \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
OPENAI_API_KEY: "{{ noble_open_webui_openai_api_key }}"
|
||||
WEBUI_SECRET_KEY: "{{ noble_open_webui_webui_secret_key }}"
|
||||
OAUTH_CLIENT_SECRET: "{{ noble_authentik_client_secret_open_webui }}"
|
||||
no_log: true
|
||||
when: noble_authentik_configure_idp | default(true) | bool
|
||||
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
|
||||
- "{{ noble_authentik_oauth2_proxy_helm_wait_timeout }}"
|
||||
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"
|
||||
- --set=config.oidc.usePKCE={{ 'true' if (noble_authentik_headlamp_oidc_use_pkce | bool) else 'false' }}
|
||||
- --force-conflicts
|
||||
- --wait
|
||||
- --timeout
|
||||
- 10m
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
changed_when: true
|
||||
|
||||
- name: Apply Headlamp static manifests (metrics RBAC + OIDC group binding)
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- apply
|
||||
- -k
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/headlamp"
|
||||
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
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"open-webui": {
|
||||
"name": "Open WebUI",
|
||||
"client_id": {{ noble_authentik_client_id_open_webui | to_json }},
|
||||
"client_secret": {{ noble_authentik_client_secret_open_webui | to_json }},
|
||||
"redirect_uri": "https://{{ noble_open_webui_public_host | trim }}/oauth/oidc/callback"
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
---
|
||||
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 }}"
|
||||
- name: AUTHENTIK_STORAGE__BACKEND
|
||||
value: "s3"
|
||||
- name: AUTHENTIK_STORAGE__S3__BUCKET_NAME
|
||||
value: "{{ noble_authentik_media_s3_bucket }}"
|
||||
- name: AUTHENTIK_STORAGE__S3__ENDPOINT
|
||||
value: "{{ noble_authentik_s3_endpoint }}"
|
||||
- name: AUTHENTIK_STORAGE__S3__ACCESS_KEY
|
||||
value: "{{ noble_authentik_s3_access_key }}"
|
||||
- name: AUTHENTIK_STORAGE__S3__SECRET_KEY
|
||||
value: "{{ noble_authentik_s3_secret_key }}"
|
||||
- name: AUTHENTIK_STORAGE__S3__REGION
|
||||
value: "{{ noble_authentik_s3_region }}"
|
||||
- name: AUTHENTIK_STORAGE__S3__ADDRESSING_STYLE
|
||||
value: "{{ noble_authentik_s3_addressing_style }}"
|
||||
{% if noble_authentik_smtp_host | default('') | trim | length > 0 %}
|
||||
- name: AUTHENTIK_EMAIL__HOST
|
||||
value: {{ noble_authentik_smtp_host | trim | to_json }}
|
||||
- name: AUTHENTIK_EMAIL__PORT
|
||||
value: {{ (noble_authentik_smtp_port | default('587') | string) | to_json }}
|
||||
- name: AUTHENTIK_EMAIL__USERNAME
|
||||
value: {{ noble_authentik_smtp_username | default('') | to_json }}
|
||||
- name: AUTHENTIK_EMAIL__PASSWORD
|
||||
value: {{ noble_authentik_smtp_password | default('') | to_json }}
|
||||
- name: AUTHENTIK_EMAIL__USE_TLS
|
||||
value: {{ (noble_authentik_smtp_use_tls | default('true') | string) | to_json }}
|
||||
- name: AUTHENTIK_EMAIL__USE_SSL
|
||||
value: {{ (noble_authentik_smtp_use_ssl | default('false') | string) | to_json }}
|
||||
- name: AUTHENTIK_EMAIL__TIMEOUT
|
||||
value: {{ (noble_authentik_smtp_timeout | default('30') | string) | to_json }}
|
||||
- name: AUTHENTIK_EMAIL__FROM
|
||||
value: {{ noble_authentik_smtp_from | trim | to_json }}
|
||||
{% endif %}
|
||||
postgresql:
|
||||
auth:
|
||||
password: "{{ noble_authentik_postgresql_password }}"
|
||||
{% if noble_authentik_ingress_extra_hosts | default([]) | length > 0 %}
|
||||
# Extra SANs on the same Authentik server (e.g. public FQDN behind Pangolin → Newt → Traefik). Helm’s last -f
|
||||
# replaces **server.ingress.hosts** / **tls[0].hosts**; primary lab host stays first.
|
||||
server:
|
||||
ingress:
|
||||
hosts:
|
||||
- {{ noble_authentik_host }}
|
||||
{% for h in noble_authentik_ingress_extra_hosts %}
|
||||
- {{ h }}
|
||||
{% endfor %}
|
||||
tls:
|
||||
- secretName: authentik-apps-noble-tls
|
||||
hosts:
|
||||
- {{ noble_authentik_host }}
|
||||
{% for h in noble_authentik_ingress_extra_hosts %}
|
||||
- {{ h }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if noble_authentik_blueprints_enabled | default(false) | bool %}
|
||||
blueprints:
|
||||
configMaps:
|
||||
- {{ noble_authentik_blueprints_configmap_name }}
|
||||
{% endif %}
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"bootstrap_email": {{ noble_authentik_bootstrap_email | default('') | trim | to_json }}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"open-webui": {
|
||||
"name": "Open WebUI",
|
||||
"client_id": {{ noble_authentik_client_id_open_webui | to_json }},
|
||||
"client_secret": {{ noble_authentik_client_secret_open_webui | to_json }},
|
||||
"redirect_uri": "https://{{ noble_open_webui_public_host | trim }}/oauth/oidc/callback"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"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 }}
|
||||
]
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
# Noble — directory groups (blueprint). Merges (in order): **`noble_authentik_blueprint_public_groups`** (optional),
|
||||
# **`noble_authentik_blueprint_extra_directory_groups`**, **`noble_authentik_blueprint_nikflix_groups`** (see role **defaults** / README).
|
||||
# Each entry: a string (**name** only), or a mapping with **name** and optional **is_superuser**, **attributes**, **parents**.
|
||||
# **parents** must reference groups that already exist: list those entries *before* children in the merged list, or rely on built-in groups.
|
||||
version: 1
|
||||
metadata:
|
||||
name: noble-directory-groups
|
||||
labels:
|
||||
blueprints.goauthentik.io/instantiate: "true"
|
||||
entries:
|
||||
{% set _all = (noble_authentik_blueprint_public_groups | default([]))
|
||||
+ (noble_authentik_blueprint_extra_directory_groups | default([]))
|
||||
+ (noble_authentik_blueprint_nikflix_groups | default([])) %}
|
||||
{% for g in _all %}
|
||||
{% set gn = (g.name if (g is mapping) else g) | trim %}
|
||||
- model: authentik_core.group
|
||||
identifiers:
|
||||
name: {{ gn | to_json }}
|
||||
{% if g is mapping and (
|
||||
(g.get('is_superuser') | default(false) | bool)
|
||||
or ((g.get('attributes') or {}) | length > 0)
|
||||
or ((g.get('parents') or []) | length > 0)
|
||||
) %}
|
||||
attrs:
|
||||
{% if g.get('is_superuser') | default(false) | bool %}
|
||||
is_superuser: true
|
||||
{% endif %}
|
||||
{% if (g.get('attributes') or {}) | length > 0 %}
|
||||
attributes: {{ g.attributes | to_json }}
|
||||
{% endif %}
|
||||
{% if (g.get('parents') or []) | length > 0 %}
|
||||
parents:
|
||||
{% for p in g.parents %}
|
||||
- !Find [authentik_core.group, [name, {{ p | trim | to_json }}]]
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
@@ -1,148 +0,0 @@
|
||||
# Noble — **lab** hostname authentication: operator-only, stricter password checks, MFA required (no WebAuthn-PWL skip).
|
||||
version: 1
|
||||
metadata:
|
||||
name: noble-lab-operator-authentication
|
||||
labels:
|
||||
blueprints.goauthentik.io/instantiate: "true"
|
||||
entries:
|
||||
- model: authentik_blueprints.metaapplyblueprint
|
||||
attrs:
|
||||
identifiers:
|
||||
name: Default - Password change flow
|
||||
required: false
|
||||
- model: authentik_stages_password.passwordstage
|
||||
id: noble-lab-password-stage
|
||||
identifiers:
|
||||
name: noble-lab-authentication-password
|
||||
attrs:
|
||||
backends:
|
||||
- authentik.core.auth.InbuiltBackend
|
||||
- authentik.sources.kerberos.auth.KerberosBackend
|
||||
- authentik.sources.ldap.auth.LDAPBackend
|
||||
- authentik.core.auth.TokenBackend
|
||||
configure_flow: !Find [authentik_flows.flow, [slug, default-password-change]]
|
||||
failed_attempts_before_cancel: {{ noble_authentik_blueprint_lab_password_failed_attempts | int }}
|
||||
- model: authentik_stages_authenticator_validate.authenticatorvalidatestage
|
||||
id: noble-lab-authenticator-validate-strict
|
||||
identifiers:
|
||||
name: noble-lab-authenticator-validate-strict
|
||||
attrs:
|
||||
not_configured_action: {{ noble_authentik_blueprint_lab_mfa_not_configured_action | trim | lower | to_json }}
|
||||
{% if noble_authentik_blueprint_lab_mfa_not_configured_action | trim | lower == 'configure' %}
|
||||
configuration_stages:
|
||||
- !Find [authentik_stages_authenticator_totp.authenticatortotpstage, [name, default-authenticator-totp-setup]]
|
||||
{% endif %}
|
||||
- model: authentik_flows.flow
|
||||
id: flow
|
||||
identifiers:
|
||||
slug: {{ noble_authentik_blueprint_lab_flow_slug | trim | to_json }}
|
||||
attrs:
|
||||
name: Noble lab (operators)
|
||||
title: Noble lab — operators only
|
||||
designation: authentication
|
||||
authentication: none
|
||||
- id: noble-lab-identification-binding
|
||||
model: authentik_flows.flowstagebinding
|
||||
identifiers:
|
||||
order: 10
|
||||
stage: !Find [authentik_stages_identification.identificationstage, [name, default-authentication-identification]]
|
||||
target: !KeyOf flow
|
||||
- id: noble-lab-password-binding
|
||||
model: authentik_flows.flowstagebinding
|
||||
identifiers:
|
||||
order: 20
|
||||
stage: !KeyOf noble-lab-password-stage
|
||||
target: !KeyOf flow
|
||||
attrs:
|
||||
re_evaluate_policies: true
|
||||
- id: noble-lab-authenticator-binding
|
||||
model: authentik_flows.flowstagebinding
|
||||
identifiers:
|
||||
order: 30
|
||||
stage: !KeyOf noble-lab-authenticator-validate-strict
|
||||
target: !KeyOf flow
|
||||
- model: authentik_flows.flowstagebinding
|
||||
identifiers:
|
||||
order: 100
|
||||
stage: !Find [authentik_stages_user_login.userloginstage, [name, default-authentication-login]]
|
||||
target: !KeyOf flow
|
||||
- model: authentik_policies_expression.expressionpolicy
|
||||
id: noble-lab-password-optional
|
||||
identifiers:
|
||||
name: noble-lab-password-optional
|
||||
attrs:
|
||||
expression: |
|
||||
flow_plan = request.context.get("flow_plan")
|
||||
if not flow_plan:
|
||||
return True
|
||||
return not hasattr(flow_plan.context.get("pending_user"), "backend")
|
||||
- model: authentik_policies_expression.expressionpolicy
|
||||
id: noble-lab-password-strength
|
||||
identifiers:
|
||||
name: noble-lab-password-strength
|
||||
attrs:
|
||||
expression: |
|
||||
import re
|
||||
_zxc = {{ noble_authentik_blueprint_lab_password_policy_check_zxcvbn | bool }}
|
||||
_zxc_thr = {{ noble_authentik_blueprint_lab_password_policy_zxcvbn_score_threshold | int }}
|
||||
pwd = request.context.get("password")
|
||||
if not pwd:
|
||||
return True
|
||||
msg = {{ noble_authentik_blueprint_lab_password_policy_error_message | trim | to_json }}
|
||||
if len(pwd) < {{ noble_authentik_blueprint_lab_password_policy_length_min | int }}:
|
||||
ak_message(msg)
|
||||
return False
|
||||
if len(re.findall(r"[A-Z]", pwd)) < {{ noble_authentik_blueprint_lab_password_policy_amount_uppercase | int }}:
|
||||
ak_message(msg)
|
||||
return False
|
||||
if len(re.findall(r"[a-z]", pwd)) < {{ noble_authentik_blueprint_lab_password_policy_amount_lowercase | int }}:
|
||||
ak_message(msg)
|
||||
return False
|
||||
if len(re.findall(r"[0-9]", pwd)) < {{ noble_authentik_blueprint_lab_password_policy_amount_digits | int }}:
|
||||
ak_message(msg)
|
||||
return False
|
||||
sym = sum(1 for c in pwd if (not c.isalnum()) and (not c.isspace()))
|
||||
if sym < {{ noble_authentik_blueprint_lab_password_policy_amount_symbols | int }}:
|
||||
ak_message(msg)
|
||||
return False
|
||||
if _zxc:
|
||||
try:
|
||||
from zxcvbn import zxcvbn
|
||||
if zxcvbn(pwd[:72])["score"] <= _zxc_thr:
|
||||
ak_message("Password is too weak for the lab policy.")
|
||||
return False
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
- model: authentik_policies_expression.expressionpolicy
|
||||
id: noble-lab-operators-only
|
||||
identifiers:
|
||||
name: {{ noble_authentik_blueprint_operator_policy_name | trim | to_json }}
|
||||
attrs:
|
||||
expression: |
|
||||
u = context.get("pending_user")
|
||||
if u is None:
|
||||
return False
|
||||
{% for g in noble_authentik_blueprint_lab_operator_groups | default([]) %}
|
||||
if ak_is_group_member(u, name={{ g | trim | to_json }}):
|
||||
return True
|
||||
{% endfor %}
|
||||
ak_message("This login URL is for administrators only. Use the public Authentik hostname instead.")
|
||||
return False
|
||||
- model: authentik_policies.policybinding
|
||||
identifiers:
|
||||
order: 5
|
||||
target: !KeyOf noble-lab-password-binding
|
||||
policy: !KeyOf noble-lab-operators-only
|
||||
- model: authentik_policies.policybinding
|
||||
identifiers:
|
||||
order: 10
|
||||
target: !KeyOf noble-lab-password-binding
|
||||
policy: !KeyOf noble-lab-password-optional
|
||||
attrs:
|
||||
failure_result: true
|
||||
- model: authentik_policies.policybinding
|
||||
identifiers:
|
||||
order: 15
|
||||
target: !KeyOf noble-lab-password-binding
|
||||
policy: !KeyOf noble-lab-password-strength
|
||||
@@ -1,80 +0,0 @@
|
||||
# Noble — **public** hostname(s): same behaviour as stock **default-authentication-flow** (optional password / MFA skip for WebAuthn PWL), isolated slug for Brand binding.
|
||||
version: 1
|
||||
metadata:
|
||||
name: noble-public-authentication-flow
|
||||
labels:
|
||||
blueprints.goauthentik.io/instantiate: "true"
|
||||
entries:
|
||||
- model: authentik_blueprints.metaapplyblueprint
|
||||
attrs:
|
||||
identifiers:
|
||||
name: Default - Password change flow
|
||||
required: false
|
||||
- model: authentik_flows.flow
|
||||
id: flow
|
||||
identifiers:
|
||||
slug: {{ noble_authentik_blueprint_public_auth_flow_slug | trim | to_json }}
|
||||
attrs:
|
||||
name: Noble public sign-in
|
||||
title: Sign in
|
||||
designation: authentication
|
||||
authentication: none
|
||||
- id: noble-public-identification-binding
|
||||
model: authentik_flows.flowstagebinding
|
||||
identifiers:
|
||||
order: 10
|
||||
stage: !Find [authentik_stages_identification.identificationstage, [name, default-authentication-identification]]
|
||||
target: !KeyOf flow
|
||||
- id: noble-public-password-binding
|
||||
model: authentik_flows.flowstagebinding
|
||||
identifiers:
|
||||
order: 20
|
||||
stage: !Find [authentik_stages_password.passwordstage, [name, default-authentication-password]]
|
||||
target: !KeyOf flow
|
||||
attrs:
|
||||
re_evaluate_policies: true
|
||||
- id: noble-public-authenticator-binding
|
||||
model: authentik_flows.flowstagebinding
|
||||
identifiers:
|
||||
order: 30
|
||||
stage: !Find [authentik_stages_authenticator_validate.authenticatorvalidatestage, [name, default-authentication-mfa-validation]]
|
||||
target: !KeyOf flow
|
||||
- model: authentik_flows.flowstagebinding
|
||||
identifiers:
|
||||
order: 100
|
||||
stage: !Find [authentik_stages_user_login.userloginstage, [name, default-authentication-login]]
|
||||
target: !KeyOf flow
|
||||
- model: authentik_policies_expression.expressionpolicy
|
||||
id: noble-public-password-optional
|
||||
identifiers:
|
||||
name: noble-public-password-optional
|
||||
attrs:
|
||||
expression: |
|
||||
flow_plan = request.context.get("flow_plan")
|
||||
if not flow_plan:
|
||||
return True
|
||||
return not hasattr(flow_plan.context.get("pending_user"), "backend")
|
||||
- model: authentik_policies_expression.expressionpolicy
|
||||
id: noble-public-authenticator-validate-optional
|
||||
identifiers:
|
||||
name: noble-public-authenticator-validate-optional
|
||||
attrs:
|
||||
expression: |
|
||||
flow_plan = request.context.get("flow_plan")
|
||||
if not flow_plan:
|
||||
return True
|
||||
return not (flow_plan.context.get("auth_method") == "auth_webauthn_pwl")
|
||||
- model: authentik_policies.policybinding
|
||||
identifiers:
|
||||
order: 10
|
||||
target: !KeyOf noble-public-password-binding
|
||||
policy: !KeyOf noble-public-password-optional
|
||||
attrs:
|
||||
failure_result: true
|
||||
- model: authentik_policies.policybinding
|
||||
identifiers:
|
||||
order: 10
|
||||
target: !KeyOf noble-public-authenticator-binding
|
||||
policy: !KeyOf noble-public-authenticator-validate-optional
|
||||
attrs:
|
||||
failure_result: true
|
||||
@@ -1,227 +0,0 @@
|
||||
# Noble — two **enrollment** flows (public vs lab) with separate **Invitation** stages (invitation token required).
|
||||
# Create rows under **Directory → Invitations** in the admin UI and pick the matching flow; share links with the
|
||||
# correct **Host** so the right Brand applies. Does **not** ship example **Invitation** objects (no prefilled emails).
|
||||
version: 1
|
||||
metadata:
|
||||
name: noble-invitation-enrollment-flows
|
||||
labels:
|
||||
blueprints.goauthentik.io/instantiate: "true"
|
||||
entries:
|
||||
- model: authentik_core.group
|
||||
id: noble-lab-invited-group
|
||||
identifiers:
|
||||
name: {{ noble_authentik_blueprint_lab_invitee_group_name | trim | to_json }}
|
||||
attrs:
|
||||
is_superuser: false
|
||||
attributes:
|
||||
"noble.ak/audience": lab
|
||||
"noble.ak/role": lab-invited
|
||||
|
||||
- model: authentik_flows.flow
|
||||
id: noble-inv-flow-public
|
||||
identifiers:
|
||||
slug: {{ noble_authentik_blueprint_public_invitation_enrollment_flow_slug | trim | to_json }}
|
||||
attrs:
|
||||
name: {{ noble_authentik_blueprint_public_invitation_flow_name | trim | to_json }}
|
||||
title: {{ noble_authentik_blueprint_public_invitation_flow_title | trim | to_json }}
|
||||
designation: enrollment
|
||||
authentication: require_unauthenticated
|
||||
|
||||
- model: authentik_flows.flow
|
||||
id: noble-inv-flow-lab
|
||||
identifiers:
|
||||
slug: {{ noble_authentik_blueprint_lab_invitation_enrollment_flow_slug | trim | to_json }}
|
||||
attrs:
|
||||
name: {{ noble_authentik_blueprint_lab_invitation_flow_name | trim | to_json }}
|
||||
title: {{ noble_authentik_blueprint_lab_invitation_flow_title | trim | to_json }}
|
||||
designation: enrollment
|
||||
authentication: require_unauthenticated
|
||||
|
||||
- model: authentik_stages_invitation.invitationstage
|
||||
id: noble-inv-stage-public
|
||||
identifiers:
|
||||
name: noble-invitation-enrollment-invitation-public
|
||||
attrs:
|
||||
continue_flow_without_invitation: false
|
||||
|
||||
- model: authentik_stages_invitation.invitationstage
|
||||
id: noble-inv-stage-lab
|
||||
identifiers:
|
||||
name: noble-invitation-enrollment-invitation-lab
|
||||
attrs:
|
||||
continue_flow_without_invitation: false
|
||||
|
||||
- id: noble-inv-prompt-field-username
|
||||
model: authentik_stages_prompt.prompt
|
||||
identifiers:
|
||||
name: noble-inv-enroll-field-username
|
||||
attrs:
|
||||
field_key: username
|
||||
label: Username
|
||||
type: username
|
||||
required: true
|
||||
placeholder: Username
|
||||
placeholder_expression: false
|
||||
order: 0
|
||||
|
||||
- id: noble-inv-prompt-field-password
|
||||
model: authentik_stages_prompt.prompt
|
||||
identifiers:
|
||||
name: noble-inv-enroll-field-password
|
||||
attrs:
|
||||
field_key: password
|
||||
label: Password
|
||||
type: password
|
||||
required: true
|
||||
placeholder: Password
|
||||
placeholder_expression: false
|
||||
order: 1
|
||||
|
||||
- id: noble-inv-prompt-field-password-repeat
|
||||
model: authentik_stages_prompt.prompt
|
||||
identifiers:
|
||||
name: noble-inv-enroll-field-password-repeat
|
||||
attrs:
|
||||
field_key: password_repeat
|
||||
label: Password (repeat)
|
||||
type: password
|
||||
required: true
|
||||
placeholder: Password (repeat)
|
||||
placeholder_expression: false
|
||||
order: 2
|
||||
|
||||
- id: noble-inv-prompt-field-name
|
||||
model: authentik_stages_prompt.prompt
|
||||
identifiers:
|
||||
name: noble-inv-enroll-field-name
|
||||
attrs:
|
||||
field_key: name
|
||||
label: Name
|
||||
type: text
|
||||
required: true
|
||||
placeholder: Name
|
||||
placeholder_expression: false
|
||||
order: 0
|
||||
|
||||
- id: noble-inv-prompt-field-email
|
||||
model: authentik_stages_prompt.prompt
|
||||
identifiers:
|
||||
name: noble-inv-enroll-field-email
|
||||
attrs:
|
||||
field_key: email
|
||||
label: Email
|
||||
type: email
|
||||
required: true
|
||||
placeholder: Email
|
||||
placeholder_expression: false
|
||||
order: 1
|
||||
|
||||
- id: noble-inv-prompt-stage-credentials
|
||||
model: authentik_stages_prompt.promptstage
|
||||
identifiers:
|
||||
name: noble-inv-enroll-prompt-credentials
|
||||
attrs:
|
||||
fields:
|
||||
- !KeyOf noble-inv-prompt-field-username
|
||||
- !KeyOf noble-inv-prompt-field-password
|
||||
- !KeyOf noble-inv-prompt-field-password-repeat
|
||||
|
||||
- id: noble-inv-prompt-stage-details
|
||||
model: authentik_stages_prompt.promptstage
|
||||
identifiers:
|
||||
name: noble-inv-enroll-prompt-details
|
||||
attrs:
|
||||
fields:
|
||||
- !KeyOf noble-inv-prompt-field-name
|
||||
- !KeyOf noble-inv-prompt-field-email
|
||||
|
||||
- id: noble-inv-user-write-public
|
||||
model: authentik_stages_user_write.userwritestage
|
||||
identifiers:
|
||||
name: noble-inv-enroll-user-write-public
|
||||
attrs:
|
||||
user_creation_mode: always_create
|
||||
user_type: {{ noble_authentik_blueprint_public_invitation_user_type | trim | lower | to_json }}
|
||||
user_path_template: {{ noble_authentik_blueprint_public_invitation_user_path | trim | to_json }}
|
||||
create_users_group: !Find [authentik_core.group, [name, {{ noble_authentik_blueprint_public_invitation_user_group | trim | to_json }}]]
|
||||
|
||||
- id: noble-inv-user-write-lab
|
||||
model: authentik_stages_user_write.userwritestage
|
||||
identifiers:
|
||||
name: noble-inv-enroll-user-write-lab
|
||||
attrs:
|
||||
user_creation_mode: always_create
|
||||
user_type: {{ noble_authentik_blueprint_lab_invitation_user_type | trim | lower | to_json }}
|
||||
user_path_template: {{ noble_authentik_blueprint_lab_invitation_user_path | trim | to_json }}
|
||||
create_users_group: !KeyOf noble-lab-invited-group
|
||||
|
||||
- id: noble-inv-user-login
|
||||
model: authentik_stages_user_login.userloginstage
|
||||
identifiers:
|
||||
name: noble-inv-enroll-user-login
|
||||
|
||||
- model: authentik_flows.flowstagebinding
|
||||
identifiers:
|
||||
target: !KeyOf noble-inv-flow-public
|
||||
stage: !KeyOf noble-inv-stage-public
|
||||
order: 5
|
||||
attrs:
|
||||
evaluate_on_plan: true
|
||||
re_evaluate_policies: true
|
||||
|
||||
- model: authentik_flows.flowstagebinding
|
||||
identifiers:
|
||||
target: !KeyOf noble-inv-flow-public
|
||||
stage: !KeyOf noble-inv-prompt-stage-credentials
|
||||
order: 10
|
||||
|
||||
- model: authentik_flows.flowstagebinding
|
||||
identifiers:
|
||||
target: !KeyOf noble-inv-flow-public
|
||||
stage: !KeyOf noble-inv-prompt-stage-details
|
||||
order: 15
|
||||
|
||||
- model: authentik_flows.flowstagebinding
|
||||
identifiers:
|
||||
target: !KeyOf noble-inv-flow-public
|
||||
stage: !KeyOf noble-inv-user-write-public
|
||||
order: 20
|
||||
|
||||
- model: authentik_flows.flowstagebinding
|
||||
identifiers:
|
||||
target: !KeyOf noble-inv-flow-public
|
||||
stage: !KeyOf noble-inv-user-login
|
||||
order: 100
|
||||
|
||||
- model: authentik_flows.flowstagebinding
|
||||
identifiers:
|
||||
target: !KeyOf noble-inv-flow-lab
|
||||
stage: !KeyOf noble-inv-stage-lab
|
||||
order: 5
|
||||
attrs:
|
||||
evaluate_on_plan: true
|
||||
re_evaluate_policies: true
|
||||
|
||||
- model: authentik_flows.flowstagebinding
|
||||
identifiers:
|
||||
target: !KeyOf noble-inv-flow-lab
|
||||
stage: !KeyOf noble-inv-prompt-stage-credentials
|
||||
order: 10
|
||||
|
||||
- model: authentik_flows.flowstagebinding
|
||||
identifiers:
|
||||
target: !KeyOf noble-inv-flow-lab
|
||||
stage: !KeyOf noble-inv-prompt-stage-details
|
||||
order: 15
|
||||
|
||||
- model: authentik_flows.flowstagebinding
|
||||
identifiers:
|
||||
target: !KeyOf noble-inv-flow-lab
|
||||
stage: !KeyOf noble-inv-user-write-lab
|
||||
order: 20
|
||||
|
||||
- model: authentik_flows.flowstagebinding
|
||||
identifiers:
|
||||
target: !KeyOf noble-inv-flow-lab
|
||||
stage: !KeyOf noble-inv-user-login
|
||||
order: 100
|
||||
@@ -1,27 +0,0 @@
|
||||
# Noble — Brands so **Host** selects authentication flow: lab hostname → operator-only hardened flow; extra hosts → public flow (**21**).
|
||||
version: 1
|
||||
metadata:
|
||||
name: noble-brands-domain-split
|
||||
labels:
|
||||
blueprints.goauthentik.io/instantiate: "true"
|
||||
entries:
|
||||
- model: authentik_brands.brand
|
||||
identifiers:
|
||||
domain: {{ noble_authentik_host | trim | to_json }}
|
||||
attrs:
|
||||
default: false
|
||||
title: {{ noble_authentik_blueprint_lab_brand_title | trim | to_json }}
|
||||
flow_authentication: !Find [authentik_flows.flow, [slug, {{ noble_authentik_blueprint_lab_flow_slug | trim | to_json }}]]
|
||||
flow_invalidation: !Find [authentik_flows.flow, [slug, default-invalidation-flow]]
|
||||
flow_user_settings: !Find [authentik_flows.flow, [slug, default-user-settings-flow]]
|
||||
{% for host in noble_authentik_ingress_extra_hosts | default([]) %}
|
||||
- model: authentik_brands.brand
|
||||
identifiers:
|
||||
domain: {{ host | trim | to_json }}
|
||||
attrs:
|
||||
default: false
|
||||
title: {{ ((noble_authentik_blueprint_public_brand_title_prefix | default('Noble public')) ~ ' (' ~ (host | trim) ~ ')') | to_json }}
|
||||
flow_authentication: !Find [authentik_flows.flow, [slug, {{ noble_authentik_blueprint_public_auth_flow_slug | trim | to_json }}]]
|
||||
flow_invalidation: !Find [authentik_flows.flow, [slug, default-invalidation-flow]]
|
||||
flow_user_settings: !Find [authentik_flows.flow, [slug, default-user-settings-flow]]
|
||||
{% endfor %}
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
# Warn when **cloudflare-dns-api-token** is missing after apply (also set in **inventory/group_vars/all.yml** when loaded).
|
||||
noble_cert_manager_require_cloudflare_secret: true
|
||||
|
||||
# Helm --wait default (~5m) can expire while startupapicheck waits on webhooks / API (busy or slow pulls).
|
||||
noble_helm_cert_manager_wait_timeout: 15m
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
# See repository **.env.sample** — copy to **.env** (gitignored).
|
||||
- name: Stat repository .env for deploy secrets
|
||||
ansible.builtin.stat:
|
||||
path: "{{ noble_repo_root }}/.env"
|
||||
register: noble_deploy_env_file
|
||||
changed_when: false
|
||||
|
||||
- name: Create cert-manager Cloudflare DNS secret from .env
|
||||
ansible.builtin.shell: |
|
||||
set -euo pipefail
|
||||
set -a
|
||||
. "{{ noble_repo_root }}/.env"
|
||||
set +a
|
||||
if [ -z "${CLOUDFLARE_DNS_API_TOKEN:-}" ]; then
|
||||
echo NO_TOKEN
|
||||
exit 0
|
||||
fi
|
||||
kubectl -n cert-manager create secret generic cloudflare-dns-api-token \
|
||||
--from-literal=api-token="${CLOUDFLARE_DNS_API_TOKEN}" \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
echo APPLIED
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
when: noble_deploy_env_file.stat.exists | default(false)
|
||||
no_log: true
|
||||
register: noble_cf_secret_from_env
|
||||
changed_when: "'APPLIED' in (noble_cf_secret_from_env.stdout | default(''))"
|
||||
@@ -1,71 +0,0 @@
|
||||
---
|
||||
- name: Create cert-manager namespace
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- apply
|
||||
- -f
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/cert-manager/namespace.yaml"
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
changed_when: true
|
||||
|
||||
- name: Install cert-manager
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- helm
|
||||
- upgrade
|
||||
- --install
|
||||
- cert-manager
|
||||
- jetstack/cert-manager
|
||||
- --namespace
|
||||
- cert-manager
|
||||
- --version
|
||||
- v1.20.2
|
||||
- -f
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/cert-manager/values.yaml"
|
||||
- --force-conflicts
|
||||
- --wait
|
||||
- --timeout
|
||||
- "{{ noble_helm_cert_manager_wait_timeout }}"
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
changed_when: true
|
||||
|
||||
- name: Apply secrets from repository .env (optional)
|
||||
ansible.builtin.include_tasks: from_env.yml
|
||||
|
||||
- name: Check Cloudflare DNS API token Secret (required for ClusterIssuers)
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- -n
|
||||
- cert-manager
|
||||
- get
|
||||
- secret
|
||||
- cloudflare-dns-api-token
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
register: noble_cf_secret
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
|
||||
- name: Warn when Cloudflare Secret is missing
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
Secret cert-manager/cloudflare-dns-api-token not found.
|
||||
Create it per clusters/noble/bootstrap/cert-manager/README.md before ClusterIssuers can succeed.
|
||||
when:
|
||||
- noble_cert_manager_require_cloudflare_secret | default(true) | bool
|
||||
- noble_cf_secret.rc != 0
|
||||
|
||||
- name: Apply ClusterIssuers (staging + prod)
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- apply
|
||||
- -k
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/cert-manager"
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
changed_when: true
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
# When true, delete **kube-system/hubble-server-certs** if **managedFields** show **argocd-controller**
|
||||
# (recover from Helm SSA conflicts after Argo synced Cilium before Ansible). Requires **kubectl** with
|
||||
# **--show-managed-fields** on the pre-check (see tasks).
|
||||
noble_cilium_repair_argo_ssa_on_hubble_secret: true
|
||||
# When true, delete **hubble-server-certs** whenever it exists (before Helm). Use only if the Argo check
|
||||
# still does not fire (older kubectl) or you need a one-shot cleanup.
|
||||
noble_cilium_delete_hubble_server_certs_if_present: false
|
||||
@@ -1,67 +0,0 @@
|
||||
---
|
||||
# Argo may have server-side-applied chart-owned Secrets during earlier runs; Helm then fails with
|
||||
# "conflict with argocd-controller". **kubectl** omits **managedFields** unless **--show-managed-fields=true**.
|
||||
# We delete the Secret only when **argocd-controller** appears there (or set **noble_cilium_delete_hubble_server_certs_if_present**).
|
||||
- name: Read hubble-server-certs Secret (if any) for SSA repair
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- get
|
||||
- secret
|
||||
- hubble-server-certs
|
||||
- -n
|
||||
- kube-system
|
||||
- --show-managed-fields=true
|
||||
- -o
|
||||
- json
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
register: noble_cilium_hubble_secret_json
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
when: noble_cilium_repair_argo_ssa_on_hubble_secret | default(true) | bool
|
||||
|
||||
- name: Remove hubble-server-certs when Argo is a field manager (Helm SSA conflict recovery)
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- delete
|
||||
- secret
|
||||
- hubble-server-certs
|
||||
- -n
|
||||
- kube-system
|
||||
- --wait=false
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
when:
|
||||
- noble_cilium_repair_argo_ssa_on_hubble_secret | default(true) | bool
|
||||
- not (noble_cilium_hubble_secret_json.skipped | default(false))
|
||||
- noble_cilium_hubble_secret_json.rc | default(-1) | int == 0
|
||||
- (noble_cilium_delete_hubble_server_certs_if_present | default(false) | bool) or ("argocd-controller" in (noble_cilium_hubble_secret_json.stdout | default("")))
|
||||
changed_when: true
|
||||
|
||||
- name: Install Cilium (required CNI for Talos cni:none)
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- helm
|
||||
- upgrade
|
||||
- --install
|
||||
- cilium
|
||||
- cilium/cilium
|
||||
- --namespace
|
||||
- kube-system
|
||||
- --version
|
||||
- "1.19.4"
|
||||
- -f
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/cilium/values.yaml"
|
||||
- --force-conflicts
|
||||
- --wait
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
changed_when: true
|
||||
|
||||
- name: Wait for Cilium DaemonSet
|
||||
ansible.builtin.command: kubectl -n kube-system rollout status ds/cilium --timeout=300s
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
changed_when: false
|
||||
@@ -1,2 +0,0 @@
|
||||
---
|
||||
noble_csi_snapshot_kubectl_timeout: 120s
|
||||
@@ -1,39 +0,0 @@
|
||||
---
|
||||
# Volume Snapshot CRDs + snapshot-controller (Velero CSI / Longhorn snapshots).
|
||||
- name: Apply Volume Snapshot CRDs (snapshot.storage.k8s.io)
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- apply
|
||||
- "--request-timeout={{ noble_csi_snapshot_kubectl_timeout | default('120s') }}"
|
||||
- -k
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/csi-snapshot-controller/crd"
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
changed_when: true
|
||||
|
||||
- name: Apply snapshot-controller in kube-system
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- apply
|
||||
- "--request-timeout={{ noble_csi_snapshot_kubectl_timeout | default('120s') }}"
|
||||
- -k
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/csi-snapshot-controller/controller"
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
changed_when: true
|
||||
|
||||
- name: Wait for snapshot-controller Deployment
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- -n
|
||||
- kube-system
|
||||
- rollout
|
||||
- status
|
||||
- deploy/snapshot-controller
|
||||
- --timeout=120s
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
changed_when: false
|
||||
@@ -1,11 +0,0 @@
|
||||
---
|
||||
- name: Apply kube-vip (Kubernetes API VIP)
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- apply
|
||||
- -k
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/kube-vip"
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
changed_when: true
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
# When true, delete **FlowSchema/kyverno-admission-controller** if **managedFields** show **argocd-controller**
|
||||
# (Helm SSA conflict after Argo synced Kyverno before Ansible).
|
||||
noble_kyverno_repair_argo_ssa_on_flowschema: true
|
||||
# When true, delete that FlowSchema whenever it exists (before Helm). One-shot escape hatch.
|
||||
noble_kyverno_delete_kyverno_admission_flowschema_if_present: false
|
||||
@@ -1,68 +0,0 @@
|
||||
---
|
||||
# Argo may have server-side-applied cluster FlowSchemas; Helm then fails with "conflict with argocd-controller".
|
||||
- name: Read kyverno-admission-controller FlowSchema (if any) for SSA repair
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- get
|
||||
- flowschemas.flowcontrol.apiserver.k8s.io
|
||||
- kyverno-admission-controller
|
||||
- --show-managed-fields=true
|
||||
- -o
|
||||
- json
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
register: noble_kyverno_flowschema_json
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
when: noble_kyverno_repair_argo_ssa_on_flowschema | default(true) | bool
|
||||
|
||||
- name: Remove kyverno-admission-controller FlowSchema when Argo is a field manager (Helm SSA conflict recovery)
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- delete
|
||||
- flowschemas.flowcontrol.apiserver.k8s.io
|
||||
- kyverno-admission-controller
|
||||
- --wait=false
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
when:
|
||||
- noble_kyverno_repair_argo_ssa_on_flowschema | default(true) | bool
|
||||
- not (noble_kyverno_flowschema_json.skipped | default(false))
|
||||
- noble_kyverno_flowschema_json.rc | default(-1) | int == 0
|
||||
- (noble_kyverno_delete_kyverno_admission_flowschema_if_present | default(false) | bool) or ("argocd-controller" in (noble_kyverno_flowschema_json.stdout | default("")))
|
||||
changed_when: true
|
||||
|
||||
- name: Create Kyverno namespace
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- apply
|
||||
- -f
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/kyverno/namespace.yaml"
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
changed_when: true
|
||||
|
||||
- name: Install Kyverno operator
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- helm
|
||||
- upgrade
|
||||
- --install
|
||||
- kyverno
|
||||
- kyverno/kyverno
|
||||
- -n
|
||||
- kyverno
|
||||
- --version
|
||||
- "3.8.0"
|
||||
- -f
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/kyverno/values.yaml"
|
||||
- --force-conflicts
|
||||
- --wait
|
||||
- --timeout
|
||||
- 15m
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
changed_when: true
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
# After the operator chart, the validating webhook can still be a few seconds behind Helm --wait.
|
||||
noble_kyverno_policies_endpoint_wait_retries: 60
|
||||
noble_kyverno_policies_endpoint_wait_delay: 5
|
||||
|
||||
# Transient "failed calling webhook ... context deadline exceeded" while admission warms up.
|
||||
noble_kyverno_policies_helm_retries: 12
|
||||
noble_kyverno_policies_helm_delay: 20
|
||||
@@ -1,60 +0,0 @@
|
||||
---
|
||||
# Helm --wait on the operator does not guarantee the first policyvalidate call from apiserver succeeds.
|
||||
- name: Wait for Kyverno admission controller Deployment rollout
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- rollout
|
||||
- status
|
||||
- deployment/kyverno-admission-controller
|
||||
- -n
|
||||
- kyverno
|
||||
- --timeout=300s
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
changed_when: false
|
||||
|
||||
- name: Wait for Kyverno webhook Service (kyverno-svc) to have endpoints
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- get
|
||||
- endpoints
|
||||
- kyverno-svc
|
||||
- -n
|
||||
- kyverno
|
||||
- -o
|
||||
- 'jsonpath={range .subsets[*].addresses[*]}{.ip}{"\n"}{end}'
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
register: noble_kyverno_policies_ep
|
||||
until: (noble_kyverno_policies_ep.stdout | default('') | trim | length) > 0
|
||||
retries: "{{ noble_kyverno_policies_endpoint_wait_retries }}"
|
||||
delay: "{{ noble_kyverno_policies_endpoint_wait_delay }}"
|
||||
changed_when: false
|
||||
|
||||
- name: Install Kyverno policy chart (PSS baseline, Audit)
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- helm
|
||||
- upgrade
|
||||
- --install
|
||||
- kyverno-policies
|
||||
- kyverno/kyverno-policies
|
||||
- -n
|
||||
- kyverno
|
||||
- --version
|
||||
- "3.8.0"
|
||||
- -f
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/kyverno/policies-values.yaml"
|
||||
- --force-conflicts
|
||||
- --wait
|
||||
- --timeout
|
||||
- 10m
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
register: noble_kyverno_policies_helm
|
||||
retries: "{{ noble_kyverno_policies_helm_retries }}"
|
||||
delay: "{{ noble_kyverno_policies_helm_delay }}"
|
||||
until: noble_kyverno_policies_helm.rc == 0
|
||||
changed_when: true
|
||||
@@ -1,61 +0,0 @@
|
||||
---
|
||||
# Regenerated when **noble_landing_urls** runs (after platform stack). Paths match Traefik + cert-manager Ingresses.
|
||||
noble_landing_urls_dest: "{{ noble_repo_root }}/ansible/output/noble-lab-ui-urls.md"
|
||||
|
||||
# When true, run kubectl to fill Argo CD / Grafana secrets and a bounded Headlamp SA token in the markdown (requires working kubeconfig).
|
||||
noble_landing_urls_fetch_credentials: true
|
||||
|
||||
# Headlamp: bounded token for UI sign-in (`kubectl create token`); cluster may cap max duration.
|
||||
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
|
||||
service: argocd-server
|
||||
url: https://argo.apps.noble.lab.pcenicni.dev
|
||||
- name: Grafana
|
||||
description: Dashboards, Loki explore (logs)
|
||||
namespace: monitoring
|
||||
service: kube-prometheus-grafana
|
||||
url: https://grafana.apps.noble.lab.pcenicni.dev
|
||||
- name: Prometheus
|
||||
description: Prometheus UI (queries, targets) — lab; protect in production
|
||||
namespace: monitoring
|
||||
service: kube-prometheus-kube-prome-prometheus
|
||||
url: https://prometheus.apps.noble.lab.pcenicni.dev
|
||||
- name: Alertmanager
|
||||
description: Alertmanager UI (silences, status)
|
||||
namespace: monitoring
|
||||
service: kube-prometheus-kube-prome-alertmanager
|
||||
url: https://alertmanager.apps.noble.lab.pcenicni.dev
|
||||
- name: Headlamp
|
||||
description: Kubernetes UI (cluster resources)
|
||||
namespace: headlamp
|
||||
service: headlamp
|
||||
url: https://headlamp.apps.noble.lab.pcenicni.dev
|
||||
- name: Longhorn
|
||||
description: Storage volumes, nodes, backups
|
||||
namespace: longhorn-system
|
||||
service: longhorn-frontend
|
||||
url: https://longhorn.apps.noble.lab.pcenicni.dev
|
||||
- name: Velero
|
||||
description: Cluster backups — no web UI (velero CLI / kubectl CRDs)
|
||||
namespace: velero
|
||||
service: velero
|
||||
url: ""
|
||||
- name: Homepage
|
||||
description: App dashboard (links to lab UIs)
|
||||
namespace: homepage
|
||||
service: homepage
|
||||
url: https://homepage.apps.noble.lab.pcenicni.dev
|
||||
@@ -1,72 +0,0 @@
|
||||
---
|
||||
# Populates template variables from Secrets + Headlamp token (no_log on kubectl to avoid leaking into Ansible stdout).
|
||||
- name: Fetch Argo CD initial admin password (base64)
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- -n
|
||||
- argocd
|
||||
- get
|
||||
- secret
|
||||
- argocd-initial-admin-secret
|
||||
- -o
|
||||
- jsonpath={.data.password}
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
register: noble_fetch_argocd_pw_b64
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Fetch Grafana admin user (base64)
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- -n
|
||||
- monitoring
|
||||
- get
|
||||
- secret
|
||||
- kube-prometheus-grafana
|
||||
- -o
|
||||
- jsonpath={.data.admin-user}
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
register: noble_fetch_grafana_user_b64
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Fetch Grafana admin password (base64)
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- -n
|
||||
- monitoring
|
||||
- get
|
||||
- secret
|
||||
- kube-prometheus-grafana
|
||||
- -o
|
||||
- jsonpath={.data.admin-password}
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
register: noble_fetch_grafana_pw_b64
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Create Headlamp ServiceAccount token (for UI sign-in)
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- -n
|
||||
- headlamp
|
||||
- create
|
||||
- token
|
||||
- headlamp
|
||||
- "--duration={{ noble_landing_urls_headlamp_token_duration | default('48h') }}"
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
register: noble_fetch_headlamp_token
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
no_log: true
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
- name: Ensure output directory for generated landing page
|
||||
ansible.builtin.file:
|
||||
path: "{{ noble_repo_root }}/ansible/output"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Fetch initial credentials from cluster Secrets (optional)
|
||||
ansible.builtin.include_tasks: fetch_credentials.yml
|
||||
when: noble_landing_urls_fetch_credentials | default(true) | bool
|
||||
|
||||
- name: Write noble lab UI URLs (markdown landing page)
|
||||
ansible.builtin.template:
|
||||
src: noble-lab-ui-urls.md.j2
|
||||
dest: "{{ noble_landing_urls_dest }}"
|
||||
mode: "0644"
|
||||
|
||||
- name: Show landing page path
|
||||
ansible.builtin.debug:
|
||||
msg: "Noble lab UI list written to {{ noble_landing_urls_dest }}"
|
||||
@@ -1,52 +0,0 @@
|
||||
# Noble lab — web UIs (LAN)
|
||||
|
||||
> **Sensitive:** This file may include **passwords read from Kubernetes Secrets** when credential fetch ran. It is **gitignored** — do not commit or share.
|
||||
|
||||
**DNS:** point **`*.apps.noble.lab.pcenicni.dev`** at the Traefik **LoadBalancer** (MetalLB **`192.168.50.211`** by default — see `clusters/noble/bootstrap/traefik/values.yaml`).
|
||||
|
||||
**TLS:** **cert-manager** + **`letsencrypt-prod`** on each Ingress (public **DNS-01** for **`pcenicni.dev`**).
|
||||
|
||||
This file is **generated** by Ansible (`noble_landing_urls` role). Use it as a temporary landing page to find services after deploy.
|
||||
|
||||
| UI | What | Kubernetes service | Namespace | URL |
|
||||
|----|------|----------------------|-----------|-----|
|
||||
{% for e in noble_lab_ui_entries %}
|
||||
| {{ e.name }} | {{ e.description }} | `{{ e.service }}` | `{{ e.namespace }}` | {% if e.url | default('') | length > 0 %}[{{ e.url }}]({{ e.url }}){% else %}—{% endif %} |
|
||||
{% endfor %}
|
||||
|
||||
## Initial access (logins)
|
||||
|
||||
| App | Username / identity | Password / secret |
|
||||
|-----|---------------------|-------------------|
|
||||
| **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** | — | 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)
|
||||
|
||||
```bash
|
||||
# Argo CD initial admin (Secret removed after you change password)
|
||||
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath='{.data.password}' | base64 -d
|
||||
echo
|
||||
|
||||
# Grafana admin user / password
|
||||
kubectl -n monitoring get secret kube-prometheus-grafana -o jsonpath='{.data.admin-user}' | base64 -d
|
||||
echo
|
||||
kubectl -n monitoring get secret kube-prometheus-grafana -o jsonpath='{.data.admin-password}' | base64 -d
|
||||
echo
|
||||
```
|
||||
|
||||
To generate this file **without** calling kubectl, run Ansible with **`-e noble_landing_urls_fetch_credentials=false`**.
|
||||
|
||||
## Notes
|
||||
|
||||
- **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.
|
||||
- **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`.
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
# Helm --wait default (5m) is often too short for first Longhorn install on several nodes
|
||||
# (image pulls + manager/driver ordering). See ansible/roles/noble_metallb/defaults/main.yml.
|
||||
noble_helm_longhorn_wait_timeout: 20m
|
||||
|
||||
# Transient Kyverno webhook timeouts during post-upgrade hooks / admission storms.
|
||||
noble_helm_longhorn_retries: 8
|
||||
noble_helm_longhorn_retry_delay: 25
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
- name: Apply Longhorn namespace (PSA) from kustomization
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- apply
|
||||
- -k
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/longhorn"
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
changed_when: true
|
||||
|
||||
- name: Install Longhorn chart
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- helm
|
||||
- upgrade
|
||||
- --install
|
||||
- longhorn
|
||||
- longhorn/longhorn
|
||||
- -n
|
||||
- longhorn-system
|
||||
- --create-namespace
|
||||
- --version
|
||||
- "1.11.2"
|
||||
- -f
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/longhorn/values.yaml"
|
||||
- --force-conflicts
|
||||
- --wait
|
||||
- --timeout
|
||||
- "{{ noble_helm_longhorn_wait_timeout }}"
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
register: noble_longhorn_helm
|
||||
retries: "{{ noble_helm_longhorn_retries | int }}"
|
||||
delay: "{{ noble_helm_longhorn_retry_delay | int }}"
|
||||
until: noble_longhorn_helm.rc == 0
|
||||
changed_when: true
|
||||
@@ -1,3 +0,0 @@
|
||||
---
|
||||
# Helm **--wait** default is often too short when images pull slowly or nodes are busy.
|
||||
noble_helm_metallb_wait_timeout: 20m
|
||||
@@ -1,40 +0,0 @@
|
||||
---
|
||||
- name: Apply MetalLB namespace (Pod Security labels)
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- apply
|
||||
- -f
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/metallb/namespace.yaml"
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
changed_when: true
|
||||
|
||||
- name: Install MetalLB chart
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- helm
|
||||
- upgrade
|
||||
- --install
|
||||
- metallb
|
||||
- metallb/metallb
|
||||
- --namespace
|
||||
- metallb-system
|
||||
- --force-conflicts
|
||||
- --wait
|
||||
- --timeout
|
||||
- "{{ noble_helm_metallb_wait_timeout }}"
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
changed_when: true
|
||||
|
||||
- name: Apply IPAddressPool and L2Advertisement
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- apply
|
||||
- -k
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/metallb"
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
changed_when: true
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
- name: Install metrics-server
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- helm
|
||||
- upgrade
|
||||
- --install
|
||||
- metrics-server
|
||||
- metrics-server/metrics-server
|
||||
- -n
|
||||
- kube-system
|
||||
- --version
|
||||
- "3.13.0"
|
||||
- -f
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/metrics-server/values.yaml"
|
||||
- --force-conflicts
|
||||
- --wait
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
changed_when: true
|
||||
@@ -1,11 +0,0 @@
|
||||
---
|
||||
# Set true after creating the newt-pangolin-auth Secret (see role / cluster docs).
|
||||
noble_newt_install: true
|
||||
|
||||
# Pangolin Integration API — idempotent HTTP resources + Traefik targets (see clusters/noble/bootstrap/newt/README.md §4).
|
||||
noble_pangolin_sync_http_resources: false
|
||||
# Extra FQDNs to sync (in addition to **noble_authentik_ingress_extra_hosts** + **noble_open_webui_public_host** when set).
|
||||
noble_pangolin_http_fqdns_extra: []
|
||||
# Traefik HTTPS backend for Pangolin targets (MetalLB / LAN VIP). Empty → **kubectl** discovers the Traefik Service.
|
||||
noble_pangolin_traefik_target_ip: ""
|
||||
noble_pangolin_traefik_target_port: 443
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
# See repository **.env.sample** — copy to **.env** (gitignored).
|
||||
- name: Stat repository .env for deploy secrets
|
||||
ansible.builtin.stat:
|
||||
path: "{{ noble_repo_root }}/.env"
|
||||
register: noble_deploy_env_file
|
||||
changed_when: false
|
||||
|
||||
- name: Create newt-pangolin-auth Secret from .env
|
||||
ansible.builtin.shell: |
|
||||
set -euo pipefail
|
||||
set -a
|
||||
. "{{ noble_repo_root }}/.env"
|
||||
set +a
|
||||
if [ -z "${PANGOLIN_ENDPOINT:-}" ] || [ -z "${NEWT_ID:-}" ] || [ -z "${NEWT_SECRET:-}" ]; then
|
||||
echo NO_VARS
|
||||
exit 0
|
||||
fi
|
||||
kubectl -n newt create secret generic newt-pangolin-auth \
|
||||
--from-literal=PANGOLIN_ENDPOINT="${PANGOLIN_ENDPOINT}" \
|
||||
--from-literal=NEWT_ID="${NEWT_ID}" \
|
||||
--from-literal=NEWT_SECRET="${NEWT_SECRET}" \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
echo APPLIED
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
when: noble_deploy_env_file.stat.exists | default(false)
|
||||
no_log: true
|
||||
register: noble_newt_secret_from_env
|
||||
changed_when: "'APPLIED' in (noble_newt_secret_from_env.stdout | default(''))"
|
||||
@@ -1,48 +0,0 @@
|
||||
---
|
||||
- name: Skip Newt when not enabled
|
||||
ansible.builtin.debug:
|
||||
msg: "noble_newt_install is false — set PANGOLIN_ENDPOINT, NEWT_ID, NEWT_SECRET in repo .env (or create the Secret manually) and set noble_newt_install=true to deploy Newt."
|
||||
when: not (noble_newt_install | bool)
|
||||
tags: [newt]
|
||||
|
||||
- name: Deploy Newt (Pangolin tunnel) and optional Pangolin HTTP resource sync
|
||||
when: noble_newt_install | bool
|
||||
tags: [newt, pangolin]
|
||||
block:
|
||||
- name: Create Newt namespace
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- apply
|
||||
- -f
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/newt/namespace.yaml"
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
changed_when: true
|
||||
|
||||
- name: Apply Newt Pangolin auth Secret from repository .env (optional)
|
||||
ansible.builtin.include_tasks: from_env.yml
|
||||
|
||||
- name: Install Newt chart
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- helm
|
||||
- upgrade
|
||||
- --install
|
||||
- newt
|
||||
- fossorial/newt
|
||||
- --namespace
|
||||
- newt
|
||||
- --version
|
||||
- "1.5.0"
|
||||
- -f
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/newt/values.yaml"
|
||||
- --force-conflicts
|
||||
- --wait
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
changed_when: true
|
||||
|
||||
- name: Optional Pangolin Integration API (HTTP resources + Traefik targets)
|
||||
ansible.builtin.include_tasks: pangolin_sync.yml
|
||||
when: noble_pangolin_sync_http_resources | default(false) | bool
|
||||
@@ -1,95 +0,0 @@
|
||||
---
|
||||
# Pangolin Integration API — public HTTP resources → Newt site → Traefik (see clusters/noble/bootstrap/newt/README.md §4).
|
||||
# Included only when **noble_pangolin_sync_http_resources** is true.
|
||||
- name: Build Pangolin HTTP FQDN list
|
||||
ansible.builtin.set_fact:
|
||||
noble_pangolin_http_fqdns_effective: >-
|
||||
{{
|
||||
(
|
||||
noble_pangolin_http_fqdns_extra | default([])
|
||||
+ (noble_authentik_ingress_extra_hosts | default([]))
|
||||
+ ([noble_open_webui_public_host | trim] if (noble_open_webui_public_host | default('') | trim | length) > 0 else [])
|
||||
) | unique | list
|
||||
}}
|
||||
|
||||
- name: Discover Traefik LoadBalancer IP for Pangolin targets (when not set explicitly)
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- get
|
||||
- svc
|
||||
- -n
|
||||
- traefik
|
||||
- -l
|
||||
- app.kubernetes.io/name=traefik
|
||||
- -o
|
||||
- jsonpath={.items[0].status.loadBalancer.ingress[0].ip}
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
register: noble_pangolin_traefik_lb_ip
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
when:
|
||||
- noble_pangolin_http_fqdns_effective | length > 0
|
||||
- noble_pangolin_traefik_target_ip | default('') | trim | length == 0
|
||||
|
||||
- name: Resolve Traefik IP for Pangolin sync
|
||||
ansible.builtin.set_fact:
|
||||
noble_pangolin_traefik_ip_resolved: >-
|
||||
{{
|
||||
(noble_pangolin_traefik_target_ip | default('') | trim)
|
||||
if (noble_pangolin_traefik_target_ip | default('') | trim | length > 0)
|
||||
else (noble_pangolin_traefik_lb_ip.stdout | default('') | trim)
|
||||
}}
|
||||
when: noble_pangolin_http_fqdns_effective | length > 0
|
||||
|
||||
- name: Require Traefik IP for Pangolin sync
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- noble_pangolin_traefik_ip_resolved | length > 0
|
||||
fail_msg: >-
|
||||
Set **noble_pangolin_traefik_target_ip** in inventory (Traefik Service LoadBalancer / MetalLB IP), or ensure
|
||||
**kubectl** can read **traefik** Services (see **clusters/noble/bootstrap/traefik/**).
|
||||
when: noble_pangolin_http_fqdns_effective | length > 0
|
||||
|
||||
- name: Stat repository .env for Pangolin API credentials
|
||||
ansible.builtin.stat:
|
||||
path: "{{ noble_repo_root }}/.env"
|
||||
register: noble_pangolin_env_file
|
||||
changed_when: false
|
||||
when: noble_pangolin_http_fqdns_effective | length > 0
|
||||
|
||||
- name: Require .env for Pangolin Integration API secrets
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- noble_pangolin_env_file.stat.exists | default(false)
|
||||
fail_msg: >-
|
||||
Pangolin sync needs **.env** at the repo root with **NOBLE_PANGOLIN_*** variables (see **.env.sample**).
|
||||
when: noble_pangolin_http_fqdns_effective | length > 0
|
||||
|
||||
- name: Sync Pangolin public HTTP resources (Integration API)
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- python3
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/newt/scripts/sync_pangolin_http_resources.py"
|
||||
- "--env-file"
|
||||
- "{{ noble_repo_root }}/.env"
|
||||
- "--fqdns"
|
||||
- "{{ noble_pangolin_http_fqdns_effective | join(',') }}"
|
||||
- "--traefik-ip"
|
||||
- "{{ noble_pangolin_traefik_ip_resolved }}"
|
||||
- "--traefik-port"
|
||||
- "{{ noble_pangolin_traefik_target_port | int | string }}"
|
||||
register: noble_pangolin_sync_cmd
|
||||
changed_when: >-
|
||||
'[create]' in (noble_pangolin_sync_cmd.stdout | default(''))
|
||||
or '[target]' in (noble_pangolin_sync_cmd.stdout | default(''))
|
||||
or 'target created' in (noble_pangolin_sync_cmd.stdout | default(''))
|
||||
when: noble_pangolin_http_fqdns_effective | length > 0
|
||||
|
||||
- name: Skip Pangolin sync (no public FQDNs configured)
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
noble_pangolin_sync_http_resources is true but the FQDN list is empty
|
||||
(set **noble_authentik_ingress_extra_hosts**, **noble_open_webui_public_host**, and/or **noble_pangolin_http_fqdns_extra**).
|
||||
when: noble_pangolin_http_fqdns_effective | length == 0
|
||||
@@ -1,22 +0,0 @@
|
||||
---
|
||||
# kubectl apply -k can hit transient etcd timeouts under load; retries + longer API deadline help.
|
||||
noble_platform_kubectl_request_timeout: 120s
|
||||
noble_platform_kustomize_retries: 5
|
||||
noble_platform_kustomize_delay: 20
|
||||
|
||||
# kube-prometheus-stack: operator Deployment uses Kubernetes default progressDeadlineSeconds (600s).
|
||||
# First install (images + cert-manager webhook TLS) can exceed that; patch + optional rollout restart, then Helm --wait.
|
||||
noble_platform_kube_prometheus_operator_progress_deadline_seconds: 1800
|
||||
noble_platform_kube_prometheus_operator_wait_retries: 60
|
||||
noble_platform_kube_prometheus_operator_wait_delay: 5
|
||||
# Longhorn PVCs + full stack often need 45-60m; node-exporter DaemonSet can be last at 3/4 until one node catches up.
|
||||
noble_platform_kube_prometheus_helm_wait_timeout: 60m
|
||||
# Loki SingleBinary + Longhorn PVC: Helm **--wait** can exceed **5m** defaults; raise if Longhorn attach is slow.
|
||||
noble_platform_loki_helm_wait_timeout: 30m
|
||||
# Before Loki (first Longhorn PVC workload), ensure CSI plugin DaemonSet is fully rolled out (avoids **FailedMount** / backend timeouts).
|
||||
noble_platform_wait_longhorn_csi_before_loki: true
|
||||
noble_platform_longhorn_csi_rollout_timeout: 15m
|
||||
|
||||
# Decrypt **clusters/noble/secrets/*.yaml** with SOPS and kubectl apply (requires **sops**, **age**, and **age-key.txt**).
|
||||
noble_apply_sops_secrets: true
|
||||
noble_sops_age_key_file: "{{ noble_repo_root }}/age-key.txt"
|
||||
@@ -1,220 +0,0 @@
|
||||
---
|
||||
# Mirrors former **noble-platform** Argo Application: Helm releases + plain manifests under clusters/noble/bootstrap.
|
||||
- name: Apply clusters/noble/bootstrap kustomize (namespaces, Grafana Loki datasource)
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- apply
|
||||
- "--request-timeout={{ noble_platform_kubectl_request_timeout }}"
|
||||
- -k
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap"
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
register: noble_platform_kustomize
|
||||
retries: "{{ noble_platform_kustomize_retries | int }}"
|
||||
delay: "{{ noble_platform_kustomize_delay | int }}"
|
||||
until: noble_platform_kustomize.rc == 0
|
||||
changed_when: true
|
||||
|
||||
- name: Stat SOPS age private key (age-key.txt)
|
||||
ansible.builtin.stat:
|
||||
path: "{{ noble_sops_age_key_file }}"
|
||||
register: noble_sops_age_key_stat
|
||||
|
||||
- name: Apply SOPS-encrypted cluster secrets (clusters/noble/secrets/*.yaml)
|
||||
ansible.builtin.shell: |
|
||||
set -euo pipefail
|
||||
shopt -s nullglob
|
||||
for f in "{{ noble_repo_root }}/clusters/noble/secrets"/*.yaml; do
|
||||
sops -d "$f" | kubectl apply -f -
|
||||
done
|
||||
args:
|
||||
executable: /bin/bash
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
SOPS_AGE_KEY_FILE: "{{ noble_sops_age_key_file }}"
|
||||
when:
|
||||
- noble_apply_sops_secrets | default(true) | bool
|
||||
- noble_sops_age_key_stat.stat.exists
|
||||
changed_when: true
|
||||
|
||||
# Helm --wait alone cannot extend the operator Deployment's progressDeadlineSeconds (default 10m).
|
||||
- name: Install kube-prometheus-stack (apply without Helm wait)
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- helm
|
||||
- upgrade
|
||||
- --install
|
||||
- kube-prometheus
|
||||
- prometheus-community/kube-prometheus-stack
|
||||
- -n
|
||||
- monitoring
|
||||
- --version
|
||||
- "85.0.3"
|
||||
- -f
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/kube-prometheus-stack/values.yaml"
|
||||
- --force-conflicts
|
||||
- --wait=false
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
changed_when: true
|
||||
|
||||
- name: Wait for prometheus-operator Deployment object
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- get
|
||||
- deployment/kube-prometheus-kube-prome-operator
|
||||
- -n
|
||||
- monitoring
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
register: noble_kube_prom_operator_deploy
|
||||
until: noble_kube_prom_operator_deploy.rc == 0
|
||||
retries: "{{ noble_platform_kube_prometheus_operator_wait_retries | int }}"
|
||||
delay: "{{ noble_platform_kube_prometheus_operator_wait_delay | int }}"
|
||||
changed_when: false
|
||||
|
||||
- name: Extend prometheus-operator Deployment progress deadline
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- patch
|
||||
- deployment/kube-prometheus-kube-prome-operator
|
||||
- -n
|
||||
- monitoring
|
||||
- --type=merge
|
||||
- -p
|
||||
- "{{ {'spec': {'progressDeadlineSeconds': (noble_platform_kube_prometheus_operator_progress_deadline_seconds | int)}} | to_json }}"
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
changed_when: true
|
||||
|
||||
- name: Restart prometheus-operator if Deployment already hit progress deadline
|
||||
ansible.builtin.shell: |
|
||||
set -euo pipefail
|
||||
dep=kube-prometheus-kube-prome-operator
|
||||
msg=$(kubectl get deployment "$dep" -n monitoring -o jsonpath='{.status.conditions[?(@.type=="Progressing")].message}' 2>/dev/null || true)
|
||||
reason=$(kubectl get deployment "$dep" -n monitoring -o jsonpath='{.status.conditions[?(@.type=="Progressing")].reason}' 2>/dev/null || true)
|
||||
combined="${reason}${msg}"
|
||||
if printf '%s' "$combined" | grep -qiE 'ProgressDeadlineExceeded|progress[[:space:]]*deadline[[:space:]]*exceeded'; then
|
||||
kubectl rollout restart deployment/"$dep" -n monitoring
|
||||
echo restarted
|
||||
fi
|
||||
args:
|
||||
executable: /bin/bash
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
register: noble_kube_prom_operator_restart
|
||||
changed_when: "'restarted' in noble_kube_prom_operator_restart.stdout"
|
||||
|
||||
# Helm --wait prints nothing until done or timeout; override noble_platform_kube_prometheus_helm_wait_timeout if needed.
|
||||
- name: Install kube-prometheus-stack (Helm wait for full release; often 30-60m silent - watch kubectl -n monitoring get pods,ds,pvc)
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- helm
|
||||
- upgrade
|
||||
- --install
|
||||
- kube-prometheus
|
||||
- prometheus-community/kube-prometheus-stack
|
||||
- -n
|
||||
- monitoring
|
||||
- --version
|
||||
- "85.0.3"
|
||||
- -f
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/kube-prometheus-stack/values.yaml"
|
||||
- --force-conflicts
|
||||
- --wait
|
||||
- --timeout
|
||||
- "{{ noble_platform_kube_prometheus_helm_wait_timeout }}"
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
changed_when: true
|
||||
|
||||
- name: Wait for Longhorn CSI plugin before Loki (PVC attach)
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- rollout
|
||||
- status
|
||||
- daemonset/longhorn-csi-plugin
|
||||
- -n
|
||||
- longhorn-system
|
||||
- --timeout={{ noble_platform_longhorn_csi_rollout_timeout }}
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
when: noble_platform_wait_longhorn_csi_before_loki | default(true) | bool
|
||||
changed_when: false
|
||||
|
||||
- name: Install Loki
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- helm
|
||||
- upgrade
|
||||
- --install
|
||||
- loki
|
||||
- grafana/loki
|
||||
- -n
|
||||
- loki
|
||||
- --version
|
||||
- "7.0.0"
|
||||
- -f
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/loki/values.yaml"
|
||||
- --force-conflicts
|
||||
- --wait
|
||||
- --timeout
|
||||
- "{{ noble_platform_loki_helm_wait_timeout }}"
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
changed_when: true
|
||||
|
||||
- name: Install Fluent Bit
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- helm
|
||||
- upgrade
|
||||
- --install
|
||||
- fluent-bit
|
||||
- fluent/fluent-bit
|
||||
- -n
|
||||
- logging
|
||||
- --version
|
||||
- "0.57.5"
|
||||
- -f
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/fluent-bit/values.yaml"
|
||||
- --force-conflicts
|
||||
- --wait
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
changed_when: true
|
||||
|
||||
- name: Install Headlamp
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- helm
|
||||
- upgrade
|
||||
- --install
|
||||
- headlamp
|
||||
- headlamp/headlamp
|
||||
- --version
|
||||
- "0.42.0"
|
||||
- -n
|
||||
- headlamp
|
||||
- -f
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/headlamp/values.yaml"
|
||||
- --force-conflicts
|
||||
- --wait
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
changed_when: true
|
||||
|
||||
- name: Apply Headlamp static manifests (metrics RBAC + OIDC group binding when used)
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- kubectl
|
||||
- apply
|
||||
- -k
|
||||
- "{{ noble_repo_root }}/clusters/noble/bootstrap/headlamp"
|
||||
environment:
|
||||
KUBECONFIG: "{{ noble_kubeconfig }}"
|
||||
changed_when: true
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user