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