Module 7: Detective Vs Preventive Guardrails

8. Project: Andes Cargo's guardrails map

Description

Lessons 2 through 7 of this module built or named eight distinct guardrail mechanisms. This project brings them together into a single document — GUARDRAILS-MAP.md, at andes-cargo-infra/'s root, alongside THREAT-MODEL.md and RISK-MAP.md — that classifies them, with no ambiguity, along two independent axes: preventive or detective (what type of control it is), and implemented, representative, or named (how built it is in this specific $0 lab). It's, precisely, the deliverable this entire module exists to produce.

Connection to the module

This document adds no new data — it organizes, with the same discipline you already applied in THREAT-MODEL.md (Module 1, lesson 7) and RISK-MAP.md (Module 1, lesson 8), what this module's six earlier lessons already built and explained. The difference from those two documents is the classification axis: while RISK-MAP.md orders risks by resolution sequence, this document orders guardrail mechanisms by type and by degree of construction — the exact question a technical interviewer would ask about any of the eight.


Step 1 — Why eight pieces, not one more or one less

Before writing the table, it's worth carefully counting what counts as a "piece" of this module, so the map doesn't get padded with artificial rows or lose a real one. The eight are:

  1. AppServerRole's permission boundary (lesson 3)
  2. Service Control Policies, as a mechanism (lessons 2 and 6)
  3. AWS Organizations, as a structure (lesson 6)
  4. AWS Control Tower, as an orchestration layer (lesson 6)
  5. CloudTrail — the andes-cargo-trail trail — (lesson 5)
  6. AWS Config (lesson 4)
  7. GuardDuty (lesson 4)
  8. The EventBridge rule + SNS topic on sensitive IAM events (lesson 7)

Notice two deliberate decisions: Organizations and Control Tower count as two separate pieces, not one — lesson 6 presented them as technically distinct concepts (one is the structure, the other is the orchestration over that structure), and collapsing them into a single row would hide that distinction. And SCP counts as a single piece, even though it appears across two lessons (2 and 6) — it's the same technical mechanism in both places, only lesson 2 introduces it by contrast with the permission boundary and lesson 6 revisits it at multi-account scale; a single row, with both lessons cited.


Step 2 — The complete document

At andes-cargo-infra/'s root, alongside THREAT-MODEL.md and RISK-MAP.md, create GUARDRAILS-MAP.md:

# GUARDRAILS-MAP.md — Preventive vs. Detective Guardrails for `cloud-security-and-guardrails-guide`

**Status:** Accepted · **Date:** this module's close · **Supersedes:** none
**Governs:** Module 7 of `cloud-security-and-guardrails-guide`
**Source:** `THREAT-MODEL.md` (`TM-03`, `TM-07`), `RISK-MAP.md`, this module's lessons 2-7

## Context

Module 7 introduced a vocabulary that governs every control this guide has built since Module 2:
**preventive** (could not happen) versus **detective** (it happened, and there is a record). This
document classifies the eight distinct guardrail mechanisms this module named or built, along two
independent axes: what **type** each one is, and how **built** each one is in this specific $0 lab.
Three of the eight mechanisms have compound build status on purpose — the resource is real and
validated, but the live enforcement or trigger could not be confirmed against this environment. That
compound status is stated explicitly in every row where it applies, never collapsed into a single
label that would overstate or understate what actually ran.

## The matrix

| # | Guardrail | Type | Build status | Lesson | Why |
|--:|---|---|---|---|---|
| 1 | Permission boundary (`AppServerRole`) | Preventive | Implemented (resource) + Representative (enforcement) | M7.3 | `aws_iam_policy` + `permissions_boundary` declared, `terraform validate`/`plan` real; the actual blocked call requires `IAM Policy Enforcement` (LocalStack Base/Ultimate only) |
| 2 | Service Control Policy (SCP) | Preventive | Named | M7.2, M7.6 | Same `IAM Policy Enforcement` dependency as row 1, plus a structural precondition (AWS Organizations) Andes Cargo does not have |
| 3 | AWS Organizations | Preventive | Named | M7.6 | Ecosystem-level gap (`VALIDACION.md`, `ALTA`, unassigned); Andes Cargo is a single account |
| 4 | AWS Control Tower | Preventive | Named | M7.6 | Orchestration layer over Organizations + SCPs; same precondition as row 3 |
| 5 | CloudTrail (`andes-cargo-trail`) | Detective | Implemented (resource) + Representative (event read) | M7.5 | `aws_cloudtrail` + dedicated bucket + policy declared, `terraform validate`/`plan` real; `lookup-events` attempted for real, failed on connection (no running LocalStack container in this environment) |
| 6 | AWS Config | Detective | Named | M7.4 | Not confirmed in LocalStack's free Hobby/Community tier |
| 7 | GuardDuty | Detective | Named | M7.4 | Not confirmed in LocalStack's free Hobby/Community tier |
| 8 | EventBridge rule + SNS (`andes-cargo-sensitive-iam-events`) | Detective | Implemented (resource) + Representative (trigger) | M7.7 | `aws_cloudwatch_event_rule` + `aws_cloudwatch_event_target` + `aws_sns_topic` declared, `terraform validate`/`plan` real; the live trigger against a real `AttachRolePolicy`/`CreateAccessKey` call attempted for real, failed on connection (same cause as row 5) |

## Reading the matrix

**Type (Preventive/Detective), 4 and 4.** Rows 1-4 could stop a bad action before it happens, at
different scopes: a single role (row 1), a single account (row 2), or a whole account hierarchy
(rows 3-4). Rows 5-8 never stop anything — they exist to leave or surface evidence after an action
already happened, at different levels of automation: a queryable history (row 5), continuous
config-state evaluation (row 6), ML-driven threat correlation (row 7), or a near-real-time push
notification (row 8).

**Build status, three distinct causes, never merged into one label.** Rows 1 and 2 share a cause:
`IAM Policy Enforcement` is a LocalStack Base/Ultimate feature, confirmed absent from the Hobby plan
this entire ecosystem uses. Rows 5 and 8 share a *different* cause: the resource itself is real HCL,
validated with the real Terraform engine, and the live confirmation was **attempted for real** in
this writing environment — it failed with `Could not connect to the endpoint URL`, the same
no-token limitation present since Module 1, unrelated to any paid LocalStack feature. Rows 3, 4, 6,
and 7 share a third cause each with its own reasoning: rows 3-4 are an ecosystem-level scope decision
(Andes Cargo is one account), rows 6-7 are an unconfirmed-coverage decision (no evidence LocalStack
Hobby includes them, unlike CloudTrail's demonstrated coverage).

## Consequences

`RISK-MAP.md` closes its final open row (`TM-03`, via row 5 of this matrix) as part of this module.
No row of this matrix is itself a new `TM-` finding — rows 1 and 8 harden `TM-07` (already `Resolved`
in M2.7) and add net-new detective coverage that `THREAT-MODEL.md` never named as a numbered
finding, the same pattern Module 1's `RISK-MAP.md` already established for Module 5 (community-rule
scanning: coverage, not resolution of a named risk).

## Alternatives considered

**Collapsing compound statuses (rows 1, 5, 8) into a single "Representative" label for simplicity.**
Rejected: it would erase the real, verifiable work this module actually did — three `terraform
validate`/`plan` runs against real HCL, not descriptions of HCL. A reader auditing this portfolio
should be able to tell, from this table alone, that the Terraform layer of these three controls is
real and the live-AWS-behavior layer is not, without reading three separate lessons to recover that
distinction.

**Treating AWS Control Tower as a fifth category alongside preventive/detective (mirroring its own
"preventive/detective/proactive" vocabulary).** Considered, rejected for this matrix specifically:
Control Tower is an orchestration layer over Organizations and SCPs, not a mechanism of its own — this
matrix classifies it by the family it belongs to in this module (multi-account governance, M7.6,
preventive), while lesson 6 documents its own three-way vocabulary in full for readers who need that
precision.

Step 3 — Verifying the document

Just like THREAT-MODEL.md and RISK-MAP.md, this document is read, not run — but its shape can be confirmed with the same simple commands as always, deterministic because you wrote the content yourself:

grep -cE '^\| [0-9] \|' GUARDRAILS-MAP.md
grep -E '^\| [0-9] \|' GUARDRAILS-MAP.md | grep -c 'Preventive'
grep -E '^\| [0-9] \|' GUARDRAILS-MAP.md | grep -c 'Detective'
grep -E '^\| [0-9] \|' GUARDRAILS-MAP.md | grep -c 'Implemented'
grep -E '^\| [0-9] \|' GUARDRAILS-MAP.md | grep -c 'Named'

What to expect (literal — you wrote this document's content yourself, so its shape is deterministic, run to write this lesson):

8
4
4
3
5

Eight total rows — not one of Step 1's eight pieces is missing, none is counted twice. Four preventive, four detective — the type axis lands perfectly balanced, without that having been a design goal, just the honest result of counting this module's real pieces. Three with compound status ("Implemented" appears in its label, alongside "Representative" in the same cell) — exactly this module's three "hands-on" lessons (3, 5, 7), each with real, validated HCL. Five purely named — SCP, Organizations, Control Tower, Config, and GuardDuty, none with a single Terraform resource declared anywhere in this module.


Why this table, and not a prose list

A list of eight paragraphs, each describing a mechanism, would contain exactly the same information — but it would lose the property that makes this table valuable as a portfolio deliverable: the ability to count at a glance. A technical interviewer, or you yourself in six months, can confirm in seconds that the balance is 4 preventive/4 detective, or that 3 of 8 have real HCL, without reading a single paragraph — the same advantage you already saw in RISK-MAP.md (Module 1, lesson 8), where the numbered table made it possible to verify with grep that no THREAT-MODEL.md risk was left off the map.


Common mistakes

Merging the five "Named" rows into a single generic "controls not built" row (over-simplification mistake). What happens: someone, reviewing this document, suggests collapsing SCP, Organizations, Control Tower, Config, and GuardDuty into a single row saying "several controls named, not built." How to spot it: if your version of the document has fewer than eight numbered rows. How to fix it: each of the five has a different reason for staying named — two share the IAM Policy Enforcement dependency (row 2, along with row 1), two share an ecosystem-scope decision (rows 3-4), two share a lack of confirmed LocalStack coverage (rows 6-7) — and merging them would hide those exact distinctions this module's lessons 2, 4, and 6 took the trouble to explain with separate evidence.

Classifying the permission boundary or CloudTrail's trail as plain "Representative," ignoring that the Terraform resource is real (underestimation mistake). What happens: someone, in a hurry, marks rows 1, 5, and 8 as purely "Representative," the same as the purely named rows. How to spot it: if your table doesn't distinguish, in any cell, between "no HCL was ever declared" (SCP, Organizations, Config, GuardDuty) and "the HCL is real, what's missing is live confirmation" (the boundary, the trail, the alarm). How to fix it: this document's Build status column deliberately uses the compound label "Implemented (resource) + Representative (...)" exactly for those three rows — losing that distinction underestimates lessons 3, 5, and 7's real work, which really ran terraform validate/plan, something none of the five named rows did.

Treating this document as if it resolved a new RISK-MAP.md row (expectation mistake, already clarified in the document's "Consequences" section). What happens: someone, finishing this project, looks to add an eighth row to RISK-MAP.md for this map. How to spot it: if your RISK-MAP.md, after this module, has more than seven rows. How to fix it: RISK-MAP.md already had its seven complete rows since Module 1 — this module closes the last one that stayed open (TM-03, via this matrix's row 5), but adds no new row. Rows 1 and 8 of this map are hardening of an already-resolved risk (TM-07) and new detective coverage that was never a numbered THREAT-MODEL.md finding — exactly the same pattern RISK-MAP.md already documented for Module 5, which also closes no row of its own.


Exercises

Exercise 1 — Reproduce the count from memory. Without looking back at Step 3, write from memory the five numbers this lesson's five grep commands would produce, and explain in one sentence why the second and third add up to exactly eight.

See solution

The five numbers are 8, 4, 4, 3, 5 — total rows, preventive, detective, with compound status ("Implemented"), and purely named, in that order. The second (4, preventive) and third (4, detective) add up to exactly eight because Type is an exhaustive, mutually exclusive axis over the same eight rows — every guardrail in this module is, without exception, one of the two types, never neither or both at once, so preventive plus detective always has to equal the total row count.

Exercise 2 — Explain why the fourth and fifth numbers (3 and 5) also add up to eight, with a different argument from Exercise 1's. The fourth command counts rows with "Implemented" in their cell, the fifth counts rows with "Named." Why do they also add up to exactly eight, and not less (if some row had neither word) or more (if some had both)?

See solution

Because Build status, just like Type, is also an exhaustive, mutually exclusive axis over the same eight rows: each of this module's eight pieces, by this guide's design, is either purely "Named" (no HCL was ever declared) or carries the compound label including "Implemented" (the resource is real) — there's no piece in this module that fits neither category (for example, something purely "Representative" with no real HCL behind it, which would be a third, distinct category), nor any piece that's simultaneously "Named" and "Implemented" at once, because those two words describe mutually exclusive states by this table's design.

Exercise 3 — Defend, in front of a technical interviewer, why only 3 of 8 controls in this module have real HCL. An interviewer, reviewing this document as a portfolio piece, asks: "why are most of the controls in your most advanced security module only 'named,' not built?" Respond with this guide's honesty criterion, without sounding defensive.

See solution

A solid answer distinguishes technical honesty from lack of effort: "This document is, precisely, proof that I understand the difference between what a $0 lab can demonstrate and what can only be declared as knowledge — five of those eight controls depend on LocalStack paid features (IAM Policy Enforcement), on a structure this single-account case study never had a reason to build (Organizations, Control Tower), or on services whose coverage on the free plan was never confirmed (Config, GuardDuty) — and each of those three reasons is documented with its own cited source, not lumped into a generic 'couldn't do it.' The three that do have real HCL — the permission boundary, the CloudTrail trail, the EventBridge alarm — really ran terraform validate and terraform plan, against Terraform's real engine, something I can show at any time. I'd rather have a document that precisely distinguishes these three causes than one that presents all eight as equally built, and have someone discover the difference only in production."


Summary and next step

In this module's final project you wrote GUARDRAILS-MAP.md, the matrix that classifies, with no ambiguity, this module's eight guardrail pieces along two independent axes: type (4 preventive, 4 detective) and degree of construction (3 with real, validated HCL, 5 purely named, each with its own cited reason). You verified the document's shape with grep, exactly as you already did with THREAT-MODEL.md and RISK-MAP.md in Module 1, and confirmed that RISK-MAP.md closes, with this module, its last open row (TM-03).

Before closing this module you should be able to: recite this matrix's eight pieces, with their type and build status, without looking at the document; explain the three distinct causes of why five of the eight stay named, without grouping them into one; and defend, against a direct question, why most controls in this module have no real HCL, without that sounding like a hidden weakness.

With this, cloud-security-and-guardrails-guide's Module 7 is complete: preventive and detective, precisely distinguished and applied to AppServerRole, to CloudTrail, and to a sensitive IAM event; SCPs, Organizations, Control Tower, Config, and GuardDuty, named with the same honesty as the rest of this guide. RISK-MAP.md closes its seven rows. Module 8 — the capstone — chains conftest, Trivy, and cosign into a real security gate inside the inherited pipeline, the final proof that a bad change can't cross it.

Resources

  1. This course, Module 1, lessons 7 and 8 — THREAT-MODEL.md and RISK-MAP.md, the same document-verified-with-grep discipline this project applies.
  2. This module, lessons 2 through 7 — the complete source for each of this matrix's eight rows, with its evidence and its source cited at the exact point where it appears.
  3. src/paths/aws-cloud-ecosystem/VALIDACION.md — the source for the HIGH-priority gap (Organizations/Control Tower/Landing Zone) cited in rows 3 and 4 of this matrix.
  4. AWS Prescriptive Guidance — Architecture decision records — the ADR format (Context, Decision/Matrix, Consequences, Alternatives considered) this document follows, already cited in RISK-MAP.md.