diff --git a/.env.sample b/.env.sample index 59f676f..e875db4 100644 --- a/.env.sample +++ b/.env.sample @@ -38,3 +38,13 @@ NOBLE_AUTHENTIK_MEDIA_S3_BUCKET= # 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 diff --git a/README.md b/README.md index d9090c6..c6dc6ef 100644 --- a/README.md +++ b/README.md @@ -184,7 +184,7 @@ Shared services used across multiple applications. - **[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`. Compose uses `${VAR}` interpolation so credentials work with Komodo’s `docker compose --env-file /.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). +**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 /.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). --- diff --git a/ansible/inventory/group_vars/all.yml b/ansible/inventory/group_vars/all.yml index 7fcb19c..3ae1adb 100644 --- a/ansible/inventory/group_vars/all.yml +++ b/ansible/inventory/group_vars/all.yml @@ -30,3 +30,4 @@ 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 +noble_authentik_blueprints_enabled: true \ No newline at end of file diff --git a/ansible/roles/noble_authentik/README.md b/ansible/roles/noble_authentik/README.md index 99d4330..fc9d31f 100644 --- a/ansible/roles/noble_authentik/README.md +++ b/ansible/roles/noble_authentik/README.md @@ -4,7 +4,7 @@ Installs **Authentik** (Helm `goauthentik/authentik`) as the cluster IdP, **oaut ## Enable -1. Copy repository **`.env.sample`** to **`.env`** and set every **`NOBLE_AUTHENTIK_*`** variable (see comments there). +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`**. @@ -18,13 +18,43 @@ See **`defaults/main.yml`**. Hostnames default to **`auth.apps.noble.lab.pcenicn 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. -In **Authentik**, add a **Brand** (or equivalent) for the new hostname if you want different titles/favicon; OAuth **redirect URIs** for each app must include issuer URLs that match what browsers use (often you keep **internal** issuer URLs in cluster apps and use the public URL only for human login, or align all apps to the public issuer — pick one strategy to avoid mixed **`iss`** / callback mismatches). +### 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`**: use a **restricted authentication flow** so only operator groups (defaults: **`noble-admins`**, **`authentik Admins`**) can complete sign-in on that hostname. 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). +- **Public Brand(s)** — one Brand per FQDN in **`noble_authentik_ingress_extra_hosts`**: use the stock **`default-authentication-flow`** (or replace with your own flow slug via a forked blueprint later). Assign general users to **`noble_authentik_blueprint_public_groups`** (defaults **`noble-public-users`**, **`noble-public-admins`**) for app policies and OAuth claims; **`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`** | Ensures **`noble_authentik_blueprint_public_groups`** exist. | +| **`20-noble-lab-operator-authentication-flow.yaml.j2`** | Flow **`noble_authentik_blueprint_lab_flow_slug`** + expression policy **`noble_authentik_blueprint_operator_policy_name`** (allowed groups **`noble_authentik_blueprint_lab_operator_groups`**). | +| **`30-noble-brands-domain-split.yaml.j2`** | Brand for **`noble_authentik_host`** → lab flow; one Brand per **`noble_authentik_ingress_extra_hosts`** → default authentication. | + +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. ### “Secondary tenant” (separate PostgreSQL schema — alpha) @@ -41,6 +71,7 @@ When **`noble_authentik_configure_idp`** is true, Ansible creates/updates OAuth2 ## 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**. 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`**. diff --git a/ansible/roles/noble_authentik/defaults/main.yml b/ansible/roles/noble_authentik/defaults/main.yml index ee1b07d..8f2e36c 100644 --- a/ansible/roles/noble_authentik/defaults/main.yml +++ b/ansible/roles/noble_authentik/defaults/main.yml @@ -26,6 +26,24 @@ noble_authentik_api_base: "{{ noble_authentik_public_url }}/api/v3" # 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 for the public Brand(s); adjust names to match your apps’ policies / OAuth claims. +noble_authentik_blueprint_public_groups: + - noble-public-users + - noble-public-admins +# 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: Noble public + 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). @@ -37,6 +55,17 @@ 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 diff --git a/ansible/roles/noble_authentik/tasks/from_env.yml b/ansible/roles/noble_authentik/tasks/from_env.yml index 9e00a3a..f107981 100644 --- a/ansible/roles/noble_authentik/tasks/from_env.yml +++ b/ansible/roles/noble_authentik/tasks/from_env.yml @@ -349,3 +349,168 @@ - 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 diff --git a/ansible/roles/noble_authentik/tasks/main.yml b/ansible/roles/noble_authentik/tasks/main.yml index d5d1ad3..1810819 100644 --- a/ansible/roles/noble_authentik/tasks/main.yml +++ b/ansible/roles/noble_authentik/tasks/main.yml @@ -39,6 +39,15 @@ 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" @@ -65,6 +74,47 @@ 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 > 0 + - noble_authentik_blueprint_lab_operator_groups | default([]) | length > 0 + - noble_authentik_blueprint_lab_flow_slug | default('') | trim | length > 0 + fail_msg: >- + When noble_authentik_blueprints_enabled is true, set noble_authentik_blueprint_public_groups (non-empty), + noble_authentik_blueprint_lab_operator_groups (non-empty), and noble_authentik_blueprint_lab_flow_slug. + See ansible/roles/noble_authentik/defaults/main.yml and README. + 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 + - 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: diff --git a/ansible/roles/noble_authentik/templates/authentik-extra-values.yaml.j2 b/ansible/roles/noble_authentik/templates/authentik-extra-values.yaml.j2 index 556ff91..eae02c4 100644 --- a/ansible/roles/noble_authentik/templates/authentik-extra-values.yaml.j2 +++ b/ansible/roles/noble_authentik/templates/authentik-extra-values.yaml.j2 @@ -25,6 +25,24 @@ global: 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 }}" @@ -46,3 +64,8 @@ server: - {{ h }} {% endfor %} {% endif %} +{% if noble_authentik_blueprints_enabled | default(false) | bool %} +blueprints: + configMaps: + - {{ noble_authentik_blueprints_configmap_name }} +{% endif %} diff --git a/ansible/roles/noble_authentik/templates/blueprints/10-noble-public-groups.yaml.j2 b/ansible/roles/noble_authentik/templates/blueprints/10-noble-public-groups.yaml.j2 new file mode 100644 index 0000000..93b6c5c --- /dev/null +++ b/ansible/roles/noble_authentik/templates/blueprints/10-noble-public-groups.yaml.j2 @@ -0,0 +1,13 @@ +# Noble — directory groups for the **public** hostname Brand (see role README). +# Groups are global to the instance; use policies and OAuth scope mappings to scope claims per app. +version: 1 +metadata: + name: noble-public-groups + labels: + blueprints.goauthentik.io/instantiate: "true" +entries: +{% for group in noble_authentik_blueprint_public_groups | default([]) %} + - model: authentik_core.group + identifiers: + name: "{{ group | trim }}" +{% endfor %} diff --git a/ansible/roles/noble_authentik/templates/blueprints/20-noble-lab-operator-authentication-flow.yaml.j2 b/ansible/roles/noble_authentik/templates/blueprints/20-noble-lab-operator-authentication-flow.yaml.j2 new file mode 100644 index 0000000..7d96c8b --- /dev/null +++ b/ansible/roles/noble_authentik/templates/blueprints/20-noble-lab-operator-authentication-flow.yaml.j2 @@ -0,0 +1,101 @@ +# Noble — authentication flow for the **lab** hostname Brand: only members of operator groups may continue. +# Reuses default identification / password / MFA / login stages; adds a policy on the password stage binding. +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_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: !Find [authentik_stages_password.passwordstage, [name, default-authentication-password]] + target: !KeyOf flow + attrs: + re_evaluate_policies: true + - id: noble-lab-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-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-authenticator-validate-optional + identifiers: + name: noble-lab-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_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: 10 + target: !KeyOf noble-lab-authenticator-binding + policy: !KeyOf noble-lab-authenticator-validate-optional + attrs: + failure_result: true diff --git a/ansible/roles/noble_authentik/templates/blueprints/30-noble-brands-domain-split.yaml.j2 b/ansible/roles/noble_authentik/templates/blueprints/30-noble-brands-domain-split.yaml.j2 new file mode 100644 index 0000000..8c995f7 --- /dev/null +++ b/ansible/roles/noble_authentik/templates/blueprints/30-noble-brands-domain-split.yaml.j2 @@ -0,0 +1,27 @@ +# Noble — Brands so **Host** selects authentication flow: lab hostname → operator-only flow; extra hosts → default login. +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, default-authentication-flow]] + flow_invalidation: !Find [authentik_flows.flow, [slug, default-invalidation-flow]] + flow_user_settings: !Find [authentik_flows.flow, [slug, default-user-settings-flow]] +{% endfor %} diff --git a/clusters/noble/bootstrap/authentik/values.yaml b/clusters/noble/bootstrap/authentik/values.yaml index ec17385..1f1e6c6 100644 --- a/clusters/noble/bootstrap/authentik/values.yaml +++ b/clusters/noble/bootstrap/authentik/values.yaml @@ -13,6 +13,8 @@ # # **Media / uploads:** **S3** (same endpoint/credentials pattern as **Velero** — see **ansible/roles/noble_authentik** and **.env.sample**). # Ansible sets **`AUTHENTIK_STORAGE__BACKEND=s3`** in **`authentik-extra-values.yaml.j2`**; use a **dedicated** media bucket, not the Velero backup bucket. +# **SMTP:** optional — set **`NOBLE_AUTHENTIK_SMTP_HOST`** + **`NOBLE_AUTHENTIK_SMTP_FROM`** in repo **`.env`** so Ansible injects **`AUTHENTIK_EMAIL__*`** into Helm **`global.env`** (see role README). +# **Blueprints:** optional **`blueprints.configMaps`** is merged by Ansible when **`noble_authentik_blueprints_enabled`** is true — see **`ansible/roles/noble_authentik/README.md`**. postgresql: enabled: true diff --git a/komodo/s3/versitygw/.env.sample b/komodo/s3/versitygw/.env.sample index 6b0ea78..a71b163 100644 --- a/komodo/s3/versitygw/.env.sample +++ b/komodo/s3/versitygw/.env.sample @@ -26,11 +26,30 @@ VERSITYGW_WEBUI_PORT=8080 # VGW_WEBUI_GATEWAYS=https://s3.example.com VGW_WEBUI_GATEWAYS= -# Public origin of the **WebUI** page (Pangolin → :8080), e.g. https://s3-ui.example.com -# Required when UI and API are on different hosts so the browser can call the API (CORS). +# Public origin of the **browser app** that calls the S3 API (no path, no trailing slash). +# Use this when the UI is on a different hostname than the API — e.g. third-party S3 consoles, +# or the built-in WebUI behind Pangolin on another host than :10000. +# Example: VGW_CORS_ALLOW_ORIGIN=https://s3-ui.pcenicni.dev +# +# VersityGW maps the browser’s preflight Access-Control-Request-Headers into +# Access-Control-Allow-Headers (Authorization, X-Amz-Date, X-Amz-Content-Sha256, Content-Type, …) +# when there is **no** per-bucket CORS configuration. You do not set those headers separately here. +# +# If you used PutBucketCors on a bucket, that config replaces this fallback for that bucket: add +# the same Origin and AllowedHeader entries (or *) there, or delete bucket CORS to rely on this. # VGW_CORS_ALLOW_ORIGIN=https://s3-ui.example.com VGW_CORS_ALLOW_ORIGIN= +# --- Pangolin (edge) vs Versity CORS --- +# If VGW_CORS_ALLOW_ORIGIN is correct but the browser still says CORS failed, the edge often +# never returns Versity’s Access-Control-* headers: Pangolin can answer OPTIONS / block preflight +# before Newt reaches :10000. S3 clients send OPTIONS without SigV4 auth; Pangolin SSO or +# “authorization” on the HTTP resource can reject that (see https://github.com/fosrl/pangolin/issues/2369 ). +# Mitigations: make the **S3 API** hostname resource public (no Pangolin auth on that resource), +# or add a rule that allows OPTIONS to pass through when Pangolin supports method-based rules; +# confirm with: curl -sv -X OPTIONS -H "Origin: https://your-s3-ui" -H "Access-Control-Request-Method: PUT" \ +# -H "Access-Control-Request-Headers: authorization,content-type" "https://your-s3-api-host/" 2>&1 | head -40 + # NFS: object metadata defaults to xattrs; most NFS mounts need sidecar mode # (compose.yaml uses --sidecar /data/sidecar). Create the host path, e.g. # mkdir -p /mnt/nfs/versity/sidecar diff --git a/komodo/s3/versitygw/compose.yaml b/komodo/s3/versitygw/compose.yaml index 4a55333..266199d 100644 --- a/komodo/s3/versitygw/compose.yaml +++ b/komodo/s3/versitygw/compose.yaml @@ -26,7 +26,8 @@ services: # Public base URL of the *S3 API* only (Pangolin → :10000). Not the WebUI hostname. # No trailing slash. If this points at the UI URL, bucket ops return 404/wrong host. VGW_WEBUI_GATEWAYS: ${VGW_WEBUI_GATEWAYS} - # Browser Origin when WebUI and API use different HTTPS hostnames (see wiki / WebGUI CORS). + # Browser Origin for cross-host S3 from the UI (maps to --cors-allow-origin). See .env.sample + # for third-party consoles vs bucket PutBucketCors overrides. VGW_CORS_ALLOW_ORIGIN: ${VGW_CORS_ALLOW_ORIGIN} ports: - "${VERSITYGW_PORT:-10000}:10000"