CI/CD SECURITY GUIDE FOR GITHUB ACTIONS | VIBEEVAL
CI/CD is how secrets, unscanned code, and open previews reach production. Harden pipelines for AI-generated apps that ship many times a day.
CI/CD Is a Prime Attack Vector
GitHub Actions workflows often have access to production secrets and deployment permissions. AI-generated workflows frequently hardcode credentials, use unpinned actions, and grant excessive permissions, making them targets for supply chain attacks and credential theft. For vibe-coded products shipping several times a day, the pipeline is the fastest path from “works on my machine” to “keys in a public log.”
Treat every workflow as privileged code. A malicious or sloppy PR that edits .github/workflows/ can mint tokens, push packages, or dump env into logs. Defenders win by least privilege, immutability (pinned actions), secret hygiene, and a security gate on the deployed URL — not by hoping the agent “got CI right.”
Scope of this guide
We focus on GitHub Actions because that is what most AI scaffolds emit. The same principles map to GitLab CI, CircleCI, and Buildkite: pin third-party logic, least-privilege tokens, environment protection for production, no secrets in logs, and a post-deploy or preview dynamic scan. Examples use Node because vibe-coded apps are mostly JS/TS; the control set is language-agnostic.
Threat model for a startup CI system
Assets attackers want from your GitHub Actions:
- Cloud deploy roles (production AWS/GCP/Azure)
- Package registry tokens (npm, Docker Hub, GHCR)
- SaaS API keys (Stripe, OpenAI, Supabase service role)
- Ability to push code to
mainor poison releases
Entry points:
- Compromised developer account without 2FA
- Malicious dependency executed in
npm test - Unpinned action that turns evil on tag move
pull_request_targetrunning untrusted PR code with secrets- Self-hosted runner on a laptop that also browses the web
AI-generated workflows increase entry-point density by copying every anti-pattern that ever made a tutorial succeed. Your job is to reduce density and blast radius until a single mistake cannot empty production.
Why AI-generated pipelines fail first
Cursor, Windsurf, Copilot, and chat agents pull workflow examples from blogs and READMEs. Those snippets optimize for “green check,” not for attacker cost. Common AI mistakes:
uses: actions/checkout@v4or@mainwithout SHA pins.permissions: write-allor omittingpermissionsso defaults stay too broad.echo $API_KEYorprintenv“for debugging” left in.- Long-lived
AWS_ACCESS_KEY_IDin repository secrets instead of OIDC. - Deploy-from-every-branch without environment reviewers.
- No secret scanning or dependency audit step.
- Security “scan” that only runs on main after production is already live.
If an agent generated your pipeline, re-read it as if an intern pasted it from Stack Overflow — because that is the threat model.
GitHub Actions Security Checklist
Follow these steps end to end. Critical items prevent credential theft and supply chain attacks.
1. Use GitHub Actions secrets (never hardcode)
Store all sensitive values in encrypted GitHub secrets or environment secrets. Never put tokens in workflow YAML, composite actions committed to the repo, or env: blocks with literal keys.
# Bad — visible to anyone with repo read access
env:
STRIPE_KEY: sk_live_example_do_not_do_this
# Good
env:
STRIPE_KEY: ${{ secrets.STRIPE_KEY }}
Prefer environment secrets (production, staging) over a single flat repository secret bag so staging cannot use prod credentials.
2. Pin action versions to commit SHA
Reference third-party actions by full commit SHA. Tags and branches move.
# Bad — movable tag
- uses: actions/checkout@v4
# Good — immutable SHA (example; replace with current verified SHA)
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
Comment the human-readable version next to the SHA. Upgrade deliberately with a PR that diffs the action repo. Prefer official actions/* and verified publishers; audit community actions before first use.
3. Restrict workflow permissions
Set top-level defaults to read-only, then grant job-scoped writes only where needed.
name: ci
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- run: npm ci && npm test
deploy:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # for OIDC
environment: production
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# deploy steps...
Avoid pull_request_target unless you understand the checkout-of-fork risk; it is a frequent AI-pasted footgun that can expose secrets to untrusted PR code.
4. Enable branch protection
Require status checks and reviews before merging to main. Prevent force-pushes and direct commits. Require that workflow changes themselves get review — attackers love PRs that only touch .github/.
5. Audit third-party actions
For each uses: line: who publishes it, last commit activity, open issues about security, whether it needs write tokens. Prefer thin official actions over mega-community “do everything” actions that request broad permissions.
6. Prevent secret logging
GitHub masks known secrets in logs, but masking is incomplete: partial prints, base64, and custom encoding leak. Never echo secrets, never pass them on the command line where process lists show them, never dump env in debug mode on public forks.
# Bad
- run: echo "Key is $STRIPE_KEY"
# Better — use env and tools that read from env without printing
- run: npm run migrate
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
7. Use environment protection rules
Configure required reviewers for production. Restrict which branches can deploy. Add wait timers if you need a human gap before prod. Staging can be automatic; production should not be a single push away for every contributor.
8. Enable dependency and secret scanning
Turn on Dependabot (or equivalent), secret scanning, and push protection on the org. In CI, fail on high severity:
- name: Dependency audit
run: npm audit --audit-level=high
- name: Secret scan
uses: gitleaks/gitleaks-action@v2 # pin to SHA in real use
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
9. Implement OIDC for cloud access
Use OpenID Connect instead of long-lived cloud credentials for AWS, Azure, or GCP.
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # pin me
with:
role-to-assume: arn:aws:iam::123456789012:role/github-deploy
aws-region: us-east-1
- run: ./scripts/deploy.sh
Cloud IAM trust policies should lock sub to your repo and branch (e.g., only repo:org/app:ref:refs/heads/main). See provider docs for the exact claim conditions.
10. Review workflow run logs
Periodically sample failed and successful runs for odd steps, unexpected workflow_dispatch actors, and new secrets usage. Disable leftover debug workflows. Rotate any secret that may have appeared in a log.
11. Separate dev and prod workflows
Different jobs, different environments, different cloud roles. A PR preview deploy should not share the production deploy role. Preview secrets should be non-production data.
12. Require approval for production
Manual approval via GitHub Environments stops both accidents and compromised developer laptops from shipping alone. Combine with CODEOWNERS on workflow paths.
Hardened starter workflow (pattern)
Illustrative pattern for a Node app with preview URL scanning. Replace SHAs and URLs; wire your real scanner CLI or action.
name: ci-and-preview-gate
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
pull-requests: read
jobs:
build-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # pin me
with:
node-version: "22"
cache: npm
- run: npm ci
- run: npm audit --audit-level=high
- run: npm test
- run: npm run build
# After your platform creates a preview URL:
dynamic-security:
needs: build-test
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Probe preview URL
env:
PREVIEW_URL: ${{ secrets.PREVIEW_URL_OR_FROM_DEPLOY_STEP }}
run: |
# Call your security gate against the live preview
# e.g. vibe-eval scan "$PREVIEW_URL" --fail-on critical
echo "Point this step at your dynamic scanner"
Pair static gates (audit, secret scan, unit tests) with a dynamic probe once a URL exists. Static tools miss open Supabase RLS; live probes catch them. See Vibe Code Scanner and environment variables security.
Common CI/CD Security Vulnerabilities
Secrets in workflow files
API keys and credentials hardcoded in .github/workflows YAML are visible to anyone with repository access (and often forever in git history). Rotate immediately if found; rewrite history only with a deliberate incident plan.
Unpinned action versions
Using @main or floating @v1 tags lets a compromised action run arbitrary code in your runners with your secrets. Pin SHA; review upgrades.
Overpermissive GITHUB_TOKEN
Workflows granted write to contents, packages, and security-events when they only need read enable privilege escalation from a single job compromise. Default to read; escalate per job.
Secrets printed in logs
Scripts that dump environment variables or use set -x with secrets in argv leak into logs. On public repos, that is world-readable.
Poisoned PR workflows
Fork PRs that change workflows, or pull_request_target checking out untrusted code with secrets, are classic CI takeovers. Restrict who can alter workflows; avoid dangerous triggers.
Self-hosted runner exposure
Self-hosted runners are high value: persist access, steal broader network credentials. Isolate them, use ephemeral runners where possible, and never attach secrets-capable workflows to shared public runner pools carelessly.
AI-generated “security theater”
A job named security that only runs echo OK or a linter without fail thresholds. Require real tools and non-zero exit on findings.
AI-generated workflow pitfalls (checklist)
When an agent edits CI, force a human pass on:
| Pitfall | What to verify |
|---|---|
| Floating tags | Every uses: has a SHA |
| Broad permissions | Top-level permissions: contents: read |
| Debug leftovers | No printenv, no secret echo |
| Prod credentials on PR | Prod secrets only on protected environments |
| Missing OIDC | No static cloud access keys if OIDC is available |
| No deploy gate | Preview/production URL scanned for keys and auth |
| Script injection | Never interpolate untrusted PR titles into run: scripts |
Script injection example to avoid:
# Dangerous — PR title is attacker-controlled
- run: echo "Building ${{ github.event.pull_request.title }}"
Use env: with a quoted env var, or avoid interpolating untrusted fields into shells entirely.
Preview environments: the silent prod clone
AI teams love preview deploys per PR. Attackers love them too when previews share production secrets or production databases.
Rules:
- Data: seed synthetic data; never clone prod PII into ephemeral previews without masking and access controls.
- Secrets: preview env uses Stripe test keys, non-prod Supabase projects, separate OAuth apps.
- Auth: preview URLs should not be indexable; consider password protection or Vercel/Netlify auth for private apps.
- RLS: a “temporary” Supabase branch with RLS off is still on the public internet if the anon key is in the preview bundle.
- Lifetime: auto-delete previews; stale
pr-403.example.comwith last month’s service role is a time bomb.
Wire dynamic scanning to the preview URL from the deploy output, not only to production after merge. Catching open RLS on a PR is cheaper than rotating keys after launch.
Monorepos, reusable workflows, and composite actions
Larger AI-assisted codebases accumulate .github/workflows/reusable-*.yml and local composite actions. Each is part of the TCB (trusted computing base):
- Pin third-party actions by SHA inside reusables too — a floating tag inside a reusable workflow bypasses top-level discipline.
- Pass secrets explicitly (
secrets: inheritonly when required); prefer named secrets maps. - Do not grant
id-token: writeto reusables that only lint. - Review composite actions for
run:steps that interpolate untrusted inputs.
When an agent “refactors CI,” treat the diff like a privilege change, not a style change.
Artifact and package publishing
Workflows that publish npm packages, container images, or GitHub releases need tighter controls:
| Publish type | Controls |
|---|---|
| npm | Trusted publishing / OIDC where available; no classic tokens in repo secrets long-term |
| Container images | Sign images (cosign); scan with Trivy before push; immutable tags |
| GitHub Releases | Separate job with contents: write only on tag push from protected branch |
| Deploy keys | Read-only where possible; never share deploy keys across repos casually |
A compromised publish job is a supply-chain incident for everyone downstream — including your own production pull-through caches.
GitHub Actions hardening for vibe-coded startups (minimal set)
If you only implement six things this week:
- Top-level
permissions: contents: read - SHA-pin
actions/checkoutandactions/setup-node - Secrets only via
${{ secrets.* }}and environment protection onproduction npm audit/ Dependabot + gitleaks (or equivalent) on PRs- Dynamic scan step on preview URL with fail-on critical
- CODEOWNERS requiring review for
.github/**
That set blocks the majority of AI-pasted CI disasters without enterprise bureaucracy.
Incident response when CI leaks a secret
- Revoke the secret at the provider immediately (cloud key, npm token, Stripe key).
- Rotate anything that shared the same scope.
- Purge workflow logs if the platform allows; assume public forks already copied logs.
- Audit git history for the secret; rewrite only with a controlled process — rotation matters more than history cosmetics.
- Review recent deploys and package publishes for unauthorized artifacts.
- Patch the workflow that printed or exposed the value; add a regression check.
Do not “fix later” after a printenv incident. Bots scrape GitHub Actions logs.
Comparing CI risk to app risk
Hardened CI with an open Supabase project still loses customer data. Open CI with perfect RLS still loses cloud accounts. Attackers pick the cheaper door:
Poisoned PR → workflow secrets → cloud admin
vs
Public anon key → missing RLS → full table dump
Secure both. The Poisoned CI pattern and live Vibe Code Scanner cover opposite ends of the same release pipeline.
Operational playbook
- Inventory all workflows and reusable actions in the org.
- Pin SHAs and set default read permissions in a single hardening PR.
- Move cloud auth to OIDC; rotate and delete old static keys.
- Split staging/prod environments with required reviewers on prod.
- Add secret scanning + dependency audit + dynamic URL scan.
- CODEOWNERS for
.github/and infrastructure paths. - Isolate preview secrets and data from production.
- Sign and scan published artifacts when you publish packages or images.
- Quarterly: re-read actions’ release notes, re-pin SHAs, review who can approve prod.
- Run a tabletop: “PR changes workflow + prints env” — confirm detection and rotation steps.
Matrix builds and secrets fan-out
AI often suggests matrix strategies (node: [18,20,22], multi-OS). Each cell multiplies secret exposure and log volume. Rules:
- Do not put production deploy secrets on matrix jobs that only lint.
- Prefer a single deploy job after a matrix of tests.
- Cache carefully — poisoned caches are a known class of CI attack; prefer official cache actions pinned by SHA.
- When testing against real cloud sandboxes, use ephemeral credentials per job via OIDC, not a shared long-lived key copied into every matrix leg.
Required reviewers and CODEOWNERS patterns
# .github/CODEOWNERS
/.github/ @org/security-owners @org/platform
/infra/ @org/platform
/supabase/migrations/ @org/backend
Combine with branch protection: require CODEOWNERS review, require status checks, block force push. AI PRs that only “fix formatting” but touch workflows should still hit the security owners path.
Comparing GitHub-hosted vs self-hosted runners for vibe teams
| Factor | GitHub-hosted | Self-hosted |
|---|---|---|
| Patching | Vendor | You |
| Network reach | Internet egress default | Often full VPC — higher blast radius |
| Ephemerality | High | Often low unless you build it |
| Cost at scale | Per-minute | CapEx/OpEx tradeoff |
| Fit for secrets | Good with OIDC | Dangerous if shared sticky runners |
Default recommendation for startups: GitHub-hosted + OIDC. Introduce self-hosted only with ephemeral VMs and strict labels so untrusted PR workflows cannot schedule onto privileged runners.
Minimal policy for AI-generated workflow PRs
Any PR that touches .github/workflows requires:
- Human review from a maintainer
- No new secrets in YAML
- Actions pinned to SHA
permissions:set to least privilege- No
pull_request_targetunless deeply understood
Example forbidden patterns
- run: echo ${{ github.event.issue.title }} # injection
- uses: some-action@main # mutable
permissions: write-all # overbroad
OIDC adoption path
- Create cloud role trusting GitHub OIDC.
- Grant only deploy permissions.
- Switch workflow to
configure-aws-credentials(pinned). - Delete static access keys from GitHub secrets.
- Monitor cloud CloudTrail for the role.
Dynamic scan gate for vibe-coded previews
Static CI (audit, Semgrep, gitleaks) misses open Supabase RLS and client-shipped keys that only appear after Vite/Next build. Wire the deploy step to emit PREVIEW_URL, then:
- name: Dynamic security gate
env:
PREVIEW_URL: ${{ steps.deploy.outputs.url }}
VIBEEVAL_TOKEN: ${{ secrets.VIBEEVAL_TOKEN }}
run: |
# Fail the job on Critical findings against the live preview
npx --yes @vibeeval/cli scan "$PREVIEW_URL" --fail-on critical
Block merge when Criticals exist. Mediums can warn. Pair with Vibe Code Scanner for interactive triage.
Secrets hierarchy for AI teams
| Secret class | Storage | Who can use |
|---|---|---|
| Production cloud deploy | GitHub Environment production + OIDC |
Main branch only, reviewers required |
| Preview host tokens | Environment preview |
PR jobs |
| SaaS test keys | Preview env | PR jobs |
| SaaS live keys | Production env only | Deploy job after approval |
| Supabase service_role | Never in VITE_/NEXT_PUBLIC_; server/OIDC only |
Backend jobs |
Document this matrix in SECURITY.md so coding agents do not invent “put the key in the workflow YAML.”
Workflow files as privileged code
Require CODEOWNERS on .github/**. Any PR that only “fixes CI” still needs security review — that is a common social-engineering path. Diff checklists:
- New
permissions:blocks - New
secrets.references - New
pull_request_targetorworkflow_runtriggers - Unpinned
uses:lines scriptsteps interpolating PR titles/bodies
Devin / agent PRs and CI
Autonomous agents often edit workflows to “make CI green” by setting continue-on-error: true on security jobs or deleting required checks. Branch protection must require the named security status checks, not merely “some check passed.” Review agents’ workflow diffs with the same severity as auth code. See Is Devin Safe?.
Supply chain: actions marketplace
Prefer:
- Official
actions/*and cloud-provider actions - SHA pins with version comments
- Minimal permissions on the job using the action
Avoid community actions that request contents: write to “commit back” unless you fully trust and pin them. Compromised actions equal secret exfiltration on every run.
Related Resources
- Environment Variables Security — Secure secrets management in CI/CD pipelines
- Vercel Security Guide — Secure Vercel deployments from GitHub Actions
- Docker Security Basics — Secure container builds in CI/CD pipelines
- Production Security Checklist — Pre-release hardening
- Poisoned CI pattern — How CI leaks and supply-chain failures show up in AI apps
- Automated Security Testing — Integrate security scans into your pipeline
Audit Your CI/CD Workflows
VibeEval helps catch what pipelines miss on the running app: hardcoded secrets that already shipped, open APIs, and auth gaps on preview and production URLs. Harden GitHub Actions with the controls above, then probe the deploy. Secure the pipeline and the product — attackers use both.
COMMON QUESTIONS
ADD A SECURITY GATE TO DEPLOYS
Scan the preview or production URL on every push. Catch keys, open APIs, and auth gaps before they merge to main.
14-day free trial · No credit card · Cancel anytime