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"
schedule:
- cron: "0 6 * * *"
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
- name: Build Docker image
run: |
docker build \
-t torrust-tracker-deployer/${{ matrix.image.name }}:latest \
-f ${{ matrix.image.dockerfile }} \
${{ matrix.image.context }}
- 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"
- 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:
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
- 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
- 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:
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"
- 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"
- 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
- 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
if: always()
permissions:
security-events: write
steps:
- name: Download all SARIF artifacts
uses: actions/download-artifact@v7
with:
pattern: sarif-project-*-${{ github.run_id }}
- 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