Agentic Covenants

Protect (PR) · Authorization

Authorization 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

Scoped RBAC Roles, IAM with explicit ARN, Kyverno or OPA admission, namespace scoping.

Artifacts (3)

aws-iam-scoped-policy.jsonview on GitHub
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowReadOnlyOnSpecificReportsBucket",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:ListBucket",
        "s3:GetBucketLocation"
      ],
      "Resource": [
        "arn:aws:s3:::reports-readonly-prod",
        "arn:aws:s3:::reports-readonly-prod/*"
      ]
    },
    {
      "Sid": "DenyDestructiveOnAnyResourceWithoutAgentTag",
      "Effect": "Deny",
      "Action": [
        "s3:DeleteBucket",
        "s3:DeleteObject",
        "s3:DeleteObjectVersion",
        "s3:PutBucketPolicy",
        "s3:PutBucketLifecycleConfiguration",
        "s3:PutBucketAcl"
      ],
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "aws:ResourceTag/agentic-covenants/agent-allowed": "claude-code-prod"
        }
      }
    },
    {
      "Sid": "DenyAllOtherS3Actions",
      "Effect": "Deny",
      "NotAction": [
        "s3:GetObject",
        "s3:ListBucket",
        "s3:GetBucketLocation"
      ],
      "Resource": "*"
    },
    {
      "Sid": "DenyIAMEscalationPrimitives",
      "Effect": "Deny",
      "Action": [
        "iam:CreateUser",
        "iam:CreateRole",
        "iam:CreatePolicy",
        "iam:CreatePolicyVersion",
        "iam:SetDefaultPolicyVersion",
        "iam:AttachUserPolicy",
        "iam:AttachRolePolicy",
        "iam:AttachGroupPolicy",
        "iam:PassRole",
        "iam:PutUserPolicy",
        "iam:PutRolePolicy",
        "iam:PutGroupPolicy",
        "iam:UpdateAssumeRolePolicy"
      ],
      "Resource": "*"
    }
  ]
}
git-pre-receive-hook.shview on GitHub
#!/usr/bin/env bash
# ABOUTME: Git server-side pre-receive hook. Cannot be bypassed by --no-verify. Install on every Git server in the org.
# ABOUTME: Blocks force-pushes to main, edits to protected paths from non-CODEOWNERS, and any diff containing secrets.

set -euo pipefail

PROTECTED_PATHS=(
  'infrastructure/prod/'
  '.github/workflows/'
  'secrets/'
  '.claude/'
)

ZERO_SHA="0000000000000000000000000000000000000000"
EXIT_CODE=0

