AID-I-001.001: Container-Based Isolation

Version 1.20261015 · Actionable sub-technique

Utilizes container technologies like Docker or Kubernetes to package and run AI workloads in isolated user-space environments. This approach provides process and filesystem isolation and allows for resource management and network segmentation.

Pillars: infra

Lifecycle: operation

Scope boundary

Owns container-level process, filesystem, privilege, resource, and network isolation for AI workloads that can accept a shared host-kernel boundary. It does not claim the stronger kernel or hardware separation of a microVM or user-space-kernel sandbox.

AID-I-001.002: AID-I-001.001 uses conventional containers and a shared host kernel; AID-I-001.002 adds a microVM, user-space kernel, WASM capability boundary, or lower-level OS sandbox. Choose AID-I-001.002 when untrusted code or workload sensitivity requires stronger-than-container isolation. A hardened container alone does not satisfy that stronger boundary.

Implementation guidance

AID-I-001.001-G001

Deploy AI models and services in hardened, minimal-footprint container images.
Concept

A hardened image must be reproducible from immutable base images and a hash-locked dependency population. Resolve both base-image digests, target platform, and output repository from a signed build profile; use a tag only as a temporary push locator and promote the resulting digest.

Build a minimal non-root image
# syntax=docker/dockerfile:1
# File: Dockerfile
ARG BUILDER_IMAGE
ARG RUNTIME_IMAGE
FROM $BUILDER_IMAGE AS builder
WORKDIR /build
COPY requirements.lock .
RUN python -m pip install --no-cache-dir --require-hashes     --only-binary=:all: --target=/opt/python -r requirements.lock

FROM $RUNTIME_IMAGE
WORKDIR /app
COPY --from=builder --chown=65532:65532 /opt/python /opt/python
COPY --chown=65532:65532 src/ /app/src/
USER 65532:65532
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONPATH=/opt/python
ENTRYPOINT ["python3", "/app/src/main.py"]
Verify the signed profile, push, and promote only the digest

The profile owns builder_image, runtime_image, platform, output_repository, release_tag, source_revision, build_context_sha256, expected_runtime_uid, expected_runtime_gid, and blocking_vulnerability_severities. Both image inputs must contain a full SHA-256 digest. The registry and signing identities are external dependencies with least-privilege write/sign permissions.

# File: ci/build_hardened_image.sh
#!/usr/bin/env bash
set -euo pipefail
umask 077

root="$(mktemp -d)"
trap 'rm -rf -- "$root"' EXIT
install -m 0400 -- "$CONTAINER_BUILD_PROFILE" "$root/profile.json"
install -m 0400 -- "$CONTAINER_BUILD_PROFILE_BUNDLE" "$root/profile.sigstore.json"
cosign verify-blob --key "$CONTAINER_BUILD_PROFILE_VERIFY_KEY"   --bundle "$root/profile.sigstore.json" "$root/profile.json" >/dev/null

jq -e '
  .schema_version == "aidefend.container-build-profile.v1"
  and (.policy_version | type == "string" and length > 0)
  and (.builder_image | type == "string" and test("@sha256:[0-9a-f]{64}$"))
  and (.runtime_image | type == "string" and test("@sha256:[0-9a-f]{64}$"))
  and (.platform | type == "string" and test("^linux/(amd64|arm64)$"))
  and (.output_repository | type == "string" and length > 0)
  and (.release_tag | type == "string" and test("^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$"))
  and (.source_revision | type == "string" and test("^[0-9a-f]{40}$"))
  and (.build_context_sha256 | type == "string" and test("^[0-9a-f]{64}$"))
  and (.expected_runtime_uid | type == "number" and floor == . and . > 0)
  and (.expected_runtime_gid | type == "number" and floor == . and . > 0)
  and (.blocking_vulnerability_severities | type == "array" and length > 0)
  and (all(.blocking_vulnerability_severities[]; IN("UNKNOWN", "LOW", "MEDIUM", "HIGH", "CRITICAL")))
  and ((.blocking_vulnerability_severities | unique | length) == (.blocking_vulnerability_severities | length))
' "$root/profile.json" >/dev/null

