The assumption
"We have a security group locked down to port 443" is a sentence that sounds like a complete security posture. It has the shape of one: a rule, a port, a scope. Early on, I treated a tightly configured security group as functionally equivalent to "this instance has a firewall in front of it," full stop. I wasn't alone in this. It's the default framing in a lot of onboarding docs, a lot of architecture diagrams that draw a single box labeled "firewall" around a security group, and a lot of security review checklists that treat "security groups reviewed" as a completed line item rather than one piece of a larger question.
The key idea
A security group decides whether traffic reaches an instance's network interface. It has no opinion about what that traffic contains, where it's ultimately headed inside your application, or whether it looks like an attack in progress rather than a permitted connection.
The problem
A security group is a stateful, instance-level allow-list operating on IP, port, and protocol. That's genuinely useful: it's the first and cheapest thing worth locking down, and most breaches I've read post-mortems of involved a security group that was wider than it needed to be, an SSH port left open to the world, a database port reachable from outside the VPC, a debug endpoint nobody remembered to close off. Tightening that surface catches a huge share of avoidable exposure for almost no cost. None of what follows is an argument against doing that first.
But "allow port 443 from anywhere" is a decision about which packets are permitted to arrive, not a decision about what happens to the (entirely permitted) HTTP request once it does. A security group cannot tell the difference between a legitimate TLS handshake and the opening move of a credential-stuffing campaign, because both are, from a security group's point of view, permitted traffic on port 443. The security group's entire model of the world stops at the four-tuple: source, destination, port, protocol. It has no concept of a request body, a header, a login attempt, or a rate.
Alibaba Cloud's Cloud Firewall product exists precisely because of this gap: it operates with visibility into traffic patterns, threat intelligence, and application-layer behavior that a security group was never built to have. But because security groups are configured earlier, more often, and by more people (every ECS instance gets one; Cloud Firewall is a deliberate, separate product decision that someone has to make and pay for), it's easy for "we have security groups" to quietly stand in for "we have a firewall" in a team's mental model, without anyone consciously deciding that's an acceptable trade-off. Nobody sits down in a meeting and says "we've decided not to inspect traffic content." It just never comes up, because the word "firewall" already got used for something else.
Why the vocabulary itself is the problem
Part of what makes this substitution so durable is that "security group" and "firewall" aren't just similar in function, they're historically tangled. Early cloud security-group implementations were explicitly modeled on host-based firewall rules, and a lot of engineers who came up through on-prem infrastructure spent years calling iptables rules "the firewall," correctly, in a context where that host-level filtering really was most of what stood between a service and the internet. Carrying that vocabulary into a cloud environment with a dedicated, separately-purchased, application-aware Cloud Firewall product creates a false cognate: the word survived the move, but the thing it points to got smaller relative to what the newer product actually does.
The experiment
I set up an ECS instance with a security group scoped tightly to port 443 from any source, the configuration most teams would sign off on as "reasonably locked down." No SSH exposed, no extraneous ports, a single narrow rule. I then ran a simulated credential-stuffing pattern against a login endpoint on that instance: several thousand authentication attempts from a rotating set of source IPs, all arriving on port 443, all completing valid TLS handshakes, all shaped to resemble a real credential-stuffing tool's traffic rather than an obvious synthetic flood.
I ran the same test twice: once against the instance with only the security group in front of it, and once with Cloud Firewall enabled and its threat-intelligence and traffic-pattern detection features turned on, so the two conditions differed only in which control was watching, not in the request pattern itself.
# generating the request pattern: rotating source IPs,
# realistic request spacing, valid credentials mixed with invalid
for ip in $(cat rotating_ips.txt); do
curl -s -o /dev/null -w "%{http_code}\n" \
--interface "$ip" \
-X POST https://target-instance/api/login \
-d "username=${USERS[$RANDOM % ${#USERS[@]}]}&password=${PW_LIST[$RANDOM % ${#PW_LIST[@]}]}"
sleep 0.05
done
The security group allowed every single request in both runs. That's not a failure. It's exactly what a security group scoped to "port 443, any source" is supposed to do: it's a gate that opens for anything wearing the right badge, and TLS-on-443 was the only badge it ever checked. The question was what, if anything, else would have noticed the pattern behind the badge.
The proof
Nothing at the security group layer flagged the pattern, logged it as anomalous, or throttled it, because rate and pattern analysis isn't a security group's job and it was never instrumented to attempt it. describeSecurityGroupAttribute calls and the security group's own hit-count metrics showed exactly one thing: a rule matched, thousands of times, as designed.
kubectl_equivalent_check() { :; } # not applicable here, ECS instance, not a cluster
# Security group hit counter after the run
aliyun ecs DescribeSecurityGroupAttribute --SecurityGroupId sg-xxxx | jq '.Permissions'
# {
# "Direction": "ingress",
# "PortRange": "443/443",
# "SourceCidrIp": "0.0.0.0/0",
# "Policy": "Accept"
# }
# No count field, no anomaly flag, no distinction between the
# 3 legitimate logins and the 4,000 stuffing attempts that hour.
With Cloud Firewall enabled in front of the same instance and its threat-intelligence and traffic-pattern features active, the same request pattern was flagged and rate-limited well before it reached the completion threshold that would represent a real compromise. Cloud Firewall's logs showed source-IP reputation scoring, request-velocity thresholds tripped per source and in aggregate, and an automatic mitigation action applied to the offending pattern, none of which the security group layer had any mechanism to produce.
Two controls, two different questions
It's also worth noting what a plausible middle ground might have looked like, and why it wouldn't have closed the gap either. Suppose the team had added a web application firewall in front of the same login endpoint but left it in a default, unmanaged rule set: signature-based detection for known exploit patterns, nothing tuned for this application's specific login behavior. A WAF in that state would likely have let the credential-stuffing traffic through too, not because a WAF is the wrong tool in principle, but because an unconfigured instance of the right tool category still answers "no" to the specific question being asked. The lesson generalizes past security groups specifically: the presence of a security product in the stack tells you almost nothing on its own. What matters is whether that specific product, in its specific configuration, was ever positioned to see the specific thing you're worried about. A security group fails that test structurally, by design, in every configuration. A misconfigured WAF fails it accidentally, which is a different problem with a different fix, but produces the identical symptom of unnoticed traffic.
It's worth being precise about what each layer actually answered. The security group answered "is this packet allowed to reach the network interface," and the answer was yes, correctly, every time. Cloud Firewall answered a different question entirely: "does this traffic, considered as a pattern over time and against known-bad indicators, look like an attack." Those aren't two implementations of the same check running at different levels of sophistication. They're two different checks, and a team that only has the first one hasn't got a weaker version of the second. It has nothing where the second would be.
The instance-level control and the traffic-inspection control caught structurally different things, and only one of them was doing the job "we have a firewall" implies when someone says it in a security review.
# Security group rule: this is the entire security posture
# a "port 443, any source" rule expresses:
Direction: Inbound
Protocol: TCP
Port range: 443/443
Authorization: 0.0.0.0/0
Action: Allow
That single rule is complete and correct for its actual job. It was never going to be the thing that noticed several thousand login attempts arriving in a pattern no legitimate user produces, because noticing patterns over time isn't a stateful packet filter's design space. Asking it to do so is like asking a door lock to also evaluate whether the person holding the correct key seems suspicious. It's not a defect in the lock. It's a mismatch between the tool and the question.
You might disagree
You might reasonably say that application-layer defenses (rate limiting, CAPTCHA, anomaly detection) belong in the application or an API gateway, not in a network security product at all, and that expecting Cloud Firewall (or any perimeter product) to be the layer that catches credential stuffing is itself a category error. There's a real version of this argument: defense in depth means the application shouldn't rely on the network layer alone, and a team that puts all its credential-stuffing defense in a perimeter product and none in the application is making a different, also real mistake, just one layer further along.
I largely agree with the underlying principle. But the point of this piece isn't "Cloud Firewall is where credential-stuffing defense belongs" as a final architectural claim. It's narrower and, I think, harder to dispute: a security group specifically was never a candidate for that job, full stop, regardless of where the job ultimately gets done. Treating "we have a security group" as satisfying the general question of "do we have traffic-level protection" skips past that fact silently, and it does so precisely because both controls report success in the same vocabulary. A security group that allowed the traffic and an application that never rate-limited it will both, if you ask them, say the request was handled without incident. Neither one is lying. But "handled without incident" and "the credential-stuffing attempt didn't matter" are not the same claim, and only careful language keeps them separate.
What I think now
I now describe security groups by what they actually do ("instance-level, stateful, IP-and-port filtering") rather than by the word "firewall," specifically because the word invites exactly the substitution I made for years. When someone tells me a workload "has a firewall," the next question is which layer: security group, Cloud Firewall, an application-layer control, or some combination, and what each one is and isn't watching for. I've started asking teams to write that mapping down explicitly, one line per layer, one sentence describing what it catches and what it structurally cannot catch, rather than a single checkbox that says "network security: done."
That exercise is more useful than it sounds. Writing "the security group cannot see request content" as a sentence, in a document someone else will read, tends to surface the gap faster than any amount of individually correct configuration does, because the gap isn't in the configuration. It's in the sentence people use to describe the configuration to each other.
The lesson learned
A security group is a real, useful, and frequently underused control: most teams should tighten theirs. But it answers "should this packet be allowed to arrive" and nothing about what happens after arrival. Calling it "the firewall" collapses a layered problem into a single control that was never built to cover the whole thing, and the collapse survives exactly as long as nobody tests it against traffic the security group was designed to let through.