Modern DevSecOps Foundations Module 3 · IaC Foundations

The Scan Matrix

Last reviewed · content updated

Advanced

What you'll learn

~18 min
  • Design the tool × phase × environment × control matrix that governs every gate
  • Layer custom organizational policy over scanners - pre-deploy rules plus runtime enforcement
  • Write the whole thing down as an ADR with honest negatives
ℹLeadership brief

What it is: one governed table deciding which security scanners run at which stage, in which environment, blocking or warning, mapped to the controls they support.

What it buys: the answer to ‘what supports this control?’ from a table filter instead of archaeology; predictable behavior when a scanner is down mid-release; the control column Module 5’s evidence pipeline keys on.

What to fund: roughly an engineer-week for a mid-sized estate to build the matrix and wire each cell to a real CI surface, then review it as an architecture decision record - not a new tool.

From a gate to a system

Lesson 3.4 wired one scanner into one gate. Zoom out and the real questions appear: which tools run at which phases against which environments enforcing which controls — and who decided, and where is that written? Most estates answer with archaeology: days of someone reading pipeline YAML every time an auditor asks ‘what supports this?’ or a scanner goes down mid-release. The matrix makes both a thirty-second lookup. That time, and that audit exposure, is what leadership is buying. The reference estate answers with the best single artifact in its portfolio: a scan matrix, recorded as an architecture decision record. This lesson builds yours.

The matrix

Four dimensions, one table — abbreviated here to show the shape:

TOOL × PHASE × ENVIRONMENT × outcome (per control family)
─────────────────────────────────────────────────────────────────────────
secretscan pre-commit + all BLOCK always (IA-5) - no
PR environment relaxes secrets
checkov PR + deploy dev WARN on MED, BLOCK HIGH+ (CM-6)
checkov PR + deploy prod BLOCK MED+ (CM-6, SC-7)
custom OPA deploy dev filtered: HIGH/MED only
custom OPA deploy prod ALL severities enforced
runtime continuous all Azure Policy: audit dev,
policy deny prod (the always-on floor)
SAST/SCA PR + nightly all BLOCK on known-exploited;
WARN new criticals for 48h,
then BLOCK (SI-2)

Read the design rules out of it: block-vs-warn is decided per cell, not per tool (the same scanner blocks harder in prod — 3.4’s severity logic, now governed); every row carries its control IDs (the compliance question “what supports CM-6?” becomes a column filter — and Module 5’s evidence pipeline will consume exactly this mapping); phases are chosen for feedback speed (secrets at pre-commit because seconds matter; SCA nightly because supply-chain moves daily); and one dimension most matrices forget — graceful degradation is specified per cell: what happens when the scanner itself is down (fail the gate? proceed with a logged waiver?). The reference ADR decides that explicitly per tool: secret scanning fails closed always; advisory layers may proceed-with-waiver during outages. Deciding it in the document beats deciding it during the outage.

ℹWhat the control IDs in that column are - and what they are not

IA-5, CM-6, SC-7, SI-2 are controls from NIST SP 800-53, the catalog every federal system (and anything FedRAMP-authorized) is assessed against. If your auditors grade you on SOC 2, ISO 27001, or CIS Controls instead, keep the column and fill it with your catalog’s IDs — but pick one primary catalog, because Module 5’s evidence pipeline keys on this column. Two honesty rules: a scanner supports evidence for a control, it never satisfies one (an assessor reads “enforces CM-6” as a claim about the whole control, and a scanner tests one condition of it); and Azure Policy here is the Azure name for a runtime policy engine; GCP Organization Policy is the closest equivalent, while on AWS the deny-mode floor is Service Control Policies — AWS Config detects and remediates but cannot refuse a deployment. For leadership: this column is what turns “are we compliant with X?” from a two-week project into a filter on a table.

The custom layer: your rules, two enforcement points

Scanners know the world’s best practices; they don’t know your rules (“every resource carries a cost-center tag,” “no public IPs in the data subnets,” “storage in sovereign regions only”). The estate’s custom layer is textbook two-point enforcement:

