From fbcd2416e6c26c438dfeaa2db6260a211ee0a40a Mon Sep 17 00:00:00 2001 From: Nikholas Pcenicni <82239765+nikpcenicni@users.noreply.github.com> Date: Thu, 14 May 2026 22:21:11 -0400 Subject: [PATCH] Add optional SMTP configuration for Authentik, including email host, port, and credentials. Update README and .env.sample to clarify usage for outbound email settings. Introduce blueprint support for enhanced deployment flexibility, with assertions in Ansible tasks to ensure required variables are set when enabled. --- .env.sample | 10 ++ README.md | 2 +- ansible/inventory/group_vars/all.yml | 1 + ansible/roles/noble_authentik/README.md | 35 +++- .../roles/noble_authentik/defaults/main.yml | 29 +++ .../roles/noble_authentik/tasks/from_env.yml | 165 ++++++++++++++++++ ansible/roles/noble_authentik/tasks/main.yml | 50 ++++++ .../templates/authentik-extra-values.yaml.j2 | 23 +++ .../blueprints/10-noble-public-groups.yaml.j2 | 13 ++ ...e-lab-operator-authentication-flow.yaml.j2 | 101 +++++++++++ .../30-noble-brands-domain-split.yaml.j2 | 27 +++ .../noble/bootstrap/authentik/values.yaml | 2 + komodo/s3/versitygw/.env.sample | 23 ++- komodo/s3/versitygw/compose.yaml | 3 +- 14 files changed, 478 insertions(+), 6 deletions(-) create mode 100644 ansible/roles/noble_authentik/templates/blueprints/10-noble-public-groups.yaml.j2 create mode 100644 ansible/roles/noble_authentik/templates/blueprints/20-noble-lab-operator-authentication-flow.yaml.j2 create mode 100644 ansible/roles/noble_authentik/templates/blueprints/30-noble-brands-domain-split.yaml.j2 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"