diff --git a/roles/authentik/tasks/main.yml b/roles/authentik/tasks/main.yml index 1947fc4..1471836 100644 --- a/roles/authentik/tasks/main.yml +++ b/roles/authentik/tasks/main.yml @@ -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) \ No newline at end of file + until: ldap_token_result.rc == 0 + when: authentik_ldap_outpost.name is defined and (blueprints_changed or ldap_token_script.changed) \ No newline at end of file diff --git a/roles/authentik/templates/docker-compose.yml.j2 b/roles/authentik/templates/docker-compose.yml.j2 index ce427f2..cd3ef1e 100644 --- a/roles/authentik/templates/docker-compose.yml.j2 +++ b/roles/authentik/templates/docker-compose.yml.j2 @@ -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 diff --git a/roles/authentik/templates/set-outpost-token.py.j2 b/roles/authentik/templates/set-outpost-token.py.j2 index ffaa56b..0b61705 100644 --- a/roles/authentik/templates/set-outpost-token.py.j2 +++ b/roles/authentik/templates/set-outpost-token.py.j2 @@ -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 - t.save(update_fields=['key']) - changed = True - -print('changed' if changed else 'ok') +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']) + print('changed') +else: + print('ok') diff --git a/roles/authentik_outpost_proxy/README.md b/roles/authentik_outpost_proxy/README.md deleted file mode 100644 index 93f2045..0000000 --- a/roles/authentik_outpost_proxy/README.md +++ /dev/null @@ -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 diff --git a/roles/authentik_outpost_proxy/defaults/main.yml b/roles/authentik_outpost_proxy/defaults/main.yml deleted file mode 100644 index a9ffe06..0000000 --- a/roles/authentik_outpost_proxy/defaults/main.yml +++ /dev/null @@ -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" diff --git a/roles/authentik_outpost_proxy/handlers/main.yml b/roles/authentik_outpost_proxy/handlers/main.yml deleted file mode 100644 index 53c0099..0000000 --- a/roles/authentik_outpost_proxy/handlers/main.yml +++ /dev/null @@ -1,3 +0,0 @@ -#SPDX-License-Identifier: MIT-0 ---- -# handlers file for authentik_outpost_proxy diff --git a/roles/authentik_outpost_proxy/meta/main.yml b/roles/authentik_outpost_proxy/meta/main.yml deleted file mode 100644 index 122e1ad..0000000 --- a/roles/authentik_outpost_proxy/meta/main.yml +++ /dev/null @@ -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: [] diff --git a/roles/authentik_outpost_proxy/tasks/main.yml b/roles/authentik_outpost_proxy/tasks/main.yml deleted file mode 100644 index 0b1ec00..0000000 --- a/roles/authentik_outpost_proxy/tasks/main.yml +++ /dev/null @@ -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 diff --git a/roles/authentik_outpost_proxy/templates/docker-compose.yml.j2 b/roles/authentik_outpost_proxy/templates/docker-compose.yml.j2 deleted file mode 100644 index b1f6b08..0000000 --- a/roles/authentik_outpost_proxy/templates/docker-compose.yml.j2 +++ /dev/null @@ -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 diff --git a/roles/authentik_outpost_proxy/tests/inventory b/roles/authentik_outpost_proxy/tests/inventory deleted file mode 100644 index 712db59..0000000 --- a/roles/authentik_outpost_proxy/tests/inventory +++ /dev/null @@ -1,2 +0,0 @@ -#SPDX-License-Identifier: MIT-0 -localhost diff --git a/roles/authentik_outpost_proxy/tests/test.yml b/roles/authentik_outpost_proxy/tests/test.yml deleted file mode 100644 index c5e7aed..0000000 --- a/roles/authentik_outpost_proxy/tests/test.yml +++ /dev/null @@ -1,6 +0,0 @@ -#SPDX-License-Identifier: MIT-0 ---- -- hosts: localhost - remote_user: root - roles: - - authentik_outpost_proxy diff --git a/roles/authentik_outpost_proxy/vars/main.yml b/roles/authentik_outpost_proxy/vars/main.yml deleted file mode 100644 index d9c7ed6..0000000 --- a/roles/authentik_outpost_proxy/vars/main.yml +++ /dev/null @@ -1,3 +0,0 @@ -#SPDX-License-Identifier: MIT-0 ---- -# vars file for authentik_outpost_proxy diff --git a/roles/garage/defaults/main.yml b/roles/garage/defaults/main.yml index 1b8a1eb..3820a03 100644 --- a/roles/garage/defaults/main.yml +++ b/roles/garage/defaults/main.yml @@ -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 diff --git a/roles/garage/meta/argument_specs.yml b/roles/garage/meta/argument_specs.yml index 69b04c4..b5cb0f5 100644 --- a/roles/garage/meta/argument_specs.yml +++ b/roles/garage/meta/argument_specs.yml @@ -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 diff --git a/roles/garage/templates/docker-compose.yml.j2 b/roles/garage/templates/docker-compose.yml.j2 index 52b3bad..7b1c017 100644 --- a/roles/garage/templates/docker-compose.yml.j2 +++ b/roles/garage/templates/docker-compose.yml.j2 @@ -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 diff --git a/roles/nextcloud/README.md b/roles/nextcloud/README.md index 5d16488..f4cafa9 100644 --- a/roles/nextcloud/README.md +++ b/roles/nextcloud/README.md @@ -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 diff --git a/roles/nextcloud/tasks/main.yml b/roles/nextcloud/tasks/main.yml index e33088b..c43bc4d 100644 --- a/roles/nextcloud/tasks/main.yml +++ b/roles/nextcloud/tasks/main.yml @@ -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