The sooner you start doing it, the easier it is to get done!
I wrote this content back in 2020, but - surprise, surprise - it's still quite relevant today.
"Attack surface"?
To paraphrase Wikipedia, “[t]he attack surface [..] is the sum of the different points (or "attack vectors") where a malicious party (the "attacker") can try to enter data to or extract data from an environment.”
What is attack surface minimization?
Attack surface minimization is a way of building software & systems such that the different points through which they might be attacked is minimized. Per Wikipedia, again - “[k]eeping the attack surface as small as possible is a basic security measure”.
Initially, it’s a way of thinking. For example, say you’re building a system. As you develop the architecture for this system, you’re constantly thinking about the different points through which it may be attacked, and considering whether any of these points can be removed or reduced as you build.
The implications of failing to minimize attack surface
Ultimately, excessive attack surface may contain actual vulnerabilities. These could be identified and exploited, leading to breach (unauthorized access, information disclosure, etc).
Other implications include:
-
The larger an application or system’s attack surface, the more exposed it is to attackers. A system that presents a significant attack surface may be more “appealing” to an attacker, as it looks like a soft target.
-
Customers may detect excessive attack surface during pre-purchase security assessments, which may cause delays in the procurement process, and may consume product / engineering time responding to customer concerns.
More generally, there’s a sometimes-accurate assumption that increased complexity leads to decreased security. Excessive attack surface is sometimes an indicator that the KISS principle has not been applied, and may be indicative of broader problems.
Layered, like an onion
Every application or system has layers, and attack surface minimization could be considered to be a per-layer discipline.
Attack Surface Layer | Minimization |
---|---|
Network traffic flow ❌ Firewall (or security group) rules accept inbound traffic to all ports on a server (or instance). ❌ Services that are only intended for use by other services within your system are externally accessible |
✅ Rules allow access only from to the ports necessary for system use - perhaps just 443 from everywhere, and 22 from trusted IPs? ✅ CloudFront or ELBs are the only externally-accessible system components; everything else is accessed through them. |
Network interfaces ❌ A locally-used daemon or container binds itself to 0.0.0.0, accepting inbound requests on any network interface. |
✅ The service binds to 127.0.0.1 or docker0, which means it is not accessible via the external-facing network interface. |
Kernel syscalls ❌ A vanilla Linux install limits some syscalls but is relatively permissive. |
✅ Use seccomp to restrict access to more sensitive syscalls, or to allow access only to those syscalls needed by your Go binary or Docker container. |
Operating system packages ❌ An instance or container image has a number of packages installed for tools that aren’t strictly needed but can be useful during development. |
✅ Instance and container images (multistage builds!) include only the packages strictly required to operate the system in question. |
Running daemons ❌ There’s a postfix service running on your web front-end EC2 instances, but they haven’t needed to send email for some time now. |
✅ Only the required services are running on your production instances. ✅ Unused services aren’t just disabled but - wherever possible - they’re removed entirely. |
Software dependencies ❌ It’s been a while since anyone reviewed the modules that are pulled in by your project, and it is starting to feel bloated. You know there are some dependencies there that are outdated & you don’t even use any more. |
✅ Software dependencies are frequently reviewed, either manually or automatically; unused dependencies are removed and those with duplicate/cross-over functionality consolidated. |
Application framework defaults ❌ An application’s API is pretty loosey-goosey about how it does input validation. It is broadly permissive about the content of input provided by application users. |
✅ Application inputs should be well-defined and validated according to requirements, in terms including type, structure, and format. |
Cloud-hosted infrastructure ❌ Production & development for multiple related services are all combined in one AWS account (one large attack surface). |
✅ Cloud-based systems are distributed across multiple accounts, creating a number of smaller attack surfaces. |
Users and administrators ❌ A slightly weird perspective to think about, but people can be part of a system’s attack surface too. It is all too easy to grant too much access, with too much privilege, to too many people. ❌Access to cloud accounts is granted broadly via multiple high-privilege, long-lived IAM accounts and associated credentials. |
✅ Access should be granted only with valid justification & the appropriate level of permissions. Access should be reviewed frequently and disabled/revoked when no longer needed! ✅ Employee access is provisioned only as needed with restricted scope & credentials that expire after a period of time and service accounts are carefully controlled. ✅ Default usernames have a unique generated credential instead of a hardcoded one. |
Measuring & managing attack surface
Processes and tools for measuring and managing attack surface may be applied differently, depending on the different layers you’re working at.
If you’re at the design stage, attack surface analysis (see OWASP cheat sheet) or threat modeling more generally is a great place to start. As you build, test, deploy, & operate, layer-specific tooling becomes more useful.
For example, network-layer attack surface can be assessed with a simple nmap port scan.
On an instance, or inside a container, a quick netcat -plnt
can show you what services are listening and what interfaces they’re bound to.
Curious about OS-level packages? Check dpkg -l
on Debian/Ubuntu, or your distribution’s equivalent.
If you’re looking at a web application or API, a combination of whitebox & blackbox review can help. Take a look through the codebase for routes or endpoints that are defined, make sure they’re all actually necessary, and remove any that are legacy or no-longer-used. You can also use a tool like gobuster to scan a web app/service for components that may be bundled with a framework & may not be explicitly defined in code.
deps.dev, Dependabot, govulncheck, npm audit, and many other tools can help manage software dependencies. If you can remove a no-longer-needed dependency then that’s less attack surface & one less thing to manage!
For cloud infrastructure, there are a range of open-source and commercial tools that can aggregate configuration information and help assess attack surface, or evaluate cloud security more generally.
Some rules of thumb
- Less attack surface is generally better, within reason of course.. we still need to deliver features & functionality.
- Keeping things simple is not just a security win, as reducing complexity makes engineering & operations easier in general.
- Reducing attack surface is almost always easier to do as you design & build a system or application, rather than after the fact.
- Got a new feature that increases attack surface but only some subset of customers want? Make it configurable, and turn it off by default!
In conclusion...
Attack surface minimization (or attack surface reduction) is more an architectural pattern and way of thinking than a technical security control.
It should be considered throughout the product engineering lifecycle, from design through build into operation and maintenance.
If you think about attack surface during design as well as implementation, you can significantly improve the security posture of the solution, often with minimal effort.