Files
ITWorx_MCP_Hub-Public/scripts/promotion_preflight.py
T

1399 lines
52 KiB
Python

#!/usr/bin/env python3
"""Fail-closed, read-only structural and optional live production preflight."""
from __future__ import annotations
import argparse
import copy
import hashlib
import ipaddress
import json
import os
import re
import shutil
import socket
import stat
import subprocess
from pathlib import Path
from typing import Any, cast
from urllib.parse import urlparse
from urllib.request import HTTPRedirectHandler, Request, build_opener
from uuid import UUID
import yaml # type: ignore[import-untyped]
ROOT = Path(__file__).resolve().parents[1]
BASE_COMPOSE = ROOT / "infrastructure/compose/docker-compose.yml"
PROD_COMPOSE = ROOT / "infrastructure/compose/docker-compose.prod.yml"
INPUT_MAP = ROOT / "infrastructure/compose/production-input-map.json"
CONTEXTFORGE_CONTRACT = ROOT / "infrastructure/images/contextforge/runtime-contract.json"
VARIABLE = re.compile(r"\$\{([A-Z][A-Z0-9_]*)(?:(:-|:\?)([^}]*))?\}")
IMAGE_DIGEST = re.compile(r"@sha256:[0-9a-f]{64}$")
CONTEXTFORGE_PRODUCTION_IMAGE = "itworx-mcp-hub/contextforge-schema:12d4a0c7789c"
def _contextforge_contract_checks(
environment: dict[str, str],
topology: dict[str, Any],
results: list[dict[str, str]],
) -> bool:
try:
raw_contract = CONTEXTFORGE_CONTRACT.read_bytes()
contract = json.loads(raw_contract)
except (OSError, json.JSONDecodeError):
return _check(
results,
"contextforge.runtime-contract",
False,
"ContextForge runtime contract is unreadable",
)
expected_digest = hashlib.sha256(raw_contract).hexdigest()
service = topology.get("services", {}).get("contextforge", {})
service_environment = service.get("environment", {})
contract_environment = contract.get("productionEnvironment", {})
configured_digest = str(
service_environment.get("ITWORX_CONTEXTFORGE_EXPECTED_CONTRACT_SHA256") or ""
)
digest_ok = (
re.fullmatch(r"[0-9a-f]{64}", configured_digest) is not None
and configured_digest == expected_digest
)
image_ok = (
service.get("image") == CONTEXTFORGE_PRODUCTION_IMAGE
and service.get("pull_policy") == "never"
and "build" not in service
)
environment_ok = isinstance(contract_environment, dict) and all(
isinstance(name, str) and isinstance(value, str) and service_environment.get(name) == value
for name, value in contract_environment.items()
)
migration_policy = contract.get("migrationPolicy", {})
migration_ok = (
isinstance(migration_policy, dict)
and migration_policy.get("startupMigrations") == "forbidden"
and migration_policy.get("skipEnv") == "MCPGATEWAY_SKIP_MIGRATIONS"
and migration_policy.get("skipValue") == "true"
and service_environment.get("MCPGATEWAY_SKIP_MIGRATIONS") == "true"
)
required_runtime_mounts = {
"../images/contextforge/entrypoint.sh:/opt/itworx/contextforge-entrypoint.sh:ro",
"../images/contextforge/runtime-contract.json:/opt/itworx/contextforge-runtime-contract.json:ro",
"../../scripts/contextforge_runtime_contract.py:/opt/itworx/contextforge-runtime-contract.py:ro",
}
runtime_mounts = {str(item) for item in service.get("volumes", []) if isinstance(item, str)}
source_bound_runtime_ok = runtime_mounts == required_runtime_mounts and len(
service.get("volumes", [])
) == len(required_runtime_mounts)
healthcheck = json.dumps(service.get("healthcheck", {}), sort_keys=True)
health_ok = (
"contextforge-runtime-contract.py --health" in healthcheck
and "127.0.0.1:4444/health" in healthcheck
)
contract_shape_ok = (
contract.get("schema") == "itworx.contextforge-runtime-contract/v1"
and re.fullmatch(r"[0-9a-f]{40}", str(contract.get("sourceCommit", ""))) is not None
and bool(str(contract.get("alembicHead", "")))
and bool(str(contract.get("mcpSdk", "")))
and contract.get("requiredSymbol") == "McpError"
)
ok = (
digest_ok
and image_ok
and environment_ok
and migration_ok
and source_bound_runtime_ok
and health_ok
and contract_shape_ok
)
return _check(
results,
"contextforge.runtime-contract",
ok,
"ContextForge production image, contract, exact-source runtime mounts and health gate are exact"
if ok
else "ContextForge runtime contract, digest or fail-closed production wiring drifted",
)
class _RejectOidcDiscoveryRedirects(HTTPRedirectHandler):
"""Prevent discovery from contacting any redirect target."""
def redirect_request(
self,
req: Request,
fp: Any,
code: int,
msg: str,
headers: Any,
newurl: str,
) -> None:
return None
_OIDC_DISCOVERY_OPENER = build_opener(_RejectOidcDiscoveryRedirects())
PLACEHOLDER = re.compile(
r"(?:REPLACE|CHANGEME|<[^>]+>|example\.(?:com|invalid)|\.invalid(?:/|$)|local/|:dev(?:@|$))",
re.IGNORECASE,
)
_RESET = object()
class _ComposeMergeLoader(yaml.SafeLoader): # type: ignore[misc]
"""SafeLoader that understands the Compose Specification's `!reset` merge tag."""
def _construct_reset(loader: yaml.SafeLoader, node: yaml.Node) -> object:
return _RESET
_ComposeMergeLoader.add_constructor("!reset", _construct_reset)
REQUIRED_ENV = (
"UNRAID_HOST",
"APPDATA_ROOT",
"SECRETS_DIR",
"PROJECTS_READONLY_ROOT",
"KNOWLEDGE_SOURCES_JSON",
"BACKUP_ROOT",
"UNRAID_REGISTRATIONS_FILE",
"HUB_BIND_IP",
"MCP_BIND_IP",
"HUB_PORT",
"MCP_PORT",
"PUID",
"PGID",
"DOCKER_SOCKET_GID",
"REGISTRY_HOST",
"REGISTRY_NAMESPACE",
"REGISTRY_CREDENTIAL_REF",
"HUB_OIDC_ISSUER",
"HUB_OIDC_AUDIENCE",
"HUB_OIDC_CLIENT_ID",
"HUB_OIDC_REDIRECT_URI",
"HUB_OIDC_ROLE_GROUP_MAP",
"HUB_MCP_ADDITIONAL_OIDC_ISSUER",
"HUB_MCP_ADDITIONAL_OIDC_AUDIENCE",
"HUB_MCP_ADDITIONAL_OIDC_CLIENT_ID",
"HUB_MCP_ADDITIONAL_OIDC_REDIRECT_URI",
"HUB_PUBLIC_MCP_URL",
"HUB_RATE_LIMIT_PER_MINUTE",
"AUTHENTIK_PROVIDER_ID",
"AUTHENTIK_HUB_APPLICATION_ID",
"AUTHENTIK_SCOPES",
"AUTHENTIK_ALLOWED_GROUPS",
"AUTHENTIK_SERVICE_ACCOUNTS",
"MCP_CLIENT_BINDINGS_JSON",
"HUB_CONTEXTFORGE_URL",
"CONTEXTFORGE_DB_HOST",
"CONTEXTFORGE_DB_NAME",
"CONTEXTFORGE_DB_USER",
"REDIS_AUTH_ENABLED",
"GITEA_ENDPOINT",
"GITEA_REPOSITORIES_JSON",
"GITEA_AUTO_DISCOVER_PROJECT",
"GITEA_TOKEN_SCOPE",
"GITEA_WRITE_TOKEN_SCOPE",
"GITEA_ENABLE_PR_WRITES",
"MOBILITYOPS_ENDPOINT",
"MOBILITYOPS_PROJECTS_JSON",
"PROJECTBRAIN_ENDPOINT",
"PROJECTBRAIN_PROJECTS",
"APPOPS_ENDPOINT",
"APPOPS_PROJECTS",
"NPM_NETWORK_NAME",
"NPM_HUB_PROXY_HOST",
"NPM_MCP_PROXY_HOST",
"NPM_CERTIFICATE_REF",
"DNS_HUB_HOSTNAME",
"DNS_MCP_HOSTNAME",
"DNS_EXPECTED_ADDRESSES_JSON",
"NETWORK_TARGETS_JSON",
"DEMO_TOKEN_REVOCATIONS_DIR_HOST",
)
REQUIRED_SECRET_FILES = (
"postgres_superuser_password",
"hub_database_password",
"contextforge_database_password",
"contextforge_auth_encryption_secret",
"contextforge_jwt_secret",
"contextforge_admin_credential",
"docker_broker_service_token",
"connector_identity_signing_key",
"demo_token_signing_key",
"authentik_oidc_client_secret",
"hub_session_signing_key",
"gitea_mcp_token",
"gitea_mcp_write_token",
"mobilityops_service_token",
"projectbrain_hub_service_token",
"appops_hub_service_token",
"stitch_api_key",
"redis_password",
)
GENERATED_SECRET_FILES = ("contextforge_api_token",)
IMAGE_ENVIRONMENT = {
"control-api": "HUB_CONTROL_IMAGE",
"worker": "HUB_WORKER_IMAGE",
"dataplane-router": "HUB_DATAPLANE_IMAGE",
"web": "HUB_WEB_IMAGE",
"connector-knowledge": "CONNECTOR_KNOWLEDGE_IMAGE",
"connector-gitea": "CONNECTOR_GITEA_IMAGE",
"connector-unraid": "CONNECTOR_UNRAID_IMAGE",
"connector-mobilityops": "CONNECTOR_MOBILITYOPS_IMAGE",
"connector-projectbrain": "CONNECTOR_PROJECTBRAIN_IMAGE",
"connector-appops": "CONNECTOR_APPOPS_IMAGE",
"connector-stitch": "CONNECTOR_STITCH_IMAGE",
"docker-read-broker": "DOCKER_READ_BROKER_IMAGE",
"observability-collector": "OBSERVABILITY_COLLECTOR_IMAGE",
}
REQUIRED_SERVICES = {
"postgres",
"redis",
"contextforge",
"control-api",
"worker",
"dataplane-router",
"web",
"connector-knowledge",
"connector-gitea",
"connector-unraid",
"connector-projectbrain",
"connector-appops",
"connector-stitch",
"docker-read-broker",
"observability-collector",
}
OWNED_NETWORKS = (
"itworx-mcp-hub-edge",
"itworx-mcp-hub-egress",
"itworx-mcp-hub-control",
"itworx-mcp-hub-connectors",
"itworx-mcp-hub-broker",
"itworx-mcp-hub-data",
"itworx-mcp-hub-observe",
)
def load_env_file(path: Path) -> dict[str, str]:
values: dict[str, str] = {}
for line_number, raw in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
line = raw.strip()
if not line or line.startswith("#"):
continue
if line.startswith("export "):
line = line[7:].lstrip()
if "=" not in line:
raise ValueError(f"malformed env-file line {line_number}")
name, value = line.split("=", 1)
name = name.strip()
if not re.fullmatch(r"[A-Z][A-Z0-9_]*", name):
raise ValueError(f"invalid env name on line {line_number}")
values[name] = value.strip().strip('"').strip("'")
return values
def _check(results: list[dict[str, str]], check_id: str, passed: bool, detail: str) -> bool:
results.append({"id": check_id, "status": "pass" if passed else "fail", "detail": detail})
return passed
def _defer(results: list[dict[str, str]], check_id: str, detail: str) -> None:
results.append({"id": check_id, "status": "deferred", "detail": detail})
def _contains_placeholder(value: str) -> bool:
return not value.strip() or bool(PLACEHOLDER.search(value)) or "${" in value
def _required_values(environment: dict[str, str], results: list[dict[str, str]]) -> bool:
required = [*REQUIRED_ENV, *IMAGE_ENVIRONMENT.values()]
missing = [name for name in required if not environment.get(name, "").strip()]
placeholders = [
name
for name in required
if environment.get(name, "").strip() and _contains_placeholder(environment[name])
]
return _check(
results,
"inputs.required",
not missing and not placeholders,
"all required inputs are non-empty and non-placeholder"
if not missing and not placeholders
else f"{len(missing)} missing and {len(placeholders)} placeholder input(s)",
)
def _decode_json(value: str, expected: type[Any]) -> Any | None:
try:
decoded = json.loads(value)
except (TypeError, json.JSONDecodeError):
return None
return decoded if isinstance(decoded, expected) and bool(decoded) else None
def _structured_input_checks(environment: dict[str, str], results: list[dict[str, str]]) -> bool:
expected_types: dict[str, type[Any]] = {
"HUB_OIDC_ROLE_GROUP_MAP": dict,
"AUTHENTIK_SERVICE_ACCOUNTS": list,
"MCP_CLIENT_BINDINGS_JSON": list,
"KNOWLEDGE_SOURCES_JSON": list,
"GITEA_REPOSITORIES_JSON": list,
"DNS_EXPECTED_ADDRESSES_JSON": dict,
"NETWORK_TARGETS_JSON": dict,
}
valid = True
for name, expected_type in expected_types.items():
decoded = _decode_json(environment.get(name, ""), expected_type)
ok = decoded is not None and not _contains_placeholder(json.dumps(decoded))
valid &= _check(
results,
f"inputs.{name.lower()}",
ok,
"structured input is non-empty and contains no placeholders"
if ok
else "structured input is invalid, empty, or contains placeholders",
)
sources = _decode_json(environment.get("KNOWLEDGE_SOURCES_JSON", ""), list) or []
source_root = Path(environment.get("PROJECTS_READONLY_ROOT", ""))
source_shape = all(
isinstance(item, dict)
and isinstance(item.get("project"), str)
and isinstance(item.get("sourceRef"), str)
and isinstance(item.get("path"), str)
and item["project"]
and item["sourceRef"]
and not Path(item["path"]).is_absolute()
and ".." not in Path(item["path"]).parts
for item in sources
)
source_paths = False
if sources and source_shape and source_root.is_dir():
try:
resolved_root = source_root.resolve(strict=True)
source_paths = all(
resolved_root == (resolved_root / item["path"]).resolve(strict=True)
or resolved_root in (resolved_root / item["path"]).resolve(strict=True).parents
for item in sources
)
except OSError:
source_paths = False
valid &= _check(
results,
"inputs.knowledge-sources-shape",
bool(sources) and source_shape and source_paths,
"knowledge sources are non-empty registered relative roots that exist"
if sources and source_shape and source_paths
else (
"knowledge sources must contain project, sourceRef, and an existing safe relative path"
),
)
return valid
def _path_checks(
environment: dict[str, str], results: list[dict[str, str]], phase: str
) -> tuple[bool, list[bytes]]:
valid = True
for name in ("APPDATA_ROOT", "SECRETS_DIR", "PROJECTS_READONLY_ROOT", "BACKUP_ROOT"):
path = Path(environment.get(name, ""))
exists = path.is_dir()
valid &= _check(
results,
f"path.{name.lower()}",
exists,
"directory exists" if exists else "required directory is missing",
)
if exists and os.name != "nt":
safe = not stat.S_IMODE(path.stat().st_mode) & 0o022
valid &= _check(
results,
f"permissions.{name.lower()}",
safe,
"directory is not group/world writable" if safe else "unsafe directory mode",
)
registrations = Path(environment.get("UNRAID_REGISTRATIONS_FILE", ""))
valid &= _check(
results,
"path.unraid-registrations-file",
registrations.is_file(),
"broker registration file exists"
if registrations.is_file()
else "broker registration file is missing",
)
revocation_directory = Path(environment.get("DEMO_TOKEN_REVOCATIONS_DIR_HOST", ""))
revocation_file = revocation_directory / "demo-token-revocations.json"
directory_ok = revocation_directory.is_dir()
if directory_ok and os.name != "nt":
directory_ok = not stat.S_IMODE(revocation_directory.stat().st_mode) & 0o022
valid &= _check(
results,
"path.demo-token-revocations-directory",
directory_ok,
"revocation directory exists and is not group/world writable"
if directory_ok
else "revocation directory is missing or unsafe",
)
file_ok = revocation_file.is_file()
if file_ok and os.name != "nt":
file_ok = stat.S_IMODE(revocation_file.stat().st_mode) == 0o600
valid &= _check(
results,
"path.demo-token-revocations-file",
file_ok,
"revocation file is a regular 0600 file"
if file_ok
else "revocation file is missing, not regular, or has an unsafe mode",
)
secrets = Path(environment.get("SECRETS_DIR", ""))
if secrets.is_dir() and os.name != "nt":
directory_ok = stat.S_IMODE(secrets.stat().st_mode) == 0o700
valid &= _check(
results,
"permissions.secrets-directory",
directory_ok,
"secret directory mode is 0700" if directory_ok else "secret directory must be 0700",
)
try:
expected_uid = int(environment.get("PUID", "-1"))
expected_gid = int(environment.get("PGID", "-1"))
socket_gid = int(environment.get("DOCKER_SOCKET_GID", "-1"))
ids_ok = expected_uid >= 0 and expected_gid >= 0 and socket_gid >= 0
except ValueError:
expected_uid = expected_gid = -1
ids_ok = False
valid &= _check(
results,
"identity.numeric",
ids_ok,
"PUID, PGID, and Docker socket GID are numeric"
if ids_ok
else "PUID, PGID, and Docker socket GID must be numeric",
)
secret_values: list[bytes] = []
secret_names = [*REQUIRED_SECRET_FILES]
if phase == "poststart":
secret_names.extend(GENERATED_SECRET_FILES)
else:
for name in GENERATED_SECRET_FILES:
_defer(results, f"secret.{name}", "generated by the ContextForge bootstrap step")
for name in secret_names:
path = secrets / name
present = path.is_file()
mode_ok = present and (os.name == "nt" or stat.S_IMODE(path.stat().st_mode) == 0o600)
owner_ok = present and (
os.name == "nt"
or (path.stat().st_uid == expected_uid and path.stat().st_gid == expected_gid)
)
content_ok = False
if present:
try:
payload = path.read_bytes()
content_ok = bool(payload.rstrip(b"\r\n")) and len(payload) <= 8192
content_ok = (
content_ok and b"\x00" not in payload and b"\r" not in payload.rstrip(b"\r\n")
)
content_ok = content_ok and b"\n" not in payload.rstrip(b"\r\n")
if name in ("connector_identity_signing_key", "demo_token_signing_key"):
content_ok = content_ok and len(payload.rstrip(b"\r\n")) >= 32
if content_ok:
secret_values.append(payload.rstrip(b"\r\n"))
except OSError:
content_ok = False
ok = present and mode_ok and owner_ok and content_ok
valid &= _check(
results,
f"secret.{name}",
ok,
"secret file satisfies owner, mode, and bounded-content policy"
if ok
else "secret file is missing, empty, unreadable, wrongly owned, or unsafe",
)
return valid, secret_values
def _substitute(value: str, environment: dict[str, str]) -> str:
def replace(match: re.Match[str]) -> str:
name, operator, fallback = match.groups()
current = environment.get(name, "")
if current:
return current
if operator == ":-":
return fallback or ""
return match.group(0)
return VARIABLE.sub(replace, value)
def _render(value: Any, environment: dict[str, str]) -> Any:
if isinstance(value, str):
return _substitute(value, environment)
if isinstance(value, list):
return [_render(item, environment) for item in value]
if isinstance(value, dict):
return {key: _render(item, environment) for key, item in value.items()}
return value
def _deep_merge(base: dict[str, Any], overlay: dict[str, Any]) -> dict[str, Any]:
result = copy.deepcopy(base)
for key, value in overlay.items():
if value is _RESET:
result.pop(key, None)
elif isinstance(value, dict) and isinstance(result.get(key), dict):
result[key] = _deep_merge(result[key], value)
else:
result[key] = copy.deepcopy(value)
return result
def _rendered_topology(environment: dict[str, str]) -> dict[str, Any]:
# _ComposeMergeLoader subclasses yaml.SafeLoader and only adds the !reset tag.
base = yaml.load(BASE_COMPOSE.read_text(encoding="utf-8"), Loader=_ComposeMergeLoader) # noqa: S506
production = yaml.load(PROD_COMPOSE.read_text(encoding="utf-8"), Loader=_ComposeMergeLoader) # noqa: S506
rendered = _render(_deep_merge(base, production), environment)
enabled_profiles = {
item.strip() for item in environment.get("COMPOSE_PROFILES", "").split(",") if item.strip()
}
if "inspector" not in enabled_profiles:
rendered.get("services", {}).pop("inspector", None)
return cast(dict[str, Any], rendered)
def _mapping_checks(topology: dict[str, Any], results: list[dict[str, str]]) -> bool:
try:
mapping = json.loads(INPUT_MAP.read_text(encoding="utf-8"))
inputs = mapping["inputs"]
except (OSError, KeyError, json.JSONDecodeError):
return _check(results, "mapping.readable", False, "production input map is invalid")
required_fields = {
"name",
"classification",
"source",
"reference",
"consumers",
"validation",
"rendered",
"phase",
}
complete = bool(inputs) and all(required_fields <= set(item) for item in inputs)
references = " ".join(str(item.get("reference", "")) for item in inputs)
covered = all(name in references for name in REQUIRED_ENV)
services = topology.get("services", {})
consumers_exist = all(
consumer in services
for item in inputs
for consumer in item.get("consumers", [])
if consumer in REQUIRED_SERVICES
)
return _check(
results,
"mapping.complete",
complete and covered and consumers_exist,
"required inputs have source, reference, consumer, validation, render policy, and phase"
if complete and covered and consumers_exist
else "production input map is incomplete or names an unknown service",
)
def _image_checks(
environment: dict[str, str],
topology: dict[str, Any],
results: list[dict[str, str]],
verify_registry: bool,
) -> bool:
valid = _contextforge_contract_checks(environment, topology, results)
registry = environment.get("REGISTRY_HOST", "").rstrip("/")
namespace = environment.get("REGISTRY_NAMESPACE", "").strip("/")
prefix = f"{registry}/{namespace}/"
for service, variable in IMAGE_ENVIRONMENT.items():
image = str(topology.get("services", {}).get(service, {}).get("image", ""))
immutable = bool(IMAGE_DIGEST.search(image)) and image.startswith(prefix)
immutable = immutable and "latest" not in image.lower() and "local/" not in image
valid &= _check(
results,
f"image.{service}",
immutable,
f"{variable} resolves to an immutable registry digest"
if immutable
else "owned image must resolve to the configured registry and immutable digest",
)
if verify_registry and immutable:
docker = shutil.which("docker")
inspected = False
if docker:
completed = subprocess.run( # noqa: S603
[docker, "manifest", "inspect", image],
capture_output=True,
text=True,
check=False,
)
inspected = completed.returncode == 0
valid &= _check(
results,
f"registry.{service}",
inspected,
"registry manifest is reachable" if inspected else "manifest inspection failed",
)
elif not verify_registry:
_defer(results, f"registry.{service}", "enable --verify-registry to contact registry")
return valid
def _service_secret_names(service: dict[str, Any]) -> set[str]:
return {
str(item if isinstance(item, str) else item.get("source", ""))
for item in service.get("secrets", [])
}
def _topology_checks(
topology: dict[str, Any], secret_values: list[bytes], results: list[dict[str, str]]
) -> bool:
services = topology.get("services", {})
valid = _check(
results,
"compose.required-services",
REQUIRED_SERVICES <= set(services),
"all required production services are present"
if REQUIRED_SERVICES <= set(services)
else "required production service is missing",
)
unresolved = "${" in json.dumps(topology, sort_keys=True)
valid &= _check(
results,
"compose.resolved",
not unresolved,
"rendered topology contains no unresolved variables"
if not unresolved
else "rendered topology contains unresolved variables",
)
socket_users = [
name
for name, service in services.items()
if "/var/run/docker.sock" in json.dumps(service.get("volumes", []))
]
valid &= _check(
results,
"compose.docker-socket",
socket_users == ["docker-read-broker"],
"only docker-read-broker receives the read-only socket"
if socket_users == ["docker-read-broker"]
else "Docker socket access violates the broker boundary",
)
ports: set[tuple[str, int]] = set()
ports_ok = True
for name, service in services.items():
published = service.get("ports", [])
if published and name not in {"web", "dataplane-router", "inspector"}:
ports_ok = False
for raw in published:
parts = str(raw).split(":")
if len(parts) != 3:
ports_ok = False
continue
host, port_text, _ = parts
try:
port = int(port_text)
except ValueError:
ports_ok = False
continue
key = (host, port)
if (
host in {"0.0.0.0", "::", ""} # noqa: S104 - fail-closed rejection.
or not 1 <= port <= 65535
or key in ports
):
ports_ok = False
ports.add(key)
valid &= _check(
results,
"compose.ports",
ports_ok,
"published ports are explicit, unique, and limited to edge services"
if ports_ok
else "ports are duplicate, wildcard, malformed, or exposed by an internal service",
)
gitea = services.get("connector-gitea", {})
gitea_ok = (
not gitea.get("ports")
and set(gitea.get("networks", [])) == {"hub-connectors", "hub-observe", "hub-egress"}
and gitea.get("read_only") is True
and gitea.get("cap_drop") == ["ALL"]
and "no-new-privileges:true" in gitea.get("security_opt", [])
and bool(gitea.get("tmpfs"))
and bool(gitea.get("pids_limit"))
and bool(gitea.get("mem_limit"))
and bool(gitea.get("cpus"))
and bool(gitea.get("healthcheck"))
and bool(gitea.get("depends_on", {}).get("contextforge"))
and "gitea_mcp_token" in _service_secret_names(gitea)
and "gitea_mcp_write_token" in _service_secret_names(gitea)
and "/run/secrets/gitea_mcp_token" == gitea.get("environment", {}).get("GITEA_TOKEN_FILE")
and "/run/secrets/gitea_mcp_write_token"
== gitea.get("environment", {}).get("GITEA_WRITE_TOKEN_FILE")
and gitea.get("environment", {}).get("GITEA_ENABLE_PR_WRITES") == "true"
and bool(gitea.get("user"))
)
valid &= _check(
results,
"compose.connector-gitea",
gitea_ok,
"Gitea connector is internal, hardened, dependent, and file-secret wired"
if gitea_ok
else "Gitea connector production wiring is incomplete",
)
required_mounts = {
"control-api": {
"authentik_oidc_client_secret",
"hub_session_signing_key",
"contextforge_api_token",
"hub_database_password",
},
"worker": {"contextforge_api_token", "hub_database_password"},
"dataplane-router": {"contextforge_api_token", "hub_database_password"},
"contextforge": {
"contextforge_database_password",
"contextforge_auth_encryption_secret",
"contextforge_jwt_secret",
"contextforge_admin_credential",
"redis_password",
},
"postgres": {
"postgres_superuser_password",
"hub_database_password",
"contextforge_database_password",
},
"redis": {"redis_password"},
"connector-gitea": {"gitea_mcp_token", "gitea_mcp_write_token"},
"connector-appops": {"appops_hub_service_token"},
"docker-read-broker": {"docker_broker_service_token"},
"connector-unraid": {"docker_broker_service_token"},
}
mounts_ok = all(
expected <= _service_secret_names(services.get(name, {}))
for name, expected in required_mounts.items()
) and set((*REQUIRED_SECRET_FILES, *GENERATED_SECRET_FILES)) <= set(topology.get("secrets", {}))
valid &= _check(
results,
"compose.secret-consumers",
mounts_ok,
"every required secret has an explicit service mount"
if mounts_ok
else "required secret declaration or service mount is missing",
)
serialized = json.dumps(topology, sort_keys=True).encode()
leaked = any(value and value in serialized for value in secret_values)
valid &= _check(
results,
"compose.secret-redaction",
not leaked,
"no secret value appears in rendered topology"
if not leaked
else "a secret value was interpolated into rendered topology",
)
return valid
def _url_checks(environment: dict[str, str], results: list[dict[str, str]]) -> bool:
valid = True
for name in (
"HUB_OIDC_ISSUER",
"HUB_OIDC_REDIRECT_URI",
"HUB_MCP_ADDITIONAL_OIDC_ISSUER",
"HUB_MCP_ADDITIONAL_OIDC_REDIRECT_URI",
"GITEA_ENDPOINT",
"PROJECTBRAIN_ENDPOINT",
):
parsed = urlparse(environment.get(name, ""))
ok = parsed.scheme == "https" and bool(parsed.hostname) and not parsed.username
ok = ok and not parsed.password and not _contains_placeholder(environment.get(name, ""))
valid &= _check(
results,
f"url.{name.lower()}",
ok,
"HTTPS endpoint is syntactically safe"
if ok
else "endpoint must be non-placeholder HTTPS",
)
contextforge = environment.get("HUB_CONTEXTFORGE_URL", "")
parsed = urlparse(contextforge)
contextforge_ok = contextforge == "http://contextforge-control:4444" or (
parsed.scheme == "https"
and bool(parsed.hostname)
and not parsed.username
and not parsed.password
)
valid &= _check(
results,
"url.contextforge",
contextforge_ok,
"ContextForge endpoint matches approved internal or HTTPS form"
if contextforge_ok
else "ContextForge endpoint is malformed or unapproved",
)
appops = environment.get("APPOPS_ENDPOINT", "")
parsed = urlparse(appops)
appops_ok = (
parsed.scheme in {"http", "https"}
and bool(parsed.hostname)
and not parsed.username
and not parsed.password
and parsed.path in {"", "/"}
and not parsed.query
and not parsed.fragment
and not _contains_placeholder(appops)
)
if appops_ok and parsed.scheme == "http":
try:
appops_ok = ipaddress.ip_address(str(parsed.hostname)).is_private
except ValueError:
appops_ok = False
valid &= _check(
results,
"url.appops_endpoint",
appops_ok,
"AppOps endpoint is private-IP HTTP or HTTPS"
if appops_ok
else "AppOps endpoint must be private-IP HTTP or HTTPS",
)
return valid
def _coherence_checks(environment: dict[str, str], results: list[dict[str, str]]) -> bool:
issuer = urlparse(environment.get("HUB_OIDC_ISSUER", ""))
redirect = urlparse(environment.get("HUB_OIDC_REDIRECT_URI", ""))
public_mcp = urlparse(environment.get("HUB_PUBLIC_MCP_URL", ""))
role_map = _decode_json(environment.get("HUB_OIDC_ROLE_GROUP_MAP", ""), dict) or {}
allowed_groups = {
item.strip() for item in environment.get("AUTHENTIK_ALLOWED_GROUPS", "").split(",")
}
scopes = {item.strip() for item in environment.get("AUTHENTIK_SCOPES", "").split(",")}
additional_redirect = urlparse(environment.get("HUB_MCP_ADDITIONAL_OIDC_REDIRECT_URI", ""))
additional_issuer = urlparse(environment.get("HUB_MCP_ADDITIONAL_OIDC_ISSUER", ""))
auth_ok = (
redirect.hostname == environment.get("DNS_HUB_HOSTNAME")
and redirect.path == "/auth/callback"
and set(role_map.values()) <= {"viewer", "operator", "admin"}
and set(role_map) <= allowed_groups
and {"openid", "profile", "email", "groups", "offline_access"} <= scopes
and additional_issuer.scheme == "https"
and additional_issuer.hostname == issuer.hostname
and additional_issuer.path == "/application/o/chatgpt-itworx-gitea/"
and not additional_issuer.query
and not additional_issuer.fragment
and additional_redirect.scheme == "https"
and additional_redirect.hostname == "chatgpt.com"
and bool(re.fullmatch(r"/connector/oauth/[A-Za-z0-9_-]+", additional_redirect.path))
and not additional_redirect.query
and not additional_redirect.fragment
and environment.get("HUB_MCP_ADDITIONAL_OIDC_AUDIENCE")
== environment.get("HUB_MCP_ADDITIONAL_OIDC_CLIENT_ID")
and environment.get("NPM_HUB_PROXY_HOST") == environment.get("DNS_HUB_HOSTNAME")
and environment.get("NPM_MCP_PROXY_HOST") == environment.get("DNS_MCP_HOSTNAME")
and public_mcp.scheme == "https"
and public_mcp.hostname == environment.get("DNS_MCP_HOSTNAME")
and not public_mcp.username
and not public_mcp.password
and public_mcp.path in {"", "/"}
)
valid = _check(
results,
"auth.coherence",
auth_ok,
"issuer/client/audience/redirect/scopes/groups are internally coherent"
if auth_ok
else "Authentik, redirect, scope, group, NPM, or DNS inputs conflict",
)
single_host_ok = environment.get("DNS_HUB_HOSTNAME") == environment.get(
"DNS_MCP_HOSTNAME"
) and environment.get("NPM_HUB_PROXY_HOST") == environment.get("NPM_MCP_PROXY_HOST")
valid &= _check(
results,
"auth.single_host",
single_host_ok,
"management and MCP dataplane share the single approved public hostname"
if single_host_ok
else "DNS_HUB_HOSTNAME/NPM_HUB_PROXY_HOST must equal DNS_MCP_HOSTNAME/NPM_MCP_PROXY_HOST "
"-- a distinct hub.example.com-style hostname is not supported",
)
cf_ok = (
environment.get("CONTEXTFORGE_DB_HOST") == "postgres"
and environment.get("CONTEXTFORGE_DB_NAME") == "contextforge"
and environment.get("CONTEXTFORGE_DB_USER") == "contextforge"
and environment.get("REDIS_AUTH_ENABLED") == "true"
)
valid &= _check(
results,
"contextforge.coherence",
cf_ok,
"ContextForge database and Redis-auth contracts are complete"
if cf_ok
else "ContextForge database settings or production Redis authentication are invalid",
)
try:
rate_limit = int(environment.get("HUB_RATE_LIMIT_PER_MINUTE", ""))
except ValueError:
rate_limit = 0
valid &= _check(
results,
"dataplane.rate-limit",
1 <= rate_limit <= 10_000,
"dataplane rate limit is a bounded positive integer"
if 1 <= rate_limit <= 10_000
else "dataplane rate limit is missing or outside 1..10000",
)
try:
upstream_concurrency = int(environment.get("HUB_UPSTREAM_MAX_CONCURRENCY", ""))
queue_timeout = float(environment.get("HUB_UPSTREAM_QUEUE_TIMEOUT_SECONDS", ""))
except ValueError:
upstream_concurrency = 0
queue_timeout = 0
admission_ok = 1 <= upstream_concurrency <= 256 and 0.1 <= queue_timeout <= 30
valid &= _check(
results,
"dataplane.admission-control",
admission_ok,
"dataplane concurrency and queue timeout are bounded"
if admission_ok
else "dataplane concurrency must be 1..256 and queue timeout 0.1..30 seconds",
)
repositories = _decode_json(environment.get("GITEA_REPOSITORIES_JSON", ""), list) or []
discovery_project = environment.get("GITEA_AUTO_DISCOVER_PROJECT", "").strip().lower()
gitea_ok = all(
isinstance(item, dict)
and all(
isinstance(item.get(key), str) and item[key].strip()
for key in ("project", "owner", "name")
)
for item in repositories
)
token_scopes = {
item.strip().lower() for item in environment.get("GITEA_TOKEN_SCOPE", "").split(",")
}
write_token_scopes = {
item.strip().lower() for item in environment.get("GITEA_WRITE_TOKEN_SCOPE", "").split(",")
}
gitea_ok = (
gitea_ok
and bool(token_scopes)
and not any("write" in scope or "admin" in scope for scope in token_scopes)
and write_token_scopes == {"write:repository", "write:issue"}
and environment.get("GITEA_ENABLE_PR_WRITES", "").lower() == "true"
and bool(discovery_project)
and discovery_project
in {str(item.get("project", "")).strip().lower() for item in repositories}
)
valid &= _check(
results,
"gitea.configuration",
bool(repositories) and gitea_ok,
"Gitea repository bootstrap, account discovery and separated read/write scopes are complete"
if repositories and gitea_ok
else "Gitea repository bootstrap or account-discovery project is malformed",
)
bindings = _decode_json(environment.get("MCP_CLIENT_BINDINGS_JSON", ""), list) or []
binding_keys: set[tuple[str, str, str]] = set()
bindings_ok = bool(bindings)
for item in bindings:
if not isinstance(item, dict):
bindings_ok = False
continue
try:
client_id = str(UUID(str(item.get("clientId", ""))))
except ValueError:
bindings_ok = False
continue
subject = item.get("subject")
project = item.get("project")
target_environment = item.get("environment")
key = (client_id, str(project), str(target_environment))
entry_ok = (
isinstance(subject, str)
and bool(subject.strip())
and isinstance(project, str)
and bool(re.fullmatch(r"[a-z0-9][a-z0-9-]{0,62}", project))
and target_environment in {"dev", "test", "staging", "prod"}
and isinstance(item.get("enabled", True), bool)
and key not in binding_keys
)
bindings_ok &= entry_ok
binding_keys.add(key)
valid &= _check(
results,
"auth.machine_bindings",
bindings_ok,
"machine subjects have unique explicit project/environment bindings"
if bindings_ok
else "machine binding inventory is malformed, empty, or ambiguous",
)
return valid
def _oidc_discovery_checks(
environment: dict[str, str], results: list[dict[str, str]], enabled: bool
) -> bool:
if not enabled:
_defer(
results,
"oidc.discovery",
"enable --verify-oidc-discovery to validate live refresh capabilities",
)
return True
valid = True
issuers = {
"hub": environment.get("HUB_OIDC_ISSUER", "").strip(),
"chatgpt": environment.get("HUB_MCP_ADDITIONAL_OIDC_ISSUER", "").strip(),
}
for name, issuer in issuers.items():
payload: dict[str, Any] = {}
try:
if urlparse(issuer).scheme != "https":
raise ValueError("OIDC issuer must use HTTPS")
discovery_url = issuer.rstrip("/") + "/.well-known/openid-configuration"
request = Request( # noqa: S310
discovery_url, headers={"Accept": "application/json"}
)
with _OIDC_DISCOVERY_OPENER.open(request, timeout=5) as response: # noqa: S310
body = response.read(65_537)
if (
response.status != 200
or response.geturl() != discovery_url
or len(body) > 65_536
):
raise ValueError("OIDC discovery response is invalid or oversized")
decoded = json.loads(body)
if isinstance(decoded, dict):
payload = decoded
except (OSError, ValueError, json.JSONDecodeError):
payload = {}
scopes = set(payload.get("scopes_supported", []))
grants = set(payload.get("grant_types_supported", []))
pkce = set(payload.get("code_challenge_methods_supported", []))
authorization = urlparse(str(payload.get("authorization_endpoint", "")))
token = urlparse(str(payload.get("token_endpoint", "")))
ok = (
payload.get("issuer") == issuer
and {"openid", "offline_access"} <= scopes
and {"authorization_code", "refresh_token"} <= grants
and "S256" in pkce
and authorization.scheme == "https"
and token.scheme == "https"
and authorization.hostname == token.hostname == urlparse(issuer).hostname
)
valid &= _check(
results,
f"oidc.{name}.discovery",
ok,
"issuer, endpoints, PKCE, offline_access and refresh grant are coherent"
if ok
else "live OIDC discovery lacks a required refresh capability",
)
return valid
def _docker_network_checks(
environment: dict[str, str], results: list[dict[str, str]], enabled: bool
) -> bool:
if not enabled:
_defer(results, "network.live", "enable --verify-docker-networks on the intended target")
return True
docker = shutil.which("docker")
valid = bool(docker)
for network in [*OWNED_NETWORKS, environment.get("NPM_NETWORK_NAME", "")]:
completed = subprocess.run( # noqa: S603
[docker or "docker", "network", "inspect", network],
capture_output=True,
text=True,
check=False,
)
valid &= _check(
results,
f"network.{network}",
completed.returncode == 0,
"Docker network exists"
if completed.returncode == 0
else "Docker network is unavailable",
)
return valid
def _target_checks(
environment: dict[str, str], results: list[dict[str, str]], enabled: bool
) -> bool:
if not enabled:
_defer(
results, "connectivity.live", "enable --verify-network-targets to open target sockets"
)
return True
targets = _decode_json(environment.get("NETWORK_TARGETS_JSON", ""), dict) or {}
valid = True
for name, target in sorted(targets.items()):
reachable = False
if isinstance(target, str) and ":" in target:
host, port_text = target.rsplit(":", 1)
try:
with socket.create_connection((host, int(port_text)), timeout=3):
reachable = True
except (OSError, ValueError):
pass
valid &= _check(
results,
f"connectivity.{name}",
reachable,
"TCP target is reachable" if reachable else "TCP target is unavailable",
)
return valid
def _dns_checks(environment: dict[str, str], results: list[dict[str, str]], enabled: bool) -> bool:
if not enabled:
_defer(results, "dns.live", "enable --verify-dns to query DNS")
return True
expected = _decode_json(environment.get("DNS_EXPECTED_ADDRESSES_JSON", ""), dict) or {}
valid = True
for variable in ("DNS_HUB_HOSTNAME", "DNS_MCP_HOSTNAME"):
hostname = environment.get(variable, "")
addresses = set(expected.get(hostname, []))
try:
resolved = {item[4][0] for item in socket.getaddrinfo(hostname, 443)}
except OSError:
resolved = set()
ok = bool(addresses & resolved)
valid &= _check(
results,
f"dns.{hostname or variable.lower()}",
ok,
"DNS matches an expected address" if ok else "DNS does not match expected addresses",
)
return valid
def _bind_port_checks(
environment: dict[str, str],
topology: dict[str, Any],
results: list[dict[str, str]],
enabled: bool,
phase: str,
) -> bool:
if not enabled:
_defer(results, "ports.live", "enable --verify-bind-ports on the intended target")
return True
valid = True
services = {"HUB": "web", "MCP": "dataplane-router"}
docker = shutil.which("docker") if phase == "poststart" else None
for prefix, service_name in services.items():
bind_ip = environment.get(f"{prefix}_BIND_IP", "")
try:
host_port = int(environment[f"{prefix}_PORT"])
except (KeyError, ValueError):
host_port = 0
available = False
if phase == "predeploy":
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
probe.bind((bind_ip, host_port))
available = True
except (OSError, ValueError):
pass
else:
published = topology.get("services", {}).get(service_name, {}).get("ports", [])
target_ports = []
for item in published:
parts = str(item).rsplit(":", 2)
if len(parts) == 3 and parts[0] == bind_ip and parts[1] == str(host_port):
target_ports.append(parts[2])
if docker and len(target_ports) == 1:
listed = subprocess.run( # noqa: S603
[
docker,
"ps",
"--filter",
"label=com.docker.compose.project=itworx-mcp-hub",
"--filter",
f"label=com.docker.compose.service={service_name}",
"--format",
"{{.ID}}",
],
capture_output=True,
text=True,
check=False,
)
container_ids = listed.stdout.splitlines() if listed.returncode == 0 else []
if len(container_ids) == 1:
inspected = subprocess.run( # noqa: S603
[
docker,
"inspect",
"--format",
"{{json .NetworkSettings.Ports}}",
container_ids[0],
],
capture_output=True,
text=True,
check=False,
)
try:
bindings = json.loads(inspected.stdout) if inspected.returncode == 0 else {}
except json.JSONDecodeError:
bindings = {}
expected = {"HostIp": bind_ip, "HostPort": str(host_port)}
available = bindings == {f"{target_ports[0]}/tcp": [expected]}
valid &= _check(
results,
f"ports.{prefix.lower()}",
available,
(
"host binding is available"
if phase == "predeploy" and available
else "expected project service owns the exact host binding"
if available
else "host binding conflicts or is invalid"
if phase == "predeploy"
else "expected project service does not own the exact host binding"
),
)
return valid
def redacted_topology(topology: dict[str, Any]) -> dict[str, Any]:
"""Return only deployable structure; omit values and host paths."""
services: dict[str, Any] = {}
for name, service in sorted(topology.get("services", {}).items()):
services[name] = {
"image": service.get("image"),
"networks": service.get("networks", []),
"ports": ["[REDACTED-HOST-BINDING]"] if service.get("ports") else [],
"secrets": sorted(_service_secret_names(service)),
"environmentKeys": sorted(service.get("environment", {})),
"readOnly": service.get("read_only", False),
"capDrop": service.get("cap_drop", []),
}
return {
"name": topology.get("name"),
"services": services,
"networks": sorted(topology.get("networks", {})),
"secrets": sorted(topology.get("secrets", {})),
}
def run(
environment: dict[str, str],
phase: str,
verify_registry: bool = False,
*,
verify_docker_networks: bool = False,
verify_network_targets: bool = False,
verify_dns: bool = False,
verify_bind_ports: bool = False,
verify_oidc_discovery: bool = False,
topology_override: dict[str, Any] | None = None,
) -> dict[str, Any]:
results: list[dict[str, str]] = []
required = _required_values(environment, results)
if not required:
return {"phase": phase, "passed": False, "checks": results, "failureCount": 1}
structured = _structured_input_checks(environment, results)
paths, secret_values = _path_checks(environment, results, phase)
try:
topology = topology_override or _rendered_topology(environment)
readable = True
except (OSError, yaml.YAMLError):
topology = {}
readable = False
_check(
results,
"compose.readable",
readable,
"production topology rendered" if readable else "Compose files are invalid",
)
mapping = _mapping_checks(topology, results) if readable else False
images = _image_checks(environment, topology, results, verify_registry) if readable else False
topology_ok = _topology_checks(topology, secret_values, results) if readable else False
urls = _url_checks(environment, results)
coherence = _coherence_checks(environment, results)
oidc_discovery = _oidc_discovery_checks(environment, results, verify_oidc_discovery)
networks = _docker_network_checks(environment, results, verify_docker_networks)
targets = _target_checks(environment, results, verify_network_targets)
dns = _dns_checks(environment, results, verify_dns)
bind_ports = _bind_port_checks(environment, topology, results, verify_bind_ports, phase)
passed = all(
(
required,
structured,
paths,
readable,
mapping,
images,
topology_ok,
urls,
coherence,
oidc_discovery,
networks,
targets,
dns,
bind_ports,
)
)
return {
"phase": phase,
"passed": passed,
"checks": results,
"failureCount": sum(item["status"] == "fail" for item in results),
"deferredCount": sum(item["status"] == "deferred" for item in results),
}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--env-file", type=Path, required=True)
parser.add_argument("--phase", choices=("predeploy", "poststart"), default="predeploy")
parser.add_argument("--verify-registry", action="store_true")
parser.add_argument("--verify-docker-networks", action="store_true")
parser.add_argument("--verify-network-targets", action="store_true")
parser.add_argument("--verify-dns", action="store_true")
parser.add_argument("--verify-bind-ports", action="store_true")
parser.add_argument("--verify-oidc-discovery", action="store_true")
parser.add_argument("--redacted-topology-output", type=Path)
args = parser.parse_args()
try:
environment = dict(os.environ)
environment.update(load_env_file(args.env_file))
result = run(
environment,
args.phase,
args.verify_registry,
verify_docker_networks=args.verify_docker_networks,
verify_network_targets=args.verify_network_targets,
verify_dns=args.verify_dns,
verify_bind_ports=args.verify_bind_ports,
verify_oidc_discovery=args.verify_oidc_discovery,
)
if args.redacted_topology_output and result["passed"]:
output = redacted_topology(_rendered_topology(environment))
args.redacted_topology_output.write_text(
json.dumps(output, indent=2, sort_keys=True) + "\n", encoding="utf-8"
)
except (OSError, ValueError) as exc:
result = {
"passed": False,
"failureCount": 1,
"checks": [{"id": "inputs.env-file", "status": "fail", "detail": str(exc)}],
}
print(json.dumps(result, indent=2, sort_keys=True))
return 0 if result["passed"] else 1
if __name__ == "__main__":
raise SystemExit(main())