Agentic Covenants

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 expirationSeconds on 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. The sub field pins the role to one specific system: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 0PR.AA-01, PR.AA-02, PR.AA-03, PR.AA-04
NIST AI RMFMANAGE 4.1
OWASP LLMLLM02, LLM06
OWASP AGENTICASI03, ASI10
OTHERNIST SP 800-207 §3.4.1, NIST SP 800-63B