Blocking Unsafe Code: Security Audits in GitHub Actions
The Problem: When a Deploy Becomes a Risk
Tests are green, code is merged to main
, deploy ships — and a week later a critical CVE drops for one of your gems. It’s already exploited in the wild. You learn about it from X or Hacker News. If that story rings a bell, this guide is for you.
The Solution: Security-First CI/CD
Add automated checks that fail the build when vulnerabilities or license issues are detected. If the audit is red, the deploy simply won’t start. That’s the whole point: make the safe path the default path.
What We Check (and With What)
- Dependency vulnerabilities —
bundler-audit
(RubySec database). - Licenses —
license_finder
(e.g., block GPL in commercial apps). - Multi-ecosystem scan — Google
OSV Scanner
(great for polyglot repos).
1# Install tools locally (example)
2gem install bundler-audit license_finder
3
4# Vulnerability check
5bundle audit check --update
6
7# License check
8license_finder --quiet
Option 1: All-in-One Workflow (Recommended)
If your deploy.yml
isn’t huge, add an audit job there and make the deploy depend on it.
1name: Deploy
2
3on:
4 push:
5 branches: [ main ]
6 pull_request:
7 paths: ['Gemfile*', '*.gemspec']
8 schedule:
9 - cron: '0 8 * * MON' # Weekly security check
10
11jobs:
12 audit:
13 name: Security & License Audit
14 runs-on: ubuntu-latest
15 steps:
16 - name: Checkout code
17 uses: actions/checkout@v4
18
19 - name: Setup Ruby
20 uses: ruby/setup-ruby@v1
21 with:
22 ruby-version: .ruby-version
23 bundler-cache: true
24
25 - name: Install audit tools
26 run: |
27 gem install bundler-audit license_finder
28
29 - name: Vulnerability scan
30 run: bundle audit check --update
31
32 - name: License compliance
33 run: license_finder --quiet
34
35 # --- OSV: way 1 (via action) ---
36 - name: OSV scan (action)
37 uses: google/osv-scanner-action@v1
38 with:
39 scan-args: '--recursive --skip-git .'
40
41 # --- OSV: way 2 (fallback, without action) ---
42 # - name: OSV scan (binary)
43 # run: |
44 # curl -sSL https://raw.githubusercontent.com/google/osv-scanner/main/scripts/install.sh | sh -s -- -b /usr/local/bin
45 # osv-scanner --recursive --skip-git .
46
47 deploy:
48 needs: [audit] # ← deploy won't start until audit is green
49 runs-on: ubuntu-latest
50 steps:
51 - uses: actions/checkout@v4
52 # ... your deploy steps
53
“Unresolved action/workflow reference” in CI?
Use a valid action ref from the action’s README. If the marketplace action is unavailable, install OSV as a binary (fallback in the snippet) — reliable and vendor-neutral.
Option 2: Split Audit and Deploy (For Larger Repos)
Put the audit in a dedicated workflow and trigger deploy only after it completes successfully.
1# .github/workflows/audit.yml
2name: Security Audit
3
4on:
5 pull_request:
6 paths: ['Gemfile*', '*.gemspec']
7 push:
8 branches: [ main ]
9 schedule:
10 - cron: '0 8 * * MON'
11
12jobs:
13 audit:
14 runs-on: ubuntu-latest
15 steps:
16 - uses: actions/checkout@v4
17 - uses: ruby/setup-ruby@v1
18 with:
19 ruby-version: .ruby-version
20 bundler-cache: true
21 - name: Install tools
22 run: gem install bundler-audit license_finder
23 - name: Vulnerability scan
24 run: bundle audit check --update
25 - name: License compliance
26 run: license_finder --quiet
27 - name: OSV scan
28 uses: google/osv-scanner-action@v1
29 with:
30 scan-args: '--recursive --skip-git .'
31
1# .github/workflows/deploy.yml
2name: Deploy
3
4on:
5 workflow_run:
6 workflows: ["Security Audit"]
7 types: [completed]
8
9jobs:
10 deploy:
11 if: ${{ github.event.workflow_run.conclusion == 'success' }}
12 runs-on: ubuntu-latest
13 steps:
14 - uses: actions/checkout@v4
15 # ... your deploy steps
16
When to Run It
Run audits where they matter most, and add a weekly check to catch new advisories:
1pull_request:
2 paths: ['Gemfile*', '*.gemspec'] # audit only when dependencies change
3
4schedule:
5 - cron: '0 8 * * MON' # Monday morning security check
6
7push:
8 branches: [ main ] # for the split audit.yml approach
9
What Happens When Issues Appear
- Vulnerability found: audit fails → deploy doesn’t start → production stays safe.
- License conflict:
license_finder
flags GPL, etc. → deploy is blocked → team is notified. - All clear: everything is green → deploy starts automatically.
Quick PR checklist
- New dependency: why this one? Are there safer alternatives?
- Is it actively maintained (recent releases, downloads)?
- Supply-chain hygiene: pinned versions, trusted source, commit SHA for
git
dependencies? - Did the PR pass
bundle audit
and license checks?
Helpful Bundler Settings & OSV Alternatives
Bundler settings
1# Protect against mixed sources (source substitution)
2bundle config set disable_multisource true
3
4# Cache and cleanup
5bundle config set cache_all true
6bundle config set clean 'true'
Alternatives & add-ons
- Snyk / Trivy as an extra layer alongside OSV.
- If the OSV action is unavailable, install the CLI binary and run it.
- Schedule a quarterly “dependency hygiene day” with your team.
About gem signatures
Signed releases are great but still uncommon. Keep defense-in-depth: audits + process + version pinning.
Wrap-up
Adding a mandatory audit step dramatically reduces the chance that vulnerable dependencies reach production. If anything goes red, the release doesn’t ship until it’s fixed — simple, predictable, safe.
My baseline: enable GitHub alerts, automate bundle audit
and license checks, add OSV as a second layer, and run regular dependency hygiene with the team.