builder_image="$(jq -r .builder_image "$root/profile.json")"
runtime_image="$(jq -r .runtime_image "$root/profile.json")"
platform="$(jq -r .platform "$root/profile.json")"
repository="$(jq -r .output_repository "$root/profile.json")"
release_tag="$(jq -r .release_tag "$root/profile.json")"
source_revision="$(jq -r .source_revision "$root/profile.json")"
expected_context_sha256="$(jq -r .build_context_sha256 "$root/profile.json")"
expected_runtime_uid="$(jq -r .expected_runtime_uid "$root/profile.json")"
expected_runtime_gid="$(jq -r .expected_runtime_gid "$root/profile.json")"
trivy_severities="$(jq -r '.blocking_vulnerability_severities | join(",")' "$root/profile.json")"

[[ "$(git rev-parse HEAD)" == "$source_revision" ]] || {
  echo "checked-out revision differs from signed profile" >&2
  exit 1
}
[[ -z "$(git status --porcelain --untracked-files=all -- Dockerfile requirements.lock src)" ]] || {
  echo "build inputs differ from the signed Git revision" >&2
  exit 1
}
actual_context_sha256="$(git archive "$source_revision" Dockerfile requirements.lock src | sha256sum | awk '{print $1}')"
[[ "$actual_context_sha256" == "$expected_context_sha256" ]] || {
  echo "build-context digest differs from signed profile" >&2
  exit 1
}

push_ref="$repository:$release_tag"
mkdir -p out

BUILDX_METADATA_PROVENANCE=max docker buildx build   --pull --platform "$platform"   --build-arg "BUILDER_IMAGE=$builder_image"   --build-arg "RUNTIME_IMAGE=$runtime_image"   --provenance=mode=max --sbom=true --push   --metadata-file out/container-build-metadata.json   --label "org.opencontainers.image.revision=$source_revision"   --tag "$push_ref" .

digest="$(jq -r '.["containerimage.digest"]' out/container-build-metadata.json)"
[[ "$digest" =~ ^sha256:[0-9a-f]{64}$ ]] || {
  echo "BuildKit did not return an immutable image digest" >&2
  exit 1
}
image_ref="$repository@$digest"
cosign sign --yes --key "$CONTAINER_IMAGE_SIGNING_KEY" "$image_ref"
cosign verify --key "$CONTAINER_IMAGE_VERIFY_KEY" "$image_ref" >/dev/null

docker pull "$image_ref" >/dev/null
expected_user="${expected_runtime_uid}:${expected_runtime_gid}"
configured_user="$(docker image inspect --format '{{.Config.User}}' "$image_ref")"
[[ "$configured_user" == "$expected_user" ]] || {
  echo "image Config.User differs from signed expected UID:GID: $configured_user" >&2
  exit 1
}
docker run --rm --read-only --network none --cap-drop=ALL   --security-opt=no-new-privileges --entrypoint python3 "$image_ref"   -c 'import os,sys; expected=(int(sys.argv[1]),int(sys.argv[2])); actual=(os.geteuid(),os.getegid()); assert actual == expected, (actual, expected)'   "$expected_runtime_uid" "$expected_runtime_gid"
trivy image --severity "$trivy_severities" --exit-code 1 "$image_ref"
printf '%s\n' "$image_ref" > out/promoted-image-ref.txt

Action: Admission must consume out/promoted-image-ref.txt, never the mutable tag. Reject an unhashed requirement, tag-only base, missing BuildKit digest, failed signature readback, absent or mismatched image Config.User, runtime EUID/EGID drift, root runtime, writable-root exception, or blocking vulnerability result according to the signed release policy.

AID-I-001.001-G002

Apply Kubernetes security contexts to restrict container privileges (e.g., runAsNonRoot).
Concept:

Kubernetes security context is your kernel-level blast-radius limiter. It should prevent privilege escalation, drop Linux capabilities, and keep filesystem writes constrained.

Apply restrictive pod and container security context

