digitalboard.core/roles/garage/templates/docker-compose.yml.j2
Simon Bärlocher 0733d5710f
feat(garage,authentik): console multi-domain + standalone proxy outpost
Two related additions for gating a cross-host service (garage WebUI on a
storage host) behind authentik without breaking on TLS or X-Forwarded-Host:

garage role:
- Add garage_webui_domains (list), mirroring garage_s3_domains. The
  console router now accepts every entry, so a DMZ reverseproxy can use a
  backend_host whose cert SAN matches an internal *.int.* name instead of
  connecting by IP (which fails acme cert verification). Defaults to the
  single garage_webui_domain so existing inventories are unaffected.

authentik_outpost_proxy role (new):
- Standalone proxy (ForwardAuth) outpost, modelled on authentik_outpost_ldap.
  Co-locate it with the protected service so the ForwardAuth subrequest
  reaches authentik over the local docker network. The embedded outpost
  only works for services on the authentik host: a cross-host subrequest
  routed through an extra reverse-proxy hop arrives with a polluted
  X-Forwarded-Host (port/comma-appended), which the outpost no longer
  matches against the provider's external_host (404).
2026-06-05 14:22:36 +02:00

80 lines
No EOL
4.3 KiB
Django/Jinja

services:
{{ garage_service_name }}:
container_name: {{ garage_service_name }}
image: {{ garage_image }}
restart: always
volumes:
- {{ garage_docker_volume_dir }}/meta:/var/lib/garage/meta
- {{ garage_docker_volume_dir }}/data:/var/lib/garage/data
- {{ garage_docker_compose_dir }}/garage.toml:/etc/garage.toml
networks:
- {{ garage_traefik_network }}
- {{ garage_internal_network }}
labels:
- traefik.enable=true
- traefik.docker.network={{ garage_traefik_network }}
# S3 API endpoint
- traefik.http.routers.{{ garage_service_name }}.rule={% for d in garage_s3_domains %}Host(`{{ d }}`){% if not loop.last %} || {% endif %}{% endfor +%}
{% if garage_use_ssl %}
- traefik.http.routers.{{ garage_service_name }}.entrypoints=websecure
- traefik.http.routers.{{ garage_service_name }}.tls=true
{% if traefik_cert_mode | default('selfsigned') == 'acme' %}
- traefik.http.routers.{{ garage_service_name }}.tls.certresolver={{ traefik_ssl_cert_resolver | default('dns') }}
{% endif %}
{% else %}
- traefik.http.routers.{{ garage_service_name }}.entrypoints=web
{% endif %}
- traefik.http.routers.{{ garage_service_name }}.service={{ garage_service_name }}-api
- traefik.http.routers.{{ garage_service_name }}.priority=50
- traefik.http.services.{{ garage_service_name }}-api.loadbalancer.server.port={{ garage_s3_api_port }}
{% if garage_webui_enabled %}
{{ garage_service_name }}-webui:
container_name: {{ garage_service_name }}-webui
image: {{ garage_webui_image }}
restart: always
depends_on:
- {{ garage_service_name }}
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:
- {{ garage_traefik_network }}
- {{ garage_internal_network }}
labels:
- traefik.enable=true
- traefik.docker.network={{ garage_traefik_network }}
- traefik.http.routers.{{ garage_service_name }}-console.rule={% for d in garage_webui_domains %}Host(`{{ d }}`){% if not loop.last %} || {% endif %}{% endfor +%}
{% if garage_use_ssl %}
- traefik.http.routers.{{ garage_service_name }}-console.entrypoints=websecure
- traefik.http.routers.{{ garage_service_name }}-console.tls=true
{% if traefik_cert_mode | default('selfsigned') == 'acme' %}
- traefik.http.routers.{{ garage_service_name }}-console.tls.certresolver={{ traefik_ssl_cert_resolver | default('dns') }}
{% endif %}
{% else %}
- traefik.http.routers.{{ garage_service_name }}-console.entrypoints=web
{% endif %}
- 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:
{{ garage_internal_network }}:
{{ garage_traefik_network }}:
external: true