Skip to main content
CleanStart

Knowledge Hub

Building CleanStart Images in GitHub Actions

The workflow below builds a CleanStart image, generates SLSA provenance, signs it with Cosign, and pushes to Artifact Registry. It runs on every merge to main and requires no manual steps.

The following diagram illustrates the complete GitHub Actions pipeline with all four stages from code commit to production deployment:

graph LR    A["Code Push to<br/>main/develop"] -->|Trigger| B["Analyze<br/>Job"]    B -->|SAST Scan| C["Trivy Security<br/>Analysis"]    C -->|Generate| D["CycloneDX<br/>SBOM"]    D -->|Upload| E["GitHub Artifacts"]     E -->|Dependency| F["Build<br/>Job"]    F -->|Checkout| G["Source Code"]    G -->|Docker Build| H["Hermetic<br/>Image Build"]    H -->|Run Tests| I["Container<br/>Tests"]    I -->|Generate| J["SLSA<br/>Provenance"]    J -->|Create| K["Build Artifacts<br/>SBOM + Provenance"]     K -->|Dependency| L["Verify<br/>Job"]    L -->|Download| M["Build Artifacts"]    M -->|Verify| N["SBOM<br/>Completeness"]    N -->|Check| O["FIPS<br/>Compliance"]    O -->|Verify| P["Image<br/>Digest"]     P -->|Dependency| Q["Push<br/>Job"]    Q -->|Authenticate| R["GCP OIDC"]    R -->|Push Image| S["Artifact<br/>Registry"]    S -->|Tag| T["Prod/Dev<br/>Registry"]    T -->|Complete| U["Pipeline<br/>Success"]

Prerequisites

1. GitHub Repository Setup

Enable the following GitHub features for enhanced security and automation. First, enable Dependabot through Settings → Code security → Enable Dependabot alerts to automatically scan your dependencies for known vulnerabilities and receive alerts. Second, GitHub Token is automatically provided by GitHub Actions and requires no manual configuration—it's available as secrets.GITHUB_TOKEN in all workflows.

2. Google Cloud Setup

# Create service account for GitHub Actionsgcloud iam service-accounts create github-actions-build \  --display-name="GitHub Actions Build Service" # Grant permissionsgcloud projects add-iam-policy-binding PROJECT_ID \  --member="serviceAccount:github-actions-build@PROJECT_ID.iam.gserviceaccount.com" \  --role="roles/artifactregistry.writer" gcloud projects add-iam-policy-binding PROJECT_ID \  --member="serviceAccount:github-actions-build@PROJECT_ID.iam.gserviceaccount.com" \  --role="roles/storage.admin" # Create workload identity binding (OIDC)gcloud iam service-accounts add-iam-policy-binding \  github-actions-build@PROJECT_ID.iam.gserviceaccount.com \  --role="roles/iam.workloadIdentityUser" \  --member="principalSet://iam.googleapis.com/projects/PROJECT_ID/locations/global/workloadIdentityPools/github/attribute.repository/org/repo"

3. Repository Secrets

Add to GitHub Settings → Secrets and Variables → Actions:

GCP_PROJECT_ID           # Your Google Cloud project IDARTIFACT_REGISTRY_URL    # us-docker.pkg.dev/PROJECT_ID/imagesGCP_WORKLOAD_IDENTITY    # github-actions-build@PROJECT_ID.iam.gserviceaccount.comCOSIGN_KEY_NAME          # kms://projects/PROJECT_ID/locations/us-central1/keyRings/cosign/cryptoKeys/github-actions

Complete Pipeline Workflow

Create .github/workflows/build-and-push.yml:

