CI/CDGitHub ActionsDevSecOpsOWASP ASVSSupply Chain SecurityCloud Run

Shipping Code with Confidence: My Zero-Trust 5-Gate GitHub Actions Security Pipeline 🛡️

Jorge Liauw Calo
Jorge Liauw CaloSecurity Engineer @ Google Cloud
•5 min read
Shipping Code with Confidence: My Zero-Trust 5-Gate GitHub Actions Security Pipeline 🛡️

If you work in Cloud Security or DevSecOps, you know a painful truth: your CI/CD pipeline is both your strongest defense and your highest-value attack surface.

Between supply-chain compromises targeting third-party GitHub Actions (like tj-actions/changed-files), secret leaks, dependency CVEs, and Content Security Policy (CSP) regressions at the CDN edge, running npm run build isn’t enough anymore.

Even for jorgeliauw.nl (built with Astro, Nginx, and Cloud Run), I wanted a pipeline that practices what I preach: Zero-Trust CI/CD modeled on OWASP Top 10 and OWASP ASVS L2 (v5.0).

Every commit passes through 5 pre-deploy security gates and 62 live post-deploy smoke checks in two minutes—with zero third-party GitHub Actions. Here is why securing the full chain matters, how it works, and how to deploy it!

GitHub Actions 5-Gate Security Report
Live GitHub Actions Summary: 5 pre-deploy gates (0 findings) and 62 post-deploy smoke checks in 2m 18s.

Beyond the Pipeline: Securing the Full SDLC Chain

As highlighted in Google Cloud Threat Intelligence’s guide on Hardening Code Pipelines and CI/CD Infrastructure, you cannot treat the pipeline in isolation. Attackers target the weakest link across the Software Development Lifecycle (SDLC):

Multi-layered SDLC security
Multi-layered SDLC security (Source: Google Cloud Threat Intelligence).
  1. Endpoint (Workstations & IDEs): Pre-commit checks, signed commits, and least-privilege IDE extensions.
  2. Code Repo (GitHub / GitLab): Branch protection, required reviews, and zero hardcoded secrets (OWASP A07).
  3. Artifact Management (NPM, Docker, PyPI): Pinned dependencies, package-lock.json audits, and image scans (OWASP A06).
  4. CI/CD (Build & Test): Zero third-party Actions (OWASP CI/CD-SEC-4), keyless OIDC, and static (CWE-79 / CWE-94), secret, and AST checks.
  5. Deployment (Live Infrastructure): Rootless Cloud Run containers and live verification of HTTP headers, TLS/DNS, and dotfile exposure (ASVS V14.4 / CWE-538).

Want a glimpse of how to build this secure development environment on Google Cloud? Software Supply Chain Security on Google Cloud unites Cloud Workstations & Cloud Code, Assured Open Source Software, Cloud Build, Artifact Registry (Artifact Analysis), and Cloud Deploy + Binary Authorization before GKE or Cloud Run (dedicated blog post coming soon!):

Google Cloud Software Supply Chain Security
Secure development environment on Google Cloud (Source: Google Cloud Docs).

How It Works: 5 Pre-Deploy Gates + Live Smoke Testing

Our workflow (.github/workflows/deploy.yml) runs two sequential jobs:

Job 1: Pre-Deploy 5-Gate Orchestrator (scripts/ci-security-scan.mjs)

