Files
jobtrackingapp/scripts/test-ingress-topology.py
cesnimda 0954876295 fix(security): enforce audited account, network and resource boundaries
Implement B01-B06 with regression coverage and retained validation evidence. Preserve the production ingress preflight; F04 runtime verification remains open.
2026-09-09 18:21:47 +02:00

127 lines
7.7 KiB
Python

#!/usr/bin/env python3
"""Bounded, synthetic nginx topology; uses an installed image, never pulls/deploys."""
import argparse
import json
from pathlib import Path
import subprocess
import tempfile
import urllib.error
import urllib.request
import uuid
ROOT = Path(__file__).resolve().parents[1]
def docker(*args):
result = subprocess.run(["docker", *args], capture_output=True, text=True, timeout=30)
if result.returncode:
raise RuntimeError(result.stderr.strip())
return result.stdout.strip()
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--image", default="nginx:1.29.8-alpine", help="Already installed nginx image with realip and wget")
parser.add_argument("--baseline", action="store_true", help="Use HEAD's pre-B06 nginx sources and demonstrate the old bypass")
args = parser.parse_args()
docker("info", "--format", "{{.ServerVersion}}")
docker("image", "inspect", args.image) # Fail before resource creation if unavailable.
prefix = "b06-" + uuid.uuid4().hex[:10]
containers, networks = [], []
artifact_root = ROOT / ".artifacts" / "ingress-topology"
artifact_root.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix=prefix, dir=artifact_root) as directory:
directory = Path(directory).resolve()
assert directory.is_relative_to(artifact_root.resolve())
def create(name, *options):
name = prefix + "-" + name
docker("create", "--pull=never", "--name", name, *options)
containers.append(name)
return name
try:
edge = prefix + "-edge"
private = prefix + "-private"
docker("network", "create", edge)
networks.append(edge)
docker("network", "create", "--internal", private)
networks.append(private)
peer = create("peer", "--network", edge, "--entrypoint", "sleep", args.image, "300")
docker("start", peer)
peer_state = json.loads(docker("inspect", peer))[0]
peer_ip = peer_state["NetworkSettings"]["Networks"][edge]["IPAddress"]
for source, target in [("nginx.conf.template", "template"), ("configure-nginx-origin.sh", "configure.sh")]:
content = (subprocess.check_output(["git", "show", f"HEAD:job-tracker-ui/{source}"], cwd=ROOT).decode()
if args.baseline else (ROOT / "job-tracker-ui" / source).read_text())
(directory / target).write_text(content, newline="\n")
(directory / "backend.conf").write_text('''server {
listen 8080;
location / { default_type application/json;
return 200 '{"ip":"$http_x_forwarded_for","scheme":"$http_x_forwarded_proto","host":"$http_host","forwardedHost":"$http_x_forwarded_host","forwarded":"$http_forwarded","session":"$http_x_session_context"}'; }
}
''', newline="\n")
backend = create("backend", "--network", private, "--network-alias", "backend-web",
"-v", f"{directory / 'backend.conf'}:/etc/nginx/conf.d/default.conf:ro", args.image)
docker("start", backend)
frontend = create("frontend", "--network", edge, "--network-alias", "frontend",
"-p", "127.0.0.1::80", "-e", "APP_PUBLIC_BASE_URL=https://jobs.example.test",
"-e", "INGRESS_MODE=proxy", "-e", f"INGRESS_PROXY_CIDRS={peer_ip}/32",
"-v", f"{directory / 'template'}:/etc/nginx/jobtracker.conf.template:ro",
"-v", f"{directory / 'configure.sh'}:/docker-entrypoint.d/15-jobtracker-origin.sh:ro",
# Invoke through sh so mounted Windows execute bits do not determine the result.
"--entrypoint", "sh", args.image, "-c",
"sh /docker-entrypoint.d/15-jobtracker-origin.sh && exec nginx -g 'daemon off;'")
docker("network", "connect", private, frontend)
docker("start", frontend)
docker("exec", frontend, "sh", "-c", "for i in 1 2 3 4 5; do wget -q -O /dev/null http://127.0.0.1/health && exit 0; sleep 1; done; exit 1")
headers = {"Host": "jobs.example.test", "X-Forwarded-For": "198.51.100.10", "X-Forwarded-Proto": "https",
"X-Forwarded-Host": "attacker.example.test", "Forwarded": "host=attacker.example.test", "X-Session-Context": "synthetic-session"}
def proxied(extra=None):
values = {**headers, **(extra or {})}
command = ["docker", "exec", peer, "wget", "-S", "-O", "-"]
for name, value in values.items():
command.append(f"--header={name}: {value}")
command.append("http://frontend/api/probe")
return subprocess.run(command, capture_output=True, text=True, timeout=10)
binding = docker("port", frontend, "80/tcp")
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
request = urllib.request.Request(f"http://{binding}/api/probe", headers=headers)
try:
with opener.open(request, timeout=10) as response:
direct_status, direct_body = response.status, response.read().decode()
except urllib.error.HTTPError as error:
direct_status, direct_body = error.code, error.read().decode()
if args.baseline:
assert direct_status == 200 and json.loads(direct_body)["ip"] == "198.51.100.10"
print(json.dumps({"baselineDirect": direct_status, "forgedIpAccepted": True}))
return
assert direct_status == 403, (direct_status, direct_body)
successful = []
for client in ["198.51.100.10", "198.51.100.11", "2001:db8::10"]:
result = proxied({"X-Forwarded-For": client})
assert result.returncode == 0, result.stderr
body = json.loads(result.stdout)
assert body == {"ip": client, "scheme": "https", "host": "jobs.example.test", "forwardedHost": "", "forwarded": "", "session": "synthetic-session"}, body
successful.append(client)
for extra in [{"X-Forwarded-For": "203.0.113.9, 198.51.100.10"}, {"X-Forwarded-Proto": "http"}, {"X-Forwarded-Proto": "https,http"}]:
result = proxied(extra)
assert result.returncode != 0 and "400" in result.stderr, result.stderr
malformed = json.loads(proxied({"X-Forwarded-For": "invalid-address"}).stdout)
assert malformed["ip"] == peer_ip # realip fails closed to the real peer, not forged data.
backend_state = json.loads(docker("inspect", backend))[0]
assert not any(backend_state["NetworkSettings"]["Ports"].values())
backend_ip = backend_state["NetworkSettings"]["Networks"][private]["IPAddress"]
# A peer attached only to the edge network cannot route directly to the internal backend.
denied = subprocess.run(["docker", "exec", peer, "wget", "-T", "2", "-qO-", f"http://{backend_ip}:8080/api/probe"], capture_output=True, timeout=5)
assert denied.returncode != 0
print(json.dumps({"directNginx": direct_status, "trustedClientIps": successful, "chainsAndBadScheme": 400,
"malformedIp": "socket-peer fallback", "directBackend": "unreachable", "backendPublishedPorts": 0}))
finally:
for name in reversed(containers):
subprocess.run(["docker", "rm", "-f", name], capture_output=True, timeout=30)
for name in reversed(networks):
subprocess.run(["docker", "network", "rm", name], capture_output=True, timeout=30)
if __name__ == "__main__":
main()