Git, CI/CD & GitOpsXC · CI Platform SecurityProductionBoundary
Production network access from CI — the production boundary
What you'll learn
- Explain why private VPC connectivity makes the CI a production-network entry point
- Distinguish identity-based access from network-based access and the blast radius of each
- Configure egress allowlists on the CI runner and audit logging on every production call
- Recognise the role of bastion hosts and jump boxes versus identity mediation
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
The CI with private VPC connectivity can reach every internal service the VPC exposes: the production database, the internal API, the message broker, the cache, the metadata service. The network boundary is no longer the boundary; the identity the runner carries is the boundary. A runner that holds a production IAM role can reach every service the role can reach, from inside the VPC or outside. The discipline is to treat the CI as a production-network entry point and to apply the controls that entry point requires.
What production network access means
A runner with private VPC connectivity has network reach to every service the VPC exposes. The list of services is the same list a production workload can reach — the runner is in the production network.
flowchart TB
CI["CI runner\n(in production VPC)"]
CI --> S1["Production database"]
CI --> S2["Internal API"]
CI --> S3["Message broker"]
CI --> S4["Cache"]
CI --> S5["Metadata service"]
CI --> S6["Internal SSH"]
The reach is the network reach. The authorisation is the identity the runner carries. The two are independent: a runner with network reach to the database but no database credential cannot read the database; a runner with a database credential but no network reach cannot connect. A runner with both is a runner that can read the database. The boundary is not the network; the boundary is the identity.
Identity-based access versus network-based access
The two models of authorisation reach the same systems through different mechanisms:
flowchart LR
subgraph NET["Network-based"]
N1["CI runner"] -->|"Network reach"| N2["Production service"]
N2 -->|"Trusts network"| N3["Allow"]
end
subgraph ID["Identity-based"]
I1["CI runner"] -->|"OIDC token, scoped role"| I2["Production service"]
I2 -->|"Validates claims"| I3["Allow or deny"]
end
- Network-based. The production service trusts the network. A runner with network reach is trusted; the service allows the call. The decision is the network’s.
- Identity-based. The production service trusts the token. A runner with a valid OIDC token for the scoped role is trusted; the service validates the token’s claims and allows the call. The decision is the service’s.
The structural difference is what happens when the network is breached. With network-based access, the attacker who reaches the network is trusted. With identity-based access, the attacker who reaches the network is still subject to the token validation: no token, no trust. The CI that has both network reach and identity-based access is the CI that has defence in depth — the network is one layer, the identity is another.
Egress allowlists on the runner
The runner’s outbound network should be restricted to the destinations the workflow requires. An egress allowlist is the operational control that limits lateral movement:
flowchart TB
R["CI runner"]
R -->|"Allow"| A1["AWS APIs"]
R --> A2["GitHub API"]
R --> A3["Container registry"]
R --> A4["Artifact store"]
R -->|"Deny"| D1["Metadata service IP"]
R -->|"Deny"| D2["Production database"]
The allowlist names the destinations the workflow requires: the cloud APIs, the registry, the artifact store. The deny list names the destinations the workflow does not require: the metadata service IP (which an SSRF could abuse to steal cloud credentials), the production database, the internal API.
Bastion hosts, jump boxes, and SSH tunnels
Legacy patterns for CI access to production rely on bastion hosts, jump boxes, and SSH tunnels. The pattern is: the runner opens an SSH tunnel through the bastion to the production host; the production host trusts the bastion; the runner has production access.
flowchart LR
R["CI runner"] -->|"SSH tunnel"| B["Bastion host"]
B -->|"Forwarded SSH"| P["Production host"]
P -->|"Trusts the bastion"| A["Allow"]
The pattern is network-based access with extra steps. A runner that can open the tunnel can reach the production host; the production host’s audit log names the bastion, not the runner. The blast radius is the production host’s permissions; the audit attribution is lost.
The structural fix is to replace the bastion with an identity-aware proxy: the runner authenticates with an OIDC token, the proxy validates the claims, and the proxy establishes the session to the production host. The session is attributed to the runner, the workflow, and the run.
Production discipline
- Identity-based access for every production service. No service trusts the network alone. Every production call requires a valid OIDC token for the scoped role.
- Egress allowlists on the runner. The allowlist names the destinations the workflow requires; the deny list names the metadata service IP and the production database range.
- Audit logging on every production call. The audit log names the workflow, the run, the role, and the target.
- No bastion-mediated CI access. Bastions are network-based access; replace with identity-aware proxies that attribute the session to the workflow.
- Private VPC connectivity is a privilege, not a default. The runner earns connectivity by declaring the workflow’s need.
Cross-course references
- Part XC-01 (What the CI platform can touch) maps the production network in the wider blast radius.
- Part XLI-04 (The Docker socket risk) covers the runner host boundary the production boundary inherits.
- AWS for Production Sysadmins — Part XXXVII (NetworkBoundary) covers VPC endpoint policies that gate identity-based access.
- Kubernetes for Production Sysadmins — Part LVI (DefenseInDepth) covers cluster-side defences.
Quiz
Knowledge check · 4 questions
Q1. A CI runner has private VPC connectivity to the production VPC and an IAM role that grants `s3:GetObject` on the production bucket. Which statement is correct?
Q2. Bastion-mediated CI access to production is structurally equivalent to identity-based access because the bastion authenticates the runner before forwarding the session.
Q3. Explain why the production boundary is the identity the CI carries, not the network the CI is on, and what the egress allowlist adds.
Q4. Diagnose a production compromise that originated from an SSRF in a CI step that reached the metadata service.
Team T's CI runners run in the production VPC with private connectivity to every internal service. The runners hold an IAM role with AdministratorAccess on the production account — issued for convenience. A fork PR opens; a malicious step uses an SSRF in a workflow action to reach the metadata service at `169.254.169.254`. The metadata service returns STS credentials for the runner's IAM role. The attacker uses the credentials to enumerate every S3 bucket in the production account and to read the contents of the production backups bucket. The egress firewall allows all outbound traffic from the runner subnet.
Passing score: 75%. Answers are checked in this browser.