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
attestorsblock 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: denyClusterRoleBindingwhose subjects include any agent ServiceAccount; deny wildcard verbs in any Role or ClusterRole; deny RoleBinding into prod namespaces with agent SA subjects. Apply withkubectl 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-receiveon every Git server. - -
aws-iam-scoped-policy.json, example IAM policy with explicitResourceARNs 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. ConfirmvalidationFailureAction: 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
podsdoes not denypods/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 0 | PR.AA-05, PR.PS-01, PR.PS-05 |
|---|---|
| NIST AI RMF | MANAGE 2.4, MANAGE 4.1 |
| OWASP LLM | LLM05, LLM06 |
| OWASP AGENTIC | ASI02, ASI03, ASI05 |
| OTHER | NIST SP 800-207, CIS Kubernetes Benchmark |
Cite this cell:
https://agenticcovenants.com/protect/authorization/server-side/