One playbook for each signal the enclave emits or records. Names use <env> (the Terraform env variable /
CDK -c sgea:env), <account>, <region> (primary) and <replica-region>. Every alarm, topic, function, table,
log group and key-policy statement below comes from the emitted IaC (emitters/terraform_emitter.py,
emitters/cdk_app/, emitters/lambda/); both engines use the same names unless a row says otherwise.
Use read-only credentials for triage. Containment and recovery steps change production resources: they need the incident lead's approval and a change record. Re-apply the IaC afterwards so the enclave matches its code and its AWS Config evidence again.
Sensitive data. The Bedrock model-invocation logs (
/sgea/<env>/bedrock/model-invocations, ands3://sgea-<env>-logs-<account>-<region>/bedrock/) and the token-meter dead-letter queue hold prompt and response text. Limit who may query them. Put counts, user ids and time windows in tickets, not the text.
| # | Signal | Source in the IaC | Where it goes | Playbook |
|---|---|---|---|---|
| A1 | Alarm sgea-<env>-tool-policy-errors: Lambda Errors > 0 in 5 min on sgea-<env>-tool-policy |
agentcore-platform (deployed when the interceptor is: GovCloud tool-path stand-in or token budgets) |
sgea-<env>-security-alarms |
3 |
| A2 | Alarm sgea-<env>-token-meter-errors: Errors > 0 in 5 min on sgea-<env>-token-meter |
token-control (Terraform) / governance (CDK) |
sgea-<env>-security-alarms |
3 |
| A3 | Alarm sgea-<env>-token-meter-silent: Invocations < 1 for 2 x 15 min, missing data = breaching |
as A2 | sgea-<env>-security-alarms |
3 |
| A4 | Alarm sgea-<env>-token-breaker-failures: AWS/States ExecutionsFailed >= 1 in 5 min on sgea-<env>-token-breaker (ADR 0003) |
token-control / governance, where token_control.model_enforce is on |
sgea-<env>-security-alarms |
2 |
| S1 | SNS sgea-<env>-log-store-events: s3:ObjectRemoved:* from the log store, the AWS Config store and the access-log sink (primary region; the replica buckets do not notify) |
enclave-logging / hardened-bucket |
subscribers you add | 5 |
| L1 | Interceptor decision log: one JSON line per tools/call (sgea, decision, reason, tool, user, team, client_id; never the token) |
emitters/lambda/tool_policy_interceptor.py |
/aws/lambda/sgea-<env>-tool-policy |
2, 3, 7 |
| L2 | Guardrail interventions: the caller sees Blocked by enclave guardrail. |
bedrock-guardrail (blocked_input_messaging / blocked_outputs_messaging) |
model-invocation logs | 1 |
| L3 | CloudTrail sgea-<env> (multi-region, log-file validation, enclave CMK; Bedrock / AgentCore / S3 data events per profile) and AWS Config recorder / delivery channel sgea-<env> |
enclave-logging, config-conformance |
log store, /sgea/<env>/cloudtrail, Config store |
4 |
| G1 | GuardDuty detector (FIFTEEN_MINUTES publishing) and Security Hub with NIST SP 800-53 Rev 5 |
security-services (unless create_guardduty / create_securityhub = false) |
GuardDuty / Security Hub consoles, your SIEM; high-severity findings also routed to sgea-<env>-security-alarms by E1 / E2 |
6 |
| C1 | Alarms sgea-<env>-ct-cloudtrail-changes, -ct-config-changes, -ct-kms-key-disable-or-delete, -ct-iam-policy-changes, -ct-s3-bucket-policy-changes, -ct-organizations-changes: CloudTrail metric filters sgea-<env>-ct-<name> (namespace SGEA/<env>/CloudTrail), Sum >= 1 in 5 min, missing data = not breaching |
enclave-monitoring (manage_monitoring / CDK sgea:manageMonitoring, default on), filters on /sgea/<env>/cloudtrail |
sgea-<env>-security-alarms |
8 |
| C2 | Alarms sgea-<env>-ct-security-group-changes, -ct-nacl-changes, -ct-network-gateway-changes, -ct-route-table-changes, -ct-vpc-changes |
as C1 | sgea-<env>-security-alarms |
9 |
| C3 | Alarms sgea-<env>-ct-root-usage, -ct-console-no-mfa, -ct-console-auth-failures, -ct-unauthorized-api |
as C1 | sgea-<env>-security-alarms |
10 |
| C4 | Alarm sgea-<env>-ct-break-glass-assumed: AssumeRole on sgea-<env>-break-glass |
as C1, only with create_break_glass_role (CDK sgea:createBreakGlassRole) |
sgea-<env>-security-alarms |
11 |
| H1 | Alarms sgea-<env>-bedrock-throttles (AWS/Bedrock InvocationThrottles, account, Sum >= 10 in 2 of 2 x 5 min), -bedrock-server-errors (InvocationServerErrors Sum >= 5 in 5 min), -bedrock-invocation-log-delivery (ModelInvocationLogsCloudWatchDeliverySuccess Minimum < 1 over 15 min; only where model-invocation logging is on) |
enclave-monitoring |
sgea-<env>-security-alarms |
15 |
| H2 | Alarm sgea-<env>-guardrail-interventions: AWS/Bedrock/Guardrails InvocationsIntervened on the baseline guardrail ARN and version, Sum >= guardrail_intervention_threshold (CDK sgea:guardrailInterventionThreshold, default 50) over 15 min |
enclave-monitoring |
sgea-<env>-security-alarms |
1 |
| H3 | Alarms sgea-<env>-gateway-system-errors (AWS/Bedrock-AgentCore SystemErrors, Operation = InvokeGateway, Protocol = MCP, Resource = the enclave gateway, Sum >= 5 in 5 min) and -gateway-throttles (Throttles, same dimensions, Sum >= 10 in 2 of 2 x 5 min) |
enclave-monitoring, only where the gateway exists |
sgea-<env>-security-alarms |
15 |
| E1 | EventBridge rule sgea-<env>-evt-guardduty: GuardDuty Finding, severity >= 7 |
enclave-monitoring (rules sgea-<env>-evt-<name>, default event bus) |
sgea-<env>-security-alarms (the raw event JSON) |
6 |
| E2 | Rules sgea-<env>-evt-securityhub (new HIGH / CRITICAL Security Hub Findings - Imported), -evt-inspector (HIGH / CRITICAL Inspector2 Finding), -evt-access-analyzer (Access Analyzer Finding, status ACTIVE; only when an analyzer is created) |
as E1; analyzers sgea-<env>-external-access (create_access_analyzer, default on) and sgea-<env>-unused-access (create_unused_access_analyzer, default off) |
sgea-<env>-security-alarms |
12 |
| E3 | Rules sgea-<env>-evt-config-compliance (Config Rules Compliance Change to NON_COMPLIANT) and -evt-stack-drift (CDK only: CloudFormation Drift Detection Status Change, DRIFTED, from the daily schedule sgea-<env>-drift-detect) |
as E1 | sgea-<env>-security-alarms |
13 |
| E4 | Rules sgea-<env>-evt-backup (backup, copy or restore job FAILED / ABORTED / EXPIRED) and -evt-secret-rotation (Secrets Manager RotationFailed) |
as E1 | sgea-<env>-security-alarms |
14 |
| B1 | Budget sgea-<env>-monthly-cost (ACTUAL > 80 %, FORECASTED > 100 % of monthly_budget_usd / CDK sgea:monthlyBudgetUsd; off at 0) and Cost Anomaly Detection subscription sgea-<env>-anomalies on monitor sgea-<env>-services (impact >= USD 100; opt-in create_cost_anomaly_monitor / sgea:createCostAnomalyMonitor) |
enclave-monitoring, commercial partition only |
sgea-<env>-security-alarms |
16 |
All C, H, E and B signals come from the enclave-monitoring layer (Terraform module modules/enclave-monitoring, CDK construct EnclaveMonitoring; design in docs/adr/0002-enclave-monitoring.md). Plan for these limits:
- The IaC creates no subscriptions on
sgea-<env>-security-alarmsorsgea-<env>-log-store-events. Subscribing the security team is a post-deploy step (MANUAL-STEPS.md). Both topics are encrypted with the enclave CMK. An SQS or Lambda subscriber needskms:Decrypton that key. - Delivery depends on the enclave CMK. If the key is disabled or pending deletion, no alarm or event reaches the topic's subscribers, including
sgea-<env>-ct-kms-key-disable-or-deletefor the enclave key itself, and CloudTrail stops delivering too. Keep the daily key check in key and secret management. - Alarm messages are CloudWatch alarm JSON (
AlarmName,NewStateReason,StateChangeTime). Event rules deliver the raw event JSON, with no input transformer. - Signals depend on toggles and on the profile:
manage_monitoring= false removes the whole layer; the health alarms H1 log delivery and H3 exist only where model-invocation logging or the gateway exist;-evt-access-analyzeronly with an analyzer;-evt-stack-driftonly in CDK (Terraform: a scheduledterraform plan -detailed-exitcodein your pipeline, see change and upgrade); B1 only when turned on, and never inaws-us-gov(billing is in the commercial payer account). - CloudTrail / AWS Config delivery failures that are not API calls (a broken key or bucket policy, a delivery-role change through another path) raise no direct alarm. They show as a Config compliance change (E3) or in the scheduled check of playbook 4.
- Alert volume: the CIS-style filters (C1–C3) fire on every legitimate IaC apply that touches IAM, buckets or the network, and
-ct-unauthorized-apiand-evt-config-complianceare noisy in a busy account. Match alerts against the change calendar first; tune the guardrail threshold or turn off parts of the layer through the toggles (ADR 0002) rather than deleting rules by hand. - The monitoring layer is new in this release: it deploys and tears down on both engines (HITRUST sandbox, 2026-10-27), but alarm and event delivery to the topic were not exercised there; test your subscription once after deploy.
- Alarm INSUFFICIENT_DATA actions are present only when the composed packs require them (
alarm_insufficient_data_action, from the pack parameters ofcloudwatch-alarm-action-check).
Check that the alarms exist and see their state:
aws cloudwatch describe-alarms --alarm-name-prefix sgea-<env>- \
--query 'MetricAlarms[].[AlarmName,StateValue,StateUpdatedTimestamp]' --output table
aws events list-rules --name-prefix sgea-<env>-evt- --query 'Rules[].[Name,State]' --output table
aws sns list-subscriptions-by-topic --topic-arn arn:aws:sns:<region>:<account>:sgea-<env>-security-alarms
aws sns list-subscriptions-by-topic --topic-arn arn:aws:sns:<region>:<account>:sgea-<env>-log-store-eventsTo find the CloudTrail events behind any sgea-<env>-ct-<name> alarm, reuse the alarm's own filter pattern (used in playbooks 8–11):
P=$(aws logs describe-metric-filters --log-group-name /sgea/<env>/cloudtrail --filter-name-prefix sgea-<env>-ct-<name> \
--query 'metricFilters[0].filterPattern' --output text)
aws logs filter-log-events --log-group-name /sgea/<env>/cloudtrail --start-time <epoch-ms> --filter-pattern "$P" \
--query 'events[].message' --output json \
| jq -r '.[] | fromjson | [.eventTime, .eventName, .userIdentity.arn, .sourceIPAddress, .errorCode] | @tsv'(GovCloud: partition aws-us-gov in every ARN.)
Trigger. Alarm sgea-<env>-guardrail-interventions (H2): the baseline guardrail intervened at least guardrail_intervention_threshold times (default 50) in 15 minutes. Or users or a tenant team report many Blocked by enclave guardrail. answers, or your own query on the invocation logs (step 2) shows more blocks than usual.
The alarm watches the guardrail ARN and version the enclave deployed; a redeploy that creates a new guardrail version moves the alarm to it. Tune the threshold with guardrail_intervention_threshold (CDK sgea:guardrailInterventionThreshold) once you know the normal rate, not by editing the alarm.
Likely causes
- Real PII / PHI in prompts. Hard identifiers are BLOCKed. Contextual entities (for example an address) are ANONYMIZEd, not blocked.
- Prompt-injection or jailbreak attempts (prompt-attack filter), or probing of denied topics.
- False positives. In the sandbox (2026-10-21) the HIGH-strength prompt-attack filter blocked a benign prompt at LOW confidence.
- A new tenant app sends system prompts or tool results as user content. The account-enforced guardrail guards
every user message (
messages: COMPREHENSIVE) and the system prompt only where it is tagged (system: SELECTIVE). - A redeploy created a new immutable guardrail version from a changed profile.
Triage
- Look up the guardrail in use:
terraform output -json enclave(Terraform) or theEnclavestack output (CDK) givesguardrail_idandguardrail_version. Then runaws bedrock get-guardrail --guardrail-identifier <id> --guardrail-version <version>. - Count the blocks over time. Run this in CloudWatch Logs Insights on
/sgea/<env>/bedrock/model-invocations:filter @message like "Blocked by enclave guardrail." | stats count() as blocks by bin(1h), requestMetadata.sgea_user, requestMetadata.sgea_teamsgea_user/sgea_teamare the request metadata the tenant runtime sets from the caller's validated JWT (the token meter uses the same fields). Calls without them come from apps that do not attribute usage. - Decide whether it is one user, one team, one app or everyone:
- One user or team usually means user behavior or misuse.
- Every app at once usually means a guardrail or profile change. Check CloudTrail for
UpdateGuardrail/CreateGuardrailVersion.
- Open a few blocked records (authorized analyst only). The guardrail assessment in each record shows which policy intervened: content filter, PII entity or denied topic.
Containment
- Misuse by one identity: go to playbook 7.
- PII from an upstream feed (a tool or knowledge-base source): stop that tenant app's ingestion until the data owner has reviewed it.
- Do not turn off or bypass the guardrail. It is enforced account-wide, and where the org policies are deployed the
DenyInvokeWithoutGuardrailSCP denies inference without it.
Recovery
- For confirmed false positives, change the profile's
guardrailsblock (filter strength,pii.action,pii.contextual_action, denied topics). Then runsgea export,sgea verifyandsgea check, and redeploy. That creates a new guardrail version. Get ISSO approval for the change and record it. - Do not edit the guardrail in the console. Control decisions live only in the resolved profile, and a console edit is drift against the evidence pack.
Evidence to keep: block counts per hour and per user, guardrail id and version JSON, CloudTrail events for guardrail changes, and the approval for any profile change.
Controls: SI-4, SI-15, AC-4, AU-6, IR-4.
Trigger. A tool call returns JSON-RPC error -32003 with
Tool call denied by enclave token-budget: per-user daily token budget exhausted (or
per-team monthly token budget exhausted); or, for a team past its monthly budget, the team's model calls fail with AccessDeniedException because the token breaker (ADR 0003) attached sgea-<env>-token-budget-exhausted to its sgea:team-tagged runtime role. This is the control working, so there is no alarm; sgea-<env>-token-breaker-failures (A4) fires only when the breaker itself fails. Per-user budgets gate gateway tool calls only; per-team budgets gate tool calls and, through the breaker, model calls.
Likely causes: legitimate heavy use; an agent stuck in a tool-call loop; someone else using the user's token
(playbook 7); many users sharing one team claim
(team_claim, default cognito:groups, first value); a budget set too low for the workload.
Triage
-
Find the denials:
aws logs filter-log-events --log-group-name /aws/lambda/sgea-<env>-tool-policy --start-time <epoch-ms> \ --filter-pattern '{ $.reason = "per-user daily token budget exhausted" }' --query 'events[].message'
-
Read the ledger. The keys are
pk=user#<sub>,sk=day#YYYY-MM-DDandpk=team#<team>,sk=month#YYYY-MM. Dates are in the profile'stoken_control.default_timezone.aws dynamodb get-item --table-name sgea-token-usage-<env> --consistent-read \ --key '{"pk":{"S":"user#<sub>"},"sk":{"S":"day#<YYYY-MM-DD>"}}'
-
Read the configured budgets (the environment is encrypted with the enclave CMK, so you need
kms:Decrypt):aws lambda get-function-configuration --function-name sgea-<env>-tool-policy \ --query 'Environment.Variables.[PER_USER_DAILY_TOKENS,PER_TEAM_MONTHLY_TOKENS,TOKEN_TIMEZONE]'
-
See where the tokens went. Run this Logs Insights query on
/sgea/<env>/bedrock/model-invocations:filter requestMetadata.sgea_user = "<sub>" | stats sum(input.inputTokenCount + output.outputTokenCount) as tokens, count() as calls by bin(15m)Usage without
sgea_useris credited toprincipal:<IAM ARN>(the meter's fallback), not to a user. -
For a team: see which roles the breaker holds and whether the deny policy is attached:
aws dynamodb query --table-name sgea-token-usage-<env> --consistent-read \ --key-condition-expression 'pk = :p' --expression-attribute-values '{":p":{"S":"breaker"}}' aws iam list-entities-for-policy --policy-arn arn:<partition>:iam::<account>:policy/sgea-<env>-token-budget-exhausted aws stepfunctions list-executions --state-machine-arn arn:<partition>:states:<region>:<account>:stateMachine:sgea-<env>-token-breaker --max-results 10
A trip with no attachment means the role is not tagged
sgea:team= that team (TR-26), or the tenant's IaC detached it (the next 15-minute heartbeat re-attaches); a failed execution is A4.
Containment: if the usage is not the user's, go to playbook 7. For a runaway agent, have the tenant team stop the session or app.
Recovery
- The user budget resets at the next day in
TOKEN_TIMEZONE. The team budget resets at the next month. - To raise a budget, change
token_control.per_user_daily_tokens/per_team_monthly_tokensin the profile, re-export and redeploy. The interceptor reads its budgets from its environment. - The breaker releases a team on the first heartbeat after month rollover or after a redeploy that raises its budget (two heartbeats: mark released, then forget), so allow up to 30 minutes.
- Emergency release for the rest of the month (approved by the ISSO / budget owner): write an override item, then wait for the next heartbeat (or invoke the meter with an empty event twice):
Do not detach the policy by hand without the override: the next heartbeat attaches it again. Delete the override item to restore enforcement.
aws dynamodb put-item --table-name sgea-token-usage-<env> \ --item '{"pk":{"S":"override"},"sk":{"S":"team#<team>#month#<YYYY-MM>"},"by":{"S":"<ticket>"}}' aws lambda invoke --function-name sgea-<env>-token-meter --payload '{}' /dev/null
- Do not edit or delete ledger usage items. They are the usage record (PITR on, covered by the backup plan).
Evidence to keep: the deny log lines, the ledger item, the per-user token query, the breaker trip / override items and the CloudTrail AttachRolePolicy / DetachRolePolicy events by sgea-<env>-token-breaker, and the approval for any budget change or override.
Controls: SC-6, AC-3, AU-6, SI-4.
Trigger. Every tools/call returns -32003, or the e2e "tool path" check fails. Alarms A1–A3 may fire,
but a deny is not a Lambda error. A fail-closed denial is logged with its reason. Only crashes and timeouts
raise sgea-<env>-tool-policy-errors. In a sandbox run (2026-10-16), fresh CDK enclaves denied every call with
token meter heartbeat stale (fail closed) until the meter first ran on its 15-minute schedule. Both engines now
invoke the meter once at deploy (Terraform aws_lambda_invocation.token_meter_first_heartbeat, CDK
Custom::SgeaMeterHeartbeat).
Likely causes: find the reason in the interceptor log
reason |
Control | Meaning | Look at |
|---|---|---|---|
token meter heartbeat stale (fail closed) |
token-budget | The ledger item pk=meter, sk=heartbeat is older than FAIL_CLOSED_HOURS (profile fail_closed_hours, default 4) or missing |
Meter health (steps 3–5) |
token ledger unavailable (fail closed): <Exception> |
token-budget | GetItem on sgea-token-usage-<env> failed |
Key state, DynamoDB gateway endpoint and routes, IAM |
caller identity unknown (token budget fails closed) |
token-budget | No sub claim (CUSTOM_JWT), or no X-Sgea-User header (AWS_IAM gateway auth) |
The tenant app's identity forwarding |
no allow rule (default deny) |
tool-policy | Interceptor tool-path mode only (TOOL_POLICY_ENFORCE=true, where the AgentCore policy engine is unavailable, for example GovCloud). The default tool_policy is [], which denies every call |
The tool_policy rules |
explicit deny |
tool-policy | A deny rule matched (deny overrides allow) | The tool_policy rules |
| no DENY line, but calls still denied | Cedar policy engine sgea_<env>_tool_policy (ENFORCE) |
The tool has no permit policy (the sample tenant adds one: permit_facts_tool) |
Policies on the engine |
Triage
- Read the reasons:
aws logs filter-log-events --log-group-name /aws/lambda/sgea-<env>-tool-policy --start-time <epoch-ms> \ --filter-pattern '{ $.decision = "DENY" }' --query 'events[].message'
- Check the heartbeat.
atis epoch seconds:aws dynamodb get-item --table-name sgea-token-usage-<env> --consistent-read \ --key '{"pk":{"S":"meter"},"sk":{"S":"heartbeat"}}'
- Check meter health: alarm state (above), the schedule, the logs and the dead-letter queue.
aws events describe-rule --name sgea-<env>-token-meter-heartbeat --query State aws logs tail /aws/lambda/sgea-<env>-token-meter --since 2h aws sqs get-queue-attributes --attribute-names ApproximateNumberOfMessages \ --queue-url "$(aws sqs get-queue-url --queue-name sgea-<env>-token-meter-dlq --query QueueUrl --output text)"
- Check the key state. It must be
Enabled, because the ledger, the function environment and the logs use it:aws kms describe-key --key-id alias/sgea-<env> --query KeyMetadata.KeyState. If it isPendingDeletionorDisabled, seekey-and-secret-management.md. - Check the network. The meter and the interceptor run in the agent subnets and reach DynamoDB through the
enclave's DynamoDB gateway endpoint (
enclave-network). Confirm the endpoint is still on those subnets' route tables.
Containment. None needed. Fail-closed is the safe state (SC-24). Do not remove the interceptor or raise
fail_closed_hours to restore service without a recorded risk decision.
Recovery
- Fix the cause (key, route, rule, identity forwarding, or tool policy through the profile and a redeploy).
- Write a heartbeat now (the same call the deploy makes):
aws lambda invoke --function-name sgea-<env>-token-meter --cli-binary-format raw-in-base64-out \ --payload '{"source":"sgea-operator"}' /tmp/sgea-meter.json
- Re-read the heartbeat item, then make one tool call. The interceptor log should show
ALLOW. - While the meter was down, failed log deliveries went to
sgea-<env>-token-meter-dlq, so usage in that window was under-credited. Record the window. The DLQ messages contain invocation-log records, so treat them as sensitive.
Evidence to keep: the deny lines, the heartbeat item before and after, alarm history
(aws cloudwatch describe-alarm-history --alarm-name sgea-<env>-token-meter-silent), the DLQ depth, and the root
cause.
Controls: SC-24, SI-4, IR-4, AU-6.
Trigger. An API call that stops or changes the trail or the recorder raises sgea-<env>-ct-cloudtrail-changes or sgea-<env>-ct-config-changes (playbook 8); disabling or scheduling deletion of a key raises sgea-<env>-ct-kms-key-disable-or-delete. Delivery failures without such a call (key, bucket policy or delivery role broken) have no direct alarm. They show up as a non-compliant conformance-pack rule (cloudtrail-enabled, multi-region-cloudtrail-enabled, cloud-trail-cloud-watch-logs-enabled, cloud-trail-log-file-validation-enabled) routed by sgea-<env>-evt-config-compliance (E3), a failed Security Hub NIST control, a gap in /sgea/<env>/cloudtrail, or the scheduled check below. Run that check at least daily. The -ct- alarms read the CloudTrail log group, so once the trail has stopped they see nothing more, and if the enclave key is unusable no alarm is delivered at all.
Likely causes
- The enclave CMK is disabled or pending deletion. CloudTrail encrypts with it (key-policy Sid
CloudTrail). AWS Config writes to the SSE-KMS store throughConfigDelivery(the Config service-linked role) andConfigServicePrincipal. - Someone edited the key policy or a bucket policy. On the log store (Terraform Sids) these are
CloudTrailAclCheckandCloudTrailWrite; the CDKTrailconstruct adds equivalent statements. On the Config store:ConfigBucketCheck,ConfigDelivery,ConfigServiceLinkedRole. - The trail was stopped (
StopLogging), deleted or had its selectors changed. Or the recorder was stopped. The SGEA sandbox teardown stops recordersgea-<env>, so that tool must never run outside a sandbox. - The CloudWatch delivery role
sgea-<env>-cloudtrail-to-cloudwatchor its policysgea-<env>-cloudtrailchanged. - The landing zone owns AWS Config (
create_config_recorder = false). The recorder is then notsgea-<env>, so escalate to the landing-zone owner.
Triage
aws cloudtrail get-trail-status --name sgea-<env> \
--query '[IsLogging,LatestDeliveryTime,LatestDeliveryError,LatestDigestDeliveryError,LatestCloudWatchLogsDeliveryError]'
aws configservice describe-configuration-recorder-status --configuration-recorder-names sgea-<env>
aws configservice describe-delivery-channel-status --delivery-channel-names sgea-<env>
aws kms describe-key --key-id alias/sgea-<env> --query 'KeyMetadata.[KeyState,DeletionDate]'
aws kms get-key-policy --key-id alias/sgea-<env> --policy-name default --query Policy --output text | jq '.Statement[].Sid'
aws s3api get-bucket-policy --bucket sgea-<env>-logs-<account>-<region> --query Policy --output text | jq '.Statement[].Sid'
# Who changed it (CloudTrail event history, 90 days of management events, independent of the trail):
for e in StopLogging DeleteTrail UpdateTrail PutEventSelectors StopConfigurationRecorder DeleteDeliveryChannel \
PutKeyPolicy DisableKey ScheduleKeyDeletion PutBucketPolicy; do
aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=$e --max-results 5 \
--query 'Events[].[EventTime,EventName,Username]' --output text
doneContainment. If the change was not made by your deployment pipeline, treat it as tampering. Run playbook 7 / 6 for the principal that made it, and preserve what is already in the log store.
Recovery
- Recover the key first if needed (
key-and-secret-management.md). - Re-apply the IaC (
terraform apply/npx cdk deploy --all). This restores the trail, the policies and the recorder settings. - If only stopped:
aws cloudtrail start-logging --name sgea-<env>;aws configservice start-configuration-recorder --configuration-recorder-name sgea-<env>. - Confirm
LatestDeliveryTimeis moving, then validate the files around the gap:aws cloudtrail validate-logs --trail-arn arn:aws:cloudtrail:<region>:<account>:trail/sgea-<env> --start-time <gap-start>. - Re-evaluate the pack rules (
aws configservice start-config-rules-evaluation, or Config console → Conformance packs → Re-evaluate). - Record the gap. Management events in the gap are still in CloudTrail event history (90 days). Data events in the gap are lost.
Note. Where the SGEA SCP bundles are deployed, the audit-protection statements (DenyAuditTampering, emitters/policy_library.py) deny cloudtrail:StopLogging / DeleteTrail / UpdateTrail / PutEventSelectors, config:StopConfigurationRecorder / DeleteConfigurationRecorder / DeleteDeliveryChannel, guardduty:DeleteDetector, securityhub:DisableSecurityHub, access-analyzer:DeleteAnalyzer, logs:DeleteLogGroup on /sgea/* and kms:ScheduleKeyDeletion / DisableKey / PutKeyPolicy on keys tagged sgea:profile, for every principal except the CDK bootstrap execution role, sgea-*-break-glass and policies.org_policy_exempt_principal_arns. A change that got through was made by one of those principals, from the management account (SCPs do not apply there), in an account without the SCPs, or by a role named to match an exemption pattern (the exemption matches role names, ADR 0002). Without the SGEA SCPs, check whether your landing zone's guardrails deny these calls.
Evidence to keep: trail and recorder status before and after, the lookup-events output, the validate-logs
output, the gap window, and the change record of the fix.
Controls: AU-5, AU-6, AU-9, AU-12, SI-4, IR-4.
Trigger. A message on sgea-<env>-log-store-events (s3:ObjectRemoved:*) from
sgea-<env>-logs-<account>-<region> (log store), sgea-<env>-config-<account>-<region> (Config store) or
sgea-<env>-access-logs-<account>-<region> (access-log sink). Normal operation deletes nothing:
- Retention is done by lifecycle rules (
retention: expire atretention_days, noncurrent versions after 30 days). S3 reports lifecycle expirations as lifecycle events, notObjectRemoved. - The only SGEA tool that deletes objects is the sandbox teardown.
Likely causes: manual cleanup by an operator, tampering to cover activity, a tenant or tool role with
s3:DeleteObject on sgea-<env>-*, or an Object Lock bypass.
Triage
- Read the SNS message:
eventName(ObjectRemoved:DeleteorObjectRemoved:DeleteMarkerCreated), bucket, key,versionId,userIdentity.principalId,requestParameters.sourceIPAddress. - Find who did it:
- Log store. The trail excludes the log store's own objects from S3 data events (to keep the trail's own
deliveries out). Use the S3 server access logs in
sgea-<env>-access-logs-<account>-<region>under the prefixsgea-<env>-logs-<account>-<region>/. Look forREST.DELETE.OBJECTandBATCH.DELETE.OBJECT, and the requester andx-amz-bypass-governance-retention. Access-log delivery can be delayed. - Config store and access-log sink. Where the profile logs S3 data events (
s3-enclave-buckets), CloudTrail data events are in the log store and in/sgea/<env>/cloudtrail. Logs Insights:filter eventName in ["DeleteObject","DeleteObjects"] and requestParameters.bucketName like /sgea-<env>-/.
- Log store. The trail excludes the log store's own objects from S3 data events (to keep the trail's own
deliveries out). Use the S3 server access logs in
- Check what is left:
A delete marker means the data is still there. Delete markers are not replicated (
aws s3api list-object-versions --bucket <bucket> --prefix <key> aws s3api get-object-retention --bucket <bucket> --key <key> --version-id <version> # Object Lock stores aws s3api head-object --bucket sgea-<env>-logs-replica-<account>-<replica-region> --key <key> --region <replica-region>
delete_marker_replicationoff), so the replica keeps the object.
Containment (needs approval, because it changes production policy): disable or revoke the principal (playbooks 6
and 7). If deletions continue, add a temporary explicit Deny s3:DeleteObject* on the affected bucket. Record it as
drift and remove it by re-applying the IaC once contained. Put legal holds on critical objects if counsel asks
(aws s3api put-object-legal-hold).
Recovery
- Delete marker: remove the marker (
aws s3api delete-object --bucket <b> --key <k> --version-id <marker-id>) before the 30-day noncurrent expiry. - Version permanently deleted: copy it back from the replica bucket, or restore the store from the AWS Backup
vault
sgea-<env>. The log and Config stores are in thesgea-<env>-enclaveselection. - For CloudTrail files, re-run
aws cloudtrail validate-logsover the affected window and record the result.
Evidence to keep: the SNS messages, access-log or CloudTrail lines that name the requester, version listings,
validate-logs output, and restore job ids.
Controls: AU-9, AU-11, AU-6, SI-4, IR-4, IR-5.
Trigger. A GuardDuty finding with severity 7 or higher in the enclave account and region, delivered to sgea-<env>-security-alarms by sgea-<env>-evt-guardduty (E1, the raw finding event). GuardDuty also imports findings into Security Hub, so the same finding can arrive a second time through sgea-<env>-evt-securityhub. Medium and low findings are not routed; your SIEM or landing zone covers them. The guardduty-non-archived-findings pack rule turns non-compliant on findings left open too long. In the FedRAMP Moderate pack the limits are 1 day for high, 7 for medium and 30 for low (pack parameters).
Triage
DET=$(aws guardduty list-detectors --query 'DetectorIds[0]' --output text)
aws guardduty list-findings --detector-id "$DET" \
--finding-criteria '{"Criterion":{"severity":{"Gte":7},"service.archived":{"Eq":["false"]}}}'
aws guardduty get-findings --detector-id "$DET" --finding-ids <id> > finding-<id>.jsonMap the affected resource to the enclave. IAM roles are named sgea-<env>-* (for example
sgea-<env>-agentcore-gateway, sgea-<env>-agentcore-memory), buckets sgea-<env>-*, functions
sgea-<env>-tool-policy / sgea-<env>-token-meter; tenant roles are the tenant's. Then pivot in CloudTrail on the
access key id or session name. Network findings: /sgea/<env>/vpc-flow-logs (or the VDMS flow logs on DoD SCCA
profiles).
Containment by resource type
| Resource | Action | Side effect to record |
|---|---|---|
| IAM role credentials | IAM console → role → Revoke active sessions | Adds the inline policy AWSRevokeOlderSessions. The enclave forbids inline policies (iam-no-inline-policy-check), so Config reports NON_COMPLIANT. Record it as an incident exception and remove the policy after rotation |
| Enclave or tenant Lambda | aws lambda put-function-concurrency --function-name <fn> --reserved-concurrent-executions 0 |
On sgea-<env>-tool-policy every tool call then fails. lambda-concurrency-check goes non-compliant |
| S3 data access | Playbook 5 | — |
| Network (ENI in the enclave VPC) | Move the ENI to a deny-all security group | Breaks that workload |
Recovery. Rotate the affected credentials. Re-apply the IaC to remove containment drift. Then archive the finding
with the ticket reference: aws guardduty archive-findings --detector-id "$DET" --finding-ids <id>.
Evidence to keep: the finding JSON, CloudTrail for the principal, flow-log extracts, containment and rotation records.
Controls: SI-4, IR-4, IR-5, IR-6, AU-6.
Context. Tenant runtimes and (when a JWT issuer is set) the gateway use a custom JWT authorizer: the OIDC
discovery_url plus allowed_clients; the Terraform runtime wrapper also takes allowed_audience. The enclave does not own the IdP. The
sample tenant's Cognito user pool is a test IdP (<name>-test-idp, public client with no secret, 60-minute
access and ID tokens, MFA optional). The authorizer checks the signature and expiry, so an access token that has
already been issued stays valid until it expires. Containment has to cover that window.
Trigger: a user report, an IdP risk alert, a GuardDuty finding, the anomalies in playbooks 1 and 2, or an
unexpected client_id in the interceptor log.
Triage
- Activity by the identity's
subin the interceptor log:aws logs filter-log-events --log-group-name /aws/lambda/sgea-<env>-tool-policy --filter-pattern '{ $.user = "<sub>" }'. - Model usage by that identity: the Logs Insights query in playbook 2, step 4.
- Runtime calls: AgentCore data events (
eventSourcebedrock-agentcore.amazonaws.com) in/sgea/<env>/cloudtrail. - IdP sign-in history. Cognito:
aws cognito-idp admin-list-user-auth-events --user-pool-id <pool> --username <user>.
Containment
- Disable the identity at the IdP. Cognito:
aws cognito-idp admin-disable-userandaws cognito-idp admin-user-global-sign-out --user-pool-id <pool> --username <user>. Global sign-out revokes refresh tokens; access tokens already issued keep working until they expire. - Cover the token lifetime:
- Interceptor tool-path mode: add
{"effect":"deny","tools":["*"],"claim":"sub","values":["<sub>"]}totool_policyand redeploy. Deny overrides allow. - Policy-engine mode: add a forbid policy for the caller to
sgea_<env>_tool_policy(check the principal format against the AgentCore Policy documentation).
- Interceptor tool-path mode: add
- If the whole client or the IdP signing keys are compromised: remove the client id from
jwt_allowed_clients/agent_jwt_allowed_clients(CDKsgea:jwtAllowedClients) and redeploy. This blocks every user of that client. Rotate the signing keys with the IdP's own procedure. - Workload role compromise (gateway AWS_IAM auth). The interceptor trusts the
X-Sgea-User/X-Sgea-Teamheaders from the IAM-authenticated workload. A stolen role credential can act as any user for budgets and tool policy. Revoke the role's sessions (playbook 6).
Recovery. Reset the user's credentials and enforce MFA at the production IdP. Remove temporary deny rules once the token lifetime has passed. Re-enable the user after the incident lead confirms.
Evidence to keep: IdP auth events, interceptor lines, invocation-log query results (counts, not text), CloudTrail extracts, and the containment change records.
Controls: AC-2, IA-2, IA-5, IR-4, IR-6, AU-6.
Trigger. One of these alarms (C1), each raised by a single matching CloudTrail event in 5 minutes:
| Alarm | Events |
|---|---|
sgea-<env>-ct-cloudtrail-changes |
CreateTrail, UpdateTrail, DeleteTrail, StartLogging, StopLogging, PutEventSelectors |
sgea-<env>-ct-config-changes |
AWS Config StopConfigurationRecorder, DeleteConfigurationRecorder, PutConfigurationRecorder, DeleteDeliveryChannel, PutDeliveryChannel |
sgea-<env>-ct-kms-key-disable-or-delete |
DisableKey, ScheduleKeyDeletion on any KMS key |
sgea-<env>-ct-iam-policy-changes |
IAM Put*Policy, Delete*Policy, Create*Policy*, Attach*Policy, Detach*Policy |
sgea-<env>-ct-s3-bucket-policy-changes |
Bucket ACL, policy, CORS, lifecycle or replication put or deleted |
sgea-<env>-ct-organizations-changes |
AWS Organizations policy, account or membership change (most events are recorded in the management account; a member account records for example LeaveOrganization) |
Every IaC apply that touches IAM or bucket settings raises some of these, and so do tenant deploys (-ct-iam-policy-changes). Check the change calendar first.
Likely causes
- An approved apply of the enclave, a tenant app or the landing zone.
- A console or CLI change outside the IaC (drift).
- Tampering to hide activity: stopping the trail or recorder, opening a bucket, removing replication or lifecycle protection, or disabling or scheduling deletion of the enclave key.
Triage
- Find the events with the alarm's filter pattern (the command under the signal inventory). Record
eventTime,eventName,userIdentity.arn,sourceIPAddressandrequestParameters. - Was the principal a deployment principal (CDK
cdk-hnb659fds-cfn-exec-role-*, your Terraform pipeline role, the landing-zone pipeline) inside an approved change window? A human role or an unknown principal is a finding. - Does the change touch an enclave resource? Trail and recorder
sgea-<env>, bucketssgea-<env>-*, roles and policiessgea-<env>-*, the keyalias/sgea-<env>(aws kms describe-key --key-id alias/sgea-<env> --query KeyMetadata.KeyIdagainstrequestParameters.keyId). - Compare the account with the code:
terraform plan(read-only) or CloudFormation drift detection (aws cloudformation detect-stack-drift --stack-name SgeaEnclave, thendescribe-stack-resource-drifts). - Where the audit-protection SCP is deployed, a denied attempt shows as
-ct-unauthorized-apiwitherrorCodeAccessDeniedand no change. The control worked, but who tried still matters (playbook 10). A tampering call that succeeded despite the SCP came from an exempt principal (see the note in playbook 4); for a role whose name matchessgea-*-break-glassorcdk-hnb659fds-cfn-exec-role-*, check when and by whom it was created (CreateRolein CloudTrail).
Containment (needs approval). Not made by an approved pipeline or change: treat it as tampering. Revoke the principal's sessions (playbook 6) or disable the identity (playbook 7). Trail or recorder affected: playbook 4. Enclave key scheduled for deletion: cancel it now (key and secret management, section 2). Bucket opened or replication removed: re-apply the IaC, or add a temporary explicit deny as in playbook 5.
Recovery. Re-apply the IaC so the account matches its code and its AWS Config evidence. Where the trail was touched, run aws cloudtrail validate-logs over the window and record any gap. Remove leftover IAM policies or attachments that no change record explains.
Evidence to keep: the alarm history, the CloudTrail events, the plan or drift output before and after, and the change record (or the incident record if there was none).
Controls: AU-6, AU-9, CM-3, CM-5, AC-6, SI-4, IR-4.
Trigger. One of sgea-<env>-ct-security-group-changes (security group created, deleted, or ingress / egress rules authorized or revoked), -ct-nacl-changes (network ACL or entry created, deleted, replaced or re-associated), -ct-network-gateway-changes (internet or customer gateway created, attached, detached or deleted), -ct-route-table-changes (route or route table created, replaced, deleted or disassociated) or -ct-vpc-changes (VPC created, deleted or modified, peering connections, ClassicLink). They cover the whole account and every region the trail records, not only the enclave VPC.
Likely causes: the enclave or a tenant deploy (interface endpoints, runtime and function security groups), the landing-zone network pipeline (the VPC and subnets are customer-owned), a manual change, or an attempt to open a path out of the enclave (an internet gateway or a 0.0.0.0/0 route on the agent subnets, a peering connection, an open ingress rule).
Triage
- Find the events with the alarm's filter pattern (command under the signal inventory). Note the VPC, security group, route table or gateway id and the principal.
- Is the resource in the enclave VPC (the
vpc_idinput / CDKsgea:vpcId)? Changes elsewhere go to the owner of that network. - For a security group: does a new rule allow
0.0.0.0/0or::/0, or a port other than 443 to the endpoints? For a route: does it send the agent subnets to an internet gateway, NAT gateway or peering connection? For peering: which account? - Look for traffic through the new path in
/sgea/<env>/vpc-flow-logs(or the VDMS flow logs on DoD SCCA profiles). The conformance-pack network rules (for examplevpc-sg-open-only-to-authorized-ports,restricted-ssh) report throughsgea-<env>-evt-config-complianceas well.
Containment (needs approval). Revert the change: revoke the rule, delete the route, detach the gateway or reject the peering. If the principal is not expected, revoke its sessions (playbook 6). If traffic left the enclave, treat it as a data incident and engage privacy / legal where PII or PHI may be involved.
Recovery. Re-apply the enclave IaC (endpoints, security groups, flow logs) and ask the network owner to re-apply theirs. Record the window during which the path was open.
Evidence to keep: the CloudTrail events, flow-log extracts for the window, the revert change record.
Controls: SC-7, AC-4, CM-3, CM-5, SI-4, IR-4.
Trigger. One of:
| Alarm | Fires on |
|---|---|
sgea-<env>-ct-root-usage |
Any root-user call that is not made by an AWS service on its behalf |
sgea-<env>-ct-console-no-mfa |
A successful console sign-in by an IAM user without MFA |
sgea-<env>-ct-console-auth-failures |
A failed console sign-in |
sgea-<env>-ct-unauthorized-api |
Any call that failed with *UnauthorizedOperation or AccessDenied* |
-ct-unauthorized-api is the noisiest alarm in the layer: a deny from an SCP, a permissions boundary, a missing kms:Decrypt or a tool that probes with Describe* calls all count.
Likely causes: a planned root task (closing the account, changing the support plan, some billing settings); an IAM user the landing zone or a team created (the enclave creates no IAM users); password guessing; a misconfigured role after a deploy; the audit-protection or other SGEA SCPs working; enumeration with stolen credentials.
Triage
- Find the events with the alarm's filter pattern (command under the signal inventory).
- Root: is there an approved root task? Check
aws iam get-account-summary --query 'SummaryMap.[AccountMFAEnabled,AccountAccessKeysPresent]'(expected1and0). - Console without MFA: which IAM user, from where? Its owner, and why it has a console password.
- Failed sign-ins: count by user name and source IP; many users from one IP, or many attempts for one user, is guessing.
- Unauthorized calls: group them in Logs Insights on
/sgea/<env>/cloudtrail:One enclave or tenant role failing the same call after a deploy is a misconfiguration: fix it through the IaC. One principal failing many different calls across services is enumeration: go to playbook 6 and playbook 7. A deny whose message names a service control policy is an SCP working as designed; record who tried.filter errorCode like /UnauthorizedOperation/ or errorCode like /^AccessDenied/ | stats count() as denies by userIdentity.arn, eventSource, eventName, errorMessage | sort denies desc
Containment (needs approval). Root not approved: secure the root user now (reset the password, check the MFA devices, delete any root access key) and treat it as an account compromise. IAM user without MFA: remove the console password (aws iam delete-login-profile --user-name <user>) or require MFA, and deactivate keys the owner cannot explain. Guessing: block the source at your IdP or network edge. Enumeration: revoke the principal's sessions.
Recovery. Rotate the credentials involved. Prefer IAM Identity Center or federation over IAM users. Record root use with its approval. If -ct-unauthorized-api fires every day for the same known cause, fix the cause rather than ignoring the alarm.
Evidence to keep: the CloudTrail events, the Logs Insights result, root or IAM-user records and approvals, rotation records.
Controls: AC-2, AC-6(9), AC-7, IA-2(1), AU-6, SI-4, IR-4.
Trigger. Alarm sgea-<env>-ct-break-glass-assumed (C4): an AssumeRole call on sgea-<env>-break-glass. The role exists only with create_break_glass_role (CDK sgea:createBreakGlassRole) and break_glass_principal_arns (CDK sgea:breakGlassPrincipalArns). It requires MFA within the last hour, sessions last at most one hour, and it has AdministratorAccess. Every assumption raises the alarm: expected during a declared emergency, an incident otherwise.
Triage
- Is there an open incident or emergency change that declared break-glass use? Contact the person who holds it.
- Find the assumption with the alarm's filter pattern (command under the signal inventory): the calling principal (
userIdentity.arn),sourceIPAddress, and the session (responseElements.assumedRoleUser.arn). - List what the session did. Logs Insights on
/sgea/<env>/cloudtrail:filter userIdentity.arn like /assumed-role\/sgea-<env>-break-glass\// | stats count() as calls by eventSource, eventName | sort calls desc - The role is exempt from the audit-protection SCP. Check whether other
-ct-alarms fired in the same window (playbook 8).
Containment (not authorized). Revoke the role's sessions (IAM console → role → Revoke active sessions; this adds the inline policy AWSRevokeOlderSessions, recorded as in playbook 6). Disable the source identity at the IdP or in IAM and treat it as a credential compromise. Remove the principal from break_glass_principal_arns and redeploy, or set create_break_glass_role to false. Act fast: the session lasts up to one hour.
Recovery. Post-use review within one business day (access review, section 6): who, why, every action from step 3, drift reconciled (terraform plan or CloudFormation drift detection), credentials rotated, ISSO sign-off.
Evidence to keep: the alarm, the AssumeRole event, the session's activity, the approval (or the incident record), the post-use review.
Controls: AC-2, AC-6, AU-6, CM-3, IR-4.
Trigger. One of these events on sgea-<env>-security-alarms (E2):
sgea-<env>-evt-securityhub: a new (workflow status NEW) HIGH or CRITICAL finding imported into Security Hub, from any integrated product. GuardDuty findings arrive here too (playbook 6).sgea-<env>-evt-inspector: a HIGH or CRITICAL Amazon Inspector finding, for example a vulnerable package in an image insgea-<env>-agentsor in a Lambda function.sgea-<env>-evt-access-analyzer: an ACTIVE finding of the IAM Access Analyzersgea-<env>-external-access(zone of trust: this account): a resource policy that grants access to a principal outside the account. Unused-access findings fromsgea-<env>-unused-access(if turned on) are reviewed in the quarterly access review.
Triage
# Security Hub: the id is in detail.findings[].Id of the event
aws securityhub get-findings --filters '{"Id":[{"Value":"<finding-id>","Comparison":"EQUALS"}]}' \
--query 'Findings[].[ProductName,Title,Severity.Label,Resources[].Id]'
# Inspector: the ARN is in detail.findingArn
aws inspector2 list-findings --filter-criteria '{"findingArn":[{"comparison":"EQUALS","value":"<finding-arn>"}]}'
# Access Analyzer: the id is in detail.id
aws accessanalyzer get-finding --id <finding-id> \
--analyzer-arn arn:aws:access-analyzer:<region>:<account>:analyzer/sgea-<env>-external-accessMap the resource to its owner: sgea-<env>-* is the enclave, tenant resources belong to the tenant. For Access Analyzer, compare the policy with the expected resource policies in the access review, section 5; access that no record explains is a finding.
Containment. Exposed resource (Access Analyzer, or a Security Hub control on public access): remove the statement now (approval), then fix the source. Exploitable vulnerability in a running image or function: stop or isolate the workload (playbook 6, table) until it is rebuilt.
Recovery. Fix through the IaC or the tenant's pipeline (rebuild the image, upgrade the runtime or the SGEA toolkit). Then close the finding with the ticket reference: Security Hub aws securityhub batch-update-findings (workflow RESOLVED, or SUPPRESSED for an accepted risk with ISSO approval), Inspector suppression rule for accepted risk, Access Analyzer aws accessanalyzer update-findings --status ARCHIVED or an archive rule for intended access.
Evidence to keep: the finding JSON, the fix change record, the risk acceptance for suppressed or archived findings.
Controls: RA-5, SI-2, SI-4, AC-3, AC-6, CA-7, IR-4.
Trigger. One of these events (E3):
sgea-<env>-evt-config-compliance: any AWS Config rule in the account and region, including the conformance-pack rules and the landing zone's rules, turned NON_COMPLIANT for a resource.sgea-<env>-evt-stack-drift(CDK only): CloudFormation drift detection found an SGEA stack DRIFTED. The EventBridge Scheduler schedulesgea-<env>-drift-detectruns drift detection daily on the SGEA stacks in the primary region (not on the log-replica stack), as rolesgea-<env>-drift-scheduler(the two drift actions on the SGEA stacks plus the AWS managedReadOnlyAccess). Terraform has no drift event: a scheduledterraform plan -detailed-exitcodein your pipeline exits 2 on drift; treat that result the same way.
Triage
- Config: read
detail.configRuleName,detail.resourceType,detail.resourceIdanddetail.newEvaluationResult.annotation. Thenaws configservice get-compliance-details-by-config-rule --config-rule-name <rule> --compliance-types NON_COMPLIANT. - Is it expected? Known findings in
MANUAL-STEPS.md(for example SGEA-KF-02), a recorded exception, a containment change from another playbook (anAWSRevokeOlderSessionsinline policy, zero Lambda concurrency), or a transient result during a deploy.docs/CONTROL-COVERAGE.mdshows how SGEA handles the rule. - Drift:
aws cloudformation describe-stack-resource-drifts --stack-name SgeaEnclave --stack-resource-drift-status-filters MODIFIED DELETEDlists what changed and how. - A rule or drift on the trail, the recorder, the key, IAM or the network: also run playbook 8 or 9.
Containment. Usually none. If the change opened access or stopped logging, follow the matching playbook.
Recovery. Re-apply the IaC after reviewing the plan or change set; this reconciles drift. If the change should stay, bring it into the profile or inputs (change and upgrade) instead. Re-evaluate the rule (aws configservice start-config-rules-evaluation --config-rule-names <rule>) and confirm it is compliant.
Evidence to keep: the event, the compliance details or drift output, and the change record that fixed or accepted it.
Controls: CM-2, CM-3, CM-6, CA-7, SI-4.
Trigger. One of these events (E4):
sgea-<env>-evt-backup: an AWS Backup backup, copy or restore job in the account and region ended FAILED, ABORTED or EXPIRED (EXPIRED: the job did not start within its window). Jobs of other backup plans in the account match too.sgea-<env>-evt-secret-rotation: Secrets Manager reportedRotationFailed, for example for the RDS-managed Aurora admin secret ofsgea-<env>-vector.
Triage
aws backup describe-backup-job --backup-job-id <id> --query '[State,StatusMessage,ResourceArn,BackupVaultName]'
aws backup list-backup-jobs --by-backup-vault-name sgea-<env> --by-state FAILED --max-results 20
aws secretsmanager describe-secret --secret-id <secret-arn> --query '[RotationEnabled,LastRotatedDate,NextRotationDate]'
aws rds describe-db-clusters --db-cluster-identifier sgea-<env>-vector --query 'DBClusters[0].MasterUserSecret.SecretStatus'Common causes: the enclave key disabled or its grants missing; the resource deleted or stopped; a change to the role sgea-<env>-backup; the vault lock minimum retention conflicting with a changed lifecycle; for rotation, the secret status impaired (RDS cannot use the key or the secret).
Containment. None needed: the previous recovery point and the current credential stay valid. Record the window without a fresh recovery point (RPO) or without rotation.
Recovery. Fix the cause, then take an on-demand backup (aws backup start-backup-job --backup-vault-name sgea-<env> --resource-arn <arn> --iam-role-arn <arn of sgea-<env>-backup>) or retry the rotation (aws secretsmanager rotate-secret --secret-id <secret-arn>). A failed restore job usually belongs to a restore test (OPERATIONS.md): repeat the test after the fix.
Evidence to keep: the job or rotation events, the fix, the new recovery point or rotation date, and the RPO gap.
Controls: CP-9, CP-10, IA-5, SI-4.
Trigger. One of the health alarms (H1, H3):
| Alarm | Means | First look |
|---|---|---|
sgea-<env>-bedrock-throttles |
At least 10 throttled model calls in each of two 5-minute periods (account total) | Service quotas (tokens and requests per minute per model), a runaway agent (playbook 2, step 4 query) |
sgea-<env>-bedrock-server-errors |
At least 5 Bedrock server errors in 5 minutes | AWS Health Dashboard for Amazon Bedrock in the region; the model in use |
sgea-<env>-bedrock-invocation-log-delivery |
Model-invocation logs not delivered to CloudWatch for 15 minutes | Logging configuration, role sgea-<env>-bedrock-invocation-logging, log group /sgea/<env>/bedrock/model-invocations, enclave key |
sgea-<env>-gateway-system-errors |
At least 5 gateway system errors on MCP InvokeGateway in 5 minutes |
Gateway targets (tenant tool functions), the interceptor (alarm A1, log L1) |
sgea-<env>-gateway-throttles |
At least 10 gateway throttles in each of two 5-minute periods | AgentCore quotas, caller retry behavior |
Triage
aws cloudwatch describe-alarm-history --alarm-name sgea-<env>-<alarm> --history-item-type StateUpdate --max-items 5
aws service-quotas list-service-quotas --service-code bedrock \
--query 'Quotas[?contains(QuotaName, `tokens per minute`)].[QuotaName,Value]' --output table
aws bedrock get-model-invocation-logging-configurationFor throttles, find the models and apps involved (Bedrock metrics by ModelId; the invocation-log query in playbook 2, step 4). For log delivery, check that the logging configuration still names the role and log group, that the role's policy is unchanged and that the key is Enabled.
Impact. Throttles and server errors fail tenant requests. Missing invocation logs are an audit gap and stop the token meter from crediting usage, so budgets under-count while it lasts (the meter heartbeat keeps running, so the interceptor does not fail closed). Gateway errors fail tool calls.
Containment. Throttles: slow down or stop the offending app or agent (tenant team), or lower its token budget. Server errors: tenant apps retry with backoff; nothing changes in the enclave. Log delivery: re-apply the IaC to restore the logging configuration.
Recovery. Request a quota increase where the load is legitimate. Record the window of missing invocation logs (counts in the S3 copy under s3://sgea-<env>-logs-<account>-<region>/bedrock/ may still be complete) and the under-credited token usage.
Evidence to keep: alarm history, quota values, the logging configuration before and after, the gap window.
Controls: SC-5, AU-5, AU-12, SI-4, IR-4.
Trigger. A notification from AWS Budgets on sgea-<env>-monthly-cost: actual cost over 80 % or forecast cost over 100 % of monthly_budget_usd (CDK sgea:monthlyBudgetUsd). Or an AWS Cost Anomaly Detection alert from sgea-<env>-anomalies: an anomaly with a total impact of at least USD 100 on the per-service monitor sgea-<env>-services (opt-in create_cost_anomaly_monitor / sgea:createCostAnomalyMonitor, because an account allows only one per-service monitor and AWS often creates a default one). Both cover the whole account, not only the enclave, and exist only in the commercial partition.
Likely causes: heavy model use by a tenant, an agent in a loop, a new tenant or a larger model, resources left behind in a test, or abuse of stolen credentials (for example compute in an unused region).
Triage
aws ce get-cost-and-usage --time-period Start=<YYYY-MM-01>,End=<YYYY-MM-DD> --granularity DAILY \
--metrics UnblendedCost --group-by Type=DIMENSION,Key=SERVICE
aws ce get-anomalies --date-interval StartDate=<YYYY-MM-DD>,EndDate=<YYYY-MM-DD> \
--query 'Anomalies[].[AnomalyStartDate,Impact.TotalImpact,RootCauses]'Bedrock cost: find who used the tokens (playbook 2, step 4 query). Cost in a region outside the profile or a service the enclave does not use: check GuardDuty (playbook 6) and treat it as a possible credential compromise. If you activated sgea:tenant or sgea:profile as cost allocation tags, group by them.
Containment. Budgets and anomaly alerts stop nothing. Have the tenant team stop a runaway app; lower token budgets through the profile; for abuse, revoke the credentials and remove the resources.
Recovery. Adjust monthly_budget_usd through the IaC when the new level is approved. Record the cause.
Evidence to keep: the alert, the cost breakdown, the cause and any budget change approval.
Controls: SC-6, SI-4, IR-4.
| Role | Name / team | Primary contact | Backup | When to engage |
|---|---|---|---|---|
| Incident lead (on call) | Every playbook | |||
| ISSO / security officer | Guardrail or budget changes, risk acceptance, break-glass use (playbook 11), every confirmed incident | |||
| Enclave platform owner (IaC) | Redeploys, containment drift, playbooks 13–15 | |||
| Landing-zone / Config / GuardDuty owner | Playbooks 4, 6, 8, 9, 12 and 13 when the landing zone owns the service or the network | |||
| IdP owner | Playbook 7 | |||
| Tenant app owner | Playbooks 1, 2, 7, 12, 15, 16 | |||
| Cost owner (FinOps) | Playbook 16 | |||
| Privacy / legal (PII, PHI) | Possible PII or PHI exposure, legal hold | |||
| AWS Support | Case in the enclave account | Service-side faults | ||
| External reporting (authorizing official, agency, covered entity) | As required by your SSP / authorization (IR-6) |
- Timeline built from CloudTrail (
validate-logspassed for the window), the interceptor and meter logs, and alarm history. - Root cause recorded. Was it a control working as designed (fail-closed, guardrail) or a failure?
- Every containment change (deny statements, revoked sessions, zero concurrency) removed or kept as a recorded
exception. IaC re-applied.
sgea checkand the conformance packs compliant again. - Gaps recorded: audit-log gap window, under-credited token usage, deleted or restored objects.
- Profile changes (guardrail, budgets, tool policy) approved by the ISSO and committed with the export.
- Alerting reviewed: did a signal in the inventory fire, and early enough? Thresholds or toggles changed through the IaC where it was noisy or silent; topic subscriptions current.
- Reporting obligations met and recorded (IR-6); privacy / legal informed where PII or PHI was involved.
- Evidence filed with the incident record. Prompt text only where the incident lead requires it.
- Playbook and contacts table updated; lessons fed into the next incident-response exercise.