CI/CD Pipeline Hardening
Guardrails For Every Deploy
In the cloud, consistency is crucial: policy in code, minimal permissions and visibility into drift.
For CI/CD Pipeline Hardening automation is leading: guardrails in code, least privilege and continuous drift control.
This way you maintain speed in the cloud without security depending on manual luck.
Immediate measures (15 minutes)
Why this matters
The core of CI/CD Pipeline Hardening is risk reduction in practice. Technical context supports the choice of measures, but implementation and assurance are central.
Defense Measures
SLSA Framework
Supply-chain Levels for Software Artifacts (SLSA, pronounced as "salsa") is a framework from Google that addresses the integrity of the software supply chain:
| SLSA Level | Requirements | What it protects against |
|---|---|---|
| Level 0 | Nothing | Nothing |
| Level 1 | Build process is documented, provenance generated | Unknown build origin |
| Level 2 | Hosted build service, authenticated provenance | Tampered build environment |
| Level 3 | Hardened build platform, non-falsifiable provenance | Compromised build service |
| Level 4 | Two-party review, hermetic builds | Insider threats |
# Example: generating SLSA provenance in GitHub Actions
- uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.9.0
with:
base64-subjects: "${{ needs.build.outputs.hashes }}"Sigstore
Sigstore is an ecosystem for signing and verifying software artifacts:
# Cosign: container image signing
# Sign an image
cosign sign --key cosign.key REGISTRY/IMAGE:TAG
# Verify an image
cosign verify --key cosign.pub REGISTRY/IMAGE:TAG
# Keyless signing with OIDC (no key management needed)
cosign sign REGISTRY/IMAGE:TAG
# Authenticates via your OIDC identity (GitHub, Google, etc.)
# In Kubernetes: Kyverno policy to only allow signed images
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-signatures
spec:
validationFailureAction: Enforce
rules:
- name: verify-cosign
match:
any:
- resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "registry.company.com/*"
attestors:
- entries:
- keyless:
subject: "https://github.com/company/*"
issuer: "https://token.actions.githubusercontent.com"Ephemeral runners
# GitHub Actions: always use GitHub-hosted runners for public repos
runs-on: ubuntu-latest # Ephemeral, clean environment per job
# Self-hosted runners: configure as ephemeral
# ./config.sh --ephemeral
# Runner is de-registered after each job and recreated
# Docker-based ephemeral runners
# Each job runs in a fresh container
# No cross-job contamination possibleLeast privilege tokens
# GitHub Actions: minimal GITHUB_TOKEN permissions
permissions:
contents: read
packages: write
# Only what is needed, nothing more
# GitLab CI: use scoped variables
variables:
DEPLOY_TOKEN:
value: $CI_DEPLOY_TOKEN
# protected: only available on protected branches
# masked: hidden in build logs
# Jenkins: use credential scoping
// Jenkinsfile
withCredentials([
usernamePassword(
credentialsId: 'deploy-staging', // Specific per environment
usernameVariable: 'USER',
passwordVariable: 'PASS'
)
]) {
// Credentials only available in this block
sh 'deploy.sh'
}OIDC Federation
# AWS OIDC with GitHub Actions (no long-lived credentials)
# Step 1: Configure the OIDC provider in AWS
# Step 2: Create an IAM role with trust policy
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::ACCOUNT:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:ORG/REPO:ref:refs/heads/main"
}
}
}
]
}
# Step 3: Use in the workflow
# - uses: aws-actions/configure-aws-credentials@v4
# with:
# role-to-assume: arn:aws:iam::ACCOUNT:role/GitHubActionsRole
# aws-region: eu-west-1
# No AWS_ACCESS_KEY_ID, no AWS_SECRET_ACCESS_KEY
# Only a short-lived session token# GCP Workload Identity Federation with GitHub Actions
# Similar principle: OIDC token → GCP service account
- uses: google-github-actions/auth@v2
with:
workload_identity_provider: 'projects/PROJECT_NUM/locations/global/workloadIdentityPools/github/providers/github-provider'
service_account: 'github-actions@PROJECT.iam.gserviceaccount.com'Tip: OIDC federation is the best way to eliminate long-lived credentials from CI/CD pipelines. But pay attention to the condition in the trust policy. If the
subclaim is too broad (e.g.repo:ORG/*instead ofrepo:ORG/REPO:ref:refs/heads/main), any repository in the organization can assume the role. Always verify the trust policy conditions.
Jenkins Hardening
Jenkins is the oldest and most widely used CI/CD server, but also the most configured-by-someone-who-did-not-actually-have-time-for-it. The default settings are downright dangerous.
Securing the Script Console
The Groovy Script Console (/script) provides full RCE
on the Jenkins server. This is the first door attackers try.
| Measure | Implementation |
|---|---|
| Restrict Script Console | Admin users only via Matrix Authorization Strategy |
| Disable CLI | jenkins.CLI.disabled=true in
jenkins.model.JenkinsLocationConfiguration.xml |
| Restrict agent-to-controller | Manage Jenkins → Security → Agent → Controller Access Control |
| CSRF Protection | Enabled by default since Jenkins 2.0, verify it has not been disabled |
Credential Management
// Jenkinsfile: credentials scoped per folder, not global
withCredentials([
usernamePassword(
credentialsId: 'deploy-prod', // Folder-scoped credential
usernameVariable: 'DEPLOY_USER',
passwordVariable: 'DEPLOY_PASS'
)
]) {
// Credentials only available in this block
sh 'deploy.sh --user $DEPLOY_USER --pass $DEPLOY_PASS'
}
// Outside the block the variables are no longer available| Measure | Implementation |
|---|---|
| Folder-scoped credentials | Credentials per project folder, not global |
| Credential rotation | Automatic rotation via external secret manager (HashiCorp Vault, AWS Secrets Manager) |
| Audit trail | Credentials Usage plugin for who uses which credential |
| No plaintext secrets | Always use the Credentials plugin, never environment variables in the UI |
Plugin Security
| Measure | Implementation |
|---|---|
| Minimize plugins | Remove all plugins that are not actively used |
| Update policy | Update plugins weekly, monitor security advisories |
| No unknown plugins | Only plugins from the official Jenkins Update Center |
| Plugin audit | Manage Jenkins → Manage Plugins → Installed review
regularly |
RBAC and Isolation
// Pipeline as code: restrict what pipelines may do
// Configure in Manage Jenkins → Configure Global Security
// → Authorization → Project-based Matrix Authorization Strategy
// Example: Sandbox for untrusted pipelines
properties([
// Restrict which agents this pipeline may use
pipelineTriggers([]),
disableConcurrentBuilds()
])| Measure | Implementation |
|---|---|
| RBAC plugin | Role-Based Authorization Strategy plugin |
| Per-project permissions | Assign read/write/execute per project |
| Ephemeral agents | Jenkins agents as containers that are destroyed after each build |
| Shared library review | All shared libraries under version control with code review |
Reference Table
| Technique | Category | MITRE ATT&CK | Platform | Complexity |
|---|---|---|---|---|
| Expression injection | Code Execution | T1059.004 - Unix Shell | GitHub Actions | Medium |
| Self-hosted runner abuse | Initial Access | T1195.002 - Software Supply Chain | GitHub/GitLab | Medium |
| GITHUB_TOKEN abuse | Credential Access | T1528 - Steal Application Access Token | GitHub Actions | Low |
| pull_request_target exploit | Credential Access | T1528 | GitHub Actions | Medium |
| Secret exfiltration via artifacts | Credential Access | T1552.001 - Credentials In Files | All platforms | Low |
| Shared runner exploitation | Lateral Movement | T1021 - Remote Services | GitLab CI | Medium |
| CI/CD variable extraction | Credential Access | T1552.001 | GitLab CI | Low |
| Protected branch bypass | Defense Evasion | T1562.001 - Disable or Modify Tools | All platforms | Medium |
| Include directive abuse | Execution | T1059.004 | GitLab CI | Medium |
| Jenkins Script Console RCE | Execution | T1059.007 - JavaScript | Jenkins | Low |
| Groovy sandbox escape | Execution | T1059 - Command and Scripting | Jenkins | High |
| Jenkins credential theft | Credential Access | T1555 - Credentials from Password Stores | Jenkins | Medium |
| Pipeline as code injection | Execution | T1195.002 | Jenkins | Medium |
| Environment variable leaks | Credential Access | T1552.001 | All platforms | Low |
| Build log secrets | Credential Access | T1552.001 | All platforms | Low |
| Terraform state secrets | Credential Access | T1552.001 | All platforms | Low |
| Dependency confusion | Initial Access | T1195.001 - Software Dependencies | npm/PyPI/NuGet | Medium |
| Typosquatting | Initial Access | T1195.001 | npm/PyPI/NuGet | Low |
| Compromised Actions/Orbs | Execution | T1195.002 | GitHub/CircleCI | Medium |
| Malicious packages | Execution | T1195.001 | All package managers | Medium |
| Build artifact tampering | Persistence | T1195.002 | All platforms | Medium |
| ArgoCD exploitation | Initial Access | T1190 - Exploit Public-Facing Application | ArgoCD/K8s | Medium |
| Helm chart injection | Execution | T1610 - Deploy Container | Kubernetes/Helm | Medium |
| values.yaml secret extraction | Credential Access | T1552.001 | Kubernetes/Helm | Low |
| Status check manipulation | Defense Evasion | T1562.001 | GitHub/GitLab | Medium |
| Review circumvention | Defense Evasion | T1562.001 | All platforms | Medium |
| SLSA provenance forgery | Defense Evasion | T1195.002 | All platforms | High |
The software supply chain is the foundation on which modern software is built. It is also a foundation with cracks that we are only beginning to see. The attacks in this chapter -- from dependency confusion to pipeline injection -- exploit not so much technical vulnerabilities as the trust we place in our tools and processes. And trust, as experience teaches, is the first thing an attacker exploits.
In the next chapters we leave the build chain and look at the cloud environments where this software ultimately runs: AWS, Azure and GCP. Different environments, same mistakes.
Further reading in the knowledge base
These articles in the portal give you more background and practical context:
- The cloud -- someone else's computer, your responsibility
- Containers and Docker -- what it is and why you need to secure it
- Encryption -- the art of making things unreadable
- Least Privilege -- only give people what they need
You need an account to access the knowledge base. Log in or register.
Related security measures
These articles provide additional context and depth: