427 lines
19 KiB
Python
427 lines
19 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import re
|
|
import uuid
|
|
from typing import Any
|
|
|
|
from sqlmodel import Session, select
|
|
|
|
from ..core.time import utc_now
|
|
from ..db.models import DeploymentJob, Policy
|
|
from ..schemas.forge_bootstrap import ForgeBootstrapPreflightV1
|
|
from .audit_logger import log_event
|
|
from .deployment_broker import DeploymentBrokerClient
|
|
from .enrollment_plane import EnrollmentPlane
|
|
from .forge_bootstrap_apply import (
|
|
_FORGE_SOURCE_URL,
|
|
_protected_forge_private_package_owner,
|
|
canonical_preflight_receipt,
|
|
forge_oidc_attestation_matches,
|
|
)
|
|
from .policy_engine import evaluate_policy
|
|
from .secret_store import get_secret, has_secret
|
|
|
|
_API_VERSION = "itworx.forge-bootstrap-preflight/v1"
|
|
_APPLICATION_ID = "itworx-forge"
|
|
_REPOSITORY = "jens/itworx-forge"
|
|
_IDEMPOTENCY = re.compile(r"^[A-Za-z0-9._:-]{8,128}$")
|
|
_CONTROLLER_CHECKS = {
|
|
"exact_main", "source_manifest", "managed_appdata", "proxy_network",
|
|
"resource_collisions", "push_validation", "image_registry", "oidc",
|
|
"secret_delivery",
|
|
}
|
|
_DETAILS = {
|
|
"deployment_apply_policy": "An explicit allowed exact-target deployment policy is required before apply.",
|
|
"controller_trust": "Stored controller trust and bootstrap secret are required for host checks.",
|
|
"managed_appdata_root_selection": "The managed Docker-root backup acknowledgement and storage selection are required before setup.",
|
|
"exact_main": "Requested Forge revision must equal the current main head.",
|
|
"source_manifest": "The source manifest must match the fixed Forge six-service topology.",
|
|
"managed_appdata": "The fixed managed PostgreSQL directory must have safe owner-only metadata.",
|
|
"proxy_network": "The required external proxy network must be present.",
|
|
"resource_collisions": "Fixed Forge container names must be free for first installation.",
|
|
"push_validation": "Exact-main CI validation cannot be verified from trusted controller configuration.",
|
|
"image_registry": "Registry publication and pull authorization require external verification.",
|
|
"oidc": "Dedicated Forge Authentik provider, application, and role groups require external verification.",
|
|
"secret_delivery": "Forge database and application secret files require external verification.",
|
|
}
|
|
_STATUS_DETAILS = {
|
|
"exact_main": {
|
|
"passed": "Requested revision equals the current protected Gitea main head.",
|
|
"blocked": "Requested revision is not the current protected Gitea main head.",
|
|
"unknown": "Current protected Gitea main head could not be verified.",
|
|
},
|
|
"source_manifest": {
|
|
"passed": "Committed Forge manifest matches the pinned production topology contract.",
|
|
"blocked": "Committed Forge manifest differs from the pinned production topology contract.",
|
|
"unknown": "Committed Forge manifest could not be read from the controller mirror.",
|
|
},
|
|
"managed_appdata": {
|
|
"passed": "Selected managed root and fixed PostgreSQL directory have safe metadata.",
|
|
"blocked": "Managed root or fixed PostgreSQL directory is missing or unsafe.",
|
|
"unknown": "Managed storage metadata could not be verified.",
|
|
},
|
|
"proxy_network": {
|
|
"passed": "The required external proxy network exists.",
|
|
"blocked": "The required external proxy network is missing.",
|
|
"unknown": "Docker network inventory is unavailable.",
|
|
},
|
|
"resource_collisions": {
|
|
"passed": "No fixed Forge container-name collisions were found.",
|
|
"blocked": "One or more fixed Forge container names already exist.",
|
|
"unknown": "Docker container inventory is unavailable.",
|
|
},
|
|
"push_validation": {
|
|
"passed": "Configured required push validation contexts passed for this exact commit.",
|
|
"blocked": "A configured required push validation context rejected this exact commit.",
|
|
"unknown": "Exact-commit push validation is not configured or cannot be verified.",
|
|
},
|
|
"image_registry": {
|
|
"passed": "The fixed private Forge image owner and both immutable image digests are authenticated and pullable.",
|
|
"blocked": "One or both immutable Forge image digests are unavailable from the registry.",
|
|
"unknown": "Registry publication or pull authorization could not be verified.",
|
|
},
|
|
"oidc": {
|
|
"passed": "The fixed Forge OIDC provider, application, and role groups are ready.",
|
|
"blocked": "The Forge OIDC proof is missing, mismatched, or differs from the protected AppOps mapping and signing-key IDs.",
|
|
"unknown": "The fixed Forge OIDC configuration could not be verified.",
|
|
},
|
|
"secret_delivery": {
|
|
"passed": "The fixed Forge secret files have the required safe metadata.",
|
|
"blocked": "One or more fixed Forge secret files are missing or unsafe.",
|
|
"unknown": "The fixed Forge secret-file metadata could not be verified.",
|
|
},
|
|
}
|
|
|
|
|
|
class ForgeBootstrapPreflightError(ValueError):
|
|
"""A fixed-target Forge preflight request failed closed."""
|
|
|
|
|
|
def _canonical_digest(value: dict[str, Any]) -> str:
|
|
return hashlib.sha256(
|
|
json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
).hexdigest()
|
|
|
|
|
|
def _idempotency_digest(value: str) -> str:
|
|
normalized = value.strip()
|
|
if not _IDEMPOTENCY.fullmatch(normalized):
|
|
raise ForgeBootstrapPreflightError("Idempotency-Key must contain 8 to 128 safe characters.")
|
|
return hashlib.sha256(f"forge-bootstrap-preflight:{normalized}".encode()).hexdigest()
|
|
|
|
|
|
def _check(name: str, status: str) -> dict[str, str]:
|
|
detail = _STATUS_DETAILS.get(name, {}).get(status, _DETAILS[name])
|
|
return {"name": name, "status": status, "detail": detail}
|
|
|
|
|
|
def _checked_controller_result(
|
|
result: object, profile: ForgeBootstrapPreflightV1
|
|
) -> tuple[list[dict[str, str]], dict[str, Any]] | None:
|
|
if not isinstance(result, dict) or (
|
|
result.get("apiVersion") != _API_VERSION
|
|
or result.get("revision") != profile.revision
|
|
or result.get("workspaceImageDigest") != profile.workspace_image_digest
|
|
or result.get("frontdoorImageDigest") != profile.frontdoor_image_digest
|
|
):
|
|
return None
|
|
raw_checks = result.get("checks")
|
|
if not isinstance(raw_checks, dict) or set(raw_checks) != _CONTROLLER_CHECKS:
|
|
return None
|
|
checks: list[dict[str, str]] = []
|
|
for name in sorted(_CONTROLLER_CHECKS):
|
|
raw = raw_checks.get(name)
|
|
if not isinstance(raw, dict) or set(raw) != {"status"} or raw.get("status") not in {"passed", "blocked", "unknown"}:
|
|
return None
|
|
checks.append(_check(name, raw["status"]))
|
|
statuses = [item["status"] for item in checks]
|
|
expected_status = "preflight_blocked" if "blocked" in statuses else "preflight_unknown" if "unknown" in statuses else "preflight_ok"
|
|
if result.get("status") != expected_status:
|
|
return None
|
|
raw_evidence = result.get("evidence", {})
|
|
if not isinstance(raw_evidence, dict) or set(raw_evidence) - {
|
|
"mainRevision", "sourceManifestSha256", "resourceCollisionCount",
|
|
"workspaceImagePresent", "frontdoorImagePresent", "attestations",
|
|
}:
|
|
return None
|
|
evidence: dict[str, Any] = {}
|
|
for name in ("mainRevision", "sourceManifestSha256"):
|
|
value = raw_evidence.get(name)
|
|
if value is not None:
|
|
pattern = r"^[0-9a-f]{40}$" if name == "mainRevision" else r"^[0-9a-f]{64}$"
|
|
if not isinstance(value, str) or not re.fullmatch(pattern, value):
|
|
return None
|
|
evidence[name] = value
|
|
count = raw_evidence.get("resourceCollisionCount")
|
|
if count is not None:
|
|
if isinstance(count, bool) or not isinstance(count, int) or not 0 <= count <= 1000:
|
|
return None
|
|
evidence["resourceCollisionCount"] = count
|
|
for name in ("workspaceImagePresent", "frontdoorImagePresent"):
|
|
value = raw_evidence.get(name)
|
|
if value is not None:
|
|
if not isinstance(value, bool):
|
|
return None
|
|
evidence[name] = value
|
|
attestations = raw_evidence.get("attestations")
|
|
oidc_rejected = False
|
|
validated_attestations: dict[str, dict[str, Any]] = {}
|
|
if attestations is not None:
|
|
if not isinstance(attestations, dict) or set(attestations) - {
|
|
"push_validation", "image_registry", "oidc", "secret_delivery",
|
|
}:
|
|
return None
|
|
push = attestations.get("push_validation")
|
|
if push is not None and (
|
|
not isinstance(push, dict)
|
|
or set(push) != {"context", "conclusion", "revision"}
|
|
or push.get("context") != "Managed validation / full (push)"
|
|
or push.get("conclusion") != "success"
|
|
or push.get("revision") != profile.revision
|
|
):
|
|
return None
|
|
if push is not None:
|
|
validated_attestations["push_validation"] = dict(push)
|
|
registry = attestations.get("image_registry")
|
|
private_owner = _protected_forge_private_package_owner()
|
|
if registry is not None and (
|
|
not isinstance(registry, dict)
|
|
or private_owner is None
|
|
or registry.get("workspacePullable") is not True
|
|
or registry.get("frontdoorPullable") is not True
|
|
or registry != {
|
|
"owner": private_owner,
|
|
"ownerVisibility": "private",
|
|
"workspaceDigest": profile.workspace_image_digest,
|
|
"frontdoorDigest": profile.frontdoor_image_digest,
|
|
"workspacePullable": True,
|
|
"frontdoorPullable": True,
|
|
"workspaceSource": _FORGE_SOURCE_URL,
|
|
"workspaceRevision": profile.revision,
|
|
"frontdoorSource": _FORGE_SOURCE_URL,
|
|
"frontdoorRevision": profile.revision,
|
|
}
|
|
):
|
|
return None
|
|
if registry is not None:
|
|
validated_attestations["image_registry"] = dict(registry)
|
|
oidc = attestations.get("oidc")
|
|
if oidc is not None:
|
|
if raw_checks["oidc"]["status"] != "passed":
|
|
return None
|
|
if forge_oidc_attestation_matches(oidc):
|
|
validated_attestations["oidc"] = dict(oidc)
|
|
else:
|
|
oidc_rejected = True
|
|
elif raw_checks["oidc"]["status"] == "passed":
|
|
oidc_rejected = True
|
|
secret_delivery = attestations.get("secret_delivery")
|
|
if secret_delivery is not None and (
|
|
not isinstance(secret_delivery, dict)
|
|
or set(secret_delivery) != {"requiredFilesReady", "metadataSha256"}
|
|
or secret_delivery.get("requiredFilesReady") is not True
|
|
or not isinstance(secret_delivery.get("metadataSha256"), str)
|
|
or not re.fullmatch(r"^[0-9a-f]{64}$", secret_delivery["metadataSha256"])
|
|
):
|
|
return None
|
|
if secret_delivery is not None:
|
|
validated_attestations["secret_delivery"] = dict(secret_delivery)
|
|
if any(
|
|
raw_checks[name]["status"] == "passed" and
|
|
(not isinstance(attestations, dict) or name not in attestations)
|
|
for name in ("push_validation", "image_registry", "secret_delivery")
|
|
):
|
|
return None
|
|
if raw_checks["oidc"]["status"] == "passed" and "oidc" not in validated_attestations:
|
|
oidc_rejected = True
|
|
if oidc_rejected:
|
|
for check in checks:
|
|
if check["name"] == "oidc":
|
|
check.update(_check("oidc", "blocked"))
|
|
break
|
|
if validated_attestations:
|
|
evidence["attestations"] = validated_attestations
|
|
return checks, evidence
|
|
|
|
|
|
class ForgeBootstrapPreflightService:
|
|
"""Read-only fixed Forge checks; this profile has no apply or rollback path."""
|
|
|
|
def __init__(self, *, broker: DeploymentBrokerClient | None = None):
|
|
self.broker = broker or DeploymentBrokerClient()
|
|
|
|
def preflight(
|
|
self,
|
|
session: Session,
|
|
profile: ForgeBootstrapPreflightV1,
|
|
*,
|
|
idempotency_key: str,
|
|
) -> dict[str, Any]:
|
|
payload = profile.model_dump(mode="json")
|
|
plan_digest = _canonical_digest(payload)
|
|
key_digest = _idempotency_digest(idempotency_key)
|
|
existing = session.exec(
|
|
select(DeploymentJob).where(DeploymentJob.idempotency_sha256 == key_digest)
|
|
).first()
|
|
if existing:
|
|
if existing.plan_sha256 != plan_digest:
|
|
raise ForgeBootstrapPreflightError(
|
|
"Idempotency-Key was already used for a different Forge profile."
|
|
)
|
|
return self.serialize_job(existing, replay=True)
|
|
|
|
exact = session.exec(
|
|
select(Policy).where(
|
|
(Policy.action == "deployment.apply") & (Policy.target == _APPLICATION_ID)
|
|
)
|
|
).all()
|
|
decision = evaluate_policy(
|
|
session, action="deployment.apply", target=_APPLICATION_ID, source="manual"
|
|
)
|
|
policy_ready = bool(
|
|
len(exact) == 1
|
|
and exact[0].manual == "allowed"
|
|
and exact[0].voice == "blocked"
|
|
and exact[0].risk == "high"
|
|
and decision.get("decision") == "allowed"
|
|
and decision.get("policy_id") == exact[0].id
|
|
)
|
|
checks = [_check("deployment_apply_policy", "passed" if policy_ready else "blocked")]
|
|
|
|
trust = EnrollmentPlane.get_trust_config(session)
|
|
expected_root = "/var/lib/docker/volumes/itworx-managed-appdata/_data"
|
|
secret = None
|
|
if trust is not None:
|
|
try:
|
|
if has_secret(session, trust.bootstrap_secret_ref):
|
|
secret = get_secret(session, trust.bootstrap_secret_ref, touch=False)
|
|
except Exception: # noqa: BLE001 - fail closed without exposing secret-store errors.
|
|
secret = None
|
|
bootstrap_ready = bool(
|
|
trust
|
|
and isinstance(secret, str)
|
|
and len(secret) >= 32
|
|
and secret.isascii()
|
|
and secret.isprintable()
|
|
)
|
|
checks.append(_check("controller_trust", "passed" if bootstrap_ready else "blocked"))
|
|
root_ready = bool(
|
|
trust
|
|
and trust.trusted_appdata_root == expected_root
|
|
and trust.managed_appdata_backup_acknowledged
|
|
)
|
|
checks.append(
|
|
_check("managed_appdata_root_selection", "passed" if root_ready else "blocked")
|
|
)
|
|
|
|
controller_result = None
|
|
if bootstrap_ready:
|
|
try:
|
|
controller_result = self.broker.forge_bootstrap_preflight(
|
|
profile, secret=secret or ""
|
|
)
|
|
except Exception: # noqa: BLE001 - transport details stay out of the preflight receipt.
|
|
controller_result = None
|
|
parsed_controller = _checked_controller_result(controller_result, profile)
|
|
if parsed_controller is None:
|
|
host_checks = [_check(name, "unknown") for name in sorted(_CONTROLLER_CHECKS)]
|
|
controller_evidence: dict[str, Any] = {}
|
|
else:
|
|
host_checks, controller_evidence = parsed_controller
|
|
checks.extend(host_checks)
|
|
blockers = [item["name"] for item in checks if item["status"] == "blocked"]
|
|
unknowns = [item["name"] for item in checks if item["status"] == "unknown"]
|
|
outcome = "blocked" if blockers else "unknown" if unknowns else "passed"
|
|
job_status = "preflight_succeeded" if outcome == "passed" else "preflight_failed"
|
|
now = utc_now()
|
|
job = DeploymentJob(
|
|
id=f"forge-preflight-{uuid.uuid4().hex[:16]}",
|
|
application_id=_APPLICATION_ID,
|
|
repository=_REPOSITORY,
|
|
revision=profile.revision,
|
|
plan_sha256=plan_digest,
|
|
idempotency_sha256=key_digest,
|
|
status=job_status,
|
|
stage="preflight_complete" if outcome == "passed" else "preflight_incomplete",
|
|
plan_json=payload,
|
|
preflight_json={
|
|
"apiVersion": _API_VERSION,
|
|
"status": outcome,
|
|
"blockers": blockers,
|
|
"unknowns": unknowns,
|
|
"checks": checks,
|
|
},
|
|
evidence_json={
|
|
"profile": "forge-production-bootstrap",
|
|
"policyId": exact[0].id if policy_ready else None,
|
|
"workspaceImageDigest": profile.workspace_image_digest,
|
|
"frontdoorImageDigest": profile.frontdoor_image_digest,
|
|
"controller": controller_evidence,
|
|
},
|
|
result_summary=(
|
|
"Forge bootstrap checks passed; production apply remains separately gated."
|
|
if outcome == "passed"
|
|
else "Forge bootstrap preflight is incomplete; see blocked and unknown checks."
|
|
),
|
|
created_at=now,
|
|
updated_at=now,
|
|
completed_at=now,
|
|
)
|
|
session.add(job)
|
|
session.commit()
|
|
session.refresh(job)
|
|
log_event(
|
|
session,
|
|
source="deployment-control-plane",
|
|
identity="operator",
|
|
action_id="deployment.preflight",
|
|
target=_APPLICATION_ID,
|
|
target_type="deployment",
|
|
risk="high",
|
|
policy_decision="allowed" if policy_ready else "blocked",
|
|
reason="Fixed Forge bootstrap preflight recorded without requesting a deployment action.",
|
|
result="success" if outcome == "passed" else "blocked",
|
|
execution_plan={"planSha256": plan_digest, "profile": "forge-production-bootstrap"},
|
|
post_checks={"preflight": job.preflight_json},
|
|
)
|
|
return self.serialize_job(job)
|
|
|
|
@staticmethod
|
|
def serialize_job(job: DeploymentJob, *, replay: bool = False) -> dict[str, Any]:
|
|
return {
|
|
"id": job.id,
|
|
"applicationId": job.application_id,
|
|
"repository": job.repository,
|
|
"revision": job.revision,
|
|
"status": job.status,
|
|
"stage": job.stage,
|
|
"preflight": job.preflight_json,
|
|
"preflightSha256": canonical_preflight_receipt(job),
|
|
"evidence": job.evidence_json,
|
|
"result": job.result_summary,
|
|
"brokerRequestId": job.broker_request_id,
|
|
"createdAt": job.created_at.isoformat(),
|
|
"updatedAt": job.updated_at.isoformat(),
|
|
"completedAt": job.completed_at.isoformat() if job.completed_at else None,
|
|
"idempotentReplay": replay,
|
|
}
|
|
|
|
@staticmethod
|
|
def refresh(session: Session, job_id: str) -> dict[str, Any] | None:
|
|
job = session.get(DeploymentJob, job_id)
|
|
if not job or not job.id.startswith("forge-preflight-"):
|
|
return None
|
|
return ForgeBootstrapPreflightService.serialize_job(job)
|
|
|
|
@staticmethod
|
|
def list_jobs(session: Session, *, limit: int = 100) -> list[dict[str, Any]]:
|
|
rows = session.exec(
|
|
select(DeploymentJob)
|
|
.where(DeploymentJob.id.startswith("forge-preflight-"))
|
|
.order_by(DeploymentJob.created_at.desc())
|
|
.limit(max(1, min(limit, 250)))
|
|
).all()
|
|
return [ForgeBootstrapPreflightService.serialize_job(row) for row in rows]
|