Implement supported ingress and TLS profiles
Dependency Audit / dependency-audit (push) Successful in 1m38s
Deployment Installer / deployment-installer (push) Successful in 5s
Security Audit / security-audit (push) Successful in 10m2s

This commit is contained in:
2026-08-03 01:15:06 +02:00
parent b40f1428fd
commit 2c515f73c2
12 changed files with 1174 additions and 37 deletions
+143 -2
View File
@@ -22,6 +22,8 @@ ENV_FILENAME = "secrets.env"
COMPOSE_FILENAME = "compose.json"
GARAGE_CONFIG_FILENAME = "garage.toml"
LOAD_BALANCER_CONFIG_FILENAME = "load-balancer.cfg"
CADDY_CONFIG_FILENAME = "Caddyfile"
EXISTING_PROXY_FILENAME = "existing-proxy.json"
PLAN_FILENAME = "plan.json"
RECEIPT_FILENAME = "receipt.json"
MANIFEST_FILENAME = "distribution-manifest.json"
@@ -87,6 +89,8 @@ class BundlePaths:
compose: Path
garage_config: Path
load_balancer_config: Path
caddy_config: Path
existing_proxy: Path
plan: Path
receipt: Path
manifest: Path
@@ -106,6 +110,8 @@ def bundle_paths(root: Path) -> BundlePaths:
compose=resolved / COMPOSE_FILENAME,
garage_config=resolved / GARAGE_CONFIG_FILENAME,
load_balancer_config=resolved / LOAD_BALANCER_CONFIG_FILENAME,
caddy_config=resolved / CADDY_CONFIG_FILENAME,
existing_proxy=resolved / EXISTING_PROXY_FILENAME,
plan=resolved / PLAN_FILENAME,
receipt=resolved / RECEIPT_FILENAME,
manifest=resolved / MANIFEST_FILENAME,
@@ -455,6 +461,10 @@ def render_compose(spec: InstallationSpec) -> dict[str, object]:
"restart": "unless-stopped",
"volumes": data_mounts,
"networks": ["internal"],
"read_only": True,
"tmpfs": ["/tmp:rw,noexec,nosuid,size=64m"],
"security_opt": ["no-new-privileges:true"],
"cap_drop": ["ALL"],
}
if dependency_conditions:
common_runtime["depends_on"] = dependency_conditions
@@ -470,6 +480,10 @@ def render_compose(spec: InstallationSpec) -> dict[str, object]:
"restart": "no",
"volumes": data_mounts,
"networks": ["internal"],
"read_only": True,
"tmpfs": ["/tmp:rw,noexec,nosuid,size=64m"],
"security_opt": ["no-new-privileges:true"],
"cap_drop": ["ALL"],
**({"depends_on": dependency_conditions} if dependency_conditions else {}),
}
services["api"] = {
@@ -515,9 +529,13 @@ def render_compose(spec: InstallationSpec) -> dict[str, object]:
"restart": "unless-stopped",
"scale": spec.replicas.web,
"environment": {"GOVOPLAN_API_UPSTREAM": "http://load-balancer:8000"},
"read_only": True,
"tmpfs": ["/tmp:rw,noexec,nosuid,size=64m"],
"security_opt": ["no-new-privileges:true"],
"cap_drop": ["ALL"],
"networks": ["internal"],
}
services["load-balancer"] = {
load_balancer: dict[str, object] = {
"image": spec.components.load_balancer.image,
"restart": "unless-stopped",
"healthcheck": {
@@ -537,7 +555,6 @@ def render_compose(spec: InstallationSpec) -> dict[str, object]:
render_load_balancer_config(spec).encode("utf-8")
).hexdigest()
},
"ports": [_published_port(spec.listen.address, spec.listen.port, 8080)],
"read_only": True,
"security_opt": ["no-new-privileges:true"],
"volumes": [
@@ -545,6 +562,53 @@ def render_compose(spec: InstallationSpec) -> dict[str, object]:
],
"networks": ["internal"],
}
if spec.ingress.mode != "managed":
load_balancer["ports"] = [
_published_port(spec.listen.address, spec.listen.port, 8080)
]
services["load-balancer"] = load_balancer
if spec.ingress.mode == "managed":
services["ingress"] = {
"image": spec.ingress.image,
"restart": "unless-stopped",
"command": [
"caddy",
"run",
"--config",
"/etc/caddy/Caddyfile",
"--adapter",
"caddyfile",
],
"healthcheck": {
"test": [
"CMD",
"caddy",
"validate",
"--config",
"/etc/caddy/Caddyfile",
"--adapter",
"caddyfile",
],
"interval": "30s",
"timeout": "5s",
"retries": 3,
},
"ports": [
_published_port("0.0.0.0", spec.ingress.http_port, 8080),
_published_port("0.0.0.0", spec.ingress.https_port, 8443),
],
"read_only": True,
"tmpfs": ["/tmp:rw,noexec,nosuid,size=64m"],
"security_opt": ["no-new-privileges:true"],
"cap_drop": ["ALL"],
"volumes": [
f"./{CADDY_CONFIG_FILENAME}:/etc/caddy/Caddyfile:ro",
"caddy-data:/data",
"caddy-config:/config",
],
"networks": ["internal"],
"depends_on": {"load-balancer": {"condition": "service_healthy"}},
}
if spec.components.redis.mode != "disabled":
services["worker"] = {
**common_runtime,
@@ -606,6 +670,9 @@ def render_compose(spec: InstallationSpec) -> dict[str, object]:
if spec.components.storage.mode == "garage":
volumes["garage-meta"] = {}
volumes["garage-data"] = {}
if spec.ingress.mode == "managed":
volumes["caddy-data"] = {}
volumes["caddy-config"] = {}
return {
"name": spec.installation_id,
@@ -642,6 +709,31 @@ api_bind_addr = "[::]:3903"
def render_load_balancer_config(spec: InstallationSpec) -> str:
health_host = urlsplit(spec.public_url).hostname or "localhost"
trusted_proxy_cidrs = (
(spec.network_subnet,)
if spec.ingress.mode == "managed"
else spec.ingress.trusted_proxy_cidrs
if spec.ingress.mode == "existing-proxy"
else ()
)
trusted_acl = (
" acl trusted_forward_proxy src " + " ".join(trusted_proxy_cidrs) + "\n"
if trusted_proxy_cidrs
else ""
)
forwarded_rules = (
" http-request set-var(txn.forwarded_proto) req.hdr(X-Forwarded-Proto) if trusted_forward_proxy\n"
" http-request del-header X-Forwarded-Proto\n"
" http-request set-header X-Forwarded-Proto https if trusted_forward_proxy { var(txn.forwarded_proto) -m str https }\n"
" http-request set-header X-Forwarded-Proto http unless { var(txn.forwarded_proto) -m str https }\n"
" http-request del-header X-Forwarded-For unless trusted_forward_proxy\n"
if trusted_proxy_cidrs
else (
" http-request del-header X-Forwarded-Proto\n"
" http-request set-header X-Forwarded-Proto http\n"
" http-request del-header X-Forwarded-For\n"
)
)
return f"""global
log stdout format raw local0
maxconn 4096
@@ -669,6 +761,9 @@ resolvers docker
frontend public_web
bind :8080
{trusted_acl}{forwarded_rules} option forwardfor
http-request del-header X-Forwarded-Host
http-request set-header X-Forwarded-Host %[req.hdr(host)]
default_backend web_replicas
backend web_replicas
@@ -690,6 +785,52 @@ backend api_replicas
"""
def render_caddy_config(spec: InstallationSpec) -> str:
if spec.ingress.mode != "managed":
return "# Managed ingress is not selected.\n"
hostname = urlsplit(spec.public_url).hostname or ""
return f"""{{
admin off
email {spec.ingress.acme_email}
http_port 8080
https_port 8443
}}
{hostname} {{
encode zstd gzip
header {{
Strict-Transport-Security "max-age=31536000; includeSubDomains"
}}
reverse_proxy load-balancer:8080 {{
header_up X-Forwarded-Proto https
}}
}}
"""
def render_existing_proxy_contract(spec: InstallationSpec) -> dict[str, object]:
return {
"schema_version": 1,
"mode": spec.ingress.mode,
"public_url": spec.public_url,
"upstream": (
f"http://{spec.listen.address}:{spec.listen.port}"
if spec.ingress.mode == "existing-proxy"
else None
),
"trusted_proxy_cidrs": list(spec.ingress.trusted_proxy_cidrs),
"required_headers": {
"Host": urlsplit(spec.public_url).hostname or "",
"X-Forwarded-Proto": "https",
"X-Forwarded-For": "client, proxy chain",
},
"health_paths": {
"load_balancer": "/health",
"api_readiness": "/health/ready",
},
}
def service_names(spec: InstallationSpec) -> tuple[str, ...]:
return tuple(render_compose(spec)["services"].keys())