Lesson 05 Security Scanning & Image Signing
Theme:

Solutions — Security Scanning & Image Signing

Lesson 05 · Cloud Infrastructure & DevOps · reference solutions

Task 1 solution — .trivyignore

Create a .trivyignore file in the repository root. One CVE ID per line. Comments (lines starting with #) are supported:

# CVE in libc6, no fix available upstream as of 2024-12
CVE-2024-12345

Reference the file in the Trivy workflow step via trivyignores:

- name: Trivy scan
  uses: aquasecurity/trivy-action@0.24.0
  with:
    image-ref: ${{ matrix.service }}:ci-${{ github.sha }}
    severity: HIGH,CRITICAL
    exit-code: 1
    ignore-unfixed: true
    trivyignores: .trivyignore

Common mistake

Placing .trivyignore next to the Dockerfile but invoking Trivy from the repository root. Trivy looks for .trivyignore relative to its working directory, not relative to the image context. If your workflow runs from the repo root (default), put .trivyignore at the repo root. If you need a different path, use the trivyignores input to specify an absolute or relative path explicitly.

Task 2 solution — sign on tag pushes

Two changes to security.yml: add tags: ['v*'] to the trigger, and update the image tags: in the build step to include github.ref_name:

on:
  push:
    branches: [main]
    tags: ['v*']

Inside the build step, update the tags: input:

      - name: Build & push ${{ matrix.service }}
        id: build
        uses: docker/build-push-action@v5
        with:
          context: ./cloud-devops/sample-app/${{ matrix.service }}
          push: true
          tags: |
            ghcr.io/${{ github.repository }}/${{ matrix.service }}:${{ github.ref_name }}
            ghcr.io/${{ github.repository }}/${{ matrix.service }}:${{ github.sha }}
          cache-from: type=gha,scope=${{ matrix.service }}
          cache-to: type=gha,mode=max,scope=${{ matrix.service }}

Common mistake

Using github.sha instead of github.ref_name for the human-readable tag. On a tag push, github.ref_name evaluates to the tag name (e.g., v0.1.0). On a branch push, it evaluates to the branch name (e.g., main). Using only github.sha produces a valid and signed image, but users have no way to pull it by version number — they would need to look up the SHA first.

Task 3 solution — verification job

Add a verify job to security.yml that runs after sign:

  verify:
    runs-on: ubuntu-latest
    needs: [sign]
    strategy:
      matrix:
        service: [api, worker]
    steps:
      - uses: sigstore/cosign-installer@v3
      - name: Verify signature
        env:
          COSIGN_EXPERIMENTAL: "true"
        run: |
          cosign verify \
            --certificate-identity-regexp "https://github.com/${{ github.repository }}/.*" \
            --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
            ghcr.io/${{ github.repository }}/${{ matrix.service }}:${{ github.sha }}

Common mistake

Running cosign verify without --certificate-identity-regexp. This verifies that the signature is cryptographically valid (the cert chain leads to Fulcio) but does not check who signed it. An attacker with a valid GitHub identity on any repository could produce a signature that passes this check. The --certificate-identity-regexp flag pins verification to your specific repository and workflow path, preventing substitution attacks.

Closing tip: Lesson 06 turns these signed images into a deploy step — pushing to a target environment with secret-scoped credentials.