Restricting Outbound Traffic from Runners
Runners in your own cloud account need five outbound destinations. Narrow the rest with security group rules, subnet ACLs, and an egress proxy in front.
You can restrict outbound traffic from GitHub Actions runners that run in your own cloud account, and on AWS the rules live on the security group that the WarpBuild CloudFormation stack attaches to every runner instance. The constraint is a short allowlist: api.warpbuild.com, github.com, api.github.com, outbound HTTPS for runner telemetry, and the package registries your builds pull from have to stay reachable, and everything else can be dropped (AWS BYOC security hardening, checked on 2026-08-13).
This page covers the documented destination list, a rule set that narrows everything else across the security group, the subnet ACLs, and an egress proxy, the intra-subnet deny that pairs with it, and the failure signature each blocked destination produces. Start from the BYOC on AWS overview if the stack does not exist yet.
Overview
The stack ships open on the way out and mostly closed on the way in, which is the opposite of where a hardened runner subnet ends up.
| Stack resource | Default configuration |
|---|---|
| Security group | All egress allowed to 0.0.0.0/0; ingress allowed from VPC subnet CIDRs only |
| Network ACLs | Default VPC ACLs, which allow all traffic |
| VPC endpoints | S3 as a gateway endpoint, ECR API and ECR Docker as interface endpoints |
| Subnets | Public subnets, with optional private subnets behind NAT gateways for static egress addresses |
Rows come from the AWS BYOC security hardening guide, checked on 2026-08-13.
Five destinations survive the edit. The third column is the part that decides how much work the change is, because a security group matches addresses and the documentation names most of these by hostname.
| Destination | Why the runner needs it | Expressible as an address range |
|---|---|---|
api.warpbuild.com | Runner registration and lifecycle against the WarpBuild control plane | Documented by hostname, so route it through a proxy or a resolved list you refresh |
github.com | Git operations, including actions/checkout | Yes, from the GitHub REST meta endpoint |
api.github.com | GitHub Actions API calls the runner makes during a job | Yes, from the same endpoint |
| WarpBuild S3 over HTTPS | Runner telemetry and system logs | Partly, through the AWS managed S3 prefix list for the region |
| Your package registries and deploy targets | Dependency installs, image pulls from outside AWS, deploy steps | Rarely, most registries publish no stable ranges |
Two traffic classes never touch that allowlist. Cache reads and writes go to the stack S3 bucket through the gateway endpoint, and image pulls go through the ECR interface endpoints, so both stay inside the AWS network and need endpoint rules rather than internet rules.
Architecture
Three enforcement points sit between a build step and the internet, and each one can express something the others cannot.
| Control | Scope | What it expresses | Its job here |
|---|---|---|---|
| Security group | The network interface on each runner instance | Allow rules only, by port and by CIDR, prefix list, or peer security group. Stateful, so return traffic needs no rule | The destination allowlist |
| Network ACL | The subnet the runners launch into | Numbered allow and deny rules evaluated in order. Stateless, so return traffic needs its own rule | The deny of instance-to-instance traffic |
| Egress proxy or name-aware firewall | A separate subnet you route through | Hostname policy, which neither of the above can match on | Registries and any hostname without stable addresses |
The intra-subnet deny is the control most teams skip, and it is the one that closes lateral movement between two jobs running side by side. Runner instances have no reason to talk to each other. The hardening guide gives two ways to enforce that: network ACLs on the runner subnets that deny traffic between instances in the same subnet while still admitting traffic to VPC endpoints and NAT gateways, or a security group whose ingress admits only the VPC endpoint interfaces instead of the full subnet CIDR. Each GitHub Actions job already runs on a distinct, freshly provisioned instance that is terminated when the job completes, so the rule protects the live window rather than any state left behind.
BYOC runs on AWS, GCP, and Azure. The rule-by-rule guidance WarpBuild publishes is written against the AWS stack, so on GCP the same shape becomes VPC firewall rules with a deny on the runner tag, and on Azure it becomes network security group rules with the equivalent priority ordering.
One case does not need wider egress at all. If the reason for opening a destination is a private service, the networking addon joins the runner to your Tailscale tailnet at job start with an ephemeral node that is removed when the job ends, which keeps the security group unchanged and moves the access decision into your tailnet ACL policy.
Configuration
Start from the documented import-mode shape, which is an empty inbound list and a single allow-all outbound rule, then replace that one outbound rule with the destination set. This is the narrowed version:
{
"InboundRules": [],
"OutboundRules": [
{
"IpProtocol": "tcp",
"FromPort": 443,
"ToPort": 443,
"DestinationPrefixListId": "pl-0github-meta",
"Description": "github.com and api.github.com ranges, refreshed from the REST meta endpoint"
},
{
"IpProtocol": "tcp",
"FromPort": 443,
"ToPort": 443,
"DestinationPrefixListId": "pl-63a5400a",
"Description": "AWS managed S3 prefix list for the region: stack cache bucket and runner telemetry"
},
{
"IpProtocol": "tcp",
"FromPort": 443,
"ToPort": 443,
"DestinationSecurityGroupId": "sg-0vpc-endpoints",
"Description": "ECR API and ECR Docker interface endpoints"
},
{
"IpProtocol": "tcp",
"FromPort": 3128,
"ToPort": 3128,
"Destination": "10.20.4.0/24",
"Description": "Egress proxy: api.warpbuild.com and package registry hostnames"
}
]
}Three details in that rule set matter more than the rest. The endpoint rule points at the security group on the interface endpoints instead of a CIDR, so it keeps working when the endpoint addresses change and it does not open a path to peer instances that happen to share the subnet range. The proxy rule is where every hostname-only destination lands, including the control plane, which keeps the hostname policy in one editable place. And port 443 is the only internet-facing port on the list, so a build step that tries to reach anything on another port fails immediately rather than quietly succeeding.
The subnet ACL carries the intra-subnet deny. Rule numbers decide the outcome here, because evaluation stops at the first match and the endpoint allows have to sit above the deny.
| Direction | Rule | Protocol and port | Source or destination | Action |
|---|---|---|---|---|
| Outbound | 100 | TCP 443 | Interface endpoint addresses, one /32 each | Allow |
| Outbound | 110 | All | The runner subnet CIDR | Deny |
| Outbound | 120 | TCP 443 | 0.0.0.0/0 | Allow |
| Inbound | 100 | TCP 1024-65535 | Interface endpoint addresses, one /32 each | Allow |
| Inbound | 110 | All | The runner subnet CIDR | Deny |
| Inbound | 120 | TCP 1024-65535 | 0.0.0.0/0 | Allow |
Rules 120 in both directions carry return traffic, since a network ACL is stateless and the ephemeral port range has to be admitted explicitly. Rule 110 is the lateral movement block. Keep the endpoint addresses at rule 100 in sync with the endpoints the stack provisions, or a stack change will take image pulls down with it.
Operations
Roll the change out in three passes rather than one. Apply the empty inbound list first and run a full workflow, since that step changes nothing about how a job reaches the outside. Then move one runner set onto the narrowed security group while the rest of the fleet stays on the permissive one, and run the noisiest pipeline you have against it, ideally one that pulls container images and installs packages from more than one registry. Then move the remaining sets and add the subnet ACL rules last, because ACL ordering mistakes fail loudly and across every instance in the subnet at once.
When something does break, the symptom identifies the rule.
| Rule you narrowed | Symptom in GitHub Actions | Check that confirms it |
|---|---|---|
| GitHub ranges prefix list | The job picks up a runner, then actions/checkout fails on a connection timeout | Compare the prefix list entries against a fresh pull of the meta endpoint |
| Proxy route for the control plane | An instance launches under the warp- name pattern and the job stays queued with an empty log | Look for a warp- instance in the stack region that never registered a runner |
| S3 prefix list | Jobs pass and runner system logs stop arriving | Run a job and check whether telemetry lands for it |
| Endpoint security group reference | Image pulls fail at the login or pull step while git traffic is healthy | Confirm the endpoint security group admits 443 from the runner group |
| Subnet ACL rule numbers | Everything in that subnet fails at once, including endpoint traffic | Read the rule order and confirm the endpoint allows sit below the deny |
| Proxy route for registries | One install step fails on the package manager's own connect or DNS timeout | Reach the registry through the proxy address from a debug job |
The GitHub ranges move, so an address-based allowlist is a job rather than a one-time edit. This workflow refreshes the managed prefix list weekly on a WarpBuild hosted runner and needs no long-lived AWS keys:
name: refresh-github-ranges
on:
schedule:
- cron: "0 6 * * 1"
workflow_dispatch:
jobs:
refresh:
runs-on: warp-ubuntu-latest-x64-2x
permissions:
id-token: write
contents: read
env:
PREFIX_LIST_ID: pl-0github-meta
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/prefix-list-writer
aws-region: us-east-1
- name: Build the entry set from the meta endpoint
run: |
curl -sS https://api.github.com/meta \
| jq '[.actions[], .api[], .git[]]
| unique
| map(select(test(":") | not))
| map({Cidr: ., Description: "github meta"})' > entries.json
- name: Apply the entries to the prefix list
run: |
VERSION=$(aws ec2 describe-managed-prefix-lists \
--prefix-list-ids "$PREFIX_LIST_ID" \
--query 'PrefixLists[0].Version' --output text)
aws ec2 modify-managed-prefix-list \
--prefix-list-id "$PREFIX_LIST_ID" \
--current-version "$VERSION" \
--add-entries file://entries.jsonTwo operational limits are worth checking before that job runs in anger. A managed prefix list has a fixed maximum entry count set at creation, and the meta endpoint returns more ranges than a small list holds, so size it from a real pull. And entries removed upstream stay in the list until you remove them, so a full reconcile beats an append-only refresh once the list has been alive for a few months.
None of this changes what the runners cost or what your workflows say. BYOC Linux and Windows runners carry a WarpBuild fee of $0.002 per minute and your own cloud account bills the compute (pricing page, checked on 2026-08-13). Every cost number here carries a source link and a checked-on date, and the same numbers appear on the pricing page. The one line item that does move is on your AWS bill: routing runners through private subnets or an egress proxy adds gateway hours and data processing charges at your account's rates.
A hardened stack registers the same labels a permissive one does, so runs-on stays as it was. For the vendor half of a security review, the attestation is SOC 2 Type 2, with trust.warpbuild.com as the linked evidence. The remaining controls, from IMDSv2 to instance profile scope, are collected in the full BYOC on AWS security checklist, the prerequisites the rules assume are in networking requirements for BYOC on AWS, the short version of this question is answered in can I restrict outbound traffic from BYOC runners, and the object the rules live on is defined in security group.
FAQ
Should I block inbound traffic before I narrow outbound rules?
Yes. An empty inbound rule set is the documented starting posture, and jobs keep running under it, because the runner agent dials out and holds that connection open for work. Narrowing egress is the step that can stall jobs, so land the inbound change first, confirm a full workflow passes, then replace the single allow-all egress rule one destination at a time.
Do network ACLs replace the security group egress rules?
No, they cover a different scope. The security group attaches to each runner instance and holds allow rules only, so it carries the destination allowlist. A network ACL attaches to the subnet, evaluates numbered allow and deny rules in order, and is stateless, which is what lets it deny instance-to-instance traffic that a security group cannot express. Most hardened stacks use both.
Does narrowing egress change my workflow files?
No. The rules sit on the security group and the subnet in your own AWS account, and the runner set keeps the same runs-on label it had before, so nothing in the workflow file changes. What changes is the failure mode: a step that reaches a destination you removed now fails on a connection timeout where it used to succeed.
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.