Signed input: Verify the versioned sandbox profile and closed schema before Helm rendering. Reject an absent field, mutable image tag, invalid UID/GID, invalid quantity, policy-digest mismatch, or rendered annotation mismatch. Sign the rendered manifest and retain its digest with the admission and runtime readback.

# File: k8s/templates/deployment.yaml
# CI verifies sandbox-profile.json with cosign, validates its closed schema,
# and passes that exact document as .Values.policy. There are no chart defaults.
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ required "policy.workloadName is required" .Values.policy.workloadName }}
  namespace: {{ required "policy.namespace is required" .Values.policy.namespace }}
  annotations:
    aidefend.io/policy-version: {{ required "policy.policyVersion is required" .Values.policy.policyVersion | quote }}
    aidefend.io/policy-sha256: {{ required "policy.policySha256 is required" .Values.policy.policySha256 | quote }}
spec:
  selector:
    matchLabels:
      app: {{ .Values.policy.workloadName }}
  template:
    metadata:
      labels:
        app: {{ .Values.policy.workloadName }}
      annotations:
        aidefend.io/policy-version: {{ .Values.policy.policyVersion | quote }}
        aidefend.io/policy-sha256: {{ .Values.policy.policySha256 | quote }}
    spec:
      automountServiceAccountToken: false
      securityContext:
        runAsNonRoot: true
        runAsUser: {{ required "policy.runAsUser is required" .Values.policy.runAsUser }}
        runAsGroup: {{ required "policy.runAsGroup is required" .Values.policy.runAsGroup }}
        fsGroup: {{ required "policy.fsGroup is required" .Values.policy.fsGroup }}
        seccompProfile:
          type: RuntimeDefault
      containers:
      - name: api
        image: {{ required "policy.imageDigest is required" .Values.policy.imageDigest | quote }}
        securityContext:
          allowPrivilegeEscalation: false
          readOnlyRootFilesystem: true
          capabilities:
            drop: ["ALL"]
        volumeMounts:
        - name: tmp
          mountPath: {{ required "policy.scratchPath is required" .Values.policy.scratchPath | quote }}
      volumes:
      - name: tmp
        emptyDir:
          sizeLimit: {{ required "policy.scratchSize is required" .Values.policy.scratchSize | quote }}
Verification
: "${NAMESPACE:?set from verified policy.namespace}"
: "${WORKLOAD_NAME:?set from verified policy.workloadName}"
: "${RENDERED_MANIFEST:?set to the signed rendered manifest}"
kubectl apply --server-side --field-manager=aidefend-sandbox -f "$RENDERED_MANIFEST"
kubectl rollout status -n "$NAMESPACE" "deployment/$WORKLOAD_NAME" --timeout=180s
kubectl exec -n "$NAMESPACE" "deployment/$WORKLOAD_NAME" -c api -- id
kubectl get deployment -n "$NAMESPACE" "$WORKLOAD_NAME" -o jsonpath="{.spec.template.spec.securityContext}{'\n'}{.spec.template.spec.containers[?(@.name=='api')].securityContext}{'\n'}"

Action: Make this security context baseline mandatory for AI workloads and block deployments that omit it in admission policy. The admission rule must cover normal, init, and ephemeral containers and bind any exception to a signed policy record.

AID-I-001.001-G003

Use network policies to enforce least-privilege communication between AI pods.
Concept and enforcement path

Kubernetes NetworkPolicy is enforced by the cluster network plugin, not by the API object itself. Start with default-deny ingress and egress, add only the gateway-to-inference path, and test the CNI data plane after every policy or plugin rollout. This example uses direct Pod IPs, so it does not require a DNS exception.

Default deny plus the two directions of the approved path
# File: k8s/policies/gateway-inference.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
  namespace: ai-production
spec:
  podSelector: {}
  policyTypes: [Ingress, Egress]
  ingress: []
  egress: []
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-gateway-to-inference-ingress
  namespace: ai-production
spec:
  podSelector:
    matchLabels:
      app: inference-server
  policyTypes: [Ingress]
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: api-gateway
    ports:
    - protocol: TCP
      port: 8080
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-gateway-to-inference-egress
  namespace: ai-production