name: Build, Sign, and Push CleanStart Image on:  push:    branches:      - main      - develop    paths-ignore:      - 'docs/**'      - '*.md'  pull_request:    branches: [main, develop] env:  REGISTRY_URL: ${{ secrets.ARTIFACT_REGISTRY_URL }} jobs:  analyze:    name: Security Analysis    runs-on: ubuntu-latest    permissions:      contents: read      security-events: write     steps:      - name: Checkout code        uses: actions/checkout@v4        with:          fetch-depth: 0       - name: Run SAST (Trivy)        uses: aquasecurity/trivy-action@master        with:          scan-type: 'fs'          scan-ref: '.'          format: 'sarif'          output: 'trivy-results.sarif'       - name: Upload SAST results        uses: github/codeql-action/upload-sarif@v2        with:          sarif_file: 'trivy-results.sarif'       - name: Generate SBOM        uses: CycloneDX/gh-action-cyclonedx@master        with:          path: .          format: spdx          output-file: sbom.spdx       - name: Upload SBOM        uses: actions/upload-artifact@v3        with:          name: sbom          path: sbom.spdx   build:    name: Hermetic Build    runs-on: ubuntu-latest    needs: analyze    permissions:      contents: read      id-token: write     steps:      - name: Checkout code        uses: actions/checkout@v4       - name: Set up Docker Buildx        uses: docker/setup-buildx-action@v2       - name: Authenticate to Google Cloud (OIDC)        uses: google-github-actions/auth@v1        with:          workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY }}          service_account: ${{ secrets.GCP_WORKLOAD_IDENTITY_SA }}       - name: Set up Cloud SDK        uses: google-github-actions/setup-gcloud@v1       - name: Configure Docker authentication        run: |          gcloud auth configure-docker ${{ secrets.ARTIFACT_REGISTRY_URL }}       - name: Determine environment and image name        id: env        run: |          if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then            echo "environment=prod" >> $GITHUB_OUTPUT            echo "registry_path=${{ secrets.ARTIFACT_REGISTRY_URL }}/prod" >> $GITHUB_OUTPUT            echo "strict_verification=true" >> $GITHUB_OUTPUT          else            echo "environment=dev" >> $GITHUB_OUTPUT            echo "registry_path=${{ secrets.ARTIFACT_REGISTRY_URL }}/dev" >> $GITHUB_OUTPUT            echo "strict_verification=false" >> $GITHUB_OUTPUT          fi           # Image name from directory or repository          IMAGE_NAME=$(echo "${{ github.repository }}" | cut -d'/' -f2)          echo "image_name=$IMAGE_NAME" >> $GITHUB_OUTPUT          echo "image_tag=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT       - name: Build hermetic image        uses: docker/build-push-action@v4        with:          context: .          push: false          load: true          tags: |            ${{ steps.env.outputs.registry_path }}/${{ steps.env.outputs.image_name }}:${{ steps.env.outputs.image_tag }}            ${{ steps.env.outputs.registry_path }}/${{ steps.env.outputs.image_name }}:latest          cache-from: type=gha          cache-to: type=gha,mode=max          outputs: type=docker       - name: Run container tests        run: |          docker run --rm \            ${{ steps.env.outputs.registry_path }}/${{ steps.env.outputs.image_name }}:latest \            /test.sh || exit 1       - name: Install clnstrt-cli        run: |          curl -sSL https://releases.cleanstart.dev/clnstrt-cli-latest.tar.gz | tar xz          chmod +x ./clnstrt-cli       - name: Generate SLSA provenance        id: slsa        run: |          ./clnstrt-cli generate-sbom \            --image ${{ steps.env.outputs.registry_path }}/${{ steps.env.outputs.image_name }}:${{ steps.env.outputs.image_tag }} \            --output image-sbom.spdx           # Create in-toto provenance (simplified)          cat > provenance.json <<EOF          {            "predicateType": "https://slsa.dev/provenance/v0.2",            "predicate": {              "builder": {                "id": "https://github.com/${{ github.repository }}/actions/workflows/${{ github.workflow }}"              },              "sourceRepository": "${{ github.repository }}",              "ref": "${{ github.ref }}",              "commit": "${{ github.sha }}",              "buildStartedOn": "$(date -u +'%Y-%m-%dT%H:%M:%SZ')",              "buildFinishedOn": "$(date -u +'%Y-%m-%dT%H:%M:%SZ')",              "completeness": {                "arguments": true,                "environment": true,                "materials": true              },              "reproducible": false            }          }          EOF          echo "provenance_file=provenance.json" >> $GITHUB_OUTPUT       - name: Upload build artifacts        uses: actions/upload-artifact@v3        with:          name: build-artifacts          path: |            image-sbom.spdx            provenance.json   verify:    name: Security Verification    runs-on: ubuntu-latest    needs: build    permissions:      contents: read      id-token: write     steps:      - name: Download artifacts        uses: actions/download-artifact@v3       - name: Install clnstrt-cli        run: |          curl -sSL https://releases.cleanstart.dev/clnstrt-cli-latest.tar.gz | tar xz          chmod +x ./clnstrt-cli       - name: Verify SBOM completeness        run: |          ./clnstrt-cli verify \            --sbom build-artifacts/image-sbom.spdx \            --min-components 100       - name: Check FIPS compliance        run: |          ./clnstrt-cli verify-fips \            --sbom build-artifacts/image-sbom.spdx       - name: Authenticate to Google Cloud (OIDC)        uses: google-github-actions/auth@v1        with:          workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY }}          service_account: ${{ secrets.GCP_WORKLOAD_IDENTITY_SA }}       - name: Set up Cloud SDK        uses: google-github-actions/setup-gcloud@v1       - name: Install Cosign        uses: sigstore/cosign-installer@v3       - name: Verify image digest        run: |          IMAGE_DIGEST=$(gcloud artifacts docker images list \            --repository=${{ secrets.ARTIFACT_REGISTRY_URL }}/dev \            --format='value(IMAGE)' | head -1)          echo "Verified image: $IMAGE_DIGEST"   push:    name: Push to Registry    runs-on: ubuntu-latest    needs: [verify, build]    permissions:      contents: read      id-token: write    if: github.event_name == 'push'     steps:      - name: Checkout code        uses: actions/checkout@v4       - name: Set up Docker Buildx        uses: docker/setup-buildx-action@v2       - name: Authenticate to Google Cloud (OIDC)        uses: google-github-actions/auth@v1        with:          workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY }}          service_account: ${{ secrets.GCP_WORKLOAD_IDENTITY_SA }}       - name: Set up Cloud SDK        uses: google-github-actions/setup-gcloud@v1       - name: Configure Docker authentication        run: |          gcloud auth configure-docker ${{ secrets.ARTIFACT_REGISTRY_URL }}       - name: Determine environment        id: env        run: |          if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then            echo "registry_path=${{ secrets.ARTIFACT_REGISTRY_URL }}/prod" >> $GITHUB_OUTPUT          else            echo "registry_path=${{ secrets.ARTIFACT_REGISTRY_URL }}/dev" >> $GITHUB_OUTPUT          fi          IMAGE_NAME=$(echo "${{ github.repository }}" | cut -d'/' -f2)          echo "image_name=$IMAGE_NAME" >> $GITHUB_OUTPUT          echo "image_tag=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT       - name: Build and push image        uses: docker/build-push-action@v4        with:          context: .          push: true          tags: |            ${{ steps.env.outputs.registry_path }}/${{ steps.env.outputs.image_name }}:${{ steps.env.outputs.image_tag }}            ${{ steps.env.outputs.registry_path }}/${{ steps.env.outputs.image_name }}:latest          cache-from: type=gha          cache-to: type=gha,mode=max       - name: Install Cosign        uses: sigstore/cosign-installer@v3       - name: Sign image with Cosign        run: |          cosign sign --key ${{ secrets.COSIGN_KEY_NAME }} \            ${{ steps.env.outputs.registry_path }}/${{ steps.env.outputs.image_name }}:${{ steps.env.outputs.image_tag }}       - name: Download and attach SLSA provenance        uses: actions/download-artifact@v3        with:          name: build-artifacts       - name: Attach attestations        run: |          cosign attest --predicate provenance.json \            --key ${{ secrets.COSIGN_KEY_NAME }} \            ${{ steps.env.outputs.registry_path }}/${{ steps.env.outputs.image_name }}:${{ steps.env.outputs.image_tag }}       - name: Create deployment record        run: |          echo "Image: ${{ steps.env.outputs.registry_path }}/${{ steps.env.outputs.image_name }}:${{ steps.env.outputs.image_tag }}" >> deployment.log          echo "Commit: ${{ github.sha }}" >> deployment.log          echo "Built at: $(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> deployment.log   notify:    name: Notify Build Status    runs-on: ubuntu-latest    needs: [analyze, build, verify, push]    if: always()     steps:      - name: Check pipeline status        run: |          if [[ "${{ needs.analyze.result }}" == "failure" || \                "${{ needs.build.result }}" == "failure" || \                "${{ needs.verify.result }}" == "failure" ]]; then            echo "::error::Pipeline failed at stage"            exit 1          fi       - name: Post success to GitHub        if: success()        run: |          echo "✅ Secure pipeline completed successfully"

