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-actionsComplete 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.comOIDC 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=jsonImage 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