The question this answers
What is allowed to reach this specific resource, and what is it allowed to reach in return?
The load balancer must accept 443 from the internet. The application must accept traffic from the load balancer and nothing else. The database must accept 5432 from the application and nothing else — including from other workloads in the same subnet.
A per-resource, default-deny inbound boundary whose rules can reference identity-like groups rather than addresses, so it stays correct as instances are replaced and addresses change.
Allow-only, per resource, and stateful
A security group is a set of allow rules attached to a resource's network interface — not to a subnet, not to a route. There is no deny rule and no rule ordering: traffic is permitted if any rule matches and dropped otherwise. That makes the model unusually easy to reason about, because a group cannot contain a subtle precedence bug. It can only be too permissive or too narrow, and both are visible by reading it.
Statefulness is the property that most changes day-to-day work. The group tracks connections, so if an inbound rule permits a request, the response is permitted automatically — you never write a return rule. A workload with a permissive outbound rule and no inbound rules can call out and receive replies while accepting nothing unsolicited. This is exactly the opposite of a network ACL, and confusing the two is the subject of Network ACLs: The Stateless Filter.
Because it is attached per resource, a security group segments *inside* a subnet. Two instances in the same subnet with different groups cannot necessarily reach each other, which is the mechanism that makes subnet-level co-location safe. Routing connects everything in the network via the local route; the group is what makes that harmless.
| Source | Protocol / port | Destination | Decision | Why |
|---|---|---|---|---|
| 0.0.0.0/0 (internet) | TCP 443 | Load balancer group | ALLOW | The entry point exists to be dialable. This is the design, not a finding. |
| 0.0.0.0/0 (internet) | TCP 5432 | Database group | DENY | A public database is the finding, regardless of password strength. |
| Load balancer group | TCP 8080 | Application group | ALLOW | Group-referenced, so it survives every instance replacement. |
| Application group | TCP 5432 | Database group | ALLOW | The only path to the data tier, named by group rather than by CIDR. |
| Application group | TCP 5432 | Application group | DENY | Peers do not need to reach each other; lateral movement is not a feature. |
| 0.0.0.0/0 (internet) | TCP 22 | Any group | DENY | Use a managed session service. An open 22 is a permanent brute-force target. |
| Bastion / session service | TCP 22 | Application group | ALLOW (audited) | One narrow, logged path in, if direct access is genuinely unavoidable. |
Reference groups, not address ranges
The rule that separates a security-group design that ages well from one that rots is whether rules name *groups* or *addresses*. An instance's address is ephemeral: it changes on replacement, on scale-out, on a rolling deploy, and on any instance refresh. A rule written as "allow 5432 from 10.20.32.0/20" grants access to whatever happens to be in that range today — which includes the next workload someone places there, and excludes any application instance that ends up somewhere else.
A rule written as "allow 5432 from the application group" is a statement about identity rather than location. It stays correct through every replacement, it stays correct when the application moves to another subnet or zone, and it is self-documenting: reading the database's group tells you exactly which workload class may reach it. It is also narrower, because a CIDR grants access to everything in the range while a group grants it only to members.
The other half of the discipline is outbound. Most environments leave egress wide open because the default is permissive and tightening it is work. That default means a compromised application instance can reach anything on the internet, which is the working half of most data-exfiltration paths. A narrow egress rule set is more effort and it is what turns a compromise into a contained one. See Egress Security and Least Privilege in Infrastructure.
resource "security_group_rule" "db_in" {
security_group_id = sg_database
type = "ingress"
protocol = "tcp"
from_port = 5432
cidr_blocks = ["10.20.32.0/20"] # "the app subnet"
}
resource "security_group_rule" "app_out" {
security_group_id = sg_app
type = "egress"
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"] # the default nobody revisits
}
# Grants 5432 to every current and future resource in that /20, including the
# batch job someone places there next quarter. And grants the app unrestricted
# outbound, which is the exfiltration path in every post-mortem.resource "security_group_rule" "db_in" {
security_group_id = sg_database
type = "ingress"
protocol = "tcp"
from_port = 5432
source_security_group_id = sg_app # members only, wherever they run
}
resource "security_group_rule" "app_out_db" {
security_group_id = sg_app
type = "egress"
protocol = "tcp"
from_port = 5432
destination_security_group_id = sg_database
}
resource "security_group_rule" "app_out_https" {
security_group_id = sg_app
type = "egress"
protocol = "tcp"
from_port = 443
prefix_list_ids = [pl_payment_api, pl_object_storage] # named, not "everywhere"
}Group references survive instance replacement and subnet moves, and they grant access to a workload class rather than to an address range that other things will move into. Narrow egress is the half that matters after a compromise, and it is the half almost everyone skips.
What a security group is not
It is not authentication. A database reachable only from the application group is still a database, and if the application is compromised the group permits exactly what it was designed to permit. Network position limits *who can attempt*, and credentials limit *who succeeds*; a design that relies on only one of them has one control, not two. See Infrastructure Trust Boundaries.
It is not a subnet control either. Because groups attach per resource, two workloads sharing a subnet can have completely different reachability, and a rule change on one has no effect on the other. That is the useful property — but it means "what can reach the data tier" is answered by reading groups, not by reading the network diagram.
And it is not egress control by default. The default outbound rule permits everything, which means the group in front of a workload is asymmetric: carefully restricted inbound, wide-open outbound. For a workload handling regulated data, the outbound half deserves the same attention, usually via an egress proxy or an allow-list of named destinations, because a security group cannot express "only this hostname".
- Inbound TCP 5432 from the application security group
- Inbound TCP 5432 from the migration-runner security group, used only by the deploy pipeline
- Outbound TCP 443 to the backup service private endpoint
- Inbound TCP 5432 from the application security group
- Inbound TCP 5432 from the migration runner, ideally only during a deploy window
- Inbound from any CIDR, including the network's own range
- Inbound from the bastion or session service — humans reach it through an audited proxy, not directly
- Outbound to 0.0.0.0/0
Blast radius: A compromised application instance can reach the database exactly as designed and is limited by database credentials and grants from there — which is why the network rule is one control and not the control. Nothing else in the network can open a connection at all, so a compromise elsewhere does not reach the data tier over the network.
Key points
- Security groups are allow-only, attached per resource, and stateful — the reply to a permitted request needs no rule.
- Rules that reference another group instead of a CIDR survive instance replacement and grant access to a workload class rather than an address range.
- Because they attach per resource, they segment inside a subnet — which is what makes the always-present local route harmless.
- The default outbound rule permits everything, and that default is the working half of most exfiltration paths.
- A group limits who can attempt a connection. Credentials limit who succeeds. A design needs both.
The loop, answered
Every field is required, which is why no lesson here can recommend something without saying what it costs and what simpler thing to consider first.
- • The group is attached to a network interface; every packet to or from that interface is evaluated against it.
- • Evaluation is a match against allow rules only — no deny rules, no ordering, no precedence to reason about.
- • The connection state is tracked, so return traffic for an allowed flow is permitted without a corresponding rule.
- • A rule may name a CIDR, a prefix list, or another security group; a group reference resolves to the current membership at evaluation time.
- • Multiple groups on one interface are unioned, so adding a group can only widen access, never narrow it.
- • Own the rule inventory as code, because a hand-added rule during an incident is the one nobody removes.
- • Own group-reference discipline: a CIDR rule between internal tiers should be treated as a review finding.
- • Own the egress half deliberately, and accept the work — narrowing outbound is where the real security gain is.
- • Own the periodic audit for 0.0.0.0/0 inbound rules on anything that is not an intended entry point.
- • Own rule-count budgets: quotas on rules per group and groups per interface are real, and prefix lists are the usual fix.
- • A missing inbound rule: connections hang and time out, identical in appearance to a routing failure.
- • A CIDR rule that silently widens when a subnet is resized or a new workload is placed in the same range.
- • Too many groups attached to one interface, hitting a quota during a deploy rather than during design.
- • An overly narrow egress rule blocking a dependency discovered only at runtime — a package mirror, an OCSP responder, a telemetry endpoint.
- • A temporary "allow all from my office IP" rule that outlives the debugging session and the office lease.
- • Rules per group and groups per interface are quotas; prefix lists consolidate many CIDRs into one referencable object.
- • Group references scale better than CIDR rules because membership changes require no rule change at all.
- • Very large rule sets become unreviewable long before they become slow — the limiting resource is human attention.
- • Evaluation cost is not a practical concern; the practical concern is whether anyone can still read the rules.
- • This is the primary segmentation control in most cloud architectures, and it is default-deny inbound out of the box.
- • Group-referenced rules express intent, which makes an audit possible: "who can reach the database" has a readable answer.
- • It is a network control only. It does not authenticate, does not inspect payloads and cannot express hostname-level policy.
- • The default permissive egress deserves a named decision rather than an inherited default. See Egress Security.
- • Security groups themselves are free on every major provider.
- • The cost is indirect: rules that are too narrow cause outages, and rules that are too broad cause incidents.
- • Egress restriction can require an egress proxy or endpoints, which are real components with real bills.
- • Reviewing rules is a recurring human cost, and it grows with the number of rules — a reason to prefer group references and prefix lists.
- • Flow logs with REJECT entries, which name the blocking rule set precisely and end most reachability debates in seconds.
- • Rule-change events, correlated with incident start times — the fastest root-cause signal in this module.
- • A periodic report of internet-facing inbound rules, diffed over time rather than reviewed once.
- • The signal that lies: a successful health check from inside the same group. It proves the process is up and says nothing about whether callers outside the group can reach it.
- • The provider's default group, for a single-instance experiment — designing a group hierarchy for one box is wasted effort.
- • A managed platform that handles service-to-service authorization for you, when there is no virtual network to segment.
- • Application-level authentication and mutual TLS as the primary control, when workloads are ephemeral and identity-based policy fits better than address-based policy.
- • A service mesh, when policy needs to be expressed per service and per route rather than per port — at a substantial complexity cost. See Scoring Operational Complexity.
- • Buys default-deny segmentation for free; costs a rule inventory that must be reviewed or it silently rots.
- • Group references buy durability; cost a level of indirection that makes the rules slightly harder to read at a glance.
- • Narrow egress buys containment; costs runtime outages when an undiscovered dependency is blocked.
Allowed out, dropped on the way back
ingress rules (allow-only, unordered) ALLOW 0.0.0.0/0 → tcp/443 → sg-lb ALLOW sg-lb → tcp/8080 → sg-app ALLOW sg-app → tcp/5432 → sg-db (implicit) DENY everything else egress rules ALLOW sg-* → tcp/443 → 0.0.0.0/0 (return traffic implicit — connection tracked)
What people believe, and what is true
A security group protects the subnet.
It attaches to a resource. Two instances in one subnet can have entirely different reachability, and a rule change on one does nothing to the other.
I need a rule to allow the response back.
Security groups are stateful. Return traffic for an allowed connection is permitted automatically — that is a network ACL's problem, not this one's.
The database is in a private subnet with a security group, so it is protected.
It is unreachable from the internet and reachable exactly as designed from the application. Credentials, grants and query-level authorization are still doing the rest of the work.