spec:
  podSelector:
    matchLabels:
      app: api-gateway
  policyTypes: [Egress]
  egress:
  - to:
    - podSelector:
        matchLabels:
          app: inference-server
    ports:
    - protocol: TCP
      port: 8080
Positive and negative CNI probe over every selected target
# File: k8s/tests/verify_gateway_inference.sh
#!/usr/bin/env bash
set -euo pipefail

NS=ai-production
IMAGE='registry.k8s.io/e2e-test-images/agnhost@sha256:99c6b4bb4a1e1df3f0b3752168c89358794d02258ebebc26bf21c29399011a85'
: "${NP_CONNECT_TIMEOUT:?load NP_CONNECT_TIMEOUT from the verified network-test policy}"
: "${NP_CONVERGENCE_ATTEMPTS:?load NP_CONVERGENCE_ATTEMPTS from the verified network-test policy}"

[[ "$NP_CONNECT_TIMEOUT" =~ ^[1-9][0-9]*(ms|s|m)$ ]] || {
  echo "invalid policy-derived NP_CONNECT_TIMEOUT" >&2
  exit 2
}
[[ "$NP_CONVERGENCE_ATTEMPTS" =~ ^[1-9][0-9]*$ ]] || {
  echo "invalid policy-derived NP_CONVERGENCE_ATTEMPTS" >&2
  exit 2
}

RUN_ID="$(printf '%x%x' "$$" "$RANDOM")"
ALLOW_PROBE="np-allow-$RUN_ID"
DENY_PROBE="np-deny-$RUN_ID"
cleanup() {
  kubectl delete pod -n "$NS" "$ALLOW_PROBE" "$DENY_PROBE"     --ignore-not-found --wait=true >/dev/null
}
trap cleanup EXIT

kubectl apply --server-side -f k8s/policies/gateway-inference.yaml
PODS_JSON="$(kubectl get pods -n "$NS" -l app=inference-server -o json)"
TOTAL="$(jq '.items | length' <<<"$PODS_JSON")"
READY="$(jq '[.items[]
  | select(.status.phase == "Running")
  | select(any(.status.conditions[]?; .type == "Ready" and .status == "True"))
  | select(.status.podIP != null)] | length' <<<"$PODS_JSON")"
if [[ "$TOTAL" -eq 0 || "$READY" -ne "$TOTAL" ]]; then
  echo "target population is empty or not completely ready" >&2
  exit 2
fi
mapfile -t TARGET_IPS < <(jq -r '.items[].status.podIP' <<<"$PODS_JSON")

kubectl run "$ALLOW_PROBE" -n "$NS" --restart=Never --image="$IMAGE"   --labels="app=api-gateway,aidefend.io/probe-run=$RUN_ID"   --command -- /agnhost pause
kubectl run "$DENY_PROBE" -n "$NS" --restart=Never --image="$IMAGE"   --labels="app=unapproved-client,aidefend.io/probe-run=$RUN_ID"   --command -- /agnhost pause
kubectl wait -n "$NS" --for=condition=Ready   --timeout="$NP_CONNECT_TIMEOUT" "pod/$ALLOW_PROBE" "pod/$DENY_PROBE"

for ip in "${TARGET_IPS[@]}"; do
  endpoint="$ip:8080"
  [[ "$ip" == *:* ]] && endpoint="[${ip}]:8080"
  allowed=false
  for ((attempt=1; attempt<=NP_CONVERGENCE_ATTEMPTS; attempt++)); do
    if kubectl exec -n "$NS" "$ALLOW_PROBE" --       /agnhost connect --timeout="$NP_CONNECT_TIMEOUT"       --protocol=tcp "$endpoint"; then
      allowed=true
      break
    fi
  done
  if [[ "$allowed" != true ]]; then
    echo "approved path failed for target $ip" >&2
    exit 1
  fi
  if kubectl exec -n "$NS" "$DENY_PROBE" --     /agnhost connect --timeout="$NP_CONNECT_TIMEOUT"     --protocol=tcp "$endpoint"; then
    echo "unapproved path reached target $ip" >&2
    exit 1
  fi
done

printf 'verified_targets=%s\n' "${#TARGET_IPS[@]}"

