jan-karel.com
Home / Security Measures / Cloud Security / CI/CD Pipeline Hardening

CI/CD Pipeline Hardening

CI/CD Pipeline Hardening

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 possible

Least 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 sub claim is too broad (e.g. repo:ORG/* instead of repo: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.

Op de hoogte blijven?

Ontvang maandelijks cybersecurity-inzichten in je inbox.

← Cloud Security ← Home