fix(sso): point the onion issuer pin at the node that publishes it

extra_hosts mapped auth.<onion> to host-gateway in both deploy modes, but
host-gateway is a per-node address while openresty publishes :80 with mode: host
under node.role == manager. Every replica scheduled on a worker therefore dialled
its own gateway, where nothing listens, and the server-side OIDC token exchange
died with 'Connection refused'. Penpot 0 of run 31340965955 measured it: 60 of
the ConnectException lines on wrk-02, 20 on wrk-01, zero on mgr-01.

In swarm the pin now resolves to the stack host's routable address -- the same
destination svc-net-tor already sends all hidden-service traffic to via
TOR_HS_BACKEND_HOST. That address reaches every task, because
plugins/action/stack_host_template.py skips any host whose IS_STACK_HOST is
false and IS_STACK_HOST is true only on a swarm manager: the compose file is
rendered once with the manager as inventory host, so ansible_facts carries the
manager's IP and swarm applies the same extra_hosts to every task.

Retargeting rather than dropping the pin, because a manager-pinned consumer
(the keycloak sso-proxy) is measurably working through this very line today.
The guard fa2f6eb7e removed would have deleted it for them too.

Keyed on compose_mode_force or DEPLOYMENT_MODE, not the bare cluster mode:
container_networks resolves attachments through the same effective mode, so a
role with an override must not lose its pin and its overlay at once.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5 committed 2026-08-11 15:19:35 +02:00
1 parent f77756faac
commit 7b1839681c
1 file changed
+2 -1
@@ -1,6 +1,7 @@
{% set _oidc_onion_host = lookup('tls', 'web-app-keycloak', 'domain') %}
{% set _oidc_onion_addr = 'host-gateway' if (compose_mode_force | default('') or DEPLOYMENT_MODE) == 'compose' else ansible_facts['default_ipv4']['address'] %}
{% if (lookup('config', application_id, 'services.sso.enabled') | bool) and (_oidc_onion_host is search('\.onion$')) %}
extra_hosts:
- "host.docker.internal:host-gateway"
- "{{ _oidc_onion_host }}:host-gateway"
- "{{ _oidc_onion_host }}:{{ _oidc_onion_addr }}"
{% endif %}