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.
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
"""Real Compose rendering plus adversarial production-config mutations; stdlib only."""
|
||||
import copy
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
spec = importlib.util.spec_from_file_location("ingress", ROOT / "scripts/validate-ingress.py")
|
||||
ingress = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(ingress)
|
||||
|
||||
|
||||
class IngressConfigTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
env = dict(os.environ, AI_SERVICE_TOKEN="synthetic-b06", APP_PUBLIC_BASE_URL="https://jobs.example.test",
|
||||
INGRESS_PROXY_CIDRS="192.0.2.10/32", WEB_PROXY_SUBNET="172.31.250.0/29",
|
||||
NEXT_PUBLIC_API_BASE_URL="/api")
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".env", delete=False) as file:
|
||||
file.write("# No repository secrets are read by this fixture.\n")
|
||||
path = file.name
|
||||
try:
|
||||
base = ["docker", "compose", "--env-file", path, "-f", "docker-compose.yml"]
|
||||
def render(extra):
|
||||
result = subprocess.run(base + extra + ["--profile", "bundled-ollama", "config", "--format", "json"],
|
||||
cwd=ROOT, env=env, capture_output=True, text=True, timeout=30, check=True)
|
||||
return json.loads(result.stdout)
|
||||
cls.production = render([])
|
||||
cls.development = render(["-f", "docker-compose.dev.yml"])
|
||||
finally:
|
||||
Path(path).unlink()
|
||||
|
||||
def test_effective_production_is_safe(self):
|
||||
ingress.validate(self.production)
|
||||
|
||||
def test_development_remains_explicit_and_cannot_pass_production_preflight(self):
|
||||
self.assertEqual(self.development["services"]["backend"]["ports"][0]["published"], "5202")
|
||||
with self.assertRaises(ValueError):
|
||||
ingress.validate(self.development)
|
||||
|
||||
def test_public_internal_ports_and_trust_bypasses_fail_closed(self):
|
||||
def change(config, service, key, value):
|
||||
config["services"][service][key] = value
|
||||
mutations = [
|
||||
lambda c: change(c, "backend", "ports", [{"target": 8080, "published": "5202"}]),
|
||||
lambda c: change(c, "ai-service", "ports", [{"target": 8001, "published": "8001"}]),
|
||||
lambda c: change(c, "ollama", "network_mode", "host"),
|
||||
lambda c: c["services"]["frontend"]["ports"].append({"target": 80, "published": "8081"}),
|
||||
lambda c: c["services"]["frontend"]["environment"].update(INGRESS_MODE="direct"),
|
||||
lambda c: c["services"]["frontend"]["environment"].update(INGRESS_PROXY_CIDRS=""),
|
||||
lambda c: c["services"]["frontend"]["environment"].update(INGRESS_PROXY_CIDRS="0.0.0.0/0"),
|
||||
lambda c: c["services"]["frontend"]["environment"].update(INGRESS_PROXY_CIDRS="::/0"),
|
||||
lambda c: c["services"]["frontend"]["environment"].update(INGRESS_PROXY_CIDRS="::/64"),
|
||||
lambda c: c["services"]["frontend"]["environment"].update(INGRESS_PROXY_CIDRS="::ffff:0:0/96"),
|
||||
lambda c: c["services"]["backend"]["environment"].update(ASPNETCORE_ENVIRONMENT="Development"),
|
||||
lambda c: c["services"]["backend"]["environment"].update(DOTNET_ENVIRONMENT="Development"),
|
||||
lambda c: c["services"]["backend"]["environment"].update(ASPNETCORE_FORWARDEDHEADERS_ENABLED="true"),
|
||||
lambda c: c["services"]["backend"]["environment"].update(Proxy__KnownNetworks__0="0.0.0.0/0"),
|
||||
lambda c: c["networks"]["web_proxy"].update(internal=False),
|
||||
lambda c: c["services"]["ai-service"]["networks"].update(web_proxy={}),
|
||||
lambda c: c["services"]["frontend"]["build"]["args"].update(NEXT_PUBLIC_API_BASE_URL="http://localhost:5202/api"),
|
||||
]
|
||||
for index, mutation in enumerate(mutations):
|
||||
with self.subTest(mutation=index):
|
||||
config = copy.deepcopy(self.production)
|
||||
mutation(config)
|
||||
with self.assertRaises(ValueError):
|
||||
ingress.validate(config)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user