Self-hosted CI/CD runners offer speed and cost advantages over managed cloud services, but they shift the security responsibility entirely to your team. A single malicious dependency, a compromised build script, or an unpatched runner OS can cascade into complete infrastructure compromise — leaked signing certificates, exfiltrated customer data, and backdoored production binaries.
The OWASP Top 10 CI/CD Security Risks report identifies self-hosted runner misconfiguration as the #3 most exploited vector in enterprise breaches. This guide covers the exact hardening measures we implement at Venelx.
Threat Model for Self-Hosted Runners
Before implementing defenses, understand what you're defending against:
| Attack Vector | Description | Impact | Mitigation |
|---|---|---|---|
| Malicious Dependency | Compromised npm/pip package executes code during install | Secret exfiltration, backdoor installation | Script blocking, SBOM scanning |
| Runner Persistence | Malware survives between builds on a shared runner | Cross-customer code theft | Ephemeral sandboxes |
| Log Injection | Secrets printed to build logs via debug mode | Credential exposure | Log scrubbing |
| Network Exfiltration | Hijacked process phones home with stolen tokens | API key theft, data breach | Egress firewall rules |
| PR Exploitation | Malicious PR triggers workflow that accesses secrets | Secret exposure via fork PRs | Require approval for fork workflows |
Ephemeral Build Sandboxes
Never reuse runner environments between builds. A compiler process should execute in an isolated environment that is created dynamically for that specific build and completely destroyed afterward. This architectural choice eliminates the entire category of persistence-based attacks.
At Venelx, every build follows this lifecycle:
[Build Request] → [Create Temp User Account] → [Clone Repo] → [npm ci] → [Compile]
↓
[Upload Artifact to S3]
↓
[Delete User + Home Dir + Keychain]
The temporary macOS user account provides filesystem isolation without the overhead of a full virtual machine. When the account is deleted, macOS removes all associated keychains, caches, preferences, and temporary files.
Blocking Malicious Dependency Scripts
By default, npm executes postinstall scripts immediately when downloading dependencies. Attackers exploit this to run arbitrary shell payloads that exfiltrate environment variables:
# Block ALL dependency lifecycle scripts in build environments
npm config set ignore-scripts true
npm ci
# Selectively rebuild only packages that require native compilation
npm rebuild sharp esbuild
For an even stronger posture, audit which packages contain install scripts before building:
# List all packages with install scripts
npm query ':attr(scripts, [postinstall])' | jq '.[].name'
Credential Sanitization and Log Scrubbing
Build logs are a gold mine for attackers. A single console.log(process.env) in a test file can dump every secret in your pipeline to a log file that gets stored in Artifact storage.
Prevent Secrets in Logs
Configure your CI log scanner to actively scrub known environmental variable patterns:
# Scrub known secret patterns from build output
build_output=$(npm run build 2>&1)
sanitized=$(echo "$build_output" | sed -E \
-e 's/(APPLE_API_KEY|AWS_SECRET|GITHUB_TOKEN)=[^ ]*/\1=***REDACTED***/g' \
-e 's/ghp_[a-zA-Z0-9]{36}/***GH_TOKEN_REDACTED***/g' \
-e 's/sk-[a-zA-Z0-9]{48}/***API_KEY_REDACTED***/g')
echo "$sanitized"
Immediate Credential Destruction
Never leave signing keys on disk longer than necessary. Perform compilation using keys, then shred them immediately:
# Perform iOS compilation using signing credentials
fastlane ios compile_and_sign
# Cryptographically shred credentials — 3-pass overwrite before deletion
shred -u -n 3 ./secrets/signing_key.p8
shred -u -n 3 ./secrets/distribution.p12
# Delete the temporary keychain
security delete-keychain build.keychain-db
Network Isolation and Egress Filtering
Restricting outbound connections prevents a hijacked dependency from calling home to exfiltrate database keys or downloading additional payloads. Block all outbound network traffic from your runners except to verified hosts.
macOS pf Firewall Configuration
# /etc/pf.anchors/ci-runner
# Block all outbound traffic except verified hosts
block out all
pass out proto tcp to github.com port 443
pass out proto tcp to registry.npmjs.org port 443
pass out proto tcp to developer.apple.com port 443
pass out proto tcp to storage.googleapis.com port 443
# Load the rules
sudo pfctl -f /etc/pf.conf
sudo pfctl -e
This ensures that even if a malicious package compromises the build process, it cannot exfiltrate data to attacker-controlled servers.
Runtime Process Monitoring
Monitor what processes execute during builds. Unexpected binaries (curl to unknown domains, python reverse shells, netcat listeners) should trigger immediate alerts:
# Monitor for suspicious process creation during builds
log stream --predicate 'processImagePath CONTAINS "curl" OR processImagePath CONTAINS "nc" OR processImagePath CONTAINS "python"' --info
For deep guides on code signing and Fastlane Match security, check our iOS Code Signing Blueprint and Fastlane Credentials Tutorial. For dependency-level scanning, read our Supply Chain Dependency Audit Guide.
References & Citations
- OWASP Top 10 CI/CD Security Risks: OWASP Foundation
- DevSecOps Hardened Infrastructure: NIST SP 800-218
- GitHub Actions Runner Security: GitHub Security Hardening Guide
- Running safe Docker containers: Docker on M-series chips