Files
computer-playbook/plugins/lookup/proxy_hops.py
T
kevinveenbirkenbachandClaude Opus 5.5 2c6c7e1464 fix(web-app-nextcloud): trust the real reverse-proxy hop so Nextcloud sees client IPs
trusted_proxies held the host's public addresses, but OpenResty reaches Nextcloud through the published port, which Docker forwards from the gateway of one of the container's networks (192.168.201.1, the central MariaDB network, on production). Nextcloud therefore ignored X-Forwarded-For, attributed every request to that gateway and brute-force throttled it. The TRUSTED_PROXIES env var was joined with commas while the image's reverse-proxy.config.php splits it on spaces, which produced the single malformed entry the admin overview rejects, and the occ value reached config:system:set as a Python repr instead of JSON, so config.php got null.

Add the proxy_hops lookup next to resolve_upstream in utils/networks/proxy.py: under compose it returns the gateway of every network the application joins, under swarm the subnets of the overlays it shares with svc-prx-openresty, both derived from the service registry and role_subnets. NEXTCLOUD_TRUSTED_PROXIES now uses it, env.j2 joins the list with spaces and system.yml passes it through to_json.

Rendered with the real roles and group_vars for the galaxyserver layout: compose trusts 192.168.41.1, 192.168.201.1 and 192.168.202.1, swarm 192.168.107.0/24; unit and integration tests cover both modes, and two mutations of proxy_hops fail them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-29 17:07:57 +01:00

62 lines
2.0 KiB
Python

"""Lookup ``proxy_hops``: the addresses the reverse proxy's requests reach an
application from, for that application's trusted-proxy list.
Usage:
{{ lookup('proxy_hops', application_id) }} # compose: ['192.168.41.1', '192.168.201.1']
# swarm: ['192.168.107.0/24']
Returns what :func:`utils.networks.proxy.proxy_hops` derives from the networks
the service_registry attaches.
"""
from __future__ import annotations
from typing import Any
from ansible.errors import AnsibleError
from ansible.plugins.loader import lookup_loader
from ansible.plugins.lookup import LookupBase
from utils.networks.lookup_context import build_context, resolve_var
from utils.networks.proxy import proxy_hops
from utils.networks.render import compute_external_network_roles
class LookupModule(LookupBase):
def run(
self,
terms: list[Any] | None,
variables: dict[str, Any] | None = None,
**kwargs: Any,
) -> list[Any]:
if not terms or len(terms) != 1:
raise AnsibleError(
"proxy_hops lookup requires exactly one term: application_id"
)
ctx = build_context(self, variables)
application_id = str(resolve_var(ctx.templar, terms[0]))
role_subnets = lookup_loader.get(
"role_subnets", loader=self._loader, templar=ctx.templar
)
def network_roles(role: str) -> list[str]:
return compute_external_network_roles(
application_id=role,
deployment_mode=ctx.deployment_mode,
registry=ctx.registry,
lookup_config=ctx.lookup_config,
lookup_database=ctx.lookup_database,
)
def subnets(roles: list[str]) -> list[str]:
return role_subnets.run([roles], variables=ctx.vars)[0]
try:
return [
proxy_hops(application_id, ctx.deployment_mode, network_roles, subnets)
]
except ValueError as exc:
raise AnsibleError(f"proxy_hops: {exc}") from exc