600 lines
23 KiB
Python
600 lines
23 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import hmac
|
|
import json
|
|
import re
|
|
from typing import Any
|
|
from uuid import RFC_4122, UUID
|
|
|
|
from sqlmodel import Session, select
|
|
|
|
from ..connectors.authentik import AuthentikConnector
|
|
from ..core.config import settings
|
|
from ..db.models import AppSetting, Policy
|
|
from ..schemas.enrollment import MANAGED_DOCKER_APPDATA_ROOT
|
|
from ..schemas.forge_bootstrap import ForgeBootstrapPreflightV1
|
|
from ..schemas.forge_oidc import FORGE_OIDC_ISSUER, FORGE_OIDC_LAUNCH_URL
|
|
from ..schemas.forge_secret_delivery import (
|
|
ForgeSecretDeliveryControllerArtifactV1,
|
|
ForgeSecretDeliveryResultV1,
|
|
)
|
|
from .audit_logger import log_event
|
|
from .deployment_broker import DeploymentBrokerClient
|
|
from .enrollment_plane import EnrollmentPlane
|
|
from .forge_oidc_provisioning import _forge_oidc_process_lock
|
|
from .policy_engine import evaluate_policy
|
|
from .secret_store import get_secret, has_secret, secret_storage_status
|
|
|
|
_ACTION = "forge.secrets.prepare"
|
|
_TARGET = "itworx-forge"
|
|
_BROKER_SECRET_REF = "FORGE_DEPLOYMENT_BROKER"
|
|
_FORGE_OIDC_BINDING_REF = "AUTHENTIK_FORGE_OIDC_BINDING"
|
|
_FORGE_OIDC_CLIENT_SECRET_REF = "ITWORX_FORGE_OIDC_CLIENT_SECRET"
|
|
_FORGE_READER_BINDING_REF = "AUTHENTIK_FORGE_READER_BINDING"
|
|
_FORGE_READER_TOKEN_REF = "ITWORX_FORGE_AUTHENTIK_READ_TOKEN"
|
|
_FLOW = re.compile(r"[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\Z")
|
|
_PROVIDER_ID = re.compile(r"[1-9][0-9]{0,9}\Z")
|
|
_UUID = re.compile(
|
|
r"[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\Z"
|
|
)
|
|
_IDEMPOTENCY = re.compile(r"[A-Za-z0-9._:-]{8,128}\Z")
|
|
_CONFIG_KEYS = {
|
|
"schema",
|
|
"issuer",
|
|
"audience",
|
|
"client_id",
|
|
"authentik_api_url",
|
|
"launch_url",
|
|
"authorization_flow",
|
|
"invalidation_flow",
|
|
"authentik_openid_mapping_id",
|
|
"authentik_email_mapping_id",
|
|
"authentik_profile_mapping_id",
|
|
"authentik_provider_id",
|
|
"authentik_signing_key_id",
|
|
"private_package_owner",
|
|
}
|
|
_RESPONSE_KEYS = {
|
|
"apiVersion",
|
|
"target",
|
|
"status",
|
|
"profile",
|
|
"controllerArtifact",
|
|
"secretFilesReady",
|
|
"masterKeyBackupVerified",
|
|
"applyPreflight",
|
|
}
|
|
_ARTIFACT_KEYS = {
|
|
"controllerVersion",
|
|
"sourceRepository",
|
|
"sourceRevision",
|
|
"sourceTree",
|
|
"artifactSha256",
|
|
}
|
|
|
|
|
|
def _exact_secret_prepare_policy_allowed(session: Session) -> str | None:
|
|
rows = session.exec(select(Policy).where(Policy.action == _ACTION)).all()
|
|
if len(rows) != 1:
|
|
return None
|
|
policy = rows[0]
|
|
if (
|
|
policy.target != _TARGET
|
|
or policy.manual != "allowed"
|
|
or policy.voice != "blocked"
|
|
or policy.risk != "critical"
|
|
):
|
|
return None
|
|
decision = evaluate_policy(
|
|
session,
|
|
action=_ACTION,
|
|
target=_TARGET,
|
|
source="manual",
|
|
)
|
|
if (
|
|
decision.get("decision") != "allowed"
|
|
or decision.get("scope_decision") != "allowed"
|
|
or decision.get("policy_id") != policy.id
|
|
):
|
|
return None
|
|
return policy.id
|
|
|
|
|
|
def _secret_store_ready() -> bool:
|
|
try:
|
|
status = secret_storage_status()
|
|
except Exception: # noqa: BLE001 - store diagnostics never enter delivery errors.
|
|
return False
|
|
return isinstance(status, dict) and status.get("productionReady") is True
|
|
|
|
|
|
def _canonical_uuid(value: Any) -> bool:
|
|
if not isinstance(value, str) or _UUID.fullmatch(value) is None:
|
|
return False
|
|
try:
|
|
parsed = UUID(value)
|
|
except (TypeError, ValueError, AttributeError):
|
|
return False
|
|
return str(parsed) == value and parsed.variant == RFC_4122 and parsed.version in range(1, 9)
|
|
|
|
|
|
def _protected_config(context: dict[str, Any]) -> bytes | None:
|
|
if not isinstance(context, dict) or set(context) != {
|
|
"applicationId", "providerId", "scopeMappingIds", "groupIds", "signingKeyId"
|
|
}:
|
|
return None
|
|
provider_id = context.get("providerId")
|
|
signing_key_id = context.get("signingKeyId")
|
|
mappings = context.get("scopeMappingIds")
|
|
if (
|
|
_PROVIDER_ID.fullmatch(provider_id or "") is None
|
|
or not _canonical_uuid(signing_key_id)
|
|
or not isinstance(mappings, dict)
|
|
or set(mappings) != {"openid", "email", "profile"}
|
|
or any(not _canonical_uuid(item) for item in mappings.values())
|
|
or len(set(mappings.values())) != 3
|
|
):
|
|
return None
|
|
if not _canonical_uuid(context.get("applicationId")):
|
|
return None
|
|
owner = _protected_forge_private_package_owner()
|
|
if owner is None:
|
|
return None
|
|
authorization_flow = settings.apis.get("AUTHENTIK_OIDC_AUTHORIZATION_FLOW")
|
|
invalidation_flow = settings.apis.get("AUTHENTIK_OIDC_INVALIDATION_FLOW")
|
|
if (
|
|
not isinstance(authorization_flow, str)
|
|
or _FLOW.fullmatch(authorization_flow) is None
|
|
or not isinstance(invalidation_flow, str)
|
|
or _FLOW.fullmatch(invalidation_flow) is None
|
|
):
|
|
return None
|
|
values = {
|
|
"schema": "itworx.forge-first-install-config/v4",
|
|
"issuer": FORGE_OIDC_ISSUER,
|
|
"audience": "itworx-forge",
|
|
"client_id": "itworx-forge",
|
|
"authentik_api_url": "https://auth.example-host.com",
|
|
"launch_url": FORGE_OIDC_LAUNCH_URL,
|
|
"authorization_flow": authorization_flow,
|
|
"invalidation_flow": invalidation_flow,
|
|
"authentik_openid_mapping_id": mappings["openid"],
|
|
"authentik_email_mapping_id": mappings["email"],
|
|
"authentik_profile_mapping_id": mappings["profile"],
|
|
"authentik_provider_id": provider_id,
|
|
"authentik_signing_key_id": signing_key_id,
|
|
"private_package_owner": owner,
|
|
}
|
|
if set(values) != _CONFIG_KEYS or any(
|
|
not isinstance(value, str) or not value or len(value) > 512
|
|
for value in values.values()
|
|
):
|
|
return None
|
|
encoded = json.dumps(values, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
|
|
return encoded if len(encoded) <= 8192 else None
|
|
|
|
|
|
def _checked_controller_response(
|
|
value: object,
|
|
*,
|
|
profile: ForgeBootstrapPreflightV1,
|
|
) -> ForgeSecretDeliveryControllerArtifactV1 | None:
|
|
if not isinstance(value, dict) or set(value) != _RESPONSE_KEYS:
|
|
return None
|
|
if (
|
|
value.get("apiVersion") != "itworx.forge-secret-delivery/v1"
|
|
or value.get("target") != _TARGET
|
|
or value.get("status") != "bundle_ready"
|
|
or value.get("profile") != profile.model_dump(mode="json")
|
|
or type(value.get("secretFilesReady")) is not int
|
|
or value.get("secretFilesReady") != 7
|
|
or value.get("masterKeyBackupVerified") is not True
|
|
or value.get("applyPreflight") != "not_evaluated"
|
|
):
|
|
return None
|
|
artifact = value.get("controllerArtifact")
|
|
if not isinstance(artifact, dict) or set(artifact) != _ARTIFACT_KEYS:
|
|
return None
|
|
try:
|
|
return ForgeSecretDeliveryControllerArtifactV1.model_validate(artifact)
|
|
except Exception: # noqa: BLE001 - controller response data is untrusted.
|
|
return None
|
|
|
|
|
|
def _result(
|
|
*,
|
|
profile: ForgeBootstrapPreflightV1,
|
|
status: str,
|
|
message: str,
|
|
artifact: ForgeSecretDeliveryControllerArtifactV1 | None = None,
|
|
) -> dict[str, Any]:
|
|
ready = status == "bundle_ready"
|
|
payload = ForgeSecretDeliveryResultV1(
|
|
success=ready,
|
|
status=status,
|
|
message=message,
|
|
target=_TARGET,
|
|
revision=profile.revision,
|
|
controllerArtifact=artifact,
|
|
secretFilesReady=7 if ready else None,
|
|
masterKeyBackupVerified=True if ready else None,
|
|
applyPreflight="not_evaluated" if ready else None,
|
|
)
|
|
return payload.model_dump(mode="json", by_alias=True, exclude_none=True)
|
|
|
|
|
|
def _audit(
|
|
session: Session,
|
|
*,
|
|
identity: str,
|
|
decision: str,
|
|
result: dict[str, Any],
|
|
policy_id: str | None,
|
|
idempotency_key: str,
|
|
) -> bool:
|
|
try:
|
|
artifact = result.get("controllerArtifact")
|
|
log_event(
|
|
session,
|
|
source="manual",
|
|
identity=identity,
|
|
action_id=_ACTION,
|
|
target=_TARGET,
|
|
target_type="forge_secret_delivery",
|
|
risk="critical",
|
|
policy_decision=decision,
|
|
reason=result["message"],
|
|
result=result["status"],
|
|
after_state={
|
|
"status": result["status"],
|
|
"revision": result["revision"],
|
|
"secretFilesReady": result.get("secretFilesReady"),
|
|
"masterKeyBackupVerified": result.get("masterKeyBackupVerified"),
|
|
"applyPreflight": result.get("applyPreflight"),
|
|
"controllerArtifact": artifact,
|
|
},
|
|
redacted_metadata={
|
|
"policyId": policy_id,
|
|
"revision": result["revision"],
|
|
"idempotencyKeySha256": hashlib.sha256(idempotency_key.encode("ascii")).hexdigest(),
|
|
"secretRefs": [
|
|
_BROKER_SECRET_REF,
|
|
_FORGE_OIDC_CLIENT_SECRET_REF,
|
|
_FORGE_READER_TOKEN_REF,
|
|
],
|
|
},
|
|
)
|
|
except Exception: # noqa: BLE001 - an audit exception may carry held secrets.
|
|
try:
|
|
session.rollback()
|
|
except Exception: # noqa: BLE001 - keep DB error detail out of the response.
|
|
pass
|
|
return False
|
|
return True
|
|
|
|
|
|
def _protected_forge_private_package_owner() -> str | None:
|
|
from .forge_bootstrap_apply import (
|
|
_protected_forge_private_package_owner as protected_owner,
|
|
)
|
|
|
|
return protected_owner()
|
|
|
|
|
|
def _fixed_authentik_connector(session: Session) -> AuthentikConnector | None:
|
|
"""Use only already-loaded, fixed Authentik runtime configuration.
|
|
|
|
The generic setup loader also migrates legacy plaintext connector secrets.
|
|
Secret delivery must not trigger that write as a side effect, so this path
|
|
verifies the existing runtime against protected/read-only inputs instead.
|
|
"""
|
|
try:
|
|
legacy_secret = session.get(AppSetting, "connector:authentik:secret")
|
|
if legacy_secret is not None:
|
|
if legacy_secret.is_secret is not True or not isinstance(legacy_secret.value_json, dict):
|
|
return None
|
|
legacy_value = legacy_secret.value_json.get("value")
|
|
if isinstance(legacy_value, str) and legacy_value.strip():
|
|
return None
|
|
if legacy_secret.value_json not in (
|
|
{},
|
|
{"secretRef": "AUTHENTIK_TOKEN", "hasSecret": True},
|
|
{"secretRef": "AUTHENTIK_TOKEN", "hasSecret": True, "migrated": True},
|
|
):
|
|
return None
|
|
|
|
connector_config = session.get(AppSetting, "connector:authentik:config")
|
|
if connector_config is not None:
|
|
if connector_config.is_secret is not False or not isinstance(connector_config.value_json, dict):
|
|
return None
|
|
if not set(connector_config.value_json).issubset(
|
|
{"url", "verifyTls", "outpostUrl", "expectEmbeddedOutpost"}
|
|
):
|
|
return None
|
|
|
|
connector = AuthentikConnector()
|
|
config = connector._config()
|
|
if (
|
|
config is None
|
|
or config.url.rstrip("/") != "https://auth.example-host.com"
|
|
or config.verify_tls is not True
|
|
or not isinstance(config.token, str)
|
|
or not config.token
|
|
):
|
|
return None
|
|
|
|
stored_admin_token = get_secret(session, "AUTHENTIK_TOKEN", touch=False)
|
|
if not isinstance(stored_admin_token, str) or not stored_admin_token:
|
|
return None
|
|
normalized = stored_admin_token.strip()
|
|
if normalized.lower().startswith("bearer "):
|
|
normalized = normalized[7:].strip()
|
|
if not hmac.compare_digest(normalized, config.token):
|
|
return None
|
|
|
|
if connector_config is not None:
|
|
stored_config = connector_config.value_json
|
|
runtime_values = {
|
|
"url": config.url,
|
|
"verifyTls": config.verify_tls,
|
|
"outpostUrl": config.outpost_url,
|
|
"expectEmbeddedOutpost": config.expect_embedded_outpost,
|
|
}
|
|
if any(stored_config.get(key) != runtime_values[key] for key in stored_config):
|
|
return None
|
|
return connector
|
|
except Exception: # noqa: BLE001 - config/store failures remain a fixed denial.
|
|
return None
|
|
|
|
|
|
def prepare_forge_secret_delivery(
|
|
session: Session,
|
|
*,
|
|
profile: ForgeBootstrapPreflightV1,
|
|
idempotency_key: str,
|
|
actor_identity: str,
|
|
) -> dict[str, Any]:
|
|
"""Prepare only the fixed Forge credential bundle through the trusted broker."""
|
|
identity = actor_identity.strip() if isinstance(actor_identity, str) else ""
|
|
request_key = idempotency_key.strip() if isinstance(idempotency_key, str) else ""
|
|
if not identity:
|
|
return _result(
|
|
profile=profile,
|
|
status="blocked",
|
|
message="forge_secret_operator_identity_required",
|
|
)
|
|
if _IDEMPOTENCY.fullmatch(request_key) is None:
|
|
outcome = _result(
|
|
profile=profile,
|
|
status="blocked",
|
|
message="forge_secret_idempotency_key_invalid",
|
|
)
|
|
_audit(
|
|
session,
|
|
identity=identity,
|
|
decision="blocked",
|
|
result=outcome,
|
|
policy_id=None,
|
|
idempotency_key="invalid-key",
|
|
)
|
|
return outcome
|
|
|
|
policy_id = _exact_secret_prepare_policy_allowed(session)
|
|
if policy_id is None:
|
|
outcome = _result(
|
|
profile=profile,
|
|
status="blocked",
|
|
message="forge_secret_exact_target_policy_required",
|
|
)
|
|
_audit(
|
|
session,
|
|
identity=identity,
|
|
decision="blocked",
|
|
result=outcome,
|
|
policy_id=None,
|
|
idempotency_key=request_key,
|
|
)
|
|
return outcome
|
|
|
|
if not _secret_store_ready():
|
|
outcome = _result(
|
|
profile=profile,
|
|
status="blocked",
|
|
message="forge_secret_store_not_production_ready",
|
|
)
|
|
audit_ok = _audit(
|
|
session,
|
|
identity=identity,
|
|
decision="allowed",
|
|
result=outcome,
|
|
policy_id=policy_id,
|
|
idempotency_key=request_key,
|
|
)
|
|
return outcome if audit_ok else _result(
|
|
profile=profile, status="failed", message="forge_secret_audit_persist_failed"
|
|
)
|
|
|
|
attempted_delivery = False
|
|
try:
|
|
with _forge_oidc_process_lock():
|
|
if _exact_secret_prepare_policy_allowed(session) != policy_id:
|
|
outcome = _result(
|
|
profile=profile,
|
|
status="blocked",
|
|
message="forge_secret_exact_target_policy_required",
|
|
)
|
|
elif not _secret_store_ready():
|
|
outcome = _result(
|
|
profile=profile,
|
|
status="blocked",
|
|
message="forge_secret_store_not_production_ready",
|
|
)
|
|
else:
|
|
trust = EnrollmentPlane.get_trust_config(session)
|
|
if (
|
|
trust is None
|
|
or trust.trusted_appdata_root != MANAGED_DOCKER_APPDATA_ROOT
|
|
or trust.managed_appdata_backup_acknowledged is not True
|
|
):
|
|
outcome = _result(
|
|
profile=profile,
|
|
status="blocked",
|
|
message="forge_secret_controller_trust_required",
|
|
)
|
|
elif not all(
|
|
has_secret(session, reference)
|
|
for reference in (
|
|
_FORGE_OIDC_BINDING_REF,
|
|
_FORGE_OIDC_CLIENT_SECRET_REF,
|
|
_FORGE_READER_BINDING_REF,
|
|
_FORGE_READER_TOKEN_REF,
|
|
)
|
|
):
|
|
outcome = _result(
|
|
profile=profile,
|
|
status="blocked",
|
|
message="forge_secret_verified_reader_required",
|
|
)
|
|
else:
|
|
from .forge_bootstrap_apply import _controller_secret
|
|
from .forge_controller_reader import (
|
|
_ReaderFailure,
|
|
_verify_forge_delivery_inputs,
|
|
)
|
|
|
|
broker_secret = _controller_secret(session)
|
|
if not isinstance(broker_secret, str) or not broker_secret:
|
|
outcome = _result(
|
|
profile=profile,
|
|
status="blocked",
|
|
message="forge_secret_controller_trust_required",
|
|
)
|
|
else:
|
|
try:
|
|
connector = _fixed_authentik_connector(session)
|
|
if connector is None:
|
|
outcome = _result(
|
|
profile=profile,
|
|
status="blocked",
|
|
message="forge_secret_authentik_configuration_required",
|
|
)
|
|
connector = None
|
|
if connector is None:
|
|
verified = None
|
|
else:
|
|
verified = _verify_forge_delivery_inputs(session, connector)
|
|
if verified is None:
|
|
pass
|
|
else:
|
|
config_bytes = _protected_config(verified.context)
|
|
if config_bytes is None:
|
|
outcome = _result(
|
|
profile=profile,
|
|
status="conflict",
|
|
message="forge_secret_protected_config_invalid",
|
|
)
|
|
elif (
|
|
verified.oidc_client_secret in config_bytes
|
|
or verified.authentik_read_token in config_bytes
|
|
):
|
|
outcome = _result(
|
|
profile=profile,
|
|
status="conflict",
|
|
message="forge_secret_config_contains_secret",
|
|
)
|
|
else:
|
|
attempted_delivery = True
|
|
broker_result = DeploymentBrokerClient().forge_secret_delivery(
|
|
profile,
|
|
config_bytes=config_bytes,
|
|
oidc_client_secret=verified.oidc_client_secret,
|
|
authentik_read_token=verified.authentik_read_token,
|
|
secret=broker_secret,
|
|
)
|
|
if (
|
|
isinstance(broker_result, dict)
|
|
and broker_result.get("status") in {"missing_config", "missing_secret"}
|
|
):
|
|
outcome = _result(
|
|
profile=profile,
|
|
status="blocked",
|
|
message="forge_secret_controller_delivery_unavailable",
|
|
)
|
|
attempted_delivery = False
|
|
elif (
|
|
isinstance(broker_result, dict)
|
|
and broker_result.get("status") == "response"
|
|
):
|
|
artifact = _checked_controller_response(
|
|
broker_result.get("response"),
|
|
profile=profile,
|
|
)
|
|
if artifact is None:
|
|
outcome = _result(
|
|
profile=profile,
|
|
status="outcome_unknown",
|
|
message="forge_secret_controller_outcome_unknown",
|
|
)
|
|
else:
|
|
outcome = _result(
|
|
profile=profile,
|
|
status="bundle_ready",
|
|
message="forge_secret_bundle_ready",
|
|
artifact=artifact,
|
|
)
|
|
else:
|
|
outcome = _result(
|
|
profile=profile,
|
|
status="outcome_unknown",
|
|
message="forge_secret_controller_outcome_unknown",
|
|
)
|
|
except _ReaderFailure as exc:
|
|
outcome = _result(
|
|
profile=profile,
|
|
status="blocked" if exc.status == "blocked" else "conflict",
|
|
message="forge_secret_reader_proof_invalid",
|
|
)
|
|
except Exception: # noqa: BLE001 - never surface connector or held input details.
|
|
outcome = _result(
|
|
profile=profile,
|
|
status=("outcome_unknown" if attempted_delivery else "failed"),
|
|
message=(
|
|
"forge_secret_controller_outcome_unknown"
|
|
if attempted_delivery
|
|
else "forge_secret_input_verification_failed"
|
|
),
|
|
)
|
|
except Exception: # noqa: BLE001 - lock and DB errors are fixed safe codes.
|
|
outcome = _result(
|
|
profile=profile,
|
|
status="outcome_unknown" if attempted_delivery else "failed",
|
|
message=(
|
|
"forge_secret_controller_outcome_unknown"
|
|
if attempted_delivery
|
|
else "forge_secret_preparation_unavailable"
|
|
),
|
|
)
|
|
|
|
audit_ok = _audit(
|
|
session,
|
|
identity=identity,
|
|
decision="allowed",
|
|
result=outcome,
|
|
policy_id=policy_id,
|
|
idempotency_key=request_key,
|
|
)
|
|
if not audit_ok:
|
|
if attempted_delivery or outcome["status"] == "bundle_ready":
|
|
return _result(
|
|
profile=profile,
|
|
status="outcome_unknown",
|
|
message="forge_secret_delivery_audit_persist_failed",
|
|
)
|
|
return _result(
|
|
profile=profile,
|
|
status="failed",
|
|
message="forge_secret_audit_persist_failed",
|
|
)
|
|
return outcome
|