Complete the platform layer that vibe apps run on: data storage, observability, guardrails, memory, gateway, and the Lambda-tool execution roles.
After this step, the substrate is structurally complete. Every property the 60-day plan called for in Phase 1 is now provisioned: audit S3 with WORM Object Lock for compliance retention, CloudTrail multi-region trail capturing every API call, GuardDuty actively scanning for threats, Bedrock invocation logging recording every agent call, content moderation + prompt injection guardrails enforced platform-wide, and three CloudWatch dashboards (operators / exec / audit) tracking everything that moves. Phase 1 apps build on this; Phase 2 PII workflows extend it without substrate migration because the lockbox-tier patterns are already provisioned (just not exercised in Phase 1).
If skipped: No audit retention (regulatory liability). No invocation logging (cannot prove which agent did what). No guardrails (vibe apps could be prompt-injected or leak PII). No dashboards (no operational visibility). Phase 1 cannot ship without these.
Steps 0-3 deployed the substrate's foundations: human identity (SSO), state backend + permission boundary (bootstrap), KMS CMKs + tier roles (identity), and the network surface (VPC + NFW + endpoints). After Step 3 the substrate existed but the platform layer the vibe apps need was still empty: no audit storage, no observability, no guardrails, no Lambda execution roles, no AgentCore Memory or Gateway scaffolding.
Step 4 closes that gap. Six modules go in, each filling a specific Phase 1 plan deliverable. After this step, vibe apps have somewhere to write audit records, somewhere to read observability data from, guardrails enforced platform-wide, and execution roles ready to host Lambda functions.
Phase 1 of the 60-day plan called for "governed data layer + WORM audit storage + observability pipeline + module library." Step 4 ships exactly that, as six independently versionable Terraform modules wired into one composition.
| Resource | Why |
|---|---|
Audit S3 bucket ai-sandbox-audit-959228203854 | Destination for CloudTrail, GuardDuty findings, future flow-log archive. Object Lock COMPLIANCE mode for 7 years (2555 days). KMS-encrypted with audit CMK. |
| Lockbox DDB token table | The PII vault. Provisioned in Phase 1 for completeness; not exercised (Phase 1 has no PII workflows). KMS-encrypted with lockbox CMK, PITR on, DDB Streams enabled for audit trail of writes. |
| Agent state DDB table | Per-session state for AgentCore Runtime and lambda-tools coordination. KMS-encrypted with agent CMK. |
| Per-zone Secrets Manager namespaces | Two secrets: agent/sor-credentials (agent CMK, holds SoR API keys for lambda-tools) and lockbox/service-credentials (lockbox CMK, holds reveal Lambda credentials, Phase 2). |
| Gateway endpoint policies | Layer 1 of the four-layer DB access boundary. S3 and DDB gateway endpoints scoped so agent subnets can only reach ai-sandbox-agent-* resources; lockbox subnets only reach ai-sandbox-lockbox-* resources. |
| Resource | Why |
|---|---|
CloudTrail multi-region trail ai-sandbox-trail | Captures every AWS API call across the account. S3 + CloudWatch destinations, KMS-encrypted, log file validation enabled. Phase 1 uses default management-event selectors (data-plane events deferred — see § 12). |
| GuardDuty detector | Active threat scanning. S3 logs data source enabled. Finding publishing every 15 minutes. CloudWatch Events stream available for downstream alerting. |
| Bedrock invocation logging | Every InvokeModel / InvokeAgent call across the account logs to /aws/bedrock/invocations/ai-sandbox CloudWatch group + S3. Read by the observability vibe app for per-agent activity panels. |
| Three CloudWatch dashboards | ai-sandbox-operators (real-time platform health), ai-sandbox-exec (cost / activity / posture), ai-sandbox-audit (CloudWatch Logs Insights query templates). |
| Resource | Why |
|---|---|
Default Bedrock guardrail ai-sandbox-default | Platform-wide content moderation (sexual/violence/hate/insults/misconduct HIGH), prompt-injection filter (HIGH on input), PII filter (anonymize email/phone/name, block SSN/credit-card). PII paths provisioned-not-exercised in Phase 1. |
| Resource | Why |
|---|---|
| AgentCore Memory service role | Substrate scaffolding. Per-agent memory stores are created in agent modules and reference this role. Encrypts with agent CMK. |
| Resource | Why |
|---|---|
| AgentCore Gateway service role | Substrate scaffolding. Per-app gateways reference this role with their own inbound auth + rate limits. Default rate limit: 60 req/min/app. |
| Resource | Why |
|---|---|
| SoR adapter execution role | Shared by all agent-tier Lambda tools (Salesforce, Monday, GitHub, GitBook clients). Reads SoR credentials from Secrets Manager; decrypts with agent CMK. |
| Reveal Lambda execution role | For the lockbox-tier reveal Lambda. Provisioned in Phase 1 for completeness; function code deploys in Phase 2. |
| Resource | Monthly | Note |
|---|---|---|
| Audit S3 bucket (Object Lock) | ~$0.02 + storage | Empty today; grows as CloudTrail + GuardDuty + Bedrock logs accumulate. Lifecycle to Glacier Deep Archive after 90 days. |
| Lockbox DDB (PAY_PER_REQUEST) | $0 | Not exercised in Phase 1. |
| Agent state DDB (PAY_PER_REQUEST) | $0 | Only billed on actual reads/writes. |
| Secrets Manager (2 secrets) | $0.80 | $0.40/secret/month. |
| CloudTrail multi-region trail | ~$2-5 | $2 per 100k events; Phase 1 management events only. |
| GuardDuty detector | ~$3-10 | Based on CloudTrail event volume + VPC flow log volume. |
| Bedrock invocation logging | ~$1-5 | CloudWatch ingest + S3 storage; grows with agent activity. |
| CloudWatch Logs (CloudTrail + Bedrock) | ~$5-15 | ~$0.50/GB ingested + storage. |
| CloudWatch dashboards (3) | $9 | $3/dashboard/month. |
| Bedrock guardrail | $0 standing | Per-token charge at invocation: $0.075 per 1k input + 0.1 per 1k output. |
| Total added | ~$35-50/month | Cumulative substrate: ~$464/month |
Step 4 is the first multi-PR substrate change to go entirely through CI (the wired path from Step 3a). Five PRs landed across one session:
cloudtrail:PutEventSelectors). Trail uses default management-events selectors for Phase 1.DenyCloudTrailTampering into DenyCloudTrailHardTampering (StopLogging + DeleteTrail + RemoveTags on all trails) and DenyCloudTrailConfigTamperingOnNonProjectTrails (UpdateTrail + PutEventSelectors only on trails NOT matching ai-sandbox-*). Applied via laptop SSO admin — the boundary itself is a bootstrap-level concern by design.0 added, 3 changed, 0 destroyed. Substrate complete.Total elapsed time: ~1 hour of apply attempts + boundary update. Plan and review time excluded.
# 1. CloudTrail logging
AWS_PROFILE=ai-sandbox aws cloudtrail get-trail-status \
--name ai-sandbox-trail \
--query '{IsLogging:IsLogging,LatestDeliveryTime:LatestDeliveryTime}'
# Expected: IsLogging=true, recent LatestDeliveryTime
# 2. GuardDuty detector active
AWS_PROFILE=ai-sandbox aws guardduty list-detectors
# Expected: array with one detector ID
# 3. Bedrock invocation logging
AWS_PROFILE=ai-sandbox aws bedrock get-model-invocation-logging-configuration \
--query 'loggingConfig.cloudWatchConfig.logGroupName'
# Expected: "/aws/bedrock/invocations/ai-sandbox"
# 4. Bedrock guardrail READY
AWS_PROFILE=ai-sandbox aws bedrock list-guardrails \
--query 'guardrails[?name==`ai-sandbox-default`].{Name:name,Status:status}'
# Expected: Status=READY
# 5. Audit S3 Object Lock
AWS_PROFILE=ai-sandbox aws s3api get-object-lock-configuration \
--bucket ai-sandbox-audit-959228203854 \
--query 'ObjectLockConfiguration.Rule.DefaultRetention'
# Expected: Mode=COMPLIANCE, Days=2555
# 6. Lockbox DDB
AWS_PROFILE=ai-sandbox aws dynamodb describe-table \
--table-name ai-sandbox-lockbox-tokens \
--query 'Table.{Status:TableStatus,SSE:SSEDescription.Status,Stream:StreamSpecification.StreamEnabled}'
# Expected: Status=ACTIVE, SSE=ENABLED, Stream=true
# 7. CloudWatch dashboards
AWS_PROFILE=ai-sandbox aws cloudwatch list-dashboards \
--dashboard-name-prefix ai-sandbox
# Expected: 3 entries (operators, exec, audit)
| What | Where | What confirms it |
|---|---|---|
| Audit S3 bucket with Object Lock | S3 → Buckets → ai-sandbox-audit-959228203854 → Properties tab → Object Lock | Status: Enabled. Default retention: 2555 days, COMPLIANCE mode. SSE-KMS encryption with audit CMK. |
| CloudTrail trail | CloudTrail → Trails → ai-sandbox-trail | Status: Logging. Multi-region: yes. Log file validation: enabled. KMS encryption: audit CMK. Most recent delivery within the last few minutes. |
| GuardDuty findings (will be empty initially) | GuardDuty → Findings | Detector ID shown. Status: Active. Findings list (likely empty for a fresh account). |
| Bedrock guardrail | Bedrock → Guardrails → ai-sandbox-default | Status: READY. Content filters: 6 enabled. PII filter: anonymize/block configured. |
| CloudWatch dashboards | CloudWatch → Dashboards | Three rows: operators, exec, audit. Click any to see panels. |
| Bedrock invocation log group | CloudWatch → Log groups → /aws/bedrock/invocations/ai-sandbox | Encrypted with audit CMK. Will populate as agents make Bedrock calls. |
| Permission boundary update (verify) | IAM → Policies → ai-sandbox-permission-boundary → JSON | Two CloudTrail Deny statements: DenyCloudTrailHardTampering (all trails) and DenyCloudTrailConfigTamperingOnNonProjectTrails (NotResource = ai-sandbox-*). |
The three CloudWatch dashboards are now the first thing to look at when an executive asks "what is the AI platform doing right now." They're not pretty (they're standard AWS dashboards), but they're the data source the observability vibe app reads from. Step 4.5 will wrap them in a polished front end.
Four of the five PRs in this step exist because the bootstrap permission boundary refused operations that look reasonable but compromise audit guarantees. cloudtrail:PutEventSelectors denied. cloudtrail:DeleteTrail denied. guardduty publishing destinations require additional principal grants. Each refusal was the doctrine working as designed.
The boundary update in PR #5 wasn't a relaxation — it was a scope refinement. Hard tampering (StopLogging, DeleteTrail, RemoveTags) stays globally denied. Configuration tampering (PutEventSelectors, UpdateTrail) is now denied only on trails this project does not own. The deploy role can manage ai-sandbox-* trails as IaC requires; cannot touch other trails.
From this step forward, every API call is in CloudTrail (encrypted, 7-year Object Lock). Every Bedrock invocation is in CloudWatch (encrypted, 90-day retention). Every threat-class finding is in GuardDuty. Every workload that uses Bedrock goes through the guardrail. The substrate's "I know what I did" property is no longer aspirational.
The lockbox DDB table, the reveal Lambda execution role, the lockbox CMK, and the per-zone lockbox secrets are all provisioned. Phase 1 apps don't exercise them. When a Phase 2 PII workflow needs them, no substrate migration is required: the resources exist, the IAM patterns exist, the doctrine is documented. Phase 2 is feature work, not infrastructure work.
Step 4 is where the substrate stops being a checklist of deployed resources and starts being a self-watching, self-defending platform. Every property the 60-day plan called for is empirically demonstrated: WORM audit retention is enforced by Object Lock; trail tampering is empirically denied by the permission boundary; PII isolation is structural at the KMS layer. The platform now has the properties it claims to have.
One session, four apply failures, five PRs, one bootstrap update. Recorded honestly because the failures are themselves substrate validation.
Initial guardrail had output_strength = "HIGH" on the PROMPT_ATTACK filter. AWS validation rejected: "PROMPT ATTACK content filter strength for response must be NONE." Prompt-injection is an input-side concern; response-side has no analogue. Fix: output_strength = "NONE".
CloudTrail validates that the destination bucket policy permits it BEFORE creating the trail. Without explicit dependency between the observability and data modules in the composition, Terraform tried to create the trail before the bucket policy was fully applied. Error: InsufficientS3BucketPolicyException. Fix: depends_on = [module.data] on the observability module.
GuardDuty exporting findings to S3 requires the guardduty.amazonaws.com service principal in BOTH the audit S3 bucket policy AND the audit CMK key policy. The audit CMK lives in the identity module, which would need to know about GuardDuty — doctrine violation (modules shouldn't depend on services that don't exist yet). Decision: defer S3 export. Findings still flow via CloudWatch Events; export becomes a Phase 2 follow-on PR that updates both module policies.
cloudtrail:PutEventSelectors denied by permission boundaryThe AWS Terraform provider always calls PutEventSelectors when creating a trail, even when no event_selector block is defined (it sets defaults). The bootstrap permission boundary denied this. Two-step recovery: (1) PR #4 removed the explicit event_selector block to confirm the provider behavior; (2) PR #5 + laptop bootstrap apply split the boundary deny into "hard tampering" (universal) and "config tampering on non-project trails" (scoped). The doctrine "compromised deploy role cannot disable audit" is preserved at the strongest scoping consistent with IaC.
cloudtrail:DeleteTrail denied (the recovery of the tainted trail)After Gotcha B, the CloudTrail trail existed in AWS but was marked tainted in Terraform state. The next plan wanted to destroy + recreate. The boundary correctly denied DeleteTrail. Recovery: terraform untaint as SSO admin (boundary doesn't apply to SSO sessions). The trail's actual config matched the desired config after the event_selector removal, so no functional change needed — just state reconciliation.
Three of the five gotchas were the permission boundary refusing operations that look like normal IaC. That's not a bug. It's the property the boundary was deployed to provide: the deploy role cannot silence audit logging, ever. Recovery requires either (a) a deliberate boundary scoping change applied via laptop SSO admin (bootstrap-level break-glass), or (b) accepting reduced functionality (defer data-plane event selectors to Phase 2). Both are legitimate. The doctrine is harder to live with than ignoring it. That's the point.
Substrate complete. Two things next, in this order:
nfw_enforcement_mode = "drop", dns_firewall_action = "BLOCK". Doctrine originally said "after 24-48h of real workload observation"; with no workloads yet, the SoR allowlist is design-correct rather than observation-tuned. Flipping now means future workloads deploy against the right enforcement floor instead of having to live through a transition. The obs app from Step 4.5 will let us watch the flip happen.At Day 28 end (today), Phase 1 of the 60-day plan is structurally complete. Phase 2 (module library) is ~50% in the form of MODULES.md doctrine + vibecode SDK API surface defined. Phase 3 (three POCs) is 0%, but two are now identified (observability vibe app + Sales monthly reports). Phase 4 (observability pipeline) is ~70% — audit stream end-to-end live; dashboards exist but the polished exec-facing front end is what Step 4.5 delivers.