Files
Unraid-AppOps-Gateway-Public/backend/app/services/forge_secret_delivery.py
T

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