AID-I-001.001: Container-Based Isolation
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.
- AID-I-001.001-G002: Apply Kubernetes security contexts to restrict container privileges (e.g., runAsNonRoot).
- AID-I-001.001-G003: Use network policies to enforce least-privilege communication between AI pods.
- AID-I-001.001-G004: Set strict resource quotas (CPU, memory, GPU) to prevent resource exhaustion attacks.
- AID-I-001.001-G005: Mount filesystems as read-only wherever possible.
AID-I-001.001-G001
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
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
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: 8080Positive 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
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
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
- Docker Engine
- Podman
- Kubernetes
- OKD
- Trivy
- Project Calico
- Cilium
Commercial / hosted tools
- Mirantis Kubernetes Engine
- Mirantis Container Runtime
- Red Hat OpenShift Container Platform
- Aqua Platform (container runtime immutability and seccomp policy enforcement)
- Palo Alto Networks Prisma Cloud
- Microsoft Azure Kubernetes Service
- Google Kubernetes Engine
- Amazon Elastic Kubernetes Service
Threat mappings
MITRE ATLAS
- AML.T0053 AI Agent Tool Invocation
- AML.T0072 Cyber Communication Channel (default-deny pod network policy blocks unapproved outbound channels)
- AML.T0029 Denial of AI Service
- AML.T0034 Cost Harvesting
- AML.T0105 Escape to Host
- AML.T0089 Enterprise Environment Discovery (container process and filesystem namespaces limit enumeration of host processes and configuration)
MAESTRO
- Compromised Container Images (L4)
- Lateral Movement (Cross-Layer)
- Agent Tool Misuse (L7)
- Resource Hijacking (L4)
- Lateral Movement (L4) (container isolation prevents lateral movement)
OWASP LLM Top 10 2026
- LLM03:2026 Excessive Agency (container isolation confines permissions and side effects of over-privileged execution)
- LLM06:2026 Unbounded Consumption
OWASP ML Top 10 2023
- ML06:2023 AI Supply Chain Attacks
OWASP Top 10 for Agentic Applications 2026
- ASI02:2026 Tool Misuse and Exploitation
- ASI05:2026 Unexpected Code Execution (RCE)
- ASI04:2026 Agentic Supply Chain Vulnerabilities (compromised container images)
- ASI10:2026 Rogue Agents
NIST Adversarial Machine Learning 2025
- N/A
Cisco Integrated AI Security and Safety Framework
- AISubtech-13.1.1 Compute Exhaustion (per-workload limits and namespace admission quotas bound resource consumption)
- AISubtech-13.1.2 Memory Flooding (per-workload limits and namespace admission quotas bound resource consumption)
- AISubtech-13.1.3 Model Denial of Service (per-workload limits and namespace admission quotas bound resource consumption)
- AISubtech-13.1.4 Application Denial of Service (per-workload limits and namespace admission quotas bound resource consumption)
- AISubtech-13.2.1 Service Misuse for Cost Inflation (per-workload limits and namespace admission quotas bound resource consumption)
- AISubtech-14.1.2 Insufficient Access Controls (container privilege and filesystem boundaries constrain access to the host)
- AITech-13.1 Disruption of Availability (per-workload limits and namespace admission quotas bound resource consumption)
- AITech-13.2 Cost Harvesting / Repurposing (per-workload limits and namespace admission quotas bound resource consumption)
- AITech-14.1 Unauthorized Access (container privilege and filesystem boundaries constrain access to the host)
- AITech-8.2 Data Exfiltration / Exposure (unauthorized data paths are denied at the container network boundary)
Google Secure AI Framework 2.0 - Risks
- IIC: Insecure Integrated Component (container isolation contains compromised components)
- RA: Rogue Actions (container isolation limits rogue action blast radius)
- DMS: Denial of ML Service (resource quotas prevent service disruption)
Databricks AI Security Framework 3.0
- Agents - Core 13.11: Unexpected RCE and Code Attacks
- Agents - Core 13.4: Resource Overload (container resource limits prevent overload)
- Algorithms 5.4: Malicious libraries (container isolation contains malicious library impact)
- Model Serving - Inference requests 9.7: Denial of Service (DoS) (resource quotas prevent DoS)
- Platform 12.4: Unauthorized privileged access