Runner and Environment Security
Learn to secure CI/CD runners with isolation, hardening, and ephemeral environments to prevent compromise and lateral movement.
Runners execute your pipeline code. If compromised, an attacker can steal secrets, modify builds, and potentially pivot to other systems. This section covers runner isolation and hardening techniques.
Runner Types and Security Tradeoffs
| Runner Type | Isolation | Security | Use Case |
|---|---|---|---|
| Shared hosted | Low | Medium | Public repos, non-sensitive builds |
| Self-hosted persistent | Medium | Low | Cost savings, specific requirements |
| Self-hosted ephemeral | High | High | Sensitive builds, compliance |
| Container-based | High | High | Reproducible, isolated builds |
GitHub Actions Runner Security
Use Ephemeral Runners
Ephemeral runners are destroyed after each job:
# Self-hosted runner with ephemeral flag
./config.sh --url https://github.com/org/repo \
--token TOKEN \
--ephemeral
For larger scale, use autoscaling solutions:
# actions-runner-controller (Kubernetes)
apiVersion: actions.summerwind.dev/v1alpha1
kind: RunnerDeployment
metadata:
name: ephemeral-runners
spec:
replicas: 5
template:
spec:
ephemeral: true
repository: org/repo
Restrict Runner Groups
Limit which repositories can use specific runners:
# Only allow production repo to use production runners
jobs:
deploy:
runs-on: [self-hosted, production] # Restricted runner group
Configure in GitHub Settings -> Actions -> Runner groups.
Network Isolation
Restrict runner network access:
# Firewall rules for runner host
# Allow only required outbound connections
iptables -A OUTPUT -d github.com -p tcp --dport 443 -j ACCEPT
iptables -A OUTPUT -d ghcr.io -p tcp --dport 443 -j ACCEPT
iptables -A OUTPUT -d registry.npmjs.org -p tcp --dport 443 -j ACCEPT
iptables -A OUTPUT -j DROP # Block everything else
GitLab CI Runner Security
Use Docker Executor with Isolation
# /etc/gitlab-runner/config.toml
[[runners]]
name = "secure-runner"
executor = "docker"
[runners.docker]
image = "alpine:latest"
privileged = false # Never run privileged!
disable_entrypoint_overwrite = true
cap_drop = ["ALL"] # Drop all capabilities
security_opt = ["no-new-privileges:true"]
network_mode = "bridge" # Isolated network
Kubernetes Executor Security
[[runners]]
name = "k8s-runner"
executor = "kubernetes"
[runners.kubernetes]
namespace = "gitlab-runners"
privileged = false
service_account = "gitlab-runner" # Limited SA
[runners.kubernetes.pod_security_context]
run_as_non_root = true
run_as_user = 1000
[runners.kubernetes.pod_annotations]
"container.apparmor.security.beta.kubernetes.io/build" = "runtime/default"
Protected Runners
Only allow protected branches to use sensitive runners:
[[runners]]
name = "production-runner"
[runners.custom_build_dir]
# Mark as protected in GitLab UI
# Settings -> CI/CD -> Runners -> Edit -> Protected
Jenkins Agent Security
Use Containerized Agents
// Jenkinsfile with container agent
pipeline {
agent {
docker {
image 'maven:3.9-eclipse-temurin-17'
args '--network=isolated --cap-drop=ALL'
}
}
stages {
stage('Build') {
steps {
sh 'mvn clean package'
}
}
}
}
Kubernetes Plugin Security
// Pod template with security context
podTemplate(
containers: [
containerTemplate(
name: 'maven',
image: 'maven:3.9',
runAsUser: '1000',
runAsGroup: '1000'
)
],
securityContext: [
runAsNonRoot: true,
fsGroup: 1000
]
) {
node(POD_LABEL) {
stage('Build') {
container('maven') {
sh 'mvn clean package'
}
}
}
}
Agent-to-Controller Security
// jenkins.yaml - restrict agent commands
jenkins:
remotingSecurity:
enabled: true
slaveAgentPort: 50000
agentProtocols:
- "JNLP4-connect" # Most secure protocol
Container Build Security
Don't Run Docker-in-Docker Privileged
# Bad - privileged DinD
services:
- docker:dind
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: "" # No TLS!
# Better - Kaniko (no daemon required)
build:
image:
name: gcr.io/kaniko-project/executor:latest
entrypoint: [""]
script:
- /kaniko/executor
--context $CI_PROJECT_DIR
--dockerfile Dockerfile
--destination $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
Use Rootless Builds
# GitHub Actions with buildx (rootless)
- name: Build image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: myapp:${{ github.sha }}
Environment Hardening Checklist
Runner Host:
[ ] Minimal OS installation
[ ] Regular security updates
[ ] No unnecessary services running
[ ] Disk encryption enabled
[ ] Audit logging configured
Runner Process:
[ ] Runs as non-root user
[ ] Limited filesystem access
[ ] Network egress restricted
[ ] No persistent storage between jobs
[ ] Capabilities dropped
Container Builds:
[ ] No privileged mode
[ ] Read-only root filesystem where possible
[ ] Resource limits configured
[ ] Seccomp/AppArmor profiles applied
[ ] No host mounts for sensitive paths
Detecting Runner Compromise
Monitor for suspicious activity:
# Add monitoring step to workflows
- name: Security audit
run: |
# Check for unexpected processes
ps aux | grep -v expected_process
# Check for unexpected network connections
netstat -tulpn
# Check for unexpected files
find /tmp -type f -newer /proc/1/exe
Set up alerts for:
- Unusual network traffic from runners
- Jobs running longer than expected
- Unexpected resource usage
- Failed authentication attempts
Part of: CI/CD Pipeline Hardening
Updated: 1/24/2025
Found an issue?