Agentic Covenants

Detect (DE) · Authorization

Authorization at the server side layer

external · Outside the agent entirely

If this concern is breached, how do we know?

What this cell does

RBAC denial events from K8s audit; IAM Access Analyzer findings; Kyverno PolicyReports; OPA decision logs centralized.

Artifacts (4)

access-analyzer-eventbridge.shview on GitHub
#!/usr/bin/env bash
# ABOUTME: Wires AWS IAM Access Analyzer findings to EventBridge to a SIEM-shipping Lambda.
# ABOUTME: Run per-region (Access Analyzer is regional). Requires events:PutRule and events:PutTargets in the operator's profile.

set -euo pipefail

REGION="${AWS_REGION:-us-east-1}"
RULE_NAME="${RULE_NAME:-access-analyzer-findings}"
LAMBDA_ARN="${LAMBDA_ARN:-}"

if [[ -z "${AWS_PROFILE:-}" ]]; then
  echo "Set AWS_PROFILE to a profile with events:PutRule." >&2
  exit 1
fi
if [[ -z "$LAMBDA_ARN" ]]; then
  echo "Set LAMBDA_ARN to the SIEM-shipping Lambda's ARN." >&2
  exit 1
fi

aws --profile "$AWS_PROFILE" --region "$REGION" events put-rule \
  --name "$RULE_NAME" \
  --description "Access Analyzer findings to SIEM" \
  --event-pattern '{"source":["aws.access-analyzer"],"detail-type":["Access Analyzer Finding"]}' \
  --state ENABLED

aws --profile "$AWS_PROFILE" --region "$REGION" events put-targets \
  --rule "$RULE_NAME" \
  --targets "Id=1,Arn=$LAMBDA_ARN"

# Lambda must have permission to be invoked by EventBridge. Add this once per
# Lambda, not per region.
aws --profile "$AWS_PROFILE" --region "$REGION" lambda add-permission \
  --function-name "$LAMBDA_ARN" \
  --statement-id "${RULE_NAME}-invoke" \
  --action lambda:InvokeFunction \
  --principal events.amazonaws.com \
  --source-arn "arn:aws:events:${REGION}:$(aws --profile "$AWS_PROFILE" sts get-caller-identity --query Account --output text):rule/${RULE_NAME}" \
  2>/dev/null || true   # idempotent: already-exists is fine

echo "EventBridge rule $RULE_NAME wired to $LAMBDA_ARN in $REGION."
echo "Repeat for every region where Access Analyzer is enabled."
opa-decision-log-config.yamlview on GitHub
# ABOUTME: OPA config that streams decision logs to the SIEM, filtered to deny decisions only.
# ABOUTME: Without filtering, OPA emits one event per admission decision and floods the SIEM.
decision_logs:
  console: false
  service: siem
  reporting:
    min_delay_seconds: 5
    max_delay_seconds: 30
  # Drop allow decisions; keep only denies. Tune per your alerting needs.
  mask_decision: |
    package system.log
    mask["/result"] {
      input.result == true
    }

services:
  - name: siem
    url: https://siem.example.com:9200/opa-decisions/_doc
    credentials:
      bearer:
        token: ${SIEM_TOKEN}
    # Index per day so retention rotation works.
    response_header_timeout_seconds: 5
ship-policy-reports.yamlview on GitHub
# ABOUTME: CronJob that ships Kyverno PolicyReports with summary.fail > 0 to the SIEM every 5 minutes.
# ABOUTME: kubectl image is pinned by digest; replace before applying. SIEM_TOKEN comes from a Secret.
apiVersion: v1
kind: ServiceAccount
metadata:
  name: kyverno-reporter
  namespace: kyverno
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: kyverno-reporter-read
rules:
- apiGroups: ["wgpolicyk8s.io"]
  resources: ["policyreports", "clusterpolicyreports"]
  verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: kyverno-reporter-read
subjects:
- kind: ServiceAccount
  name: kyverno-reporter
  namespace: kyverno
roleRef:
  kind: ClusterRole
  name: kyverno-reporter-read
  apiGroup: rbac.authorization.k8s.io
---
apiVersion: v1
kind: Secret
metadata:
  name: siem-credentials
  namespace: kyverno
type: Opaque
stringData:
  SIEM_TOKEN: "REPLACE_WITH_BEARER_TOKEN"
  SIEM_URL: "https://siem.example.com:9200/agent-sentinel-policy/_doc"
---
apiVersion: batch/v1
kind: CronJob
metadata:
  name: ship-policy-reports
  namespace: kyverno
