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 forsummary.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 ondecision == falseso 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
Auditmode silently logs but does not enforce. Always pair the policies you care about withEnforce. - - 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-ichecks count as denials in audit log. Filter onverbandresourceto 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 0 | DE.CM-01, DE.CM-09, DE.AE-02 |
|---|---|
| OTHER | NIST SP 800-207 |
Cite this cell:
https://agenticcovenants.com/detect/authorization/server-side/