while read -r oldrev newrev refname; do

  # 1. Reject force-pushes to main / master / release branches.
  case "$refname" in
    refs/heads/main|refs/heads/master|refs/heads/release/*)
      if [[ "$oldrev" != "$ZERO_SHA" && "$newrev" != "$ZERO_SHA" ]]; then
        if ! git merge-base --is-ancestor "$oldrev" "$newrev" 2>/dev/null; then
          echo "BLOCKED: force-push to ${refname} is not permitted" >&2
          EXIT_CODE=1
          continue
        fi
      fi
      ;;
  esac

  # Skip the per-commit checks on branch deletion.
  if [[ "$newrev" == "$ZERO_SHA" ]]; then
    continue
  fi

  # 2. Reject edits to protected paths from authors not listed in CODEOWNERS.
  if [[ "$oldrev" == "$ZERO_SHA" ]]; then
    # New branch: scan all reachable commits.
    CHANGED=$(git diff-tree --no-commit-id --name-only -r "$newrev")
  else
    CHANGED=$(git diff --name-only "$oldrev" "$newrev")
  fi

  AUTHOR_EMAIL=$(git log -1 --pretty=format:%ae "$newrev")

  while IFS= read -r path; do
    [[ -z "$path" ]] && continue
    for protected in "${PROTECTED_PATHS[@]}"; do
      if [[ "$path" == "$protected"* ]]; then
        if ! git show "$newrev:CODEOWNERS" 2>/dev/null \
            | grep -E "^\s*${protected}" \
            | grep -q "$AUTHOR_EMAIL"; then
          echo "BLOCKED: ${path} is a protected path; ${AUTHOR_EMAIL} is not in CODEOWNERS for it" >&2
          EXIT_CODE=1
        fi
      fi
    done
  done <<< "$CHANGED"

  # 3. Run gitleaks against the diff. Skipped if gitleaks is not installed
  # so the hook fails closed only on real findings, not infrastructure issues.
  if command -v gitleaks >/dev/null 2>&1; then
    DIFF_OUTPUT=$(git diff "$oldrev..$newrev" 2>/dev/null || true)
    if [[ -n "$DIFF_OUTPUT" ]]; then
      if ! echo "$DIFF_OUTPUT" \
          | gitleaks detect --source - --no-banner --no-color --exit-code 1 >/dev/null 2>&1; then
        echo "BLOCKED: secrets detected by gitleaks in ${refname} (${oldrev:0:8}..${newrev:0:8})" >&2
        EXIT_CODE=1
      fi
    fi
  else
    echo "WARN: gitleaks not installed on server; secret-scan step skipped" >&2
  fi

done

exit "$EXIT_CODE"
kyverno-no-cluster-roles.yamlview on GitHub
# ABOUTME: Kyverno ClusterPolicy denying ClusterRoleBindings, wildcard verbs, and prod-namespace bindings on agent SAs.
# ABOUTME: Verified against Kyverno CLI 1.17.1 on 2026-08-17 by tests/kyverno/. validationFailureAction must be Enforce, not Audit.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: agents-no-cluster-roles
  annotations:
    policies.kyverno.io/title: Agents must use namespace-scoped Roles only
    policies.kyverno.io/category: Agentic Covenants / Authorization
    policies.kyverno.io/severity: high
spec:
  validationFailureAction: Enforce
  background: true
  rules:

  - name: deny-clusterrolebinding-for-agent-sa
    match:
      any:
      - resources:
          kinds: [ClusterRoleBinding]
    validate:
      message: >-
        Agents must use namespace-scoped RoleBindings, never ClusterRoleBindings.
        ServiceAccounts in namespaces matching agent-* may not be ClusterRoleBinding subjects.
      foreach:
      - list: "request.object.subjects"
        deny:
          conditions:
            all:
            - key: "{{ element.kind }}"
              operator: Equals
              value: "ServiceAccount"
            - key: "{{ element.namespace }}"
              operator: AnyIn
              value: ["agent-*"]

  - name: deny-wildcard-verbs-in-roles
    match:
      any:
      - resources:
          kinds: [Role, ClusterRole]
    validate:
      message: >-
        Wildcard verbs are not permitted. List the verbs explicitly: get, list, watch, etc.
      # foreach/deny rather than a pattern anchor: verbs is a list of strings,
      # so a nested map pattern never matches and denies every Role including
      # correctly-scoped ones. Verified by tests/kyverno/.
      foreach:
      - list: "request.object.rules[]"
        deny:
          conditions:
            any:
            - key: "{{ contains(element.verbs, '*') }}"
              operator: Equals
              value: true

  - name: deny-wildcard-resources-in-roles
    match:
      any:
      - resources:
          kinds: [Role, ClusterRole]
    validate:
      message: >-
        Wildcard resources are not permitted. List resources explicitly, including subresources
        such as pods/exec where access must be explicit.
      foreach:
      - list: "request.object.rules[]"
        deny:
          conditions:
            any:
            - key: "{{ contains(element.resources, '*') }}"
              operator: Equals
              value: true

  - name: deny-agent-sa-binding-into-prod-namespaces
    match:
      any:
      - resources:
          kinds: [RoleBinding]
          namespaces:
          - "prod-*"
          - "production"
          - "prd"
    validate:
      message: >-
        Agent ServiceAccounts (namespaces matching agent-*) cannot be bound into production namespaces.
        Production work belongs to a human operator with their own credentials.
      foreach:
      - list: "request.object.subjects"
        deny:
          conditions:
            all:
            - key: "{{ element.kind }}"
              operator: Equals
              value: "ServiceAccount"
            - key: "{{ element.namespace }}"
              operator: AnyIn
              value: ["agent-*"]

  - name: require-explicit-resource-on-rolebinding
    match:
      any:
      - resources:
          kinds: [RoleBinding, ClusterRoleBinding]
    validate:
      message: >-
        RoleBinding/ClusterRoleBinding must reference a Role or ClusterRole that exists.
        roleRef.name must be set.
      pattern:
        roleRef:
          name: "?*"

Cell notes

Authorization / Server-side

Control. Scoped RBAC Roles, never ClusterRoles. IAM policies scoped to specific resources with explicit ARN. Kyverno or OPA admission policies. Namespace-scoped permissions. Deny * verbs. Deny prod namespaces from agent ServiceAccounts. Server-side Git pre-receive hooks for repo-level enforcement.

Strength. Deterministic and external to both the agent and the operator's machine. Bypass requires escalation primitives in RBAC (escalate, bind, impersonation), aggregated roles missed by the policy author, subresource access not denied (pods/exec when only pods is denied), admission webhook fail-open, IAM condition logic bugs, or operator manipulation through a persuasive PR description.

Tooling

  • - Kubernetes RBAC (built-in).
  • - Kyverno 1.18+ (older releases use a different attestors block shape) or OPA Gatekeeper.
  • - Kubernetes-native admission (no controller to install): ValidatingAdmissionPolicy (GA since 1.30) and MutatingAdmissionPolicy (GA and default-on in 1.36 "Haru", April 2026). These are in-tree CEL admission policies with no webhook, which removes the "admission webhook fail-open" bypass listed below. Prefer VAP for the deny-wildcard-verbs / deny-ClusterRoleBinding rules where you want zero external dependencies; reach for Kyverno/OPA when you need verifyImages, generate rules, or cross-cluster policy libraries. The two compose.
  • - AWS IAM, GCP IAM, or Azure RBAC.
  • - Managed deterministic pre-action authorization (this cell as a product). Amazon Bedrock AgentCore Policy went GA March 3, 2026: authorization rules written in the Cedar policy language, default-deny, evaluated at the Gateway on every agent-to-tool request, outside the agent's code, outside the model's reasoning, and therefore not reachable by prompt injection. Microsoft shipped comparable runtime enforcement starting with Copilot in Q1 2026. This is the same control the rest of this cell builds by hand; if you are on Bedrock, use it rather than reimplementing it, and keep the Kubernetes-side admission policies as the second layer for anything the gateway does not mediate. The design point to preserve either way: policy is evaluated before the tool executes, by something the agent does not control.
  • - Server-side Git pre-receive hooks (every Git server in your org, not just origin).

Files in this directory

  • - kyverno-no-cluster-roles.yaml, ClusterPolicy with three rules: deny ClusterRoleBinding whose subjects include any agent ServiceAccount; deny wildcard verbs in any Role or ClusterRole; deny RoleBinding into prod namespaces with agent SA subjects. Apply with kubectl apply -f.
  • - git-pre-receive-hook.sh, server-side pre-receive hook. Rejects force-pushes to main, blocks edits to protected paths from non-CODEOWNERS, runs gitleaks against the diff. Install in /var/lib/git/<repo>.git/hooks/pre-receive on every Git server.
  • - aws-iam-scoped-policy.json, example IAM policy with explicit Resource ARNs for the allow list and a tagged-deny clause for everything else. Substitute resource ARNs for your environment.

Verification


# 1. Confirm Kyverno blocks wildcard verbs
kubectl apply -f - <<EOF
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: bad-role
  namespace: agent-claude-prod
rules:
- apiGroups: ["*"]
  resources: ["*"]
  verbs: ["*"]
EOF
# expected: rejected by Kyverno

# 2. Confirm pre-receive hook blocks --no-verify bypass
cd /tmp/test-repo
echo "test" >> infrastructure/prod/main.tf
git add . && git commit --no-verify -m "test"
git push origin main
# expected: failure at server-side pre-receive

# 3. Confirm IAM denies cross-resource access
aws --profile claude-code-prod s3 ls s3://other-bucket
# expected: AccessDenied

# 4. Confirm Kyverno background scan finds existing violations
kubectl get clusterpolicyreport -A
# expected: report of any pre-existing wildcard roles

Common mistakes

  • - Kyverno installed in audit mode (Audit), which logs but does not enforce. Confirm validationFailureAction: Enforce.
  • - Pre-receive hook installed only on origin; clones to other Git remotes do not enforce. Make it a server-wide hook on every Git server in the org.
  • - IAM with "Resource": "" and a forgotten "Action": "" next to it.
  • - ClusterRole created for legitimate operator use, then accidentally bound to an agent SA via a copy-pasted RoleBinding.
  • - Forgetting subresources: denying pods does not deny pods/exec, pods/portforward, pods/attach. List them.
  • - Webhook timeout failurePolicy: Ignore, under load, the policy fails open and admits the violating resource.

Citation

NIST CSF 2.0 PR.AA-05 (least privilege, separation of duties), PR.PS-01 (configuration management practices), PR.PS-05 (unauthorized software prevented). NIST SP 800-207 (Zero Trust). OWASP ASI02, ASI03, ASI05. CIS Kubernetes Benchmark.

Primary bypasses

Documented, not hypothetical. A control whose bypass is undocumented is worse than no control, because somebody trusted it.

  • escalate/bind/impersonate in RBAC
  • subresource access not denied
  • admission webhook fail-open

Crosswalk

NIST CSF 2 0PR.AA-05, PR.PS-01, PR.PS-05
NIST AI RMFMANAGE 2.4, MANAGE 4.1
OWASP LLMLLM05, LLM06
OWASP AGENTICASI02, ASI03, ASI05
OTHERNIST SP 800-207, CIS Kubernetes Benchmark