ci: harden GitHub Actions workflows (#185)

* Add GitHub Actions audit job (actionlint + zizmor) to CI

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Configure dependabot for GitHub Actions, bundler, and Docker

Batches all action updates into a single weekly PR. Adds cooldown
periods to all ecosystems.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add local GitHub Actions linting (actionlint + zizmor) to bin/setup and bin/ci

Install actionlint, shellcheck, and zizmor in bin/setup. Run both
linters as CI steps in config/ci.rb alongside existing style checks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Pin all GitHub Actions to SHA hashes

Run pinact to pin action versions to specific commit SHAs,
preventing supply chain attacks from tag mutation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix high severity zizmor findings

- Suppress unpinned-images for redis service containers (digest
  pinning is nontrivial for service containers)
- Move workflow-level permissions to job-level in publish-image.yml
  (build gets full set, manifest gets only what it needs)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix medium severity zizmor findings

- Add persist-credentials: false to all checkout steps
- Add permissions: {} at workflow level in ci.yml
- Add job-level permissions (contents: read) to all CI jobs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix informational template-injection findings in publish-image.yml

Move steps.meta.outputs.tags from inline ${{ }} expressions to env
vars in both the manifest creation and cosign signing steps.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Update brakeman to 8.0.4

bin/brakeman uses --ensure-latest which fails if not on the newest version.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mike Dalessio
2026-03-20 19:26:25 -04:00
committed by GitHub
parent dde94b06ed
commit 3fada3d997
6 changed files with 136 additions and 49 deletions

32
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: "/"
groups:
github-actions:
patterns:
- "*"
schedule:
interval: weekly
cooldown:
default-days: 7
- package-ecosystem: bundler
directory: "/"
schedule:
interval: weekly
cooldown:
semver-major-days: 7
semver-minor-days: 3
semver-patch-days: 2
default-days: 7
- package-ecosystem: docker
directory: "/"
schedule:
interval: weekly
cooldown:
semver-major-days: 7
semver-minor-days: 3
semver-patch-days: 2
default-days: 7

View File

@@ -6,16 +6,22 @@ on:
pull_request:
branches: [ main ]
permissions: {}
jobs:
scan:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false
- name: Set up Ruby
uses: ruby/setup-ruby@v1
uses: ruby/setup-ruby@319994f95fa847cf3fb3cd3dbe89f6dcde9f178f # v1.295.0
with:
ruby-version: .ruby-version
bundler-cache: true
@@ -25,12 +31,16 @@ jobs:
lint:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false
- name: Set up Ruby
uses: ruby/setup-ruby@v1
uses: ruby/setup-ruby@319994f95fa847cf3fb3cd3dbe89f6dcde9f178f # v1.295.0
with:
ruby-version: .ruby-version
bundler-cache: true
@@ -38,11 +48,32 @@ jobs:
- name: Lint code for consistent style
run: bin/rubocop
lint-actions:
name: GitHub Actions audit
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false
- name: Run actionlint
uses: rhysd/actionlint@393031adb9afb225ee52ae2ccd7a5af5525e03e8 # v1.7.11
- name: Run zizmor
uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
with:
advanced-security: false
test:
runs-on: ubuntu-latest
permissions:
contents: read
services:
redis:
image: redis
image: redis # zizmor: ignore[unpinned-images] -- version tag is fine for service containers
ports:
- 6379:6379
options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
@@ -51,10 +82,12 @@ jobs:
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y libsqlite3-0 libvips curl ffmpeg
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false
- name: Set up Ruby
uses: ruby/setup-ruby@v1
uses: ruby/setup-ruby@319994f95fa847cf3fb3cd3dbe89f6dcde9f178f # v1.295.0
env:
REDIS_URL: redis://localhost:6379/0
with:
@@ -66,9 +99,11 @@ jobs:
test_system:
runs-on: ubuntu-latest
permissions:
contents: read
services:
redis:
image: redis
image: redis # zizmor: ignore[unpinned-images] -- version tag is fine for service containers
ports:
- 6379:6379
options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
@@ -77,10 +112,12 @@ jobs:
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y libsqlite3-0 libvips curl ffmpeg
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false
- name: Set up Ruby
uses: ruby/setup-ruby@v1
uses: ruby/setup-ruby@319994f95fa847cf3fb3cd3dbe89f6dcde9f178f # v1.295.0
env:
REDIS_URL: redis://localhost:6379/0
with:

View File

@@ -13,11 +13,7 @@ concurrency:
group: publish-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
packages: write
id-token: write
attestations: write
permissions: {}
env:
IMAGE_DESCRIPTION: Campfire is a web-based chat application with multiple rooms, direct messages, file attachments with previews, search, web push notifications, @mentions, and bot integrations. Single-tenant; production-ready image with web app, background jobs, caching, file serving, and SSL.
@@ -27,6 +23,11 @@ jobs:
build:
name: Build and push image (${{ matrix.arch }})
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
id-token: write
attestations: write
timeout-minutes: 45
strategy:
fail-fast: false
@@ -43,13 +44,15 @@ jobs:
IMAGE_NAME: ${{ github.repository }}
steps:
- name: Checkout
uses: actions/checkout@v5.0.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.11.1
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Log in to GHCR
uses: docker/login-action@v3.5.0
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -66,7 +69,7 @@ jobs:
- name: Extract Docker metadata (tags, labels) with arch suffix
id: meta
uses: docker/metadata-action@v5.8.0
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
with:
images: ${{ steps.vars.outputs.canonical }}
tags: |
@@ -84,7 +87,7 @@ jobs:
- name: Build and push (${{ matrix.platform }})
id: build
uses: docker/build-push-action@v6.18.0
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
file: Dockerfile
@@ -102,7 +105,7 @@ jobs:
- name: Attest image provenance (per-arch)
if: github.event_name != 'pull_request'
uses: actions/attest-build-provenance@v3.0.0
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
with:
subject-name: ${{ steps.vars.outputs.canonical }}
subject-digest: ${{ steps.build.outputs.digest }}
@@ -113,16 +116,20 @@ jobs:
needs: build
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
timeout-minutes: 20
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
steps:
- name: Set up Docker Buildx (for imagetools)
uses: docker/setup-buildx-action@v3.11.1
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Log in to GHCR
uses: docker/login-action@v3.5.0
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -139,7 +146,7 @@ jobs:
- name: Compute base tags (no suffix)
id: meta
uses: docker/metadata-action@v5.8.0
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
with:
images: ${{ steps.vars.outputs.canonical }}
tags: |
@@ -157,9 +164,11 @@ jobs:
- name: Create multi-arch manifests
shell: bash
env:
TAGS: ${{ steps.meta.outputs.tags }}
run: |
set -eu
tags="${{ steps.meta.outputs.tags }}"
tags="$TAGS"
echo "Creating manifests for tags:"
printf '%s\n' "$tags"
while IFS= read -r tag; do
@@ -185,13 +194,15 @@ jobs:
done <<< "$tags"
- name: Install Cosign
uses: sigstore/cosign-installer@v3.9.2
uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2
- name: Cosign sign all tags (keyless OIDC)
shell: bash
env:
TAGS: ${{ steps.meta.outputs.tags }}
run: |
set -eu
tags="${{ steps.meta.outputs.tags }}"
tags="$TAGS"
printf '%s\n' "$tags"
while IFS= read -r tag; do
[ -z "$tag" ] && continue

View File

@@ -137,7 +137,7 @@ GEM
bcrypt (3.1.20)
benchmark (0.5.0)
bigdecimal (3.3.1)
brakeman (7.1.2)
brakeman (8.0.4)
racc
builder (3.3.0)
capybara (3.40.0)
@@ -165,10 +165,7 @@ GEM
erubi (1.13.1)
faker (3.5.2)
i18n (>= 1.8.11, < 2)
ffi (1.17.2-aarch64-linux-gnu)
ffi (1.17.2-arm64-darwin)
ffi (1.17.2-x86_64-darwin)
ffi (1.17.2-x86_64-linux-gnu)
ffi (1.17.2)
geared_pagination (1.2.0)
activesupport (>= 5.0)
addressable (>= 2.5.0)
@@ -212,6 +209,7 @@ GEM
mini_magick (5.3.1)
logger
mini_mime (1.1.5)
mini_portile2 (2.8.9)
minitest (5.26.2)
mocha (2.7.1)
ruby2_keywords (>= 0.0.5)
@@ -231,13 +229,8 @@ GEM
net-smtp (0.5.1)
net-protocol
nio4r (2.7.5)
nokogiri (1.18.10-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.10-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.10-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.18.10-x86_64-linux-gnu)
nokogiri (1.18.10)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
openssl (3.3.0)
ostruct (0.6.3)
@@ -306,11 +299,11 @@ GEM
rake (>= 10.0, < 14.0)
resque (>= 1.22, < 3)
rexml (3.4.1)
rqrcode (3.1.0)
rqrcode (3.2.0)
chunky_png (~> 1.0)
rqrcode_core (~> 2.0)
rqrcode_core (2.0.0)
rubocop (1.80.0)
rqrcode_core (2.1.0)
rubocop (1.80.2)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
@@ -364,10 +357,8 @@ GEM
rack-protection (= 4.1.1)
rack-session (>= 2.0.0, < 3)
tilt (~> 2.0)
sqlite3 (2.7.3-aarch64-linux-gnu)
sqlite3 (2.7.3-arm64-darwin)
sqlite3 (2.7.3-x86_64-darwin)
sqlite3 (2.7.3-x86_64-linux-gnu)
sqlite3 (2.7.3)
mini_portile2 (~> 2.8.0)
stimulus-rails (1.3.4)
railties (>= 6.0.0)
stringio (3.1.8)
@@ -381,9 +372,9 @@ GEM
tsort (0.2.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (3.1.5)
unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
unicode-display_width (3.2.0)
unicode-emoji (~> 4.1)
unicode-emoji (4.2.0)
uri (1.1.1)
useragent (0.16.11)
web-push (3.0.2)

View File

@@ -100,6 +100,20 @@ if ! redis_running; then
fi
fi
# Install GitHub Actions linting tools
for tool in actionlint shellcheck zizmor; do
if ! command -v "$tool" &> /dev/null; then
if command -v brew &> /dev/null; then
step "Installing $tool" brew install "$tool"
elif command -v pacman &> /dev/null; then
step "Installing $tool" sudo pacman -S --noconfirm "$tool"
else
echo "Error: install $tool manually" >&2
exit 1
fi
fi
done
step "Cleaning up logs and tempfiles" rails log:clear tmp:clear
step "Restarting services" rails restart

View File

@@ -4,6 +4,8 @@ CI.run do
step "Setup", "bin/setup --skip-server"
step "Style: Ruby", "bin/rubocop"
step "Style: GitHub Actions (actionlint)", "actionlint"
step "Style: GitHub Actions (zizmor)", "zizmor ."
step "Security: Gem audit", "bin/bundler-audit"
step "Security: Importmap vulnerability audit", "bin/importmap audit"