Protect (PR) · Identity
Identity at the server side layer
external · Outside the agent entirely
If the agent decides to violate this concern, what stops it at this layer?
What this cell does
Dedicated ServiceAccount or IAM principal per agent. OIDC federation. Short-TTL bound tokens.
Artifacts (7)
aws-iam-trust-policy.jsonview on GitHub{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowAssumeRoleByOneSpecificAgentSA",
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/EXAMPLE_OIDC_ID"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.us-east-1.amazonaws.com/id/EXAMPLE_OIDC_ID:sub": "system:serviceaccount:agent-claude-prod:claude-code",
"oidc.eks.us-east-1.amazonaws.com/id/EXAMPLE_OIDC_ID:aud": "sts.amazonaws.com"
}
}
}
]
}
namespace.yamlview on GitHub# ABOUTME: Dedicated namespace for one agent with restricted Pod Security Standards enforced at admission.
# ABOUTME: Substitute agent-claude-prod with your agent name; one namespace per agent, never shared.
apiVersion: v1
kind: Namespace
metadata:
name: agent-claude-prod
labels:
# Pod Security Standards: enforce restricted at admission. This blocks
# pods that request privileged capabilities, host networking, host paths,
# or run as root.
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: latest
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted
# Tag identifying this as an agent namespace; consumed by Kyverno policies
# in controls/authorization/server-side/ and NetworkPolicy selectors in
# controls/blast-radius/server-side/.
agentic-covenants.io/role: agent
agentic-covenants.io/agent-name: claude-code-prod
pod-with-projected-token.yamlview on GitHub# ABOUTME: Agent Pod that mounts a projected ServiceAccount token with 15-minute TTL and audience binding.
# ABOUTME: Image is pinned by digest; substitute with your registry. The 900s TTL is the load-bearing line.
apiVersion: v1
kind: Pod
metadata:
name: claude-code
namespace: agent-claude-prod
labels:
app: claude-code
agentic-covenants.io/agent-name: claude-code-prod
spec:
serviceAccountName: claude-code
automountServiceAccountToken: false # using projected token volume below
# Restricted Pod Security Standards compliance.
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: claude
# Pin the agent image by digest. Resolve current digest with:
# crane digest registry.example.com/claude-agent:v1
image: registry.example.com/claude-agent@sha256:REPLACE_WITH_DIGEST_FROM_CRANE
imagePullPolicy: IfNotPresent
securityContext:
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefault
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 1
memory: 1Gi
ephemeral-storage: 1Gi
volumeMounts:
- name: agent-token
mountPath: /var/run/secrets/agents
readOnly: true
- name: workdir
mountPath: /workspace
volumes:
- name: agent-token
projected:
sources:
- serviceAccountToken:
path: token
# 15 minutes. The kubelet rotates the token automatically before
# expiration. The agent process must re-read the file periodically;
# most Kubernetes client libraries do this transparently.
expirationSeconds: 900
# Audience scopes the token to a specific consumer. A token with
# audience=agent-claude-prod cannot be replayed against any other
# API. Auth providers MUST verify the audience claim.
audience: agent-claude-prod
- name: workdir
emptyDir:
sizeLimit: 500Mi
role-and-binding.yamlview on GitHub# ABOUTME: Namespace-scoped Role and RoleBinding for one agent. Never a ClusterRole. Never wildcard verbs.
# ABOUTME: Lists every API group, resource, and verb explicitly. Add lines as needed; do not collapse to "*".
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: claude-code
namespace: agent-claude-prod
rules:
# Read-only on workload state in this namespace. The agent diagnoses but does
# not mutate.
- apiGroups: [""]
resources: ["pods", "configmaps", "services", "endpoints"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments", "replicasets", "statefulsets", "daemonsets"]
verbs: ["get", "list", "watch"]
- apiGroups: ["batch"]
resources: ["jobs", "cronjobs"]
verbs: ["get", "list", "watch"]
# Pod logs and exec. Note: pods/exec is a separate resource from pods.
# Denying pods does not deny pods/exec; subresources must be enumerated.
- apiGroups: [""]
resources: ["pods/log"]
verbs: ["get"]
# Deliberately omitted: pods/exec, pods/portforward, secrets, serviceaccounts,
# rolebindings, anything that would let the agent escalate or read credentials.
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: claude-code
namespace: agent-claude-prod
subjects:
- kind: ServiceAccount
name: claude-code
namespace: agent-claude-prod
roleRef:
kind: Role
name: claude-code
apiGroup: rbac.authorization.k8s.io
serviceaccount.yamlview on GitHub# ABOUTME: Per-agent ServiceAccount with IRSA annotation linking to the AWS IAM role for the agent.
# ABOUTME: One ServiceAccount per agent; never shared. The IRSA annotation must match aws-iam-trust-policy.json.
apiVersion: v1
kind: ServiceAccount
metadata:
name: claude-code
namespace: agent-claude-prod
annotations:
# IRSA: this annotation tells the EKS pod-identity webhook to project an
# OIDC token that the AWS IAM role's trust policy can verify. The role
# ARN here MUST match the role whose trust policy is in
# aws-iam-trust-policy.json.
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/claude-code-prod
# GCP Workload Identity equivalent; uncomment if on GKE.
# iam.gke.io/gcp-service-account: claude-code-prod@PROJECT.iam.gserviceaccount.com
# Azure Workload Identity equivalent; uncomment if on AKS.
# azure.workload.identity/client-id: 00000000-0000-0000-0000-000000000000
automountServiceAccountToken: false # we use projected tokens via the Pod spec instead
spire-entry.shview on GitHub#!/usr/bin/env bash
# ABOUTME: Registers an agent's SPIFFE identity for cross-cluster workload attestation via SPIRE.
# ABOUTME: Optional. Use only when the agent operates across more than one cluster or off-cluster components.
set -euo pipefail
TRUST_DOMAIN="${TRUST_DOMAIN:-example.com}"
AGENT_NAME="${1:-claude-code-prod}"
NAMESPACE="${NAMESPACE:-agent-${AGENT_NAME}}"
SERVICE_ACCOUNT="${SERVICE_ACCOUNT:-claude-code}"
TTL_SECONDS="${TTL_SECONDS:-900}"
if ! command -v spire-server >/dev/null 2>&1; then
echo "spire-server CLI not found in PATH." >&2
echo "Install: https://spiffe.io/docs/latest/spire-about/getting-started/" >&2
exit 1
fi
# Register the agent's SPIFFE ID. The selectors pin the identity to a
# specific Kubernetes ServiceAccount in a specific namespace; SPIRE will only
# issue an SVID to a workload that matches every selector.
spire-server entry create \
-spiffeID "spiffe://${TRUST_DOMAIN}/agent/${AGENT_NAME}" \
-parentID "spiffe://${TRUST_DOMAIN}/k8s_workload/${NAMESPACE}" \
-selector "k8s:ns:${NAMESPACE}" \
-selector "k8s:sa:${SERVICE_ACCOUNT}" \
-selector "k8s:pod-label:app:${SERVICE_ACCOUNT}" \
-ttl "$TTL_SECONDS"
echo "Registered spiffe://${TRUST_DOMAIN}/agent/${AGENT_NAME}"
echo "TTL: ${TTL_SECONDS}s. The agent must request fresh SVIDs from the SPIRE Workload API."
verify.shview on GitHub#!/usr/bin/env bash
# ABOUTME: Verifies server-side identity controls: dedicated SA, short TTL, identity-bound IAM, cross-agent isolation.
# ABOUTME: Run with kubectl context targeting the cluster. Exits non-zero on any failed check.
set -euo pipefail
NAMESPACE="${1:-agent-claude-prod}"
SA_NAME="${2:-claude-code}"
PASS=0
FAIL=0
check() {
local name="$1" result="$2"
if [[ "$result" == "ok" ]]; then
echo "PASS: $name"
PASS=$((PASS + 1))
else
echo "FAIL: $name -- $result"
FAIL=$((FAIL + 1))
fi
}
# 1. Dedicated identity per agent
SA_COUNT=$(kubectl get sa -A -o jsonpath='{.items[*].metadata.name}' \
| tr ' ' '\n' | grep -cx "$SA_NAME" || true)
if [[ "$SA_COUNT" -eq 1 ]]; then
check "ServiceAccount $SA_NAME exists exactly once cluster-wide" "ok"
else
check "ServiceAccount $SA_NAME exists exactly once cluster-wide" "found in $SA_COUNT namespaces (must be 1)"
fi
# 2. Token TTL is 15 minutes (900s) or shorter
POD=$(kubectl get pod -n "$NAMESPACE" -l app="$SA_NAME" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
if [[ -z "$POD" ]]; then
check "agent Pod found in $NAMESPACE" "no Pod with label app=$SA_NAME"
else
TTL=$(kubectl get pod -n "$NAMESPACE" "$POD" -o jsonpath='{.spec.volumes[?(@.projected)].projected.sources[?(@.serviceAccountToken)].serviceAccountToken.expirationSeconds}')
if [[ -z "$TTL" ]]; then
check "projected token TTL is set" "no expirationSeconds on the projected ServiceAccountToken"
elif [[ "$TTL" -le 900 ]]; then
check "projected token TTL is <= 900s ($TTL)" "ok"
else
check "projected token TTL is <= 900s" "TTL is ${TTL}s (must be <= 900)"
fi
fi
# 3. AWS role assumption is identity-bound (only runs if AWS_PROFILE is set)
if [[ -n "${AWS_PROFILE:-}" ]] && command -v aws >/dev/null 2>&1; then
ROLE_ARN=$(aws --profile "$AWS_PROFILE" sts get-caller-identity --query Arn --output text 2>/dev/null || true)
if [[ "$ROLE_ARN" == *":role/${SA_NAME}-"* || "$ROLE_ARN" == *":role/${SA_NAME}/"* || "$ROLE_ARN" == *":assumed-role/${SA_NAME}-"* ]]; then
check "AWS role assumption returns dedicated role ($ROLE_ARN)" "ok"
else
check "AWS role assumption returns dedicated role" "got $ROLE_ARN"
fi
fi
# 4. Cross-agent isolation: this agent's pod cannot read another agent's token
OTHER_NS=$(kubectl get ns -l agentic-covenants.io/role=agent -o jsonpath='{.items[*].metadata.name}' \
| tr ' ' '\n' | grep -vx "$NAMESPACE" | head -1 || true)
if [[ -n "$OTHER_NS" && -n "$POD" ]]; then
if kubectl exec -n "$NAMESPACE" "$POD" -- ls "/var/run/secrets/${OTHER_NS}/" 2>/dev/null | grep -q .; then
check "agent cannot see another agent's token volume" "found another agent's token mount"
else
check "agent cannot see another agent's token volume" "ok"
fi
fi
echo ""
echo "Summary: $PASS passed, $FAIL failed"
exit "$FAIL"
Cell notes
Identity / Server-side
Control. Dedicated ServiceAccount or IAM principal per agent. OIDC federation with workload identity. Short-lived bound tokens (15-minute TTL). Auth provider rejects requests where agent identity is missing or stale. SPIFFE/SPIRE for cross-cluster identity.
Strength. Deterministic and external to the agent. Bypass requires IdP compromise, token replay within the TTL window, or a misconfigured trust policy that accepts tokens from unintended issuers.
Tooling
- - Kubernetes 1.34+ (for
expirationSecondson projected ServiceAccount tokens, restricted Pod Security Standards). Current stable is 1.36 "Haru" (April 2026); 1.33 reached end-of-life June 28, 2026. - - AWS IAM Roles for Service Accounts (IRSA), GCP Workload Identity, or Azure Workload Identity.
- - SPIFFE/SPIRE if you have multiple clusters or off-cluster components that need a single identity story.
- - An OIDC IdP (Okta, Auth0, Keycloak, Dex).
> Direction of travel (mid-2026): the MCP 2026-07-28 protocol revision hardens authorization toward OAuth 2.0 / OpenID Connect with iss validation (RFC 9207) and issuer-bound credentials, and the NIST NCCoE agent-identity concept paper (Feb 5, 2026) proposes exactly this stack (OAuth 2.0 + SPIFFE/SPIRE + MCP). The covenant here, identity established by an external IdP, carried by the agent, never asserted by it, is now the ecosystem's own direction, not a contrarian stance. See ../../../BYPASSES.md for the protocol-transition caveats.
Files in this directory
- -
namespace.yaml, dedicated namespace per agent with restricted Pod Security Standards. - -
serviceaccount.yaml, per-agent ServiceAccount with the IRSA annotation linking to the AWS role. - -
role-and-binding.yaml, namespace-scoped Role and RoleBinding (never ClusterRole). Verbs are explicit; no wildcards. - -
pod-with-projected-token.yaml, Pod that mounts a projected ServiceAccount token with 15-minute TTL and audience binding. - -
aws-iam-trust-policy.json, trust policy with strict OIDC subject condition. Thesubfield pins the role to one specificsystem:serviceaccount:<ns>:<sa>pair. - -
spire-entry.sh, SPIRE registration command for cross-cluster identity. Optional but recommended if you have agents in more than one cluster. - -
verify.sh, confirms dedicated identity per agent, short token TTL, identity-bound role assumption, cross-agent isolation.
Verification
./verify.sh agent-claude-prod claude-code
The script runs four checks against the live cluster:
- 1. Dedicated SA exists per agent and is not shared.
- 2. Token TTL is 15 minutes (not 1-hour or unbounded default).
- 3. AWS role assumption returns the dedicated role ARN.
- 4. The agent's Pod cannot read another agent's mounted token.
Common mistakes
- - ServiceAccount with cluster-wide scope (the 1.21-and-older default behavior).
- - Token TTL set to the default (1 hour) or unbounded.
- - Trust policy with
"sub": "system:serviceaccount::", any SA in any namespace can assume the role. - - Storing the token in a regular Secret instead of a projected volume; the Secret persists past Pod lifecycle.
- - Multiple agents sharing one IAM role with broad permissions, defeating per-agent attribution.
- - Forgetting to pin the audience. Without
audience: agent-<name>, the token is reusable by any service.
Citation
NIST CSF 2.0 PR.AA-01, PR.AA-02 (identities proofed and bound), PR.AA-03, PR.AA-04 (identity assertions protected, conveyed, verified). NIST SP 800-207 §3.4.1 (per-session authentication). NIST SP 800-63B Rev. 4. NIST NCCoE Concept Paper on Software and AI Agent Identity and Authorization (Feb 5, 2026).
Primary bypasses
Documented, not hypothetical. A control whose bypass is undocumented is worse than no control, because somebody trusted it.
- IdP compromise
- token replay within TTL
- misconfigured trust policy accepting unintended issuers
Crosswalk
| NIST CSF 2 0 | PR.AA-01, PR.AA-02, PR.AA-03, PR.AA-04 |
|---|---|
| NIST AI RMF | MANAGE 4.1 |
| OWASP LLM | LLM02, LLM06 |
| OWASP AGENTIC | ASI03, ASI10 |
| OTHER | NIST SP 800-207 §3.4.1, NIST SP 800-63B |
Cite this cell:
https://agenticcovenants.com/protect/identity/server-side/