spec:
  schedule: "*/5 * * * *"
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 1
  failedJobsHistoryLimit: 3
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccountName: kyverno-reporter
          restartPolicy: OnFailure
          containers:
          - name: shipper
            # Pin by digest. Resolve current digest with: crane digest bitnami/kubectl:1.34
            image: bitnami/kubectl@sha256:REPLACE_WITH_DIGEST_FROM_CRANE
            envFrom:
            - secretRef:
                name: siem-credentials
            command: ["/bin/sh", "-c"]
            args:
            - |
              set -eu
              kubectl get policyreports,clusterpolicyreports -A -o json \
              | jq -c '.items[] | select(.summary.fail > 0) | {
                  event: "policy_report",
                  namespace: (.metadata.namespace // "cluster"),
                  policy: (.metadata.labels."policy.kyverno.io/policy-name" // ""),
                  failed: .summary.fail,
                  results: [.results[] | select(.result == "fail")],
                  ts: (now | strftime("%Y-%m-%dT%H:%M:%SZ"))
                }' \
              | while IFS= read -r line; do
                  curl -sS -X POST "$SIEM_URL" \
                    -H "Authorization: Bearer $SIEM_TOKEN" \
                    -H "Content-Type: application/json" \
                    -d "$line"
                done
sigma-rbac-denial-spike.yamlview on GitHub
# ABOUTME: Sigma rule firing when an agent namespace accumulates >10 RBAC denials within 5 minutes.
# ABOUTME: Probing or misconfiguration indicator. Filter on auth-can-i checks if those flood at baseline.
title: RBAC denial spike in agent namespace
id: 8c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f
status: experimental
description: >
  Detects more than 10 RBAC denials within a 5-minute window in any single
  agent namespace. Either an agent probing for a permission gap or an
  in-progress misconfiguration. The rule excludes "kubectl auth can-i" style
  preflight checks because those denial-by-design events are baseline noise.
references:
  - https://github.com/peopleforrester/agentic-covenants/blob/main/SENTINELS_MATRIX.md
author: agentic-covenants
date: 2026/05/08
logsource:
  product: kubernetes
  service: audit
detection:
  selection:
    user.username|startswith: 'system:serviceaccount:agent-'
    annotations.authorization.k8s.io/decision: 'forbid'
  filter_can_i:
    requestURI|contains: '/access-review'
  timeframe: 5m
  condition: (selection and not filter_can_i) | count(objectRef.namespace) > 10
falsepositives:
  - First-deploy of a new agent (the missing permissions show up as a wave).
  - Operator running an exploratory script under the agent SA (treat as a tuning signal).
level: medium
tags:
  - agent
  - authorization
  - sentinels

Cell notes

Sentinels, Authorization / Server-side

Control. RBAC denial events from Kubernetes audit. IAM Access Analyzer findings reporting unused permissions. Kyverno PolicyReports surface admission failures. OPA decision logs centralized.

Strength. Deterministic and external. Failure modes: Kyverno in Audit mode (logs but does not enforce; the violation already happened); OPA decision log streams everything (floods SIEM unless filtered); Access Analyzer is regional (configure per-region).

Tooling

  • - Kyverno 1.18+ Reports controller.
  • - OPA Gatekeeper with decision logging configured.
  • - AWS IAM Access Analyzer enabled per region.
  • - A SIEM with field-level filtering.

Files in this directory

  • - ship-policy-reports.yaml, CronJob that reads Kyverno PolicyReports across all namespaces every 5 minutes, filters for summary.fail > 0, and ships each failure to the SIEM as a structured event.
  • - opa-decision-log-config.yaml, OPA config snippet that streams decision logs to the SIEM. Filters on decision == false so the SIEM is not flooded with allow events.
  • - access-analyzer-eventbridge.sh, wires AWS IAM Access Analyzer findings to EventBridge → Lambda → SIEM.
  • - sigma-rbac-denial-spike.yaml, SIEM rule firing when more than 10 RBAC denials occur in a single namespace within 5 minutes.

Verification


# 1. Trigger a Kyverno deny, find it in PolicyReport
kubectl apply -f - <<EOF
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: bad-binding-test
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- kind: ServiceAccount
  name: claude-code
  namespace: agent-claude-prod
EOF
# expected: rejected; PolicyReport in kyverno namespace shows fail entry

# 2. Ingestion job runs and ships
kubectl logs -n kyverno -l job-name=ship-policy-reports --tail=50

# 3. Access Analyzer surfaces unused permission
aws accessanalyzer list-findings --analyzer-arn $ANALYZER_ARN \
  --filter '{"status":{"eq":["ACTIVE"]},"resourceType":{"eq":["AWS::IAM::Role"]}}' \
  | jq '.findings[].principalArn'
# expected: agent role ARN if it has unused permissions

Common mistakes

  • - Kyverno running in Audit mode silently logs but does not enforce. Always pair the policies you care about with Enforce.
  • - OPA decision log streams everything, including allows. Filter at the OPA side or the SIEM is unusable.
  • - Access Analyzer is regional. Configure per-region if multi-region.
  • - RBAC denials from kubectl auth can-i checks count as denials in audit log. Filter on verb and resource to avoid noise.

Citation

NIST CSF 2.0 DE.CM-01, DE.CM-09, DE.AE-02. NIST SP 800-207.

Primary failure modes

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

  • Kyverno in Audit mode (logs but does not enforce)
  • OPA decision log streams everything (floods SIEM)
  • Access Analyzer is regional; multi-region misses

Crosswalk

NIST CSF 2 0DE.CM-01, DE.CM-09, DE.AE-02
OTHERNIST SP 800-207