Skip to content

Docker Security Scan #256

Docker Security Scan

Docker Security Scan #256

name: Docker Security Scan
on:
push:
branches: [main, develop]
paths:
- "docker/**"
- "templates/docker-compose/**"
- "src/**"
- ".github/workflows/docker-security-scan.yml"
pull_request:
paths:
- "docker/**"
- "templates/docker-compose/**"
- "src/**"
- ".github/workflows/docker-security-scan.yml"
# Scheduled scans are important because new CVEs appear
# even if the code or images didn't change
schedule:
- cron: "0 6 * * *" # Daily at 6 AM UTC
workflow_dispatch:
jobs:
scan-project-images:
name: Scan Project-Built Docker Images
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
strategy:
fail-fast: false
matrix:
image:
- dockerfile: docker/deployer/Dockerfile
context: .
name: deployer
- dockerfile: docker/provisioned-instance/Dockerfile
context: docker/provisioned-instance
name: provisioned-instance
- dockerfile: docker/ssh-server/Dockerfile
context: docker/ssh-server
name: ssh-server
- dockerfile: docker/backup/Dockerfile
context: docker/backup
name: tracker-backup
steps:
- name: Checkout code
uses: actions/checkout@v5
# Build images locally so Trivy scans exactly
# what this repository produces
- name: Build Docker image
run: |
docker build \
-t torrust-tracker-deployer/${{ matrix.image.name }}:latest \
-f ${{ matrix.image.dockerfile }} \
${{ matrix.image.context }}
# Human-readable output in logs
# This NEVER fails the job; it's only for visibility
- name: Display vulnerabilities (table format)
uses: aquasecurity/trivy-action@0.35.0
with:
image-ref: torrust-tracker-deployer/${{ matrix.image.name }}:latest
format: "table"
severity: "HIGH,CRITICAL"
exit-code: "0"
# SARIF generation for GitHub Code Scanning
#
# IMPORTANT:
# - exit-code MUST be 0
# - Trivy sometimes exits with 1 even when no vulns exist
# - GitHub Security UI is responsible for enforcement
- name: Generate SARIF (Code Scanning)
uses: aquasecurity/trivy-action@0.35.0
with:
image-ref: torrust-tracker-deployer/${{ matrix.image.name }}:latest
format: "sarif"
output: "trivy-${{ matrix.image.name }}.sarif"
severity: "HIGH,CRITICAL"
exit-code: "0"
scanners: "vuln"
- name: Upload SARIF artifact
uses: actions/upload-artifact@v6
if: always()
with:
name: sarif-project-${{ matrix.image.name }}-${{ github.run_id }}
path: trivy-${{ matrix.image.name }}.sarif
retention-days: 30
extract-images:
name: Extract Third-Party Docker Images from Source
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
# JSON array of Docker image references for use in scan matrix
# Example: ["torrust/tracker:develop","mysql:8.4","prom/prometheus:v3.11.2","grafana/grafana:13.0.0","caddy:2.11.2"]
images: ${{ steps.extract.outputs.images }}
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
- name: Build deployer CLI
run: cargo build --release
# Creates a minimal environment config with all optional services
# enabled so that all third-party Docker images appear in the output.
# - MySQL: enables mysql image in docker_images output
# - Prometheus: enables prometheus image in docker_images output
# - Grafana: enables grafana image in docker_images output
# Uses fixture SSH keys (already committed to the repository).
- name: Create environment config for image extraction
run: |
cat > /tmp/ci-images-env.json <<EOF
{
"environment": { "name": "ci-images" },
"ssh_credentials": {
"private_key_path": "$GITHUB_WORKSPACE/fixtures/testing_rsa",
"public_key_path": "$GITHUB_WORKSPACE/fixtures/testing_rsa.pub"
},
"provider": {
"provider": "lxd",
"profile_name": "ci-profile"
},
"tracker": {
"core": {
"database": {
"driver": "mysql",
"host": "mysql",
"port": 3306,
"database_name": "torrust_tracker",
"username": "tracker_user",
"password": "tracker_password"
},
"private": false
},
"udp_trackers": [{ "bind_address": "0.0.0.0:6969" }],
"http_trackers": [{ "bind_address": "0.0.0.0:7070" }],
"http_api": { "bind_address": "0.0.0.0:1212", "admin_token": "ci-token" },
"health_check_api": { "bind_address": "127.0.0.1:1313" }
},
"prometheus": { "scrape_interval_in_secs": 15 },
"grafana": { "admin_user": "admin", "admin_password": "admin" }
}
EOF
- name: Create minimal environment (no infrastructure provisioned)
run: |
./target/release/torrust-tracker-deployer \
--working-dir /tmp/ci-workspace \
create environment \
--env-file /tmp/ci-images-env.json
# Extract Docker images from show command JSON output.
# The show command lists all configured service images in docker_images.
# Caddy is always in the docker-compose stack but is not tracked as
# a domain service, so it is appended to the list manually.
- name: Extract Docker images
id: extract
run: |
show_output=$(./target/release/torrust-tracker-deployer \
--working-dir /tmp/ci-workspace \
show ci-images)
images=$(echo "$show_output" | \
jq -c '[
.docker_images.tracker,
.docker_images.mysql,
.docker_images.prometheus,
.docker_images.grafana
] | map(select(. != null)) + ["caddy:2.11.2"]')
echo "Detected images: $images"
echo "images=$images" >> "$GITHUB_OUTPUT"
scan-third-party-images:
name: Scan Third-Party Docker Images
needs: extract-images
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
# Dynamic image list extracted from the deployer CLI at build time.
# Images come from domain config constants — no manual maintenance needed.
image: ${{ fromJson(needs.extract-images.outputs.images) }}
steps:
- name: Display vulnerabilities (table format)
uses: aquasecurity/trivy-action@0.35.0
with:
image-ref: ${{ matrix.image }}
format: "table"
severity: "HIGH,CRITICAL"
exit-code: "0"
# Third-party images should NEVER block CI.
# We only report findings to GitHub Security.
- name: Generate SARIF (Code Scanning)
uses: aquasecurity/trivy-action@0.35.0
with:
image-ref: ${{ matrix.image }}
format: "sarif"
output: "trivy.sarif"
severity: "HIGH,CRITICAL"
exit-code: "0"
scanners: "vuln"
# Needed to produce stable artifact names
- name: Sanitize image name
id: sanitize
run: |
echo "name=$(echo '${{ matrix.image }}' | tr '/:' '-')" >> "$GITHUB_OUTPUT"
- name: Upload SARIF artifact
uses: actions/upload-artifact@v6
if: always()
with:
name: sarif-third-party-${{ steps.sanitize.outputs.name }}-${{ github.run_id }}
path: trivy.sarif
retention-days: 30
# Use the supported CodeQL upload action so category tracking works
# for dynamic third-party image configurations.
- name: Upload third-party SARIF
if: always()
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: trivy.sarif
category: docker-third-party-${{ steps.sanitize.outputs.name }}
continue-on-error: true
upload-sarif-results:
name: Upload SARIF Results to GitHub Security
runs-on: ubuntu-latest
needs:
- scan-project-images
# Always run so we don't lose security visibility
if: always()
permissions:
security-events: write
steps:
- name: Download all SARIF artifacts
uses: actions/download-artifact@v7
with:
pattern: sarif-project-*-${{ github.run_id }}
# Upload each SARIF file with CodeQL Action using unique categories.
# The category parameter enables proper alert tracking per image.
#
# VIEWING RESULTS:
# - For pull requests: /security/code-scanning?query=pr:NUMBER+is:open
# - For branches: /security/code-scanning?query=is:open+branch:BRANCH-NAME
# - For main branch: /security/code-scanning?query=is:open+branch:main (default view)
# The default Security tab filters by "is:open branch:main" which only shows
# alerts from the main branch, not from PR branches.
- name: Upload project provisioned-instance SARIF
if: always()
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: sarif-project-provisioned-instance-${{ github.run_id }}/trivy-provisioned-instance.sarif
category: docker-project-provisioned-instance
continue-on-error: true
- name: Upload project ssh-server SARIF
if: always()
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: sarif-project-ssh-server-${{ github.run_id }}/trivy-ssh-server.sarif
category: docker-project-ssh-server
continue-on-error: true