Compare commits
No commits in common. "feat/authentik-proxy-outpost-tokens" and "main" have entirely different histories.
feat/authe
...
main
17 changed files with 79 additions and 240 deletions
|
|
@ -49,32 +49,22 @@
|
|||
until: blueprint_wait_result.rc == 0
|
||||
when: blueprints_changed
|
||||
|
||||
# Pin known tokens on the LDAP outpost and on any proxy outpost that
|
||||
# declares one, so co-located outposts (e.g. a proxy outpost on another
|
||||
# host) can authenticate against the authentik server with the token from
|
||||
# the vault instead of the auto-generated one the blueprint creates.
|
||||
- name: Set fact whether any outpost needs a pinned token
|
||||
ansible.builtin.set_fact:
|
||||
_authentik_outpost_tokens: >-
|
||||
{{ (authentik_ldap_outpost.name is defined)
|
||||
or (authentik_proxy_outposts | default([]) | selectattr('token', 'defined') | list | length > 0) }}
|
||||
|
||||
- name: Render outpost token script
|
||||
- name: Render LDAP outpost token script
|
||||
template:
|
||||
src: set-outpost-token.py.j2
|
||||
dest: "{{ authentik_docker_volume_dir }}/data/set-outpost-token.py"
|
||||
mode: '0644'
|
||||
when: _authentik_outpost_tokens | bool
|
||||
register: outpost_token_script
|
||||
when: authentik_ldap_outpost.name is defined
|
||||
register: ldap_token_script
|
||||
|
||||
- name: Set known tokens for outposts
|
||||
- name: Set known token for LDAP outpost
|
||||
community.docker.docker_compose_v2_exec:
|
||||
project_src: "{{ authentik_docker_compose_dir }}"
|
||||
service: server
|
||||
command: ak shell -c "exec(open('/data/set-outpost-token.py').read())"
|
||||
register: outpost_token_result
|
||||
changed_when: "'changed' in outpost_token_result.stdout"
|
||||
register: ldap_token_result
|
||||
changed_when: "'changed' in ldap_token_result.stdout"
|
||||
retries: 30
|
||||
delay: 10
|
||||
until: outpost_token_result.rc == 0
|
||||
when: (_authentik_outpost_tokens | bool) and (blueprints_changed or outpost_token_script.changed)
|
||||
until: ldap_token_result.rc == 0
|
||||
when: authentik_ldap_outpost.name is defined and (blueprints_changed or ldap_token_script.changed)
|
||||
|
|
@ -23,9 +23,7 @@ services:
|
|||
command: server
|
||||
healthcheck:
|
||||
test: ["CMD", "ak", "healthcheck"]
|
||||
# Cold first boot runs DB migrations + app init (~90s observed);
|
||||
# start_period must cover that or compose --wait trips unhealthy.
|
||||
start_period: 120s
|
||||
start_period: 30s
|
||||
interval: 10s
|
||||
retries: 5
|
||||
timeout: 5s
|
||||
|
|
|
|||
|
|
@ -1,26 +1,10 @@
|
|||
from authentik.outposts.models import Outpost
|
||||
from authentik.core.models import Token
|
||||
|
||||
# (outpost name, desired token key) for every outpost we pin a known
|
||||
# token on: the LDAP outpost plus any proxy outpost that declares a
|
||||
# `token` (co-located proxy outposts authenticate with it against the
|
||||
# authentik server).
|
||||
_targets = [
|
||||
{% if authentik_ldap_outpost.name is defined %}
|
||||
('{{ authentik_ldap_outpost.name }}', '{{ authentik_ldap_outpost.token }}'),
|
||||
{% endif %}
|
||||
{% for o in authentik_proxy_outposts | default([]) if o.token is defined %}
|
||||
('{{ o.name }}', '{{ o.token }}'),
|
||||
{% endfor %}
|
||||
]
|
||||
|
||||
changed = False
|
||||
for name, key in _targets:
|
||||
o = Outpost.objects.get(name=name)
|
||||
t = Token.objects.get(identifier=o.token_identifier)
|
||||
if t.key != key:
|
||||
t.key = key
|
||||
o = Outpost.objects.get(name='{{ authentik_ldap_outpost.name }}')
|
||||
t = Token.objects.get(identifier=o.token_identifier)
|
||||
if t.key != '{{ authentik_ldap_outpost.token }}':
|
||||
t.key = '{{ authentik_ldap_outpost.token }}'
|
||||
t.save(update_fields=['key'])
|
||||
changed = True
|
||||
|
||||
print('changed' if changed else 'ok')
|
||||
print('changed')
|
||||
else:
|
||||
print('ok')
|
||||
|
|
|
|||
|
|
@ -1,58 +0,0 @@
|
|||
# authentik_outpost_proxy
|
||||
|
||||
Deploys an [authentik](https://goauthentik.io) proxy (ForwardAuth)
|
||||
outpost via Docker Compose. The outpost serves the
|
||||
`/outpost.goauthentik.io/auth/*` endpoints locally, so a Traefik
|
||||
`ForwardAuth` middleware on the same host can gate services behind
|
||||
authentik without routing the auth subrequest through additional reverse
|
||||
proxies.
|
||||
|
||||
Use this when the protected service runs on a **different host** than the
|
||||
authentik server: the embedded outpost only works for services co-located
|
||||
with authentik (the subrequest must reach it without an intermediate proxy
|
||||
mangling `X-Forwarded-Host`). Co-locating a proxy outpost with the service
|
||||
keeps the ForwardAuth subrequest on the local docker network.
|
||||
|
||||
The outpost connects back to an authentik server using an outpost token
|
||||
issued in the authentik admin interface (register the outpost there and
|
||||
assign the proxy providers it should serve). The image version must match
|
||||
the authentik server version.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Docker and Docker Compose on the target host (e.g. via
|
||||
`digitalboard.core.base`)
|
||||
- Ansible collection: `community.docker`
|
||||
- An authentik proxy outpost registered on the server, with the proxy
|
||||
providers for the protected services assigned to it.
|
||||
|
||||
## Role variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `authentik_outpost_proxy_image` | `ghcr.io/goauthentik/proxy:2026.2.2` | Outpost image (match the server version). |
|
||||
| `authentik_outpost_proxy_host` | `https://authentik.local.test` | URL of the authentik server. |
|
||||
| `authentik_outpost_proxy_token` | `changeme` | Outpost token — **override this**. |
|
||||
| `authentik_outpost_proxy_insecure` | `"true"` | Skip TLS verification toward the authentik server. |
|
||||
| `authentik_outpost_proxy_network` | `proxy` | Docker network the local Traefik routers and this outpost share. |
|
||||
| `authentik_outpost_proxy_extra_hosts` | `[]` | Extra `host:ip` entries for in-container DNS. |
|
||||
|
||||
The local ForwardAuth middleware then points at the container over the
|
||||
shared network, e.g.
|
||||
`http://authentik-outpost-proxy-proxy-1:9000/outpost.goauthentik.io/auth/traefik`.
|
||||
|
||||
## Example
|
||||
|
||||
```yaml
|
||||
- hosts: storage
|
||||
become: true
|
||||
roles:
|
||||
- role: digitalboard.core.authentik_outpost_proxy
|
||||
vars:
|
||||
authentik_outpost_proxy_host: "https://auth.example.com"
|
||||
authentik_outpost_proxy_token: "{{ vault_authentik_proxy_outpost_token }}"
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT-0
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
#SPDX-License-Identifier: MIT-0
|
||||
---
|
||||
# defaults file for authentik_outpost_proxy
|
||||
|
||||
# Base directory configuration (inherited from base role or defined here)
|
||||
docker_compose_base_dir: /etc/docker/compose
|
||||
docker_volume_base_dir: /srv/data
|
||||
|
||||
# Service configuration
|
||||
authentik_outpost_proxy_service_name: authentik-outpost-proxy
|
||||
authentik_outpost_proxy_docker_compose_dir: "{{ docker_compose_base_dir }}/{{ authentik_outpost_proxy_service_name }}"
|
||||
|
||||
# Container image (must match authentik server version)
|
||||
authentik_outpost_proxy_image: "ghcr.io/goauthentik/proxy:2026.2.2"
|
||||
|
||||
# Connection to authentik server
|
||||
authentik_outpost_proxy_host: "https://authentik.local.test"
|
||||
authentik_outpost_proxy_token: "changeme"
|
||||
authentik_outpost_proxy_insecure: "true"
|
||||
|
||||
# Traefik network the service-side routers live on, so the local
|
||||
# ForwardAuth middleware can reach this outpost over the docker network.
|
||||
authentik_outpost_proxy_network: "proxy"
|
||||
|
||||
# Extra hosts for DNS resolution within the container (e.g. pinning the
|
||||
# authentik FQDN to a backend IP when the public name is unreachable).
|
||||
authentik_outpost_proxy_extra_hosts: []
|
||||
# - "auth.example.com:192.168.56.11"
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
#SPDX-License-Identifier: MIT-0
|
||||
---
|
||||
# handlers file for authentik_outpost_proxy
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
#SPDX-License-Identifier: MIT-0
|
||||
galaxy_info:
|
||||
author: digitalboard
|
||||
description: Deploy an authentik proxy (ForwardAuth) outpost via Docker Compose, co-located with the services it protects
|
||||
company: Digitalboard
|
||||
license: MIT-0
|
||||
|
||||
min_ansible_version: "2.14"
|
||||
|
||||
platforms:
|
||||
- name: Debian
|
||||
versions:
|
||||
- bookworm
|
||||
- name: Ubuntu
|
||||
versions:
|
||||
- jammy
|
||||
- noble
|
||||
|
||||
galaxy_tags:
|
||||
- authentik
|
||||
- proxy
|
||||
- forwardauth
|
||||
- outpost
|
||||
- sso
|
||||
- docker
|
||||
- digitalboard
|
||||
|
||||
dependencies: []
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
#SPDX-License-Identifier: MIT-0
|
||||
---
|
||||
# tasks file for authentik_outpost_proxy
|
||||
|
||||
- name: Create docker compose directory
|
||||
file:
|
||||
path: "{{ authentik_outpost_proxy_docker_compose_dir }}"
|
||||
state: directory
|
||||
mode: '0755'
|
||||
|
||||
- name: Create docker-compose file for authentik proxy outpost
|
||||
template:
|
||||
src: docker-compose.yml.j2
|
||||
dest: "{{ authentik_outpost_proxy_docker_compose_dir }}/docker-compose.yml"
|
||||
mode: '0644'
|
||||
|
||||
- name: Start authentik proxy outpost container
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ authentik_outpost_proxy_docker_compose_dir }}"
|
||||
state: present
|
||||
wait: true
|
||||
wait_timeout: 120
|
||||
retries: 3
|
||||
delay: 15
|
||||
register: result
|
||||
until: result is not failed
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
services:
|
||||
proxy:
|
||||
image: {{ authentik_outpost_proxy_image }}
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
AUTHENTIK_HOST: {{ authentik_outpost_proxy_host }}
|
||||
AUTHENTIK_TOKEN: {{ authentik_outpost_proxy_token }}
|
||||
AUTHENTIK_INSECURE: "{{ authentik_outpost_proxy_insecure }}"
|
||||
{% if authentik_outpost_proxy_extra_hosts | length > 0 %}
|
||||
extra_hosts:
|
||||
{% for host in authentik_outpost_proxy_extra_hosts %}
|
||||
- "{{ host }}"
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
labels:
|
||||
- traefik.enable=false
|
||||
networks:
|
||||
- {{ authentik_outpost_proxy_network }}
|
||||
|
||||
networks:
|
||||
{{ authentik_outpost_proxy_network }}:
|
||||
external: true
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
#SPDX-License-Identifier: MIT-0
|
||||
localhost
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
#SPDX-License-Identifier: MIT-0
|
||||
---
|
||||
- hosts: localhost
|
||||
remote_user: root
|
||||
roles:
|
||||
- authentik_outpost_proxy
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
#SPDX-License-Identifier: MIT-0
|
||||
---
|
||||
# vars file for authentik_outpost_proxy
|
||||
|
|
@ -20,15 +20,7 @@ garage_image: "dxflrs/garage:v2.1.0"
|
|||
garage_s3_domains:
|
||||
- "storage.local.test"
|
||||
garage_web_domain: "web.storage.local.test"
|
||||
# Canonical WebUI console hostname (first entry of garage_webui_domains).
|
||||
garage_webui_domain: "console.storage.local.test"
|
||||
# FQDNs the WebUI console router accepts. The first entry is the canonical
|
||||
# public domain; further entries cover internal *.int.* names so a DMZ
|
||||
# reverseproxy can use a backend_host whose cert SAN matches (same pattern
|
||||
# as garage_s3_domains). Defaults to just the canonical domain so existing
|
||||
# inventories that only set garage_webui_domain keep working unchanged.
|
||||
garage_webui_domains:
|
||||
- "{{ garage_webui_domain }}"
|
||||
|
||||
# Garage WebUI configuration
|
||||
garage_webui_enabled: true
|
||||
|
|
|
|||
|
|
@ -45,16 +45,7 @@ argument_specs:
|
|||
garage_webui_domain:
|
||||
type: str
|
||||
default: console.storage.local.test
|
||||
description: Canonical WebUI console hostname (first entry of C(garage_webui_domains)).
|
||||
garage_webui_domains:
|
||||
type: list
|
||||
elements: str
|
||||
default: ['console.storage.local.test']
|
||||
description:
|
||||
- FQDNs the WebUI console router accepts. The first entry is the
|
||||
canonical public domain; further entries cover internal
|
||||
C(*.int.*) names so a DMZ reverseproxy can use a C(backend_host)
|
||||
whose cert SAN matches. Defaults to just C(garage_webui_domain).
|
||||
description: Hostname serving the WebUI console.
|
||||
|
||||
garage_webui_enabled:
|
||||
type: bool
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ services:
|
|||
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 +%}
|
||||
- traefik.http.routers.{{ garage_service_name }}-console.rule=Host(`{{ garage_webui_domain }}`)
|
||||
{% if garage_use_ssl %}
|
||||
- traefik.http.routers.{{ garage_service_name }}-console.entrypoints=websecure
|
||||
- traefik.http.routers.{{ garage_service_name }}-console.tls=true
|
||||
|
|
|
|||
|
|
@ -14,6 +14,11 @@ backends.
|
|||
LDAP via `occ` — every setting is read first and only written when
|
||||
the stored value differs, so re-runs don't churn
|
||||
- Sets up notify_push (when enabled)
|
||||
- Applies an in-container PHP source workaround for the upstream
|
||||
`UserConfig::getValueBool` TypeError (nextcloud/server#59629, fixed in
|
||||
master via PR #59646 with no stable33 backport before 33.0.4).
|
||||
Idempotent via grep guard; remove the patch task once
|
||||
`nextcloud_image` is >= 33.0.4.
|
||||
|
||||
## Requirements
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,61 @@
|
|||
project_src: "{{ nextcloud_docker_compose_dir }}"
|
||||
state: present
|
||||
|
||||
# nextcloud/server#59629: UserConfig::getValueBool() passes a non-string from
|
||||
# getTypedValue() into strtolower() under PHP 8.x + OPcache, throwing a
|
||||
# TypeError on every authenticated request once user_ldap is involved. Fix
|
||||
# is in master (PR #59646) but no stable33 backport landed before 33.0.4.
|
||||
# Apply the (string) cast in-container; idempotent via grep guard. Remove
|
||||
# this block once nextcloud_image >= 33.0.4.
|
||||
- name: Discover nextcloud php containers needing the UserConfig patch
|
||||
ansible.builtin.shell:
|
||||
cmd: >-
|
||||
docker ps --filter "label=com.docker.compose.project={{ nextcloud_docker_compose_dir | basename }}"
|
||||
--filter "label=com.docker.compose.service=nextcloud"
|
||||
--format '{% raw %}{{.Names}}{% endraw %}'
|
||||
register: _nextcloud_php_containers
|
||||
changed_when: false
|
||||
|
||||
- name: Check UserConfig.php patch status per container
|
||||
ansible.builtin.shell:
|
||||
# rc 0 -> already patched; rc 1 -> still the unpatched original; rc 2 ->
|
||||
# neither marker present (upstream drift -> the guard task below fails loud).
|
||||
cmd: >-
|
||||
docker exec {{ item }} sh -c '
|
||||
grep -q "strtolower((string)\$this->getTypedValue" /var/www/html/lib/private/Config/UserConfig.php && exit 0;
|
||||
grep -q "strtolower(\$this->getTypedValue" /var/www/html/lib/private/Config/UserConfig.php && exit 1;
|
||||
exit 2'
|
||||
loop: "{{ _nextcloud_php_containers.stdout_lines }}"
|
||||
register: _nextcloud_userconfig_check
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Fail if the UserConfig.php source drifted from the expected upstream line
|
||||
ansible.builtin.fail:
|
||||
msg: >-
|
||||
Neither the patched nor the expected original strtolower($this->getTypedValue(...))
|
||||
line was found in {{ item.item }}:/var/www/html/lib/private/Config/UserConfig.php.
|
||||
The nextcloud/server#59629 workaround can no longer locate its target — the upstream
|
||||
source likely changed. Re-verify whether the fix shipped (then drop this block) or
|
||||
update the sed expression. Silently skipping would let the TypeError regress.
|
||||
loop: "{{ _nextcloud_userconfig_check.results }}"
|
||||
loop_control:
|
||||
label: "{{ item.item }}"
|
||||
when:
|
||||
- item.rc | default(2) == 2
|
||||
|
||||
- name: Apply UserConfig::getValueBool string-cast workaround
|
||||
ansible.builtin.shell:
|
||||
cmd: >-
|
||||
docker exec {{ item.item }}
|
||||
sed -i 's|$b = strtolower($this->getTypedValue|$b = strtolower((string)$this->getTypedValue|'
|
||||
/var/www/html/lib/private/Config/UserConfig.php
|
||||
loop: "{{ _nextcloud_userconfig_check.results }}"
|
||||
loop_control:
|
||||
label: "{{ item.item }}"
|
||||
when:
|
||||
- item.rc | default(2) == 1
|
||||
|
||||
- name: Wait for Nextcloud to be ready
|
||||
ansible.builtin.shell:
|
||||
cmd: docker compose exec -T nextcloud php /var/www/html/occ status --output=json
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue