Security Group
A security group is a stateful firewall attached to a cloud instance, holding allow rules for inbound and outbound traffic. How runner rules are written.
A security group is a stateful firewall attached to a cloud instance, holding a list of allow rules for traffic arriving at the instance and a second list for traffic leaving it. Traffic matching no rule is dropped, and because the filter tracks connections, the response to a connection the rules already allowed is admitted without a matching rule in the other direction.
For a machine that runs GitHub Actions jobs, that statefulness moves nearly all of the interesting configuration to the outbound list. The runner agent dials out and waits for work, so the machine can carry zero inbound rules and still pick up jobs.
Definition
Two things define a security group: the network interfaces it is attached to, and its rules. Attachment happens at the interface, so two instances in the same subnet can carry entirely different rule sets, which is what separates a security group from a subnet-wide filter.
Each rule is a small record. The fields are the same whichever cloud is underneath, even where the names differ:
| Field | What it holds | Example value |
|---|---|---|
| Direction | inbound or outbound, kept as separate lists | outbound |
| Protocol | tcp, udp, icmp, or all protocols | tcp |
| Port range | one port or a range, meaningful for TCP and UDP | 443 |
| Source or destination | a CIDR block, another security group, or a managed prefix list | 10.0.0.0/16 |
| Description | free text carried with the rule for audit | "container registry endpoint" |
Five properties follow from that shape, and together they explain most of the behavior people find surprising.
Rules only allow. There is no deny rule in the AWS model. The default action is to drop, and every rule you write carves an exception out of that default. Narrowing access means removing rules rather than adding blocking ones (AWS security group reference, checked on 2026-08-13).
Evaluation is a union and order does not matter. Several groups can attach to one interface, and the effective policy is every rule from every attached group merged into one set. A permissive rule anywhere in that union wins, so an audit has to read all of the attached groups rather than the one with the relevant name.
The filter is stateful. A connection that an outbound rule permits gets its return packets admitted automatically, on whatever ephemeral port the kernel chose. This is why an outbound-only group is a working configuration rather than a half-written one.
A fresh group starts closed inbound and open outbound. A newly created group has no inbound rules and a single outbound rule permitting all traffic. The starting posture is already correct for a runner on the inbound side, and the tightening work is on the outbound side.
A rule can name another group instead of an address range. Using a group ID as the source is how "any instance carrying this role" is expressed without knowing addresses in advance, which matters when instances are created and destroyed per job and never hold a stable IP.
Security groups compared with network ACLs
The two mechanisms sit at different layers and are often confused, because both look like lists of network rules.
| Property | Security group | Network ACL |
|---|---|---|
| Attaches to | a network interface | a subnet |
| Rule types | allow only | allow and deny |
| Evaluation | all rules as one union, order independent | in rule-number order, first match wins |
| Connection state | stateful, return traffic admitted | stateless, return traffic needs its own rule |
| Typical use | per-instance policy | coarse boundary for everything in a subnet |
The stateless column is where mistakes concentrate. An ACL that permits outbound HTTPS on port 443 and nothing else silently breaks every connection, because the reply arrives on an ephemeral high port that no inbound ACL rule covers (AWS network ACL reference, checked on 2026-08-13).
The same idea across clouds
The term "security group" comes from the AWS and OpenStack model. The equivalents elsewhere carry the same per-instance intent with different rule semantics, which is worth knowing before a policy written for one cloud is translated to another.
| Cloud | Name | Attaches to | Deny rules | Reference |
|---|---|---|---|---|
| AWS | security group | network interface | none, allow only | Security groups |
| Azure | network security group | subnet or network interface | yes, with numeric priorities | Network security groups |
| Google Cloud | VPC firewall rule | the network, targeted by tag or service account | yes, with numeric priorities | VPC firewall rules |
Example
Take a GitHub Actions job that checks out a repository, installs dependencies, builds a container image, and pushes it to a registry. The job runs on a self-hosted runner in a private subnet.
name: build-and-push
on:
push:
branches: [main]
jobs:
image:
runs-on: warp-ubuntu-latest-x64-4x
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: npm ci
- name: Build the image
run: docker build -t "$REGISTRY/app:$GITHUB_SHA" .
- name: Push the image
run: docker push "$REGISTRY/app:$GITHUB_SHA"Reading the steps top to bottom gives the destination list the security group has to permit. actions/checkout and the runner agent itself talk to GitHub over HTTPS. npm ci reaches the package registry. docker push reaches the container registry. Nothing in the job accepts a connection from anywhere.
So the group attached to the runner looks like this:
| Direction | Protocol | Port | Destination | Why |
|---|---|---|---|---|
| Inbound | none | none | none | the job never accepts a connection |
| Outbound | tcp | 443 | GitHub HTTPS endpoints | agent registration, job polling, checkout, log upload |
| Outbound | tcp | 443 | package registry endpoints | dependency installation |
| Outbound | tcp | 443 | container registry endpoint | image pull and push |
| Outbound | tcp | 443 | object storage prefix list | cache and artifact traffic, kept off general internet egress |
Written as Terraform, the empty inbound list is the whole point of the resource:
resource "aws_security_group" "runner" {
name = "actions-runner"
description = "GitHub Actions runners: no inbound, narrowed egress"
vpc_id = var.vpc_id
# no ingress blocks: nothing may open a connection to a runner
}
resource "aws_vpc_security_group_egress_rule" "github" {
security_group_id = aws_security_group.runner.id
description = "GitHub API, git over HTTPS, and agent long poll"
ip_protocol = "tcp"
from_port = 443
to_port = 443
cidr_ipv4 = "0.0.0.0/0"
}
resource "aws_vpc_security_group_egress_rule" "s3_gateway" {
security_group_id = aws_security_group.runner.id
description = "Object storage traffic, routed via the S3 prefix list"
ip_protocol = "tcp"
from_port = 443
to_port = 443
prefix_list_id = data.aws_prefix_list.s3.id
}Three details in that snippet are worth calling out.
The absence of an ingress block is the strongest rule on the machine. A group with no inbound rules drops every unsolicited packet, and the job still runs, because the agent's outbound connection carries its replies back under the stateful rule.
The prefix list on the second egress rule is how an address range that the cloud provider owns and updates gets referenced without hardcoding CIDRs. It keeps object storage traffic on the provider's internal network instead of routing it out through the path used for general internet egress.
The 0.0.0.0/0 destination on the first rule is the honest starting point rather than the finished state. Narrowing it by CIDR is fragile, because package registries and container registries sit behind content delivery networks whose address ranges rotate. The durable ways to tighten egress are an outbound proxy that filters by hostname, a firewall that resolves domain rules, or private endpoints for the services that offer them. Each rule keeps its description, so a later reviewer can tell which build step a destination belongs to.
Related Terms
- Networking requirements for GitHub Actions runners in your own AWS account: subnets, outbound address translation, and the address capacity a runner fleet needs.
- Restricting outbound traffic from GitHub Actions runners: the destinations a build needs, and the proxy and endpoint patterns for narrowing egress past a CIDR rule.
- VPC, the private network a security group operates inside: the address space, subnets, and route tables the rules above sit on top of.
- AWS security groups reference: the upstream definition, the rule fields, and the default group behavior.
- AWS network ACL reference: the stateless subnet filter and the ephemeral port range its return rules need.
- WarpBuild BYOC security hardening documentation: the runner security group posture, inter-instance isolation, and the egress checklist.
- WarpBuild BYOC AWS configuration documentation: the VPC, subnet, and security group prerequisites for a runner stack.
- WarpBuild pricing: per minute rates by runner type.
FAQ
What is a security group in cloud networking?
A security group is a stateful firewall attached to the network interface of a cloud instance. It holds two lists of allow rules, one for inbound traffic and one for outbound traffic, and each rule names a protocol, a port range, and a source or destination such as a CIDR block or another security group. Traffic that matches no rule is dropped, and because the filter tracks connections, the response to an allowed connection is admitted without a rule in the opposite direction.
What is the difference between a security group and a network ACL?
A security group attaches to a network interface, carries allow rules only, evaluates every rule as one set, and is stateful. A network ACL attaches to a subnet, carries both allow and deny rules, evaluates them in rule-number order until one matches, and is stateless, so return traffic needs its own rule covering the ephemeral port range. They are commonly used together, with the ACL as a coarse subnet-wide boundary and the group as the per-instance rule set.
Can a GitHub Actions runner work with no inbound security group rules?
Yes. The runner agent opens an outbound HTTPS connection to GitHub and holds it, so nothing needs to reach the machine from outside. Because a security group is stateful, the responses on that connection are admitted with no inbound rule present. Removing every inbound rule is the usual starting posture for a runner group, after which the work is deciding which outbound destinations a build actually needs.
Start with $10 in free credits
Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.