Blocking Unsafe Code: Security Audits in GitHub Actions
The Problem: When a Deploy Becomes a Risk
Picture this: tests are green, code merged to main
, deployed to production — and a week later a critical vulnerability pops up in one of your gems. It’s already being exploited, and you learn about it from the news. Sounds familiar? This guide is for you.
The Solution: Security-First CI/CD
We’ll add automated checks to your pipeline that fail the build if vulnerabilities or license issues are found. The deploy won’t start until the audit is green.
What We Check (and With What)
- Dependency vulnerabilities —
bundler-audit
(RubySec database). - Licenses —
license_finder
(e.g., block GPL in a commercial app). - OSV database — Google OSV Scanner (works well in mixed 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 right 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 # If you use the official action — set a valid ref (check the README).
38 # If GitHub can't resolve the action, use way 2 below.
39 uses: google/osv-scanner-action@v1
40 with:
41 scan-args: '--recursive --skip-git .'
42
43 # --- OSV: way 2 (fallback, without action) ---
44 # - name: OSV scan (binary)
45 # run: |
46 # curl -sSL https://raw.githubusercontent.com/google/osv-scanner/main/scripts/install.sh | sh -s -- -b /usr/local/bin
47 # osv-scanner --recursive --skip-git .
48
49 deploy:
50 needs: [audit] # ← deploy won't start until audit is green
51 runs-on: ubuntu-latest
52 steps:
53 - uses: actions/checkout@v4
54 # ... your deploy steps
55
If you see “Unresolved action/workflow reference”
Either use a correct action ref (check the action’s README) or install OSV as a binary (fallback above). It’s reliable and not dependent on the marketplace.
Option 2: Split Audit and Deploy (For Larger Repos)
Put the audit in a separate workflow and trigger the deploy after it finishes 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
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 on Issues
- Vulnerability found: audit fails → deploy doesn’t start → production stays safe.
- License conflict:
license_finder
flags GPL etc. → deploy is blocked → team gets notified. - All clear: everything is green → deploy starts automatically.
Quick PR checklist
- New dependency: why this one? Any alternatives?
- Is it actively maintained (releases/downloads)?
- Supply chain risk covered: 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 next to OSV.
- If the OSV action isn’t available — install the CLI binary and run it.
- Run a quarterly “dependency hygiene day” with your team.
About gem signatures
Signed releases are nice, but still rare. Keep defense in depth: audits + process + version pinning.
Wrap-up
Adding one mandatory audit step dramatically lowers the chance vulnerable deps reach production. If anything goes red, the release won’t ship until it’s fixed.
Personally, I turn on GitHub alerts, automate bundle audit
and license checks, add OSV as a second layer, and schedule regular dependency hygiene with the team.