Most threat modeling exercises produce a PDF that nobody reads after the sprint ends. The model sits in a shared drive while the actual system drifts β new services deployed, ports opened, dependencies added β and the model never gets updated. By the time an incident occurs, the document describes a system that hasn't existed for months.
This guide takes a different approach: threat modeling as a living artifact tied to your actual infrastructure. We start with assets, not adversaries. We map controls to specific threats on specific assets. And we build verification steps that fail loudly when the model diverges from reality. Tested on a three-person team managing Ubuntu 24.04 servers, a GitLab CI pipeline, and a handful of self-hosted services.
Before you start, gather: your architecture diagram (even a napkin sketch), a list of every external dependency (CDNs, APIs, managed services), and your incident history from the last 12 months. If you don't have an incident history, that's your first finding β you're not detecting what's already happening.
1. Where Your Identity Actually Leaks
Adversary-first modeling ("who might attack us?") produces generic threat lists. Asset-first modeling ("what breaks if this fails?") produces actionable priorities. Start with a structured inventory.
1.1 Asset Classification Template
For each asset, record:
Asset ID: A-001
Name: Customer PII Database
Type: Data store (PostgreSQL 16 on Ubuntu 24.04)
Location: VPC subnet 10.0.2.0/24, us-east-1a
Sensitivity: HIGH (GDPR Art. 9 special categories)
Integrity Requirement: TAMPER-EVIDENT
Availability Requirement: RPO 1h, RTO 4h
Owners: @backend-team, @dba-team
Dependencies:
- AWS RDS managed service (control plane)
- Application servers (read/write)
- Backup encryption keys (KMS key alias/prod/pii-backup)
- Log aggregation (Datadog, 30-day retention)
Threat Surface:
- SQL injection via API endpoints
- Credential reuse from CI/CD secrets
- Snapshot exfiltration via compromised IAM
- Insider query abuse (no row-level audit)
1.2 Dependency Mapping: The Hidden Attack Surface
Every managed service you use is a trust boundary. Document what you control vs. what the provider controls:
| Dependency | Provider Controls | You Control | Verification Method |
|---|---|---|---|
| AWS RDS (PostgreSQL) | OS patching, replication, backup storage, network isolation | Database users, encryption keys, query audit, parameter groups | Monthly: verify encryption at rest, check parameter group drift, test point-in-time recovery |
| Cloudflare (WAF + DNS) | Edge network, DDoS mitigation, certificate management | WAF rules, DNS records, origin certificates, rate limit configs | Weekly: export WAF rules to Git, verify DNSSEC, test origin cert renewal |
| GitLab SaaS (CI/CD) | Runner infrastructure, container registry, web UI | Pipeline definitions, variables, protected branches, runner tags | Per-deploy: verify no secret leakage in job logs, check protected branch enforcement |
| SendGrid (Transactional Email) | IP reputation, delivery infrastructure, suppression lists | API keys, domain authentication, template content, webhook endpoints | Monthly: rotate API keys, verify SPF/DKIM/DMARC alignment, check suppression list growth |
2. The Mitigation Plan
Stop modeling "nation-state actors" unless you have evidence they're targeting you. Model capabilities you've actually observed or that match your exposure.
2.1 Capability Tiers (Observed in Our Environment)
| Tier | Label | Capabilities | Observed Indicators | Assets at Risk |
|---|---|---|---|---|
| 1 | Opportunistic Automation | Credential stuffing, default credential scanning, known CVE exploitation (<30 days old), cryptojacking | Daily SSH brute force on port 22, weekly vulnerability scanner hits on /wp-admin/, monthly crypto miner deployments on exposed Docker APIs | All internet-facing compute, any service with default credentials |
| 2 | Targeted Crimeware | Phishing with domain spoofing, session token theft, supply chain compromise (npm/PyPI), ransomware with data exfiltration | Quarterly phishing campaigns against team, two supply chain incidents in 2024 (malicious dependency updates), one ransomware attempt via compromised contractor VPN | Identity systems, CI/CD pipelines, developer workstations, backup systems |
| 3 | Persistent Access | Living-off-the-land, kernel rootkits, firmware implants, BGP hijacking, cloud control plane abuse | Zero observed directly. Modeled based on industry reporting for our sector (SaaS, <100 employees). Included for completeness. | Core infrastructure, encryption key hierarchy, network backbone, logging pipeline |
2.2 Capability-to-Asset Mapping
For each asset, ask: which tiers can reach it, and through what path?
Asset A-001 (Customer PII Database):
Tier 1: NO direct path (private subnet, no internet egress)
Tier 2: YES via compromised application server (A-003) β SQL injection β data exfil
Tier 3: YES via compromised DBA workstation (A-007) β SSH key reuse β direct DB access
Tier 3: YES via compromised AWS IAM (A-011) β RDS snapshot copy β cross-account share
3. Step-by-Step Execution
Controls without threat mappings are waste. Threats without controls are gaps. Build a bidirectional traceability matrix.
3.1 Control Catalog with Threat Traceability
| Control ID | Control Description | Addresses Threat | Asset(s) Protected | Verification Method | Owner |
|---|---|---|---|---|---|
| C-001 | Parameterized queries enforced via ORM; raw SQL blocked by CI lint | Tier 2: SQL injection via API | A-001, A-002 | CI check: semgrep rule blocks raw query strings; quarterly manual review | @backend-team |
| C-002 | RDS encryption at rest with customer-managed KMS key; key rotation annually | Tier 2: Snapshot exfiltration; Tier 3: Cross-account snapshot share | A-001 | Monthly: AWS Config rule checks encryption; annual key rotation test | @dba-team |
| C-003 | pgAudit extension logging all DML/DDL to CloudWatch; 90-day retention | Tier 2: Insider query abuse; Tier 3: Persistent access data staging | A-001, A-002 | Weekly: log volume alert; monthly: sample query review for anomalies | @dba-team |
| C-004 | GitLab protected branches: main requires 2 approvals, signed commits, pipeline pass | Tier 2: Supply chain compromise via malicious PR | A-003, A-004, A-005 | Per-MR: enforcement verified; quarterly: audit branch protection settings | @platform-team |
| C-005 | Hardware security keys (YubiKey 5) required for all GitLab, AWS, Cloudflare access | Tier 2: Credential stuffing, session theft; Tier 3: Valid accounts | A-003 through A-012 | Monthly: enumerate users without registered keys; quarterly: attestation verification | @security-team |
| C-006 | VPC flow logs to S3 + Athena; GuardDuty enabled on all accounts | Tier 1: Reconnaissance; Tier 2: Data exfiltration; Tier 3: Lateral movement | A-008, A-009, A-010 | Daily: GuardDuty findings review; weekly: flow log query for anomalous egress | @platform-team |
| C-007 | Backup encryption with separate KMS key; cross-region replication; quarterly restore test | Tier 2: Ransomware encryption; Tier 3: Backup destruction | A-001, A-002, A-006 | Quarterly: full restore to isolated VPC; verify data integrity checksums | @dba-team |
3.2 Gap Analysis: Unmapped Threats and Orphan Controls
Run this query monthly against your control catalog:
# Threats with no controlling control
SELECT t.threat_id, t.description
FROM threats t
LEFT JOIN controls c ON c.addresses_threat = t.threat_id
WHERE c.control_id IS NULL;
# Controls protecting no mapped threat
SELECT c.control_id, c.description
FROM controls c
LEFT JOIN threats t ON t.threat_id = c.addresses_threat
WHERE t.threat_id IS NULL;
In our last review, this surfaced three gaps: no control for BGP hijacking of our Cloudflare prefixes (Tier 3), no control for malicious GitLab runner registration (Tier 2), and C-008 (automated dependency scanning) was protecting a threat we'd reclassified as out-of-scope.
4. Staying Invisible: Ongoing Habits
A threat model that doesn't detect its own obsolescence is theater. Build automated drift checks into your deployment pipeline.
4.1 Infrastructure Drift Checks
#!/bin/bash
# drift-check.sh β runs in CI after every terraform apply
set -euo pipefail
echo "=== Asset Inventory Drift ==="
# Compare actual AWS resources against asset inventory
aws resourcegroupstaggingapi get-resources \
--tag-filters Key=AssetID,Values=* \
--query 'ResourceTagMappingList[].ResourceARN' \
--output text | tr '\t' '\n' | sort > /tmp/actual-assets.txt
comm -23 /tmp/actual-assets.txt docs/asset-inventory.txt > /tmp/new-assets.txt
comm -13 /tmp/actual-assets.txt docs/asset-inventory.txt > /tmp/removed-assets.txt
if [ -s /tmp/new-assets.txt ]; then
echo "NEW ASSETS DETECTED β update threat model:"
cat /tmp/new-assets.txt
exit 1
fi
echo "=== Control Implementation Drift ==="
# Verify C-001: parameterized query enforcement
if ! grep -r "semgrep.*sql" .gitlab/ci/lint.yml; then
echo "C-001 MISSING: SQL injection lint rule not in pipeline"
exit 1
fi
# Verify C-005: hardware key enforcement
USERS_WITHOUT_KEYS=$(aws iam get-credential-report --query 'Content' --output text | base64 -d | awk -F, '$4=="false" && $8=="true" {print $1}')
if [ -n "$USERS_WITHOUT_KEYS" ]; then
echo "C-005 DRIFT: Users without MFA keys: $USERS_WITHOUT_KEYS"
exit 1
fi
# Verify C-007: backup restore test completed in last 90 days
LAST_RESTORE=$(aws s3 ls s3://backup-restore-logs/ | tail -1 | awk '{print $1}')
DAYS_SINCE=$(( ($(date +%s) - $(date -d "$LAST_RESTORE" +%s)) / 86400 ))
if [ $DAYS_SINCE -gt 90 ]; then
echo "C-007 STALE: Last backup restore test was $DAYS_SINCE days ago"
exit 1
fi
echo "All drift checks passed."
4.2 Organizational Drift Checks
Technical controls drift. So do human processes.
- Ownership changes: When an asset owner leaves, their assets become orphaned. Quarterly: cross-reference asset owners against current team roster.
- Dependency changes: New SaaS tools added without security review. Monthly: diff approved vendor list against expense reports and OAuth grants.
- Incident feedback loop: Every post-incident review must answer: "Did our threat model predict this? If not, what capability or asset was missing?" Update the model within 5 business days.
5. Identity Questions
The model lives in Git alongside infrastructure code. Changes follow the same review process.
5.1 Repository Structure
threat-model/
βββ assets/
β βββ inventory.yaml # Machine-readable asset list
β βββ classification.md # Human-readable definitions
βββ adversaries/
β βββ capability-tiers.md # This document's Section 2
β βββ attck-mapping.yaml # Technique-to-tier mapping
βββ controls/
β βββ catalog.yaml # Machine-readable control list
β βββ traceability.csv # ControlβThreatβAsset matrix
βββ drift/
β βββ drift-check.sh # Automated checks (Section 4.1)
β βββ organizational-checks.md # Manual checks (Section 4.2)
βββ reviews/
βββ 2026-Q1-review.md # Quarterly review template
βββ incident-feedback/ # Per-incident model updates
5.2 Quarterly Review Template
# Threat Model Review β 2026-Q1
## Assets Added/Removed
- ADDED: A-013 (Grafana Loki log aggregation) β Tier 2 exposure via ingest API
- REMOVED: A-004 (Legacy Jenkins) β decommissioned March 15
## Capability Updates
- Tier 2: Added T1608.001 (Stage Capabilities: Upload Malware) β observed in supply chain incident INC-2026-003
- Tier 3: No changes
## Control Changes
- C-008 RETIRED: Dependency scanning moved to GitLab SAST (covers same threat)
- C-010 ADDED: Loki query rate limiting + tenant isolation (mitigates A-013 log injection)
## Drift Incidents
- 2026-02-14: drift-check.sh caught untagged RDS instance (A-014) β tagged and modeled same day
- 2026-03-01: Organizational check found 3 users without hardware keys β enrolled within 24h
## Incident Feedback
- INC-2026-003 (supply chain): Model predicted Tier 2 supply chain but missed malicious runner registration vector. Added T1608.001 and C-011 (runner registration approval).
## Open Questions
- Should we model Tier 3 firmware implants for our Dell PowerEdge fleet? (Cost: $2k/quarter for verification)
- Cloudflare BGP hijack monitoring: build vs. buy?
6. Verification Checklist: Model Health Indicators
Before trusting the model, verify these points:
- Asset inventory completeness: Every resource in cloud provider tagged with AssetID; zero untagged resources.
- Traceability closure: Zero threats without controls; zero controls without threats.
- Drift check passing: Automated drift-check.sh exits 0 in CI for 30 consecutive days.
- Quarterly review completed: Review document merged within 10 business days of quarter end.
- Incident feedback loop: Every incident since last review has a model update linked in incident-feedback/.
- Owner accountability: Every asset and control has a current team member as owner.
- Capability calibration: Tier definitions reviewed against observed incidents and sector threat intel.
The difference between a threat model that guides decisions and one that decorates a wiki is whether it fails loudly when reality diverges. Our drift-check.sh has caught 12 infrastructure changes that would have gone unmodeled β an untagged RDS instance, a security group opened to 0.0.0.0/0, a new IAM user without hardware MFA. Each catch is a validation that the process works.
Sources & References
- NIST SP 800-154: Guide to Data-Centric System Threat Modeling β asset-centric methodology
- MITRE ATT&CK: https://attack.mitre.org/ β technique framework for capability mapping
- Microsoft Threat Modeling Tool: SDL Threat Modeling β STRIDE and data flow diagramming
- OWASP Threat Modeling: OWASP Threat Modeling Guide β community methodology and templates
- AWS Well-Architected Security Pillar: Security Pillar Whitepaper β cloud-specific threat modeling
Threat modeling processes are validated against infrastructure before publication. This model reflects our actual production environment as of September 2026. Adapt asset classifications, capability tiers, and control mappings to your specific context.