#!/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.")