feat(security): verify mirrored init containers

This commit is contained in:
ogt
2026-07-14 21:26:13 +08:00
parent 5b86dbbf17
commit c8edd73971
2 changed files with 174 additions and 5 deletions

View File

@@ -20,6 +20,7 @@ from typing import Any, Sequence
SCHEMA_VERSION = "awoooi_runtime_image_mirror_policy_v1"
RECEIPT_SCHEMA_VERSION = "awoooi_runtime_image_mirror_receipt_v1"
ALLOWED_KINDS = {"deployment", "daemonset", "statefulset"}
ALLOWED_CONTAINER_KINDS = {"container", "init_container"}
DIGEST_PATTERN = re.compile(r"^sha256:[0-9a-f]{64}$")
NAME_PATTERN = re.compile(r"^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$")
TRACE_PATTERN = re.compile(r"^[A-Za-z0-9._:-]{3,160}$")
@@ -35,6 +36,7 @@ class Workload:
namespace: str
name: str
container: str
container_kind: str
@dataclass(frozen=True)
@@ -119,7 +121,7 @@ def load_policy(path: Path) -> Policy:
raise ControllerError("image_policy_empty")
images: list[ImagePolicy] = []
asset_ids: set[str] = set()
workloads: set[tuple[str, str, str, str]] = set()
workloads: set[tuple[str, str, str, str, str]] = set()
for row in image_rows:
if not isinstance(row, dict) or not isinstance(row.get("workload"), dict):
raise ControllerError("image_policy_invalid")
@@ -152,12 +154,18 @@ def load_policy(path: Path) -> Policy:
namespace=_require_name(workload_row.get("namespace"), "namespace"),
name=_require_name(workload_row.get("name"), "workload_name"),
container=_require_name(workload_row.get("container"), "container"),
container_kind=_require_text(
workload_row.get("container_kind", "container"), "container_kind"
).lower(),
)
if workload.container_kind not in ALLOWED_CONTAINER_KINDS:
raise ControllerError("container_kind_not_allowed")
workload_key = (
workload.kind,
workload.namespace,
workload.name,
workload.container,
workload.container_kind,
)
if workload_key in workloads:
raise ControllerError("duplicate_workload_target")
@@ -476,13 +484,26 @@ class Kubernetes:
except json.JSONDecodeError as exc:
raise ControllerError("workload_readback_invalid") from exc
@staticmethod
def _spec_container_field(item: ImagePolicy) -> str:
if item.workload.container_kind == "init_container":
return "initContainers"
return "containers"
@staticmethod
def _status_container_field(item: ImagePolicy) -> str:
if item.workload.container_kind == "init_container":
return "initContainerStatuses"
return "containerStatuses"
def current_image(self, item: ImagePolicy) -> str:
workload = self.workload(item)
container_field = self._spec_container_field(item)
containers = (
workload.get("spec", {})
.get("template", {})
.get("spec", {})
.get("containers", [])
.get(container_field, [])
)
values = [
row.get("image")
@@ -494,12 +515,13 @@ class Kubernetes:
return values[0]
def patch_image(self, item: ImagePolicy, image_ref: str, *, dry_run: bool) -> None:
container_field = self._spec_container_field(item)
patch = json.dumps(
{
"spec": {
"template": {
"spec": {
"containers": [
container_field: [
{"name": item.workload.container, "image": image_ref}
]
}
@@ -566,7 +588,8 @@ class Kubernetes:
raise ControllerError("workload_pods_missing")
verified = 0
for pod in active:
statuses = pod.get("status", {}).get("containerStatuses", [])
status_field = self._status_container_field(item)
statuses = pod.get("status", {}).get(status_field, [])
matches = [
status
for status in statuses
@@ -576,7 +599,14 @@ class Kubernetes:
if len(matches) != 1:
raise ControllerError("pod_container_status_missing")
status = matches[0]
if status.get("ready") is not True:
if item.workload.container_kind == "init_container":
terminated = status.get("state", {}).get("terminated", {})
completed = status.get("ready") is True or (
isinstance(terminated, dict) and terminated.get("exitCode") == 0
)
if not completed:
raise ControllerError("pod_init_container_not_completed")
elif status.get("ready") is not True:
raise ControllerError("pod_container_not_ready")
image_id = status.get("imageID")
if not isinstance(image_id, str) or not image_id.endswith(