Threat model for a legacy system
Learn to build a threat model for a legacy system: identify assets, trust boundaries, and attack paths. This hands-on security lesson covers practical steps, troubleshooting, and links to the next topic in the Security foundations track.
Focus: build a threat model for a legacy system
You've just inherited an application that predates your cloud provider, your CISO, and possibly your own coding career. It's monolithic, it's on a VM that 'nobody really touches,' and it's connected to your Active Directory. Everyone knows it's a problem, but nobody knows where to even start. The good news: you don't need a magic wand or a crystal ball. You need a threat model. This lesson shows you how to build a threat model for a legacy system — a repeatable, methodical process to turn vague anxiety into a concrete, prioritized action plan.
The problem this lesson solves
Legacy systems are the silent back doors of the modern enterprise. They run critical business logic — payroll, inventory, patient records — but they were built before multi-factor authentication, before API gateways, before 'cloud-native' meant anything. They're rarely patched, often run on end-of-life OSes, and are frequently the target of attackers because they're easy to compromise.
Without a threat model, you're flying blind. You might spend weeks hardening an obscure internal dashboard while an attacker is already exfiltrating data through an exposed database port you never noticed. Or you might panic and try to 'fix everything' at once, burn out your team, and leave the system with more vulnerabilities than before. A threat model forces you to systematically answer the questions that matter:
- What data is on this system, and why does it matter?
- Who or what can reach it, and from where?
- What happens if each component is compromised?
- What's the most efficient path to a data breach?
There's also a human factor: legacy systems often have no dedicated owner, no documentation, and a single 'guru' who holds all the knowledge. A threat model is a way to capture that knowledge in a structured format that you can defend to management, auditors, and your future self.
Core concept / mental model
Think of a threat model not as a security audit, but as a blueprint of an attack. If a bank robber wants to rob a bank, they don't walk in the front door with a ski mask (well, not always). They study the building — where the vault is, which doors are alarmed, where the security cameras are blind. A threat model is that same reconnaissance, but for your software.
The anatomy of a threat model
Building a threat model for a legacy system follows a core mental model with five components:
- Asset: anything of value — data, functionality, credentials, even the system's availability.
- Attack surface: every point where an attacker can interact with an asset — network ports, APIs, UIs, file shares, physical console.
- Trust boundary: a line that separates zones of different trust levels. For a legacy system, these are usually: untrusted internet, corporate network, DMZ, and internal trusted network.
- Threat agent: who or what would attack this system — a script kiddie, a disgruntled insider, a state-sponsored actor.
- Threat scenario: a narrative that describes how an attacker could exploit an attack surface to reach an asset — e.g., 'A remote attacker uses SQL injection in the search box to dump the customer table.'
A useful mnemonic is STRIDE, the classic threat-modeling framework:
- Spoofing
- Tampering
- Repudiation
- Information disclosure
- Denial of service
- Elevation of privilege
Your threat model is essentially a list of STRIDE categories applied to each component, with a risk rating for each scenario.
Pro tip: You don't have to invent a proprietary process. The industry-standard threat models are STRIDE (Microsoft) and PASTA (narrative-driven). For a legacy system, START with a simple asset-and-surface inventory; the framework is secondary.
How it works step by step
Building a threat model for a legacy system is a disciplined, step-by-step process. Follow these steps in order, and you'll get a complete picture.
Step 1: Define the system boundaries
Before you can model threats, you must know what's in the system and what's outside. For a legacy system, this is often the hardest part. Draw a box around the system, including:
- Application code (which language, version?)
- Runtime environment (OS, virtual machine, containers?)
- Databases and data stores
- External integrations (payment gateways, LDAP, email servers)
- Network segments and firewalls that control access
Use your existing network diagrams, DNS records, and configuration management tools as starting points. Ask the old hands: 'What else talks to this system?'
Step 2: Identify assets and their value
List every piece of data and every function that, if compromised, would hurt the business. Rank them by impact:
- High: customer PII, financial records, credentials, any data that would trigger breach notification laws.
- Medium: internal emails, business plans, availability of a critical process.
- Low: public marketing content, non-sensitive operational metrics.
Step 3: Map the attack surface
For every component inside the boundary, enumerate how an attacker could touch it:
- Open TCP/UDP ports (use
nmap) - HTTP endpoints and methods
- Database connections that are open to the network
- Admin panels with weak or default credentials
- Physical access points (web kiosks, legacy workstations)
- File shares and unencrypted backups
Pro tip: For a legacy system, don't forget the human attack surface. Social engineering exploits the trust boundary between 'support' and 'the maintenance account' — and legacy systems are notoriously prone to shared passwords and admin-on-demand access.
Step 4: Apply STRIDE (or a lightweight equivalent)
For each component, walk through the six STRIDE categories and ask, 'Can this happen?' For example, an old Java app:
- Spoofing: Can an attacker guess or reuse credentials? Is there any session fixation?
- Tampering: Can an attacker modify data in transit or at rest? Is the communication encrypted?
- Repudiation: Are there logs? Can a user deny they did something?
- Information disclosure: Are error messages revealing stack traces? Is sensitive data exposed via direct object references (e.g.,
/user/1)? - Denial of service: Can a simple flood of requests exhaust the thread pool? Is there a rate limiter?
- Elevation of privilege: Can a low-privilege user escalate via a known CVE in the framework?
Document each scenario you find, along with its likelihood and impact.
Step 5: Prioritize risks and design mitigations
You won't be able to fix everything tomorrow. Instead, rank each threat scenario by a simple score: Impact × Likelihood. Use a scale of 1–3 for each, so your worst score is 9 and the most urgent need your attention first.
For each risk, define a mitigation strategy. Sometimes the only realistic mitigation is to retire a feature, isolate the component behind a network segment, or compensate with monitoring. This step is about making decisions — not writing code.
Step 6: Document and share
A threat model is only useful if people read it. Create a living document (a Markdown file in your repo, a Confluence page, or a spreadsheet) that includes:
- System description and diagram
- Assets and their values
- Attack surface list
- Threat scenarios and risk scores
- Chosen mitigations and owners
- Review date (make it a recurring calendar event)
Hands-on walkthrough
Let's build a threat model for a realistic legacy system: a simple monolith running Apache on CentOS 6 with a phpMyAdmin instance exposed to the corporate network, and a MySQL database storing customer records. The system is at 10.1.2.3/24.
Step 1: Discover the attack surface with nmap
nmap -sV -p- 10.1.2.3 --open
Expected output (abbreviated):
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 5.3 (protocol 2.0)
80/tcp open http Apache httpd 2.2.15
3306/tcp open mysql MySQL 5.1.73
8080/tcp open http-proxy Apache Tomcat/Coyote JSP engine 1.1
You've already learned something critical: MySQL is exposed directly to the network — that's a high-severity finding because it's a data store, not an app server. And Apache Tomcat on port 8080 could be running a vulnerable application.
Step 2: Map assets and apply STRIDE (using a table)
Create a table like this in your document:
| Component | Attack surface | STRIDE threats found | Likelihood (1-3) | Impact (1-3) | Risk score |
|---|---|---|---|---|---|
| MySQL (port 3306) | Direct DB access from corporate LAN | Info disclosure (weak auth, plaintext traffic), Tampering (SQL injection on stored procedures) | 3 | 3 | 9 |
| phpMyAdmin (port 80) | Web UI reachable from corp net | Credential brute-force (Spoofing), Code execution if CVE-2016-5734 applies (Elevation of privilege) | 3 | 3 | 9 |
| Apache Tomcat (port 8080) | Internal web app | Remote code execution if any old app has a known RCE (Elevation of privilege), DoS via thread exhaustion | 2 | 2 | 4 |
| SSH (port 22) | Remote admin access | Credential brute-force (no fail2ban), Privilege escalation if password reuse exists | 3 | 2 | 6 |
Step 3: Automate vulnerability triage (the practical part)
Now you have a risk-ranked list. Let's write a quick script to automate the initial low-hanging fruit checks — because legacy systems are often full of known CVEs.
# simple_legacy_scan.py
import subprocess
import re
HOST = "10.1.2.3"
BAD_VERSIONS = {
"OpenSSH 5.3": "CVE-2018-15473 (user enumeration)",
"Apache httpd 2.2.15": "CVE-2012-0053 (error page XSS)",
"MySQL 5.1.73": "CVE-2012-2122 (auth bypass)",
}
def run_nmap():
result = subprocess.run(["nmap", "-sV", HOST], capture_output=True, text=True)
return result.stdout
def find_findings(output):
findings = []
for key, cve in BAD_VERSIONS.items():
if re.search(re.escape(key), output, re.IGNORECASE):
findings.append((key, cve))
return findings
if __name__ == "__main__":
print("Scanning legacy host...")
output = run_nmap()
findings = find_findings(output)
if findings:
print("Critical findings:")
for service, cve in findings:
print(f" - {service}: {cve}")
else:
print("No known vulnerable versions found.")
Run it with:
python simple_legacy_scan.py
Expected output:
Scanning legacy host...
Critical findings:
- MySQL 5.1.73: CVE-2012-2122 (auth bypass)
- Apache httpd 2.2.15: CVE-2012-0053 (error page XSS)
This is a huge win — in under a minute, you've automated evidence for two critical findings that you previously had to research by hand. This script is the basic foundation of a threat-modeling tool that you can expand with more CVEs as you go.
Step 4: Use the threat model to choose mitigations
Now that you have a risk-ranked list, decide what to do:
- MySQL on port 3306: Highest risk (score 9). Mitigation: Move the data store to a separate network segment, or at the very least restrict access to specific app-server IPs at the firewall. Add
--skip-networkingif it's only used locally. - phpMyAdmin: Score 9. Best mitigation: Remove it entirely! Use
mysqlCLI from an admin jump host. If you must keep it, put it behind a VPN and require 2FA (Apache modulemod_auth_mellonwith SAML). - SSH: Score 6. Mitigation: Disable password authentication, use SSH keys, and install
fail2ban. If you can't restart the service, at least setMaxAuthTries 3. - Apache/Tomcat: Score 4, but a single RCE would be catastrophic. Mitigation: Monitor logs, patch if possible, and put the management ports behind a firewall.
Pro tip: In a legacy system, you can't always 'just update.' The mitigation is often network isolation + monitoring rather than a patch you can't deploy. That's acceptable if the risk is documented and accepted.
Compare options / when to choose what
There are several established ways to structure a threat model. Here's a quick comparison:
| Approach | Best for | Methodology | Output | Legacy-system fit |
|---|---|---|---|---|
| STRIDE (Microsoft) | Classic engineering teams | Categorize per component | Checklist of threats per component | Great for prioritizing patches |
| PASTA (Process for Attack Simulation & Threat Analysis) | Security professionals, high-risk systems | Attack-centric, requires deep security knowledge | Attack tree and risk score | Overkill for a quick five-step process |
| Attack trees | Adversarial thinking | Model attacker's objectives as tree branches | Graphical but not a standard | Useful before you have a full inventory |
| Risk-catalog driven (e.g., vendor reports) | Fast, evidence-based patching | Match software versions against CVEs | List of vulnerabilities | Excellent first pass; doesn't model attacker behavior |
| Custom lightweight (like our exercise) | Teams under time pressure | Asset + attack surface + STRIDE rating | Actionable prioritized list | Perfect balance for most legacy systems |
- STRIDE is the most structured and easiest to defend in a security review.
- PASTA is more thorough but requires deeper expertise to do well.
- Attack trees are great for illustrating a specific attack scenario but get unwieldy for entire systems.
For a legacy system you've just inherited, start with the custom approach (Step 4 above) because it's quick and yields immediate action items. Later, if your organization demands rigor, you can align it to STRIDE or PASTA.
Troubleshooting & edge cases
Problem: I have no network diagram, no documentation, and the 'expert' who built this is long gone.
- Solution: Use
nmapand thenetstatoutput on the host to build the inventory yourself. Checkps auxto list what's running. Look at the Apache config (httpd.conf) for virtual hosts. It's tedious but doable.
Problem: You scan the system for known CVEs and find an RCE, but you can't patch it.
- Solution: Don't ignore it. Document it as a known risk with a mitigation. Isolate the system with a specific firewall rule that only allows the minimum required traffic. Add a script to monitor for unusual outbound connections — attackers often trigger the RCE to establish a reverse shell.
# Quick one-liner to watch for new outbound connections from the legacy host
sudo tcpdump -i eth0 'tcp[13] & 2 != 0' -l | grep -E '10\.1\.2\.3' >> /var/log/legacy_exfil.log
Problem: Your highest-risk item is 'internal admin panel' but the panel has no TLS, and you can't change it because 'it breaks.'
- TLS is usually near-impossible to retrofit — but you can put the panel behind a VPN (even an SSH tunnel) so that it's never exposed on the open LAN. That's a practical mitigation.
Problem: You get stuck trying to decide whether a risk is 'high' or 'critical'.
- Solution: Use the score (Impact × Likelihood) to force a decision. If your scoring says 9/9, is it really critical? Then treat it as such. Don't let feelings dictate risk levels.
Edge case: The system is so old it's not even connected to a version control system, so you can't see what changed.
- Solution: The threat model is your source of truth — it's a snapshot of today's risk. Set a calendar reminder to re-run it after any change, or every quarter.
What you learned & what's next
You can build a threat model for a legacy system. You now know how to:
- Define the system boundary and list assets with business context.
- Map the attack surface using network scanning and component lists.
- Apply STRIDE to systematically identify threats.
- Prioritize risks with a simple score, and choose effective mitigations — even if 'patch' isn't an option.
- Document your process so it's auditable and maintainable.
You've also proven that 'legacy' doesn't mean 'impossible.' A systematic, evidence-based threat model takes the guesswork out of your security work.
Next in the track: You're now ready to dive deeper into the CIA triad and how to map threat models to confidentiality, integrity, and availability. You'll learn which security controls protect which of the three pillars, and how to build a balanced security strategy. That's the perfect next step to turn your threat model into a concrete security roadmap.
Final pro tip: A threat model is a living document. Revisit it at least quarterly, and always after any major change. The first time is the hardest — the second time is the reward.
Practice recap
Now try building a threat model for an imaginary legacy system on your own. List its assets, draw the trust boundaries, and do a quick nmap scan if you have access to a lab VM. Score the risks and explain to a friend or colleague why you chose your top three fixes. The more you practice, the faster this process becomes.
Common mistakes
- Skipping the asset inventory and jumping straight to vulnerability scanning — you end up with a pile of CVEs but no business context to prioritize them.
- Trusting the network diagram from five years ago instead of actually scanning the host to find what's exposed right now.
- Treating 'we can't patch, so we do nothing' as a valid outcome. Network isolation and monitoring are legitimate mitigations for legacy systems.
- Rating every risk as 'critical' out of fear — that makes the model useless. Use a score like Impact × Likelihood to force prioritization.
Variations
- Use a dedicated threat-modeling tool like OWASP Threat Dragon or Microsoft Threat Modeling Tool to generate diagrams and reports automatically.
- Adopt the PASTA methodology if your organization requires a more attack-centric, risk-aware approach.
- Run a version-based CVE scan (like the script in the lesson, or with tools like Nmap's vulnerability scripts) as a fast pre-step before any manual threat modeling.
Real-world use cases
- A healthcare startup inherits a legacy patient-billing app on Windows Server 2008, and the threat model exposes an unpatched RDP port that must be firewalled now.
- A bank uses threat modeling to decide whether to upgrade a 20-year-old clearinghouse service or isolate it behind a new micro-segmentation firewall until it's retired.
- A government agency builds a threat model for an old social-benefits database to justify a migration project to management with a documented risk score.
Key takeaways
- A threat model is a structured way to turn general anxiety about a legacy system into a prioritized, defensible action plan.
- Define the system boundary first — you can't protect what you don't know exists.
- Inventory assets and attack surfaces: each open port, endpoint, and admin panel is a potential entry point.
- Apply STRIDE to systematically uncover threats; it's a checklist that keeps you from missing categories.
- Risk = Impact × Likelihood; this simple score forces you to focus on what matters most.
- The output is a living document — schedule a recurring review to keep it relevant.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.