PRE-DEPLOY OPA/Conftest over the Terraform plan JSON - ~135 vendored Rego
rules in the estate; runs in the pipeline as another gated step
(same JUnit/SARIF surfacing as 3.4). Environment-scoped
EXCEPTION FILES per the matrix: dev's filter admits only
HIGH/MED rules; prod enforces everything - same rule set,
declared filters, no forked policy code.
RUNTIME Azure Policy as the always-on floor - audit-mode in dev,
deny-mode in prod - catching whatever arrives OUTSIDE the
pipeline: the portal edit, the imported resource, the drift.

Two points because they fail differently: the pipeline gate gives fast feedback but only sees what flows through pipelines; runtime policy sees everything but after the fact (or, in deny-mode, at deployment against your tenant — which 3.4’s preflight now surfaces at plan). The pair, plus the environment-scoped exceptions living in declared files rather than divergent rule copies, is what “two-layer policy-as-code” means when it’s real. (Cloud Modernization’s policy lesson taught this stack’s silhouette; this is its wiring diagram.)

Write it down: the ADR

The matrix’s natural home is an architecture decision record, and the reference estate’s ADR collection is worth imitating for form alone — Context, Decision, Consequences, with the consequence section containing honest negatives. The scan-matrix ADR’s own negatives, paraphrased: pipeline time increases measurably; environment-scoped exceptions require review discipline or they drift into permanent relaxation; two policy layers mean two places a rule can live, so the matrix must name which layer owns each rule class. An ADR whose negatives section is empty is advocacy, not a decision record — the honest costs are what make the next reader trust the recorded benefits. (You’ve now seen this honesty rule as the five-value grading scale in Zero Trust and the per-phase ROI table in Cloud Modernization: same virtue, decision-record edition.)

Produce our scan-matrix ADR: (1) the full matrix for our estate - tools
(secretscan, checkov, our OPA layer, runtime policy, SAST/SCA), phases,
environments, block/warn per cell, control IDs per row, and a graceful-
degradation column deciding each tool's outage behavior; (2) the two-layer
custom policy design - which rule classes live pre-deploy vs runtime, the
per-environment exception file format (with the reason-string requirement);
(3) ADR form: context, decision, consequences WITH at least three honest
negatives; (4) the implementation checklist mapping each matrix cell to
the template step (3.4) or policy assignment that enforces it - a cell
with no implementing artifact is flagged ASPIRATIONAL, not silently
counted as covered.

That last flag is this training’s recurring honesty device pointed at the matrix itself: the reference estate’s biggest wart (3.4 mentioned it) is scans that exist but default off — a matrix full of aspirational cells. The checklist makes the gap enumerable, which makes it burn-downable.

KNOWLEDGE CHECK

Writing the ADR, the team debates where the 'no public IPs in data subnets' rule should live: the OPA pre-deploy layer, runtime Azure Policy, or both. The matrix's design principles answer this — how?

Practice status — among mature regulated delivery programs, commercial and federal

(a few rows carry a more specific status - principle, canon, suspended - where one of the five would mislead)

PracticeStatusAlso called
scan matrix as an ADRreference-shopsecurity-gate decision record
severity-gated blocking per cellcommon baselinepolicy-as-code gates
two enforcement points (pre-deploy + runtime)common baseline deny-mode engines: Azure Policy / GCP Org Policy / AWS SCPs (AWS Config = detect and remediate)—
control-ID column on every rowstrong optionalcontrol mapping - supports evidence, never satisfies
outage behavior per cellstrong optionalfail-open / fail-closed policy

Scale: required | common baseline | strong optional | reference-shop (seen only at organizations that publish their own practice) | emerging

Key takeaway

Govern the gates as a system: one matrix deciding tool × phase × environment × outcome with control IDs on every row and outage behavior on every cell; custom rules layered pre-deploy (fast, filtered per environment by declared exception files) and at runtime (the always-on floor, owning the hard invariants); all of it recorded as an ADR whose negatives are honest and whose aspirational cells are flagged, not counted. Module 3 is complete — your infrastructure has a paved, pinned, gated road. Module 4 turns to the software riding on it.

LEADERSHIP DECISION approve one governed matrix (an ADR) as the record of
which gates run where - and fund nothing that bypasses
it
PRACTITIONER ACTION build the matrix, wire every cell to a real CI surface,
encode outage behavior per cell
SUCCESS MEASURE time to answer 'what supports control X?' drops from
days of archaeology to a table filter; zero silently-
unwired stacks
Search lessons