Action: run this probe in a controlled pre-production namespace that uses the same CNI and admission policy as production. A successful kubectl get networkpolicy is only configuration readback; release the policy only when the approved probe reaches every selected inference Pod and the unapproved probe reaches none. Reject incomplete target enumeration, missing probe results, host-network Pods, or an allow-all policy instead of treating them as successful enforcement.

AID-I-001.001-G004

Set strict resource quotas (CPU, memory, GPU) to prevent resource exhaustion attacks.
Concept and policy ownership

Resource requests and limits bound one workload; ResourceQuota bounds aggregate namespace admission. Kubernetes extended resources cannot be overcommitted, so GPU quota uses requests.nvidia.com/gpu only. Capacity, tenant budgets, readback deadlines, and negative-test requests must come from a signature-verified policy; ResourceQuota rejects new or updated objects that exceed quota but does not retroactively throttle a running Pod.

Workload and namespace controls
# File: k8s/deployment-with-resources.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: gpu-inference-server
  namespace: ai-production
spec:
  selector:
    matchLabels:
      app: gpu-inference-server
  template:
    metadata:
      labels:
        app: gpu-inference-server
        aidefend.io/quota-governed: "true"
    spec:
      automountServiceAccountToken: false
      containers:
      - name: server
        image: registry.example.com/ai/gpu-server:2026.04.14
        resources:
          requests:
            cpu: "1000m"
            memory: "4Gi"
            nvidia.com/gpu: "1"
          limits:
            cpu: "2000m"
            memory: "8Gi"
            nvidia.com/gpu: "1"
---
# File: k8s/resourcequota-ai-production.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
  name: ai-production-quota
  namespace: ai-production
spec:
  hard:
    requests.cpu: "40"
    limits.cpu: "80"
    requests.memory: "160Gi"
    limits.memory: "320Gi"
    requests.nvidia.com/gpu: "8"

The displayed values are deployment examples. Render the actual manifest from the signed capacity and tenant-budget policy, require immutable workload image digests at admission, and protect the ResourceQuota from unauthorized update or deletion.

Read back effective quota and exercise admission without persisting a Pod

The verified policy has schema aidefend.resource-quota-policy.v1. Each namespaces entry contains namespace, quota_name, the exact Kubernetes-normalized expected_hard object, policy-owned readback timing, and a non-empty admission_negative_tests array. Each negative test names the exceeded quota resource and supplies a complete Pod resources object, with requests and limits, generated from that quota. The policy build must cover CPU, memory, and every configured extended resource.

# File: k8s/tests/verify_resource_quota.sh
#!/usr/bin/env bash
set -euo pipefail

POLICY_SOURCE=policy/resource-quota-policy.json
SIGNATURE_SOURCE=policy/resource-quota-policy.sig
PUBLIC_KEY=policy/trust/resource-quota-policy.pub
PROBE_IMAGE='registry.k8s.io/e2e-test-images/agnhost@sha256:99c6b4bb4a1e1df3f0b3752168c89358794d02258ebebc26bf21c29399011a85'

TMP_ROOT="$(mktemp -d)"
chmod 700 "$TMP_ROOT"
trap 'rm -rf -- "$TMP_ROOT"' EXIT
POLICY="$TMP_ROOT/resource-quota-policy.json"
SIGNATURE="$TMP_ROOT/resource-quota-policy.sig"
install -m 0400 -- "$POLICY_SOURCE" "$POLICY"
install -m 0400 -- "$SIGNATURE_SOURCE" "$SIGNATURE"
cosign verify-blob --key "$PUBLIC_KEY" --bundle "$SIGNATURE" "$POLICY" >/dev/null
jq -e '
  .schema_version == "aidefend.resource-quota-policy.v1"
  and (.namespaces | type == "array" and length > 0)
  and all(.namespaces[];
    (.namespace | type == "string" and length > 0)
    and (.quota_name | type == "string" and length > 0)
    and (.expected_hard | type == "object" and length > 0)
    and (.readback_timeout_seconds | type == "number" and . > 0 and floor == .)
    and (.poll_interval_seconds | type == "number" and . > 0)
    and (.admission_negative_tests | type == "array" and length > 0)
  )
' "$POLICY" >/dev/null


while IFS= read -r entry; do
  namespace="$(jq -r '.namespace' <<<"$entry")"
  quota_name="$(jq -r '.quota_name' <<<"$entry")"
  expected_hard="$(jq -cS '.expected_hard' <<<"$entry")"
  timeout_seconds="$(jq -r '.readback_timeout_seconds' <<<"$entry")"
  poll_seconds="$(jq -r '.poll_interval_seconds' <<<"$entry")"
  expected_test_resources="$(jq -cS '[.expected_hard | keys[] | select(test("^(requests|limits)\\."))]' <<<"$entry")"
  observed_test_resources="$(jq -cS '[.admission_negative_tests[].quota_resource] | unique' <<<"$entry")"
  if [[ "$observed_test_resources" != "$expected_test_resources" ]]; then
    echo "quota negative tests do not cover every compute resource in $namespace/$quota_name" >&2
    exit 2
  fi

  observed_spec="$(kubectl get resourcequota "$quota_name" -n "$namespace"     -o json | jq -cS '.spec.hard')"
  if [[ "$observed_spec" != "$expected_hard" ]]; then
    echo "ResourceQuota spec differs in $namespace/$quota_name" >&2
    exit 1
  fi

  deadline=$(( $(date +%s) + timeout_seconds ))
  observed_status=''
  while (( $(date +%s) < deadline )); do
    observed_status="$(kubectl get resourcequota "$quota_name" -n "$namespace"       -o json | jq -cS '.status.hard // {}')"
    [[ "$observed_status" == "$expected_hard" ]] && break
    sleep "$poll_seconds"
  done
  if [[ "$observed_status" != "$expected_hard" ]]; then
    echo "ResourceQuota status never converged in $namespace/$quota_name" >&2
    exit 2
  fi

  while IFS= read -r test_case; do
    case_id="$(jq -r '.case_id' <<<"$test_case")"
    quota_resource="$(jq -r '.quota_resource' <<<"$test_case")"
    resources="$(jq -c '.pod_resources' <<<"$test_case")"
    if [[ ! "$case_id" =~ ^[a-z0-9][a-z0-9-]{0,39}$ ]]       || [[ -z "$quota_resource" ]]       || ! jq -e '         type == "object"         and (keys | sort) == ["limits", "requests"]         and (.requests | type == "object" and length > 0 and all(.[]; type == "string" and length > 0))         and (.limits | type == "object" and length > 0 and all(.[]; type == "string" and length > 0))       ' <<<"$resources" >/dev/null; then
      echo "invalid quota negative-test case" >&2
      exit 2
    fi

    manifest="$TMP_ROOT/$case_id.json"
    jq -n       --arg name "quota-negative-$case_id"       --arg namespace "$namespace"       --arg image "$PROBE_IMAGE"       --argjson resources "$resources"       '{
        apiVersion:"v1", kind:"Pod",
        metadata:{name:$name, namespace:$namespace},
        spec:{
          restartPolicy:"Never",
          automountServiceAccountToken:false,
          containers:[{
            name:"probe", image:$image,
            command:["/agnhost","pause"],
            resources:$resources,
            securityContext:{
              allowPrivilegeEscalation:false,
              readOnlyRootFilesystem:true,
              capabilities:{drop:["ALL"]}
            }
          }]
        }
      }' >"$manifest"

    set +e
    rejection="$(kubectl create --dry-run=server -f "$manifest" 2>&1)"
    status=$?
    set -e
    if [[ "$status" -eq 0 ]]       || [[ "$rejection" != *"exceeded quota: $quota_name"* ]]       || [[ "$rejection" != *"$quota_resource"* ]]; then
      echo "expected quota rejection was not observed for $namespace/$case_id" >&2
      printf '%s\n' "$rejection" >&2
      exit 1
    fi
  done < <(jq -c '.admission_negative_tests[]' <<<"$entry")
done < <(jq -c '.namespaces[]' "$POLICY")

Action: promote the quota only after spec.hard and status.hard match the signed policy for every governed namespace and every policy-owned over-budget case is rejected by the real API-server admission path. A different admission error, a missing governed namespace, a missing quota status, or an untested resource is unresolved rather than proof that quota enforcement works. Reconcile already-running Pods separately because ResourceQuota admission does not shrink or terminate them.

AID-I-001.001-G005

Mount filesystems as read-only wherever possible.
Concept:

Read-only root filesystem blocks many post-exploitation actions (dropping binaries, modifying startup scripts, tampering local config). Only explicitly mounted temporary paths should be writable.

Apply read-only root filesystem with explicit writable scratch path

Signed input: Verify the versioned writable-path profile and closed schema before Helm rendering. Reject an absent field, mutable image tag, invalid quantity, policy-digest mismatch, or rendered annotation mismatch. Sign the rendered manifest and retain its digest with the admission, mount-table, and write-probe readback.

# File: k8s/templates/readonly-fs-deployment.yaml
# CI verifies sandbox-profile.json with cosign, validates its closed schema,
# and passes that exact document as .Values.policy. There are no chart defaults.
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ required "policy.workloadName is required" .Values.policy.workloadName }}
  namespace: {{ required "policy.namespace is required" .Values.policy.namespace }}
  annotations:
    aidefend.io/policy-version: {{ required "policy.policyVersion is required" .Values.policy.policyVersion | quote }}
    aidefend.io/policy-sha256: {{ required "policy.policySha256 is required" .Values.policy.policySha256 | quote }}
spec:
  selector:
    matchLabels:
      app: {{ .Values.policy.workloadName }}
  template:
    metadata:
      labels:
        app: {{ .Values.policy.workloadName }}
      annotations:
        aidefend.io/policy-version: {{ .Values.policy.policyVersion | quote }}
        aidefend.io/policy-sha256: {{ .Values.policy.policySha256 | quote }}
    spec:
      automountServiceAccountToken: false
      containers:
      - name: api
        image: {{ required "policy.imageDigest is required" .Values.policy.imageDigest | quote }}
        securityContext:
          readOnlyRootFilesystem: true
          allowPrivilegeEscalation: false
          capabilities:
            drop: ["ALL"]
        volumeMounts:
        - name: tmp-storage
          mountPath: {{ required "policy.scratchPath is required" .Values.policy.scratchPath | quote }}
      volumes:
      - name: tmp-storage
        emptyDir:
          sizeLimit: {{ required "policy.scratchSize is required" .Values.policy.scratchSize | quote }}
Verification
: "${NAMESPACE:?set from verified policy.namespace}"
: "${WORKLOAD_NAME:?set from verified policy.workloadName}"
kubectl rollout status -n "$NAMESPACE" "deployment/$WORKLOAD_NAME" --timeout=180s
kubectl exec -n "$NAMESPACE" "deployment/$WORKLOAD_NAME" -c api -- sh -ceu 'probe=/tmp/aidefend-write-probe; : > "$probe"; rm -f "$probe"; if touch /root/aidefend-should-fail 2>/dev/null; then rm -f /root/aidefend-should-fail; exit 1; fi'
kubectl get deployment -n "$NAMESPACE" "$WORKLOAD_NAME" -o jsonpath="{.spec.template.spec.containers[?(@.name=='api')].securityContext}{'\n'}{.spec.template.spec.volumes}{'\n'}"

Action: Enforce read-only root filesystem by default for AI runtime Pods, bound every writable mount by path, medium, size, owner, and lifecycle, and require a signed exception for any additional write surface.

Open-source tools

Commercial / hosted tools

Threat mappings

MITRE ATLAS

MAESTRO

OWASP LLM Top 10 2026

OWASP ML Top 10 2023

OWASP Top 10 for Agentic Applications 2026

NIST Adversarial Machine Learning 2025

Cisco Integrated AI Security and Safety Framework

Google Secure AI Framework 2.0 - Risks

Databricks AI Security Framework 3.0

AID-I-001.001: Container-Based Isolation | AIDEFEND

Loading interactive view…

A Practical Knowledge Base for AI Security Defenses

Version