From c27584cd9cd484e888c27af30f9dc143cb6a48f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20B=C3=A4rlocher?= Date: Tue, 26 May 2026 14:03:38 +0200 Subject: [PATCH] feat(drawio,garage): optional Authentik ForwardAuth in front of UIs Add `*_authentik_forward_auth` + `*_authentik_forward_auth_url` knobs to both roles. When enabled: * drawio: traefik attaches a ForwardAuth middleware pointing at the authentik embedded outpost; unauthenticated requests get redirected to log in and downstream sees X-Authentik-* identity headers. * garage WebUI: same ForwardAuth wiring, and `AUTH_USER_PASS` is dropped from the container env so authentik is the only gate. Tasks now key the htpasswd hash workflow off `_garage_webui_htpasswd_active` (`webui_enabled AND NOT authentik_forward_auth`); when authentik fronts the UI we skip hashing entirely. htpasswd hash is also now cached on disk and re-verified via `htpasswd -vbB` so unchanged passwords stop showing as `changed=true` on every run. Both knobs default to `false`, preserving existing htpasswd/plain behaviour. --- roles/drawio/defaults/main.yml | 9 ++- roles/drawio/templates/docker-compose.yml.j2 | 9 +++ roles/garage/defaults/main.yml | 12 +++- roles/garage/tasks/main.yml | 75 ++++++++++++++++++-- roles/garage/templates/docker-compose.yml.j2 | 12 ++++ 5 files changed, 110 insertions(+), 7 deletions(-) diff --git a/roles/drawio/defaults/main.yml b/roles/drawio/defaults/main.yml index 7b67976..2b2b758 100644 --- a/roles/drawio/defaults/main.yml +++ b/roles/drawio/defaults/main.yml @@ -17,4 +17,11 @@ drawio_extra_hosts: [] # Traefik configuration drawio_traefik_network: "proxy" -drawio_use_ssl: true \ No newline at end of file +drawio_use_ssl: true + +# Optional Authentik ForwardAuth (set to true and provide the URL to gate +# drawio behind an authentik proxy provider). Expects the authentik +# embedded outpost to expose the /outpost.goauthentik.io/auth/traefik +# endpoint on the configured URL (typically the public auth.* FQDN). +drawio_authentik_forward_auth: false +drawio_authentik_forward_auth_url: "" \ No newline at end of file diff --git a/roles/drawio/templates/docker-compose.yml.j2 b/roles/drawio/templates/docker-compose.yml.j2 index b6b9ef5..c9b0c9a 100644 --- a/roles/drawio/templates/docker-compose.yml.j2 +++ b/roles/drawio/templates/docker-compose.yml.j2 @@ -22,6 +22,15 @@ services: {% else %} - traefik.http.routers.{{ drawio_service_name }}.entrypoints=web {% endif %} +{% if drawio_authentik_forward_auth | default(false) %} + # ForwardAuth via the authentik embedded outpost. Unauthenticated + # requests get redirected to authentik to log in; authentik then + # sets X-Authentik-* headers traefik forwards downstream. + - traefik.http.middlewares.{{ drawio_service_name }}-authentik.forwardauth.address={{ drawio_authentik_forward_auth_url }} + - traefik.http.middlewares.{{ drawio_service_name }}-authentik.forwardauth.trustForwardHeader=true + - traefik.http.middlewares.{{ drawio_service_name }}-authentik.forwardauth.authResponseHeaders=X-authentik-username,X-authentik-groups,X-authentik-entitlements,X-authentik-email,X-authentik-name,X-authentik-uid,X-authentik-jwt,X-authentik-meta-jwks,X-authentik-meta-outpost,X-authentik-meta-provider,X-authentik-meta-app,X-authentik-meta-version + - traefik.http.routers.{{ drawio_service_name }}.middlewares={{ drawio_service_name }}-authentik +{% endif %} networks: {{ drawio_traefik_network }}: diff --git a/roles/garage/defaults/main.yml b/roles/garage/defaults/main.yml index 091e318..5a207eb 100644 --- a/roles/garage/defaults/main.yml +++ b/roles/garage/defaults/main.yml @@ -25,10 +25,20 @@ garage_webui_domain: "console.storage.local.test" garage_webui_enabled: true garage_webui_image: "khairul169/garage-webui:latest" garage_webui_port: 3909 -# WebUI basic auth credentials (plaintext, will be hashed automatically) +# WebUI basic auth credentials (plaintext, will be hashed automatically). +# Ignored when garage_webui_authentik_forward_auth is true — in that case +# authentik handles authentication via the ForwardAuth middleware below. garage_webui_username: "admin" garage_webui_password: "admin" +# Optional Authentik ForwardAuth in front of the WebUI. When true: +# - the AUTH_USER_PASS env-var is dropped from the container so htpasswd +# isn't enforced; authentik is the only gate. +# - traefik attaches a ForwardAuth middleware pointing at the URL below. +# Leave false to keep classic htpasswd protection. +garage_webui_authentik_forward_auth: false +garage_webui_authentik_forward_auth_url: "" + # Garage ports garage_s3_api_port: 3900 garage_s3_web_port: 3902 diff --git a/roles/garage/tasks/main.yml b/roles/garage/tasks/main.yml index 4aebbeb..4478f51 100644 --- a/roles/garage/tasks/main.yml +++ b/roles/garage/tasks/main.yml @@ -26,12 +26,77 @@ dest: "{{ garage_docker_compose_dir }}/garage.toml" mode: '0644' -- name: Generate bcrypt hash for webui password using htpasswd - ansible.builtin.shell: | - htpasswd -nbBC 10 "{{ garage_webui_username }}" "{{ garage_webui_password }}" - register: _garage_webui_password_hash +- name: Set webui htpasswd activation fact + ansible.builtin.set_fact: + # htpasswd only runs when the WebUI is enabled AND authentik ForwardAuth + # is not handling authentication. When authentik is in front, the + # compose template drops AUTH_USER_PASS so no hash is needed. + _garage_webui_htpasswd_active: >- + {{ + garage_webui_enabled + and not (garage_webui_authentik_forward_auth | default(false)) + }} + +- name: Read cached webui htpasswd hash + ansible.builtin.slurp: + src: "{{ garage_docker_compose_dir }}/webui.htpasswd" + register: _garage_webui_htpasswd_cached + failed_when: false changed_when: false - when: garage_webui_enabled + when: _garage_webui_htpasswd_active + +- name: Verify cached webui htpasswd hash still matches password + ansible.builtin.command: + argv: + - htpasswd + - -vbB + - "{{ garage_docker_compose_dir }}/webui.htpasswd" + - "{{ garage_webui_username }}" + - "{{ garage_webui_password }}" + register: _garage_webui_htpasswd_verify + failed_when: false + changed_when: false + no_log: true + when: + - _garage_webui_htpasswd_active + - _garage_webui_htpasswd_cached.content is defined + +- name: Generate bcrypt hash for webui password using htpasswd + ansible.builtin.command: + argv: + - htpasswd + - -nbBC + - "10" + - "{{ garage_webui_username }}" + - "{{ garage_webui_password }}" + register: _garage_webui_password_hash_new + changed_when: true + when: + - _garage_webui_htpasswd_active + - (_garage_webui_htpasswd_cached.content is not defined) + or (_garage_webui_htpasswd_verify.rc | default(1) != 0) + +- name: Persist webui htpasswd hash on disk + ansible.builtin.copy: + content: "{{ _garage_webui_password_hash_new.stdout }}\n" + dest: "{{ garage_docker_compose_dir }}/webui.htpasswd" + mode: '0600' + when: + - _garage_webui_htpasswd_active + - _garage_webui_password_hash_new is changed + +- name: Load current webui htpasswd hash + ansible.builtin.slurp: + src: "{{ garage_docker_compose_dir }}/webui.htpasswd" + register: _garage_webui_htpasswd_current + changed_when: false + when: _garage_webui_htpasswd_active + +- name: Expose current webui htpasswd hash to template + ansible.builtin.set_fact: + _garage_webui_password_hash: + stdout: "{{ (_garage_webui_htpasswd_current.content | b64decode).strip() }}" + when: _garage_webui_htpasswd_active - name: Create docker-compose file for garage template: diff --git a/roles/garage/templates/docker-compose.yml.j2 b/roles/garage/templates/docker-compose.yml.j2 index d344e5f..7b1c017 100644 --- a/roles/garage/templates/docker-compose.yml.j2 +++ b/roles/garage/templates/docker-compose.yml.j2 @@ -38,7 +38,9 @@ services: environment: API_BASE_URL: "http://{{ garage_service_name }}:{{ garage_admin_port }}" S3_ENDPOINT_URL: "http://{{ garage_service_name }}:{{ garage_s3_api_port }}" +{% if not (garage_webui_authentik_forward_auth | default(false)) %} AUTH_USER_PASS: '{{ _garage_webui_password_hash.stdout | replace("$", "$$") }}' +{% endif %} volumes: - {{ garage_docker_compose_dir }}/garage.toml:/etc/garage.toml:ro networks: @@ -60,6 +62,16 @@ services: - traefik.http.routers.{{ garage_service_name }}-console.service={{ garage_service_name }}-console - traefik.http.routers.{{ garage_service_name }}-console.priority=10 - traefik.http.services.{{ garage_service_name }}-console.loadbalancer.server.port={{ garage_webui_port }} +{% if garage_webui_authentik_forward_auth | default(false) %} + # ForwardAuth via the authentik embedded outpost. Unauthenticated + # requests are redirected to authentik; authentik then forwards + # X-Authentik-* identity headers downstream. htpasswd is disabled + # in the env block above so authentik is the only gate. + - traefik.http.middlewares.{{ garage_service_name }}-console-authentik.forwardauth.address={{ garage_webui_authentik_forward_auth_url }} + - traefik.http.middlewares.{{ garage_service_name }}-console-authentik.forwardauth.trustForwardHeader=true + - traefik.http.middlewares.{{ garage_service_name }}-console-authentik.forwardauth.authResponseHeaders=X-authentik-username,X-authentik-groups,X-authentik-entitlements,X-authentik-email,X-authentik-name,X-authentik-uid,X-authentik-jwt,X-authentik-meta-jwks,X-authentik-meta-outpost,X-authentik-meta-provider,X-authentik-meta-app,X-authentik-meta-version + - traefik.http.routers.{{ garage_service_name }}-console.middlewares={{ garage_service_name }}-console-authentik +{% endif %} {% endif %} networks: