Harden runtime distribution acceptance
Dependency Audit / dependency-audit (push) Successful in 1m45s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 10m49s

This commit is contained in:
2026-08-03 17:32:52 +02:00
parent a5a0731d20
commit ff8ee991c3
8 changed files with 976 additions and 29 deletions
+104 -15
View File
@@ -27,13 +27,52 @@ from govoplan_deploy.model import default_spec # noqa: E402
DIGEST_IMAGE = re.compile(r"^[^@\s]+@sha256:[0-9a-f]{64}$")
def _run(argv: list[str], *, check: bool = True) -> subprocess.CompletedProcess[str]:
return subprocess.run(
argv,
check=check,
capture_output=True,
text=True,
timeout=60,
def _run(
argv: list[str],
*,
check: bool = True,
input_text: str | None = None,
) -> subprocess.CompletedProcess[str]:
try:
return subprocess.run(
argv,
check=check,
capture_output=True,
input=input_text,
text=True,
timeout=60,
)
except subprocess.CalledProcessError as exc:
stderr = exc.stderr.strip()
if stderr:
print(stderr, file=sys.stderr)
raise
def _write_volume_file(
*,
image: str,
volume: str,
filename: str,
content: str,
) -> None:
if not re.fullmatch(r"[A-Za-z0-9_.-]+", filename):
raise ValueError(f"invalid config filename: {filename!r}")
_run(
[
"docker",
"run",
"--rm",
"--interactive",
"--mount",
f"type=volume,src={volume},dst=/govoplan-config",
"--entrypoint",
"sh",
image,
"-c",
f"umask 022; cat > /govoplan-config/{filename}",
],
input_text=content,
)
@@ -85,15 +124,33 @@ def main() -> int:
backend = f"govoplan-ingress-backend-{suffix}"
data_volume = f"govoplan-ingress-data-{suffix}"
config_volume = f"govoplan-ingress-config-{suffix}"
backend_config_volume = f"govoplan-ingress-backend-config-{suffix}"
ingress_config_volume = f"govoplan-ingress-caddy-config-{suffix}"
load_balancer_config_volume = f"govoplan-ingress-haproxy-config-{suffix}"
cleanup = [
["docker", "rm", "--force", ingress, backend],
["docker", "network", "rm", network],
["docker", "volume", "rm", data_volume, config_volume],
[
"docker",
"volume",
"rm",
data_volume,
config_volume,
backend_config_volume,
ingress_config_volume,
load_balancer_config_volume,
],
]
try:
_run(["docker", "network", "create", network])
_run(["docker", "volume", "create", data_volume])
_run(["docker", "volume", "create", config_volume])
for volume in (
data_volume,
config_volume,
backend_config_volume,
ingress_config_volume,
load_balancer_config_volume,
):
_run(["docker", "volume", "create", volume])
with tempfile.TemporaryDirectory(prefix="govoplan-ingress-") as directory:
root = Path(directory)
backend_config = root / "backend.Caddyfile"
@@ -127,6 +184,24 @@ def main() -> int:
encoding="utf-8",
)
load_balancer_config.chmod(0o644)
_write_volume_file(
image=args.load_balancer_image,
volume=load_balancer_config_volume,
filename="haproxy.cfg",
content=load_balancer_config.read_text(encoding="utf-8"),
)
_write_volume_file(
image=args.caddy_image,
volume=backend_config_volume,
filename="Caddyfile",
content=backend_config.read_text(encoding="utf-8"),
)
_write_volume_file(
image=args.caddy_image,
volume=ingress_config_volume,
filename="Caddyfile",
content=ingress_config.read_text(encoding="utf-8"),
)
_run(
[
"docker",
@@ -136,7 +211,11 @@ def main() -> int:
"--cap-drop",
"ALL",
"--mount",
f"type=bind,src={load_balancer_config},dst=/usr/local/etc/haproxy/haproxy.cfg,readonly",
(
"type=volume,"
f"src={load_balancer_config_volume},"
"dst=/usr/local/etc/haproxy,readonly"
),
args.load_balancer_image,
"haproxy",
"-c",
@@ -154,16 +233,22 @@ def main() -> int:
backend,
"--network",
network,
"--network-alias",
"load-balancer",
"--read-only",
"--tmpfs",
"/tmp:rw,noexec,nosuid,size=16m",
"--mount",
f"type=bind,src={backend_config},dst=/etc/caddy/Caddyfile,readonly",
(
"type=volume,"
f"src={backend_config_volume},"
"dst=/govoplan-config,readonly"
),
args.caddy_image,
"caddy",
"run",
"--config",
"/etc/caddy/Caddyfile",
"/govoplan-config/Caddyfile",
]
)
ingress_command = [
@@ -186,7 +271,11 @@ def main() -> int:
"--publish",
"127.0.0.1::8443",
"--mount",
f"type=bind,src={ingress_config},dst=/etc/caddy/Caddyfile,readonly",
(
"type=volume,"
f"src={ingress_config_volume},"
"dst=/govoplan-config,readonly"
),
"--mount",
f"type=volume,src={data_volume},dst=/data",
"--mount",
@@ -195,7 +284,7 @@ def main() -> int:
"caddy",
"run",
"--config",
"/etc/caddy/Caddyfile",
"/govoplan-config/Caddyfile",
]
_run(ingress_command)
http_port = _published_port(ingress, 8080)
+579
View File
@@ -0,0 +1,579 @@
#!/usr/bin/env python3
"""Exercise a pinned GovOPlaN runtime image pair on one OCI platform."""
from __future__ import annotations
import argparse
import base64
from datetime import UTC, datetime
import json
import os
from pathlib import Path
import re
import secrets
import subprocess
import time
from typing import Callable, Sequence
PLATFORMS = frozenset({"linux/amd64", "linux/arm64"})
DIGEST_IMAGE = re.compile(r"^[^\s@]+@sha256:[0-9a-f]{64}$")
BASE_MODULES = (
"tenancy",
"organizations",
"identity",
"idm",
"access",
"admin",
"dashboard",
"policy",
"audit",
"docs",
"ops",
)
class SmokeError(RuntimeError):
"""A runtime image failed its bounded acceptance drill."""
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--api-metadata", type=Path, required=True)
parser.add_argument("--web-metadata", type=Path, required=True)
parser.add_argument("--postgres-image", required=True)
parser.add_argument("--redis-image", required=True)
parser.add_argument("--platform", choices=sorted(PLATFORMS), required=True)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--timeout-seconds", type=float, default=600.0)
return parser
def _utc_now() -> str:
return datetime.now(UTC).isoformat().replace("+00:00", "Z")
def _digest_image(value: object, label: str) -> str:
if not isinstance(value, str) or DIGEST_IMAGE.fullmatch(value) is None:
raise SmokeError(f"{label} must be an exact sha256 image reference")
return value
def platform_image(path: Path, platform: str, label: str) -> str:
try:
payload = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
raise SmokeError(f"cannot read {label} OCI metadata") from exc
if not isinstance(payload, dict) or not isinstance(payload.get("platforms"), dict):
raise SmokeError(f"{label} OCI metadata has no platform map")
return _digest_image(payload["platforms"].get(platform), f"{label} {platform}")
def _tail(value: str, *, limit: int = 4000) -> str:
return value[-limit:].strip()
def _run(
arguments: Sequence[str],
*,
check: bool = True,
timeout: float = 600.0,
redactions: Sequence[str] = (),
) -> subprocess.CompletedProcess[str]:
try:
result = subprocess.run(
list(arguments),
check=False,
capture_output=True,
text=True,
timeout=timeout,
)
except (OSError, subprocess.TimeoutExpired) as exc:
raise SmokeError(f"container command could not complete: {type(exc).__name__}") from exc
if check and result.returncode != 0:
detail = _tail(result.stderr or result.stdout or "no diagnostic output")
for secret in redactions:
if secret:
detail = detail.replace(secret, "[redacted]")
raise SmokeError(f"container command failed: {detail}")
return result
def _wait_for(
label: str,
probe: Callable[[], subprocess.CompletedProcess[str]],
*,
timeout: float,
) -> None:
deadline = time.monotonic() + timeout
last = ""
while time.monotonic() < deadline:
result = probe()
if result.returncode == 0:
return
last = _tail(result.stderr or result.stdout)
time.sleep(2.0)
raise SmokeError(f"{label} did not become ready: {last or 'probe failed'}")
def _environment(
*,
database_password: str,
master_key: str,
platform_slug: str,
) -> dict[str, str]:
database = (
f"postgresql+psycopg://govoplan:{database_password}@postgres:5432/govoplan"
)
return {
"APP_ENV": "dev",
"GOVOPLAN_INSTALL_PROFILE": "evaluation",
"GOVOPLAN_INSTALLATION_ID": f"runtime-smoke-{platform_slug}",
"GOVOPLAN_STATE_PROFILE": "host-shared",
"GOVOPLAN_RUNTIME_HEARTBEAT_SECONDS": "5",
"GOVOPLAN_RUNTIME_STALE_AFTER_SECONDS": "30",
"GOVOPLAN_EXPECTED_API_REPLICAS": "1",
"GOVOPLAN_EXPECTED_WORKER_REPLICAS": "1",
"DATABASE_URL": database,
"GOVOPLAN_DATABASE_URL_PGTOOLS": (
f"postgresql://govoplan:{database_password}@postgres:5432/govoplan"
),
"GOVOPLAN_DB_CONNECTION_LIMIT": "100",
"GOVOPLAN_DB_CONNECTION_RESERVE": "10",
"REDIS_URL": "redis://redis:6379/0",
"CELERY_ENABLED": "true",
"CELERY_QUEUES": "default",
"CELERY_WORKER_CONCURRENCY": "1",
"ENABLED_MODULES": ",".join(BASE_MODULES),
"GOVOPLAN_MIGRATION_TRACK": "release",
"DEV_AUTO_MIGRATE_ENABLED": "false",
"DEV_BOOTSTRAP_ENABLED": "false",
"AUTH_LOGIN_THROTTLE_ENABLED": "true",
"AUTH_COOKIE_SECURE": "false",
"CORS_ORIGINS": "http://localhost",
"GOVOPLAN_TRUSTED_HOSTS": "127.0.0.1,localhost,api",
"FORWARDED_ALLOW_IPS": "127.0.0.1",
"MASTER_KEY_B64": master_key,
"FILE_STORAGE_BACKEND": "local",
"FILE_STORAGE_LOCAL_ROOT": "/var/lib/govoplan/files",
"GOVOPLAN_MODULE_LIVE_APPLY_ENABLED": "false",
}
def _env_arguments(values: dict[str, str], *, role: str, node_id: str) -> list[str]:
arguments: list[str] = []
for key, value in sorted(
{**values, "GOVOPLAN_RUNTIME_ROLE": role, "GOVOPLAN_NODE_ID": node_id}.items()
):
arguments.extend(("--env", f"{key}={value}"))
return arguments
def run_smoke(
*,
api_image: str,
web_image: str,
postgres_image: str,
redis_image: str,
platform: str,
timeout: float,
) -> dict[str, object]:
for label, value in (
("API image", api_image),
("Web image", web_image),
("PostgreSQL image", postgres_image),
("Redis image", redis_image),
):
_digest_image(value, label)
if platform not in PLATFORMS:
raise SmokeError(f"unsupported runtime smoke platform: {platform}")
slug = platform.replace("linux/", "").replace("/", "-")
suffix = secrets.token_hex(4)
prefix = f"govoplan-runtime-{slug}-{suffix}"
names = {
"network": f"{prefix}-network",
"volume": f"{prefix}-data",
"postgres": f"{prefix}-postgres",
"redis": f"{prefix}-redis",
"api": f"{prefix}-api",
"web": f"{prefix}-web",
"worker": f"{prefix}-worker",
}
database_password = secrets.token_hex(20)
master_key = base64.urlsafe_b64encode(os.urandom(32)).decode("ascii")
redactions = (database_password, master_key)
environment = _environment(
database_password=database_password,
master_key=master_key,
platform_slug=slug,
)
checks: list[dict[str, object]] = []
started = time.monotonic()
def record(check_id: str, began: float) -> None:
checks.append(
{
"id": check_id,
"state": "passed",
"duration_seconds": round(time.monotonic() - began, 3),
}
)
common_runtime = [
"--platform",
platform,
"--network",
names["network"],
"--read-only",
"--tmpfs",
"/tmp:rw,noexec,nosuid,size=64m",
"--security-opt",
"no-new-privileges:true",
"--cap-drop",
"ALL",
"--mount",
f"type=volume,source={names['volume']},target=/var/lib/govoplan",
]
try:
_run(("docker", "network", "create", names["network"]), timeout=timeout)
_run(("docker", "volume", "create", names["volume"]), timeout=timeout)
began = time.monotonic()
_run(
(
"docker",
"run",
"--detach",
"--platform",
platform,
"--name",
names["postgres"],
"--network",
names["network"],
"--network-alias",
"postgres",
"--env",
"POSTGRES_DB=govoplan",
"--env",
"POSTGRES_USER=govoplan",
"--env",
f"POSTGRES_PASSWORD={database_password}",
"--tmpfs",
"/var/lib/postgresql/data:rw,noexec,nosuid,size=384m",
postgres_image,
),
timeout=timeout,
redactions=redactions,
)
_run(
(
"docker",
"run",
"--detach",
"--platform",
platform,
"--name",
names["redis"],
"--network",
names["network"],
"--network-alias",
"redis",
"--read-only",
"--tmpfs",
"/data:rw,noexec,nosuid,size=64m",
redis_image,
"redis-server",
"--save",
"",
"--appendonly",
"no",
),
timeout=timeout,
)
_wait_for(
"PostgreSQL",
lambda: _run(
(
"docker",
"exec",
names["postgres"],
"pg_isready",
"--username",
"govoplan",
"--dbname",
"govoplan",
),
check=False,
timeout=30,
),
timeout=timeout,
)
_wait_for(
"Redis",
lambda: _run(
("docker", "exec", names["redis"], "redis-cli", "ping"),
check=False,
timeout=30,
),
timeout=timeout,
)
record("managed_dependencies_ready", began)
began = time.monotonic()
_run(
(
"docker",
"run",
"--rm",
"--name",
f"{prefix}-migrate",
*common_runtime,
*_env_arguments(
environment,
role="migration",
node_id=f"runtime-smoke-{slug}-migration",
),
api_image,
"python",
"-m",
"govoplan_core.commands.init_db",
"--migration-track",
"release",
),
timeout=timeout,
redactions=redactions,
)
record("release_migrations", began)
began = time.monotonic()
_run(
(
"docker",
"run",
"--detach",
"--name",
names["api"],
"--network-alias",
"api",
*common_runtime,
*_env_arguments(
environment,
role="api",
node_id=f"runtime-smoke-{slug}-api",
),
api_image,
),
timeout=timeout,
redactions=redactions,
)
_wait_for(
"GovOPlaN API",
lambda: _run(
(
"docker",
"exec",
names["api"],
"python",
"-c",
(
"import urllib.request;"
"r=urllib.request.Request('http://127.0.0.1:8000/health/ready',"
"headers={'Host':'127.0.0.1'});"
"assert urllib.request.urlopen(r,timeout=3).status==200"
),
),
check=False,
timeout=30,
),
timeout=timeout,
)
_run(
(
"docker",
"exec",
names["api"],
"python",
"-c",
"import os; assert os.getuid() == 10001",
),
timeout=30,
)
record("api_non_root_readiness", began)
began = time.monotonic()
_run(
(
"docker",
"run",
"--detach",
"--platform",
platform,
"--name",
names["web"],
"--network",
names["network"],
"--network-alias",
"web",
"--read-only",
"--tmpfs",
"/tmp:rw,noexec,nosuid,size=64m",
"--security-opt",
"no-new-privileges:true",
"--cap-drop",
"ALL",
web_image,
),
timeout=timeout,
)
_wait_for(
"GovOPlaN WebUI",
lambda: _run(
(
"docker",
"exec",
names["api"],
"python",
"-c",
(
"import urllib.request;"
"assert urllib.request.urlopen('http://web:8080/health',timeout=3).status==200;"
"assert urllib.request.urlopen('http://web:8080/',timeout=3).status==200"
),
),
check=False,
timeout=30,
),
timeout=timeout,
)
_run(
("docker", "exec", names["web"], "sh", "-c", "test \"$(id -u)\" = 101"),
timeout=30,
)
record("web_non_root_readiness", began)
began = time.monotonic()
_run(
(
"docker",
"run",
"--detach",
"--name",
names["worker"],
*common_runtime,
*_env_arguments(
environment,
role="worker",
node_id=f"runtime-smoke-{slug}-worker",
),
api_image,
"python",
"-m",
"celery",
"-A",
"govoplan_core.celery_app:celery",
"worker",
"--queues",
"default",
"--pool",
"solo",
"--concurrency",
"1",
"--hostname",
f"runtime-smoke-{slug}@%h",
"--loglevel",
"WARNING",
),
timeout=timeout,
redactions=redactions,
)
_wait_for(
"GovOPlaN worker",
lambda: _run(
(
"docker",
"exec",
names["api"],
"python",
"-c",
(
"from govoplan_core.celery_app import celery;"
"result=celery.send_task('govoplan.ping',queue='default');"
"assert result.get(timeout=10)=='pong'"
),
),
check=False,
timeout=30,
),
timeout=timeout,
)
_run(("docker", "stop", "--time", "20", names["worker"]), timeout=30)
record("worker_delivery_and_shutdown", began)
except SmokeError as exc:
for role in ("api", "web", "worker", "postgres", "redis"):
result = _run(
("docker", "logs", "--tail", "100", names[role]),
check=False,
timeout=30,
)
if result.stdout or result.stderr:
detail = _tail(result.stdout + result.stderr, limit=8000)
for secret in redactions:
detail = detail.replace(secret, "[redacted]")
print(f"--- {role} logs ---\n{detail}")
raise exc
finally:
for role in ("worker", "web", "api", "redis", "postgres"):
_run(
("docker", "rm", "--force", names[role]),
check=False,
timeout=30,
)
_run(("docker", "volume", "rm", "--force", names["volume"]), check=False)
_run(("docker", "network", "rm", names["network"]), check=False)
return {
"schema_version": "1",
"evidence_kind": "govoplan.runtime-image-smoke",
"captured_at": _utc_now(),
"platform": platform,
"images": {
"api": api_image,
"web": web_image,
"postgres": postgres_image,
"redis": redis_image,
},
"result": {"state": "passed"},
"checks": checks,
"duration_seconds": round(time.monotonic() - started, 3),
}
def main() -> int:
args = build_parser().parse_args()
try:
api_image = platform_image(args.api_metadata, args.platform, "API")
web_image = platform_image(args.web_metadata, args.platform, "Web")
evidence = run_smoke(
api_image=api_image,
web_image=web_image,
postgres_image=args.postgres_image,
redis_image=args.redis_image,
platform=args.platform,
timeout=args.timeout_seconds,
)
except (OSError, SmokeError, ValueError) as exc:
print(f"runtime image smoke failed: {exc}")
return 1
args.output.parent.mkdir(parents=True, exist_ok=True)
temporary = args.output.with_suffix(args.output.suffix + ".tmp")
temporary.write_text(json.dumps(evidence, indent=2, sort_keys=True) + "\n", encoding="utf-8")
temporary.chmod(0o644)
temporary.replace(args.output)
print(f"Runtime image smoke evidence written to {args.output}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+40 -8
View File
@@ -6,11 +6,13 @@ from __future__ import annotations
import argparse
from hashlib import sha256
from pathlib import Path
import zipapp
from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
ROOT = Path(__file__).resolve().parent
DEFAULT_OUTPUT = ROOT.parent.parent / "runtime" / "deployment" / "govoplan-deploy.pyz"
ZIP_TIMESTAMP = (1980, 1, 1, 0, 0, 0)
PYTHON_FILE_MODE = 0o100644
def main(argv: list[str] | None = None) -> int:
@@ -25,13 +27,7 @@ def main(argv: list[str] | None = None) -> int:
temporary = output.with_name(f".{output.name}.tmp")
if temporary.exists():
temporary.unlink()
zipapp.create_archive(
ROOT,
target=temporary,
interpreter="/usr/bin/env python3",
compressed=True,
filter=_include_source,
)
_write_reproducible_zipapp(temporary)
temporary.chmod(0o755)
temporary.replace(output)
digest = sha256(output.read_bytes()).hexdigest()
@@ -39,6 +35,42 @@ def main(argv: list[str] | None = None) -> int:
return 0
def _write_reproducible_zipapp(target: Path) -> None:
sources = tuple(
path
for path in sorted(ROOT.rglob("*"), key=lambda item: item.as_posix())
if path.is_file() and _include_source(path.relative_to(ROOT))
)
if not any(path.relative_to(ROOT).as_posix() == "__main__.py" for path in sources):
raise ValueError("deployment source has no __main__.py")
for path in sources:
if path.is_symlink():
raise ValueError(f"deployment source must not contain symlinks: {path}")
with target.open("wb") as handle:
handle.write(b"#!/usr/bin/env python3\n")
with ZipFile(
handle,
mode="w",
compression=ZIP_DEFLATED,
compresslevel=9,
strict_timestamps=True,
) as archive:
for source in sources:
relative = source.relative_to(ROOT).as_posix()
info = ZipInfo(relative, date_time=ZIP_TIMESTAMP)
info.compress_type = ZIP_DEFLATED
info.create_system = 3
info.external_attr = PYTHON_FILE_MODE << 16
info.flag_bits |= 0x800
archive.writestr(
info,
source.read_bytes(),
compress_type=ZIP_DEFLATED,
compresslevel=9,
)
def _include_source(path: Path) -> bool:
return (
"__pycache__" not in path.parts