135 lines
5.6 KiB
Python

"""Fail-closed authentication and policy checks for HUX-12 evidence.
The evidence producer is a loopback-only companion process. It receives a
dedicated Vault key and a non-secret policy file; neither the browser relay nor
the agent worker can use its trust class. This module deliberately performs no
network discovery: the producer supplies observations and this boundary binds
them to the configured Jenkins, Harbor, Flux, workload, pod and health facts.
"""
from __future__ import annotations
import json
import os
import re
import stat
from collections.abc import Mapping
from pathlib import Path
from typing import Any
from urllib.parse import urlsplit
from hux.errors import Forbidden, Invalid
KEY_FILE_ENV = "HUX_RELEASE_EVIDENCE_KEY_FILE"
POLICY_FILE_ENV = "HUX_RELEASE_EVIDENCE_POLICY_FILE"
MAX_KEY_BYTES = 4096
MAX_POLICY_BYTES = 64 * 1024
POLICY_FIELDS = {"schema", "max_evidence_age_seconds", "workloads"}
WORKLOAD_FIELDS = {
"review_url_prefix", "jenkins_job_url", "image_repository",
"flux_kustomization", "health_url",
}
def _regular_file(path: str, modes: set[int], limit: int) -> bytes:
"""Read one bounded regular file without accepting a symlink or loose mode."""
if not path:
raise Invalid("HUX-12 evidence configuration is missing")
flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)
descriptor = -1
try:
descriptor = os.open(Path(path), flags)
info = os.fstat(descriptor)
if not stat.S_ISREG(info.st_mode) or stat.S_IMODE(info.st_mode) not in modes:
raise Invalid("HUX-12 evidence file permissions are invalid")
data = os.read(descriptor, limit + 1)
except OSError as error:
raise Invalid("HUX-12 evidence configuration is unavailable") from error
finally:
if descriptor >= 0:
os.close(descriptor)
if not data or len(data) > limit:
raise Invalid("HUX-12 evidence file is empty or too large")
return data
def evidence_key(environ: Mapping[str, str]) -> str:
"""Return the file-only producer key; inline environment secrets are ignored."""
data = _regular_file(environ.get(KEY_FILE_ENV, ""), {0o400}, MAX_KEY_BYTES)
try:
key = data.decode("utf-8", errors="strict").strip()
except UnicodeDecodeError as error:
raise Invalid("HUX-12 evidence key is invalid") from error
if len(key) < 32:
raise Invalid("HUX-12 evidence key is too short")
return key
def _https_url(value: Any, field: str, *, prefix: bool = False) -> str:
if not isinstance(value, str) or not value or len(value) > 500:
raise Invalid(f"{field} is missing or too long")
parsed = urlsplit(value)
if parsed.scheme != "https" or not parsed.netloc or parsed.username or parsed.password or parsed.query or parsed.fragment:
raise Invalid(f"{field} must be a credential-free HTTPS URL")
if prefix and not value.endswith("/"):
raise Invalid(f"{field} must end with /")
return value
def load_policy(environ: Mapping[str, str]) -> dict[str, Any]:
"""Load and strictly validate the non-secret producer policy."""
raw = _regular_file(environ.get(POLICY_FILE_ENV, ""), {0o400, 0o440, 0o444}, MAX_POLICY_BYTES)
try:
policy = json.loads(raw)
except (UnicodeDecodeError, json.JSONDecodeError) as error:
raise Invalid("HUX-12 evidence policy is not valid JSON") from error
if not isinstance(policy, dict) or set(policy) != POLICY_FIELDS or policy.get("schema") != "hux.release_evidence_policy.v1":
raise Invalid("HUX-12 evidence policy shape is invalid")
age = policy.get("max_evidence_age_seconds")
workloads = policy.get("workloads")
if not isinstance(age, int) or isinstance(age, bool) or not 60 <= age <= 86400:
raise Invalid("HUX-12 evidence age bound is invalid")
if not isinstance(workloads, dict) or not workloads or len(workloads) > 16:
raise Invalid("HUX-12 workload policy is invalid")
for name, item in workloads.items():
if not isinstance(name, str) or not isinstance(item, dict) or set(item) != WORKLOAD_FIELDS:
raise Invalid("HUX-12 workload policy shape is invalid")
_https_url(item["review_url_prefix"], "review_url_prefix", prefix=True)
_https_url(item["jenkins_job_url"], "jenkins_job_url")
_https_url(item["health_url"], "health_url")
if not re.fullmatch(r"[a-z0-9./_-]+", str(item["image_repository"])):
raise Invalid("HUX-12 image repository is invalid")
if not re.fullmatch(r"[a-z0-9-]{1,63}", str(item["flux_kustomization"])):
raise Invalid("HUX-12 Flux kustomization is invalid")
return policy
def configured(environ: Mapping[str, str]) -> bool:
"""Whether producer authentication and policy are both healthy."""
try:
evidence_key(environ)
load_policy(environ)
except Invalid:
return False
return True
def workload_policy(environ: Mapping[str, str], workload: str) -> dict[str, Any]:
"""Return the exact allowlist for a workload or fail closed."""
item = load_policy(environ)["workloads"].get(workload)
if item is None:
raise Invalid("workload is not enabled for release evidence")
return item
def require_review_creator(trust: str) -> None:
"""Only the authenticated browser path may record the human review decision."""
if trust != "router":
raise Forbidden("reviewed releases require router trust")
def require_evidence_producer(trust: str) -> None:
"""Only the dedicated producer may advance a reviewed release."""
if trust != "evidence":
raise Forbidden("release transitions require evidence trust")