A zero-dependency Node.js script (scripts/ci-security-scan.mjs) runs locally and in CI (--require-all-binaries), normalizing findings into { scanner, severity, file, line, ruleId, why, remediation }:

  • Gate 1 — Syntax & E2E Suite: Runs node --check, builds Astro, and executes 34 assertions (scripts/test-security-suite.mjs) verifying CSP, rel="noopener noreferrer", and Nginx rules.
  • Gate 2 — OWASP ASVS L2 Invariants: Blocks CWE-79 DOM XSS sinks (innerHTML, document.write), CWE-94 eval(), inline style="", inline on*=, and token prefixes.
  • Gate 3 — Trivy (v0.69.3): Audits package-lock.json and Dockerfile for HIGH/CRITICAL CVEs and misconfigs (catching 5 transitive CVEs in zod, vite, undici, source-map-js, and sharp on run #1!).
  • Gate 4 — Gitleaks (v8.30.1): Runs gitleaks detect --no-git across source and dist/ files so credentials never leak.
  • Gate 5 — Semgrep SAST (.semgrep.yml): Evaluates AST rules blocking DOM XSS sinks, eval, ReDoS (new RegExp(var)), execSync, and wildcard postMessage(..., "*").

If any gate fails, it emits GitHub annotations (::error file=...,line=...::), writes a Markdown table to $GITHUB_STEP_SUMMARY, comments on the commit/PR, and exits 1.

Job 2: Keyless Cloud Run Deploy + 62-Check Smoke Test (scripts/live-security-smoke.mjs)

When Job 1 passes on main, Job 2 exchanges GitHub’s OIDC JWT with Google Cloud STS, deploys to Cloud Run, invalidates Cloud CDN, and runs scripts/live-security-smoke.mjs against https://jorgeliauw.nl:

  1. Dotfile & Source Exposure: Asserts /.env, /.git/HEAD, /package.json, /nginx.conf, and /%3Cscript%3Ealert(1)%3C/script%3E return 403/404 with zero source leakage or reflected <script>.
  2. Strict Headers & SHA-256 Integrity: Verifies Content-Security-Policy, Strict-Transport-Security, nosniff, DENY, COOP, and CORP—and checks live asset SHA-256 hashes.
  3. Method Allowlists & CORS: Ensures DELETE, PUT, PATCH, and TRACE return 405/403, and foreign Origin headers are rejected.
  4. Transport, security.txt & DNS: Verifies HTTPS 301 redirects, /.well-known/security.txt, and DNS CAA, SPF, DMARC, and DNSSEC via DoH.

How to Deploy This in Your Own Pipeline

Adopt this pattern in three steps:

1. Replace third-party Actions with raw Git & pinned binaries

In .github/workflows/deploy.yml, check out code with git fetch and install pinned binaries:

- name: Zero-Trust Checkout & Scanner Install
  run: |
    git init . && git remote add origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"
    git -c http.extraheader="AUTHORIZATION: basic $(printf 'x-access-token:%s' "${GH_TOKEN}" | base64 -w 0)" \
      fetch --no-tags --prune --depth=1 origin "${GITHUB_SHA}"
    git checkout --force FETCH_HEAD
    curl -sSfL "https://github.com/aquasecurity/trivy/releases/download/v0.69.3/trivy_0.69.3_Linux-64bit.tar.gz" | sudo tar -xz -C /usr/local/bin trivy
    curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_linux_x64.tar.gz" | sudo tar -xz -C /usr/local/bin gitleaks
    pipx install semgrep

2. Add AST Guardrails in .semgrep.yml

Add .semgrep.yml in your repo root so Gate 5 catches dangerous AST patterns:

rules:
  - id: asvs-cwe-79-dom-xss-sink
    languages: [javascript, typescript]
    severity: ERROR
    message: "OWASP ASVS L2 (CWE-79): Unsafe DOM XSS sink detected."
    pattern-either:
      - pattern: $EL.innerHTML = $VAL
      - pattern: document.write(...)
  - id: asvs-cwe-345-postmessage-wildcard
    languages: [javascript, typescript]
    severity: ERROR
    message: "OWASP ASVS L2 (CWE-345): postMessage with wildcard '*' origin."
    pattern: $WIN.postMessage($DATA, "*")

3. Gate deployment on Job 1 & run post-deploy smoke tests

Require needs: security-and-syntax-check on your deploy job, then run the smoke test after deployment:

- name: Run Post-Deploy Live Black-Box Security Smoke Test
  env:
    SMOKE_BASE_URL: "https://your-domain.com"
  run: |
    for attempt in 1 2 3; do
      if node scripts/live-security-smoke.mjs; then exit 0; fi
      sleep 10
    done
    exit 1

Explore scripts/ci-security-scan.mjs, scripts/live-security-smoke.mjs, and .github/workflows/deploy.yml directly in my GitHub repo.

Secure the full chain, ditch third-party supply-chain risk, and ship with confidence. VAMOS! 🚀🛡️

Jorge Liauw Calo
Written By

Jorge Liauw Calo

Security Engineer at Google Cloud (Google Cybershield) with 13+ years of experience in Cybersecurity across highly regulated industries including Semiconductor, Banking, Fintech, and Insurance. Active member of the Google Cloud Community BeNeLux and Google Cloud Security Community Amsterdam.