Home/Blog/Securing CI/CD Pipelines
SECURITY

Securing CI/CD Pipelines

calendar_todayFeb 10, 2026schedule5 min readpersonArtem Porubai

Your CI/CD pipeline has more access to production infrastructure than most engineers on your team. It holds cloud credentials, can push container images, deploy to Kubernetes clusters, and modify DNS records. A compromised pipeline is a full-blown breach. This article covers practical hardening measures for GitHub Actions, starting with the highest-impact changes.

1. Replace Long-Lived Credentials with OIDC Federation

The single most impactful security improvement is eliminating static AWS access keys from your CI environment. GitHub Actions supports OpenID Connect (OIDC) federation, which lets your workflow assume an IAM role using a short-lived, automatically rotated JWT -- no secrets stored anywhere.

Setting Up the IAM OIDC Provider

# terraform/modules/github-oidc/main.tf
resource "aws_iam_openid_connect_provider" "github" {
  url             = "https://token.actions.githubusercontent.com"
  client_id_list  = ["sts.amazonaws.com"]
  thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}
 
resource "aws_iam_role" "github_actions" {
  name = "github-actions-deploy"
 
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Principal = {
        Federated = aws_iam_openid_connect_provider.github.arn
      }
      Action = "sts:AssumeRoleWithWebIdentity"
      Condition = {
        StringEquals = {
          "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
        }
        StringLike = {
          # Lock to specific repo and branch
          "token.actions.githubusercontent.com:sub" = "repo:myorg/myapp:ref:refs/heads/main"
        }
      }
    }]
  })
}
 
resource "aws_iam_role_policy_attachment" "deploy" {
  role       = aws_iam_role.github_actions.name
  policy_arn = aws_iam_policy.deploy_permissions.arn
}

Using OIDC in the Workflow

# .github/workflows/deploy.yaml
permissions:
  id-token: write   # Required for OIDC
  contents: read
 
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
          aws-region: eu-west-1
          # No access keys needed -- OIDC handles authentication

The condition repo:myorg/myapp:ref:refs/heads/main ensures that only workflows triggered from the main branch of a specific repository can assume this role. A forked repo or a feature branch cannot.

2. Least-Privilege IAM Policies

Your deploy role should have exactly the permissions it needs and nothing more. Start with zero permissions and add them as the pipeline fails:

# Example: minimal ECR + EKS deploy permissions
resource "aws_iam_policy" "deploy_permissions" {
  name = "github-actions-deploy-policy"
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "ECRPushPull"
        Effect = "Allow"
        Action = [
          "ecr:GetAuthorizationToken",
          "ecr:BatchCheckLayerAvailability",
          "ecr:PutImage",
          "ecr:InitiateLayerUpload",
          "ecr:UploadLayerPart",
          "ecr:CompleteLayerUpload"
        ]
        Resource = "arn:aws:ecr:eu-west-1:123456789012:repository/myapp"
      },
      {
        Sid    = "EKSAccess"
        Effect = "Allow"
        Action = [
          "eks:DescribeCluster",
          "eks:ListClusters"
        ]
        Resource = "arn:aws:eks:eu-west-1:123456789012:cluster/production"
      }
    ]
  })
}

3. Container Image Signing with Cosign

Signing your container images creates a cryptographic proof of origin. Kubernetes admission controllers (like Kyverno or Sigstore's policy-controller) can then enforce that only signed images are deployed.

# Keyless signing with cosign + GitHub OIDC
- name: Sign container image
  env:
    COSIGN_EXPERIMENTAL: "1"
  run: |
    cosign sign --yes \
      --oidc-issuer https://token.actions.githubusercontent.com \
      ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}

With keyless signing, there are no private keys to manage. Cosign uses the GitHub OIDC token to obtain a short-lived certificate from Sigstore's Fulcio CA, signs the image, and uploads the signature to the Rekor transparency log. Anyone can verify the signature using the public transparency log.

Enforcing Signed Images in Kubernetes

# kyverno-policy.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-signed-images
spec:
  validationFailureAction: Enforce
  rules:
    - name: check-cosign-signature
      match:
        resources:
          kinds: ["Pod"]
      verifyImages:
        - imageReferences:
            - "123456789012.dkr.ecr.eu-west-1.amazonaws.com/myapp:*"
          attestors:
            - entries:
                - keyless:
                    subject: "https://github.com/myorg/myapp/.github/workflows/*"
                    issuer: "https://token.actions.githubusercontent.com"

4. Supply Chain Security with SLSA

SLSA (Supply-chain Levels for Software Artifacts) is a framework for ensuring the integrity of your build process. At SLSA Level 3, the build is fully defined by source code, uses a hardened build platform, and produces a signed provenance attestation.

# Generate SLSA provenance for container images
- uses: slsa-framework/slsa-github-generator/.github/workflows/[email protected]
  with:
    image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
    digest: ${{ steps.build.outputs.digest }}
  secrets:
    registry-username: ${{ github.actor }}
    registry-password: ${{ secrets.GITHUB_TOKEN }}

5. Workflow Hardening Checklist

Beyond OIDC and signing, apply these hardening measures to every workflow:

  • Pin actions by SHA, not by tag. Tags are mutable: uses: actions/checkout@b4ffde65f4
  • Set minimal permissions at the workflow level. Default to permissions: {} and add only what is needed per job.
  • Disable fork PR workflows for repos with deployment credentials. Use pull_request_target only when absolutely necessary.
  • Use environment protection rules. Require manual approval for production deployments.
  • Audit third-party actions. Periodically review every action in your workflows. Use actions/dependency-review-action to catch vulnerable dependencies.
  • Enable branch protection with required reviews, status checks, and signed commits.

6. Detecting Compromised Workflows

Set up monitoring to catch anomalous CI behavior:

  • Alert on workflows that run outside business hours or from unexpected branches.
  • Monitor for new OIDC role assumptions in CloudTrail.
  • Track which actions are used across your org with gh api /orgs/{org}/actions/permissions.
  • Use GitHub's audit log API to detect changes to workflow files, secrets, and environment configurations.

Key Takeaways

  • OIDC federation eliminates the highest-risk secret in your CI: static cloud credentials.
  • Signing images with cosign creates an auditable chain of trust from source to deployment.
  • SLSA provenance attestations prove your artifacts were built from the expected source, on the expected platform.
  • Defense in depth: even with OIDC, apply least-privilege, pin actions, and monitor for anomalies.