Files
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

83 lines
4.5 KiB
Python

#!/usr/bin/env python3
"""Validate rendered production Compose, never print its environment/secrets."""
import ipaddress
import json
import sys
from urllib.parse import urlsplit
def proxy_network(value):
try:
network = ipaddress.ip_network(value, strict=True)
except ValueError:
raise ValueError("Invalid ingress proxy CIDR") from None
private_ranges = (ipaddress.ip_network(value) for value in ("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "fc00::/7"))
private = any(network.version == item.version and network.subnet_of(item) for item in private_ranges)
minimum = (24 if network.version == 4 else 64) if private else network.max_prefixlen
if network.prefixlen < minimum:
raise ValueError("Ingress proxy CIDRs must be narrow private networks or individual public proxy addresses")
return network
def validate(config):
services = config["services"]
for name, service in services.items():
if service.get("network_mode") == "host":
raise ValueError("Production services must not use host networking")
if name != "frontend" and service.get("ports"):
raise ValueError("Only frontend ingress may publish a production port")
front = services["frontend"]
ports = front.get("ports", [])
if len(ports) != 1 or ports[0].get("target") != 80 or str(ports[0].get("published")) != "3000" or ports[0].get("protocol") != "tcp":
raise ValueError("Production ingress must publish only the documented frontend 3000-to-80 mapping")
env = front["environment"]
if env.get("INGRESS_MODE") != "proxy":
raise ValueError("Production frontend must require the trusted proxy")
peers = env.get("INGRESS_PROXY_CIDRS", "").split()
if not peers:
raise ValueError("INGRESS_PROXY_CIDRS must identify the observed Traefik socket peers")
for peer in peers:
proxy_network(peer)
backend = services["backend"]["environment"]
if backend.get("ASPNETCORE_ENVIRONMENT") != "Production":
raise ValueError("Production backend environment must be explicit")
if backend.get("DOTNET_ENVIRONMENT") not in (None, "Production"):
raise ValueError("DOTNET_ENVIRONMENT must not override production")
if backend.get("ASPNETCORE_FORWARDEDHEADERS_ENABLED", "false").lower() != "false":
raise ValueError("Use explicit proxy trust, not automatic framework forwarding")
origin = backend.get("App__PublicBaseUrl", "")
parsed = urlsplit(origin)
if (parsed.scheme != "https" or not parsed.hostname or parsed.username or parsed.password
or parsed.path not in ("", "/") or parsed.query or parsed.fragment
or env.get("APP_PUBLIC_BASE_URL") != origin):
raise ValueError("Frontend and backend must share the canonical HTTPS origin")
if front.get("build", {}).get("args", {}).get("NEXT_PUBLIC_API_BASE_URL") not in (None, "", "/api"):
raise ValueError("Production frontend must use the same-origin /api route")
if backend.get("Proxy__TrustForwardedHeaders") != "true":
raise ValueError("Production backend must resolve nginx forwarding metadata")
proxy = config["networks"]["web_proxy"]
if not proxy.get("internal") or proxy.get("external"):
raise ValueError("The nginx-to-backend network must be dedicated and internal")
networks = proxy["ipam"]["config"]
if len(networks) != 1:
raise ValueError("Exactly one nginx-to-backend subnet is supported")
subnet = proxy_network(networks[0]["subnet"])
if not subnet.is_private:
raise ValueError("The nginx-to-backend subnet must be private")
known = {key: value for key, value in backend.items() if key.startswith("Proxy__KnownNetworks__")}
if known != {"Proxy__KnownNetworks__0": str(subnet)}:
raise ValueError("Backend trust must exactly match the dedicated proxy subnet")
members = {name for name, service in services.items() if "web_proxy" in service.get("networks", {})}
if members != {"frontend", "backend"}:
raise ValueError("Only frontend and backend may join the trusted proxy network")
if __name__ == "__main__":
try:
validate(json.load(sys.stdin))
except (ValueError, KeyError, TypeError) as error:
# Validation errors describe keys/contracts, never configuration values.
print(f"Ingress preflight rejected the rendered production configuration: {error}", file=sys.stderr)
sys.exit(1)
print("Production ingress configuration validated; observed host/proxy/firewall checks are still required.")