Dockerfile for CleanStart

# Multi-stage build: dev/builder → prod FROM cleanstart-gcc:14-builder AS builderRUN apk add --no-cache git make autoconf automake libtool COPY . /srcWORKDIR /src # Hermetic build: no external networkRUN ./configure --prefix=/usr/local \    && make -j$(nproc) \    && make test # Generate SBOM during buildRUN clnstrt-cli generate-sbom \    --output /build-artifacts/sbom.spdx \    --format spdx FROM cleanstart-gcc:14-prodRUN groupadd -r appuser && useradd -r -g appuser appuserUSER appuser COPY --from=builder /usr/local /usr/localCOPY --from=builder /build-artifacts /attestations HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \  CMD /usr/local/bin/healthcheck || exit 1 ENTRYPOINT ["/usr/local/bin/myapp"]

Deployment Verification

After pipeline completes:

# Verify image signaturecosign verify --certificate-identity=https://github.com/org/repo/actions \  gcr.io/my-project/app:latest # View SLSA provenancecosign verify-attestation --type slsa \  gcr.io/my-project/app:latest # Check SBOMcosign verify-attestation --type sbom \  gcr.io/my-project/app:latest | jq .

Troubleshooting

Cosign Verification Fails

Cosign verification failures typically stem from certificate identity mismatches. Check that the certificate identity matches your GitHub Actions workload by listing the service account keys to ensure they're properly configured:

gcloud iam service-accounts keys list \  --iam-account=github-actions-build@PROJECT_ID.iam.gserviceaccount.com

OIDC Token Exchange Fails

OIDC token exchange failures indicate problems with workload identity pool configuration. Verify your workload identity pools are properly configured by listing all pools and describing the GitHub pool in detail:

gcloud iam workload-identity-pools list --location=globalgcloud iam workload-identity-pools describe github \  --location=global --format=json

Image Not Found in Registry

Images that cannot be found in the registry often result from Docker authentication not being properly configured. Confirm that Docker auth is configured for the Artifact Registry and that you can manually pull images:

gcloud auth configure-docker us-docker.pkg.devdocker pull us-docker.pkg.dev/PROJECT_ID/images/app:latest