From defaaaa0319b7e9def3ac56ede10eb8b83304b9d Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 23 Dec 2025 17:15:44 +0000 Subject: [PATCH 01/15] feat: [#251] implement basic trivy scanning workflow --- .github/workflows/docker-security-scan.yml | 89 ++++++++++++++++++++++ README.md | 2 +- 2 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/docker-security-scan.yml diff --git a/.github/workflows/docker-security-scan.yml b/.github/workflows/docker-security-scan.yml new file mode 100644 index 00000000..34fce8dd --- /dev/null +++ b/.github/workflows/docker-security-scan.yml @@ -0,0 +1,89 @@ +name: Docker Security Scan + +on: + push: + branches: + - main + - develop + paths: + - "docker/**" + - "templates/docker-compose/**" + - ".github/workflows/docker-security-scan.yml" + pull_request: + paths: + - "docker/**" + - "templates/docker-compose/**" + - ".github/workflows/docker-security-scan.yml" + schedule: + - cron: "0 6 * * *" # Daily at 6 AM UTC + workflow_dispatch: # Allow manual triggering + +jobs: + scan-project-images: + name: Scan Project-Built Docker Images + runs-on: ubuntu-latest + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + image: + - dockerfile: docker/provisioned-instance/Dockerfile + context: docker/provisioned-instance + name: provisioned-instance + - dockerfile: docker/ssh-server/Dockerfile + context: docker/ssh-server + name: ssh-server + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Build Docker image + run: | + docker build -t torrust-tracker-deployer/${{ matrix.image.name }}:latest \ + -f ${{ matrix.image.dockerfile }} \ + ${{ matrix.image.context }} + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + image-ref: torrust-tracker-deployer/${{ matrix.image.name }}:latest + format: "sarif" + output: "trivy-results-${{ matrix.image.name }}.sarif" + severity: "HIGH,CRITICAL" + exit-code: "1" + + - name: Upload Trivy results to GitHub Security + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: "trivy-results-${{ matrix.image.name }}.sarif" + + scan-third-party-images: + name: Scan Third-Party Docker Images + runs-on: ubuntu-latest + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + # NOTE: These images must match the ones used in templates/docker-compose/docker-compose.yml.tera + # TODO: Automate image detection from docker-compose templates - see https://github.com/torrust/torrust-tracker-deployer/issues/252 + image: + - torrust/tracker:develop + - mysql:8.0 + - grafana/grafana:11.4.0 + - prom/prometheus:v3.0.1 + steps: + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + image-ref: ${{ matrix.image }} + format: "sarif" + output: "trivy-results.sarif" + severity: "HIGH,CRITICAL" + exit-code: "1" + + - name: Upload Trivy results to GitHub Security + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: "trivy-results.sarif" diff --git a/README.md b/README.md index ab07bda4..95365445 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Linting](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/linting.yml/badge.svg)](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/linting.yml) [![Testing](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/testing.yml/badge.svg)](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/testing.yml) [![E2E Infrastructure Tests](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/test-e2e-infrastructure.yml/badge.svg)](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/test-e2e-infrastructure.yml) [![E2E Deployment Tests](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/test-e2e-deployment.yml/badge.svg)](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/test-e2e-deployment.yml) [![Test LXD Container Provisioning](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/test-lxd-provision.yml/badge.svg)](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/test-lxd-provision.yml) [![Coverage](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/coverage.yml/badge.svg)](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/coverage.yml) +[![Linting](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/linting.yml/badge.svg)](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/linting.yml) [![Testing](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/testing.yml/badge.svg)](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/testing.yml) [![E2E Infrastructure Tests](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/test-e2e-infrastructure.yml/badge.svg)](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/test-e2e-infrastructure.yml) [![E2E Deployment Tests](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/test-e2e-deployment.yml/badge.svg)](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/test-e2e-deployment.yml) [![Test LXD Container Provisioning](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/test-lxd-provision.yml/badge.svg)](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/test-lxd-provision.yml) [![Coverage](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/coverage.yml/badge.svg)](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/coverage.yml) [![Docker Security Scan](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/docker-security-scan.yml/badge.svg)](https://github.com/torrust/torrust-tracker-deployer/actions/workflows/docker-security-scan.yml) # Torrust Tracker Deployer From c032e71c9a67b546ba0ceeb5ff14e2ebd120332a Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 23 Dec 2025 17:35:28 +0000 Subject: [PATCH 02/15] fix: [#251] update docker build context and action versions --- .github/workflows/docker-security-scan.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docker-security-scan.yml b/.github/workflows/docker-security-scan.yml index 34fce8dd..2d16f12e 100644 --- a/.github/workflows/docker-security-scan.yml +++ b/.github/workflows/docker-security-scan.yml @@ -41,10 +41,10 @@ jobs: run: | docker build -t torrust-tracker-deployer/${{ matrix.image.name }}:latest \ -f ${{ matrix.image.dockerfile }} \ - ${{ matrix.image.context }} + . - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@master + uses: aquasecurity/trivy-action@0.28.0 with: image-ref: torrust-tracker-deployer/${{ matrix.image.name }}:latest format: "sarif" @@ -53,7 +53,7 @@ jobs: exit-code: "1" - name: Upload Trivy results to GitHub Security - uses: github/codeql-action/upload-sarif@v3 + uses: github/codeql-action/upload-sarif@v4 if: always() with: sarif_file: "trivy-results-${{ matrix.image.name }}.sarif" @@ -74,7 +74,7 @@ jobs: - prom/prometheus:v3.0.1 steps: - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@master + uses: aquasecurity/trivy-action@0.28.0 with: image-ref: ${{ matrix.image }} format: "sarif" @@ -83,7 +83,7 @@ jobs: exit-code: "1" - name: Upload Trivy results to GitHub Security - uses: github/codeql-action/upload-sarif@v3 + uses: github/codeql-action/upload-sarif@v4 if: always() with: sarif_file: "trivy-results.sarif" From c11eb106bfe8f36b9e72ebb8e557818319b67c05 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 23 Dec 2025 17:51:01 +0000 Subject: [PATCH 03/15] feat: [#251] add human-readable vulnerability output to workflow logs --- .github/workflows/docker-security-scan.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/docker-security-scan.yml b/.github/workflows/docker-security-scan.yml index 2d16f12e..c512c822 100644 --- a/.github/workflows/docker-security-scan.yml +++ b/.github/workflows/docker-security-scan.yml @@ -43,6 +43,14 @@ jobs: -f ${{ matrix.image.dockerfile }} \ . + - name: Display vulnerabilities (table format) + uses: aquasecurity/trivy-action@0.28.0 + with: + image-ref: torrust-tracker-deployer/${{ matrix.image.name }}:latest + format: "table" + severity: "HIGH,CRITICAL" + exit-code: "0" # Don't fail here, just display + - name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@0.28.0 with: @@ -73,6 +81,14 @@ jobs: - grafana/grafana:11.4.0 - prom/prometheus:v3.0.1 steps: + - name: Display vulnerabilities (table format) + uses: aquasecurity/trivy-action@0.28.0 + with: + image-ref: ${{ matrix.image }} + format: "table" + severity: "HIGH,CRITICAL" + exit-code: "0" # Don't fail here, just display + - name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@0.28.0 with: From c13afa8a5c029a17fa0ec4343d1b7172a59a981e Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 23 Dec 2025 18:04:13 +0000 Subject: [PATCH 04/15] fix: [#251] disable secret scanning for test containers with SSH keys --- .github/workflows/docker-security-scan.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/docker-security-scan.yml b/.github/workflows/docker-security-scan.yml index c512c822..790dab94 100644 --- a/.github/workflows/docker-security-scan.yml +++ b/.github/workflows/docker-security-scan.yml @@ -50,6 +50,7 @@ jobs: format: "table" severity: "HIGH,CRITICAL" exit-code: "0" # Don't fail here, just display + scanners: "vuln" # Only vulnerabilities, skip secrets (test containers have legitimate SSH keys) - name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@0.28.0 @@ -59,6 +60,7 @@ jobs: output: "trivy-results-${{ matrix.image.name }}.sarif" severity: "HIGH,CRITICAL" exit-code: "1" + scanners: "vuln" # Only vulnerabilities, skip secrets (test containers have legitimate SSH keys) - name: Upload Trivy results to GitHub Security uses: github/codeql-action/upload-sarif@v4 From 7013bc041b9986d13948b3fbde5a2058e157aa37 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 23 Dec 2025 18:20:59 +0000 Subject: [PATCH 05/15] fix: [#251] focus CVE scanning only, display all issues for visibility --- .github/workflows/docker-security-scan.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-security-scan.yml b/.github/workflows/docker-security-scan.yml index 790dab94..349f46ff 100644 --- a/.github/workflows/docker-security-scan.yml +++ b/.github/workflows/docker-security-scan.yml @@ -50,7 +50,6 @@ jobs: format: "table" severity: "HIGH,CRITICAL" exit-code: "0" # Don't fail here, just display - scanners: "vuln" # Only vulnerabilities, skip secrets (test containers have legitimate SSH keys) - name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@0.28.0 @@ -99,6 +98,7 @@ jobs: output: "trivy-results.sarif" severity: "HIGH,CRITICAL" exit-code: "1" + scanners: "vuln" # Focus on CVEs, not secrets - name: Upload Trivy results to GitHub Security uses: github/codeql-action/upload-sarif@v4 From afb653c0d90b4a958873fce002092d8d12815fc3 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 23 Dec 2025 18:21:59 +0000 Subject: [PATCH 06/15] chore: [#251] update trivy-action from v0.28.0 to v0.33.1 --- .github/workflows/docker-security-scan.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-security-scan.yml b/.github/workflows/docker-security-scan.yml index 349f46ff..de3f0bcd 100644 --- a/.github/workflows/docker-security-scan.yml +++ b/.github/workflows/docker-security-scan.yml @@ -44,7 +44,7 @@ jobs: . - name: Display vulnerabilities (table format) - uses: aquasecurity/trivy-action@0.28.0 + uses: aquasecurity/trivy-action@0.33.1 with: image-ref: torrust-tracker-deployer/${{ matrix.image.name }}:latest format: "table" @@ -52,7 +52,7 @@ jobs: exit-code: "0" # Don't fail here, just display - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@0.28.0 + uses: aquasecurity/trivy-action@0.33.1 with: image-ref: torrust-tracker-deployer/${{ matrix.image.name }}:latest format: "sarif" @@ -83,7 +83,7 @@ jobs: - prom/prometheus:v3.0.1 steps: - name: Display vulnerabilities (table format) - uses: aquasecurity/trivy-action@0.28.0 + uses: aquasecurity/trivy-action@0.33.1 with: image-ref: ${{ matrix.image }} format: "table" @@ -91,7 +91,7 @@ jobs: exit-code: "0" # Don't fail here, just display - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@0.28.0 + uses: aquasecurity/trivy-action@0.33.1 with: image-ref: ${{ matrix.image }} format: "sarif" From 382f4305feb59f263bce8972d8a55f12fdc9c91d Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 23 Dec 2025 18:42:51 +0000 Subject: [PATCH 07/15] refactor: [#251] separate SARIF upload to dedicated job with minimal permissions - Scan jobs now upload SARIF files as artifacts - New dedicated upload-sarif-results job with only security-events:write permission - Display steps show all security issues (CVEs + secrets) for visibility - Scan steps only fail on HIGH/CRITICAL CVE vulnerabilities --- .github/workflows/docker-security-scan.yml | 40 ++++++++++++++++++---- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docker-security-scan.yml b/.github/workflows/docker-security-scan.yml index de3f0bcd..a8851c4d 100644 --- a/.github/workflows/docker-security-scan.yml +++ b/.github/workflows/docker-security-scan.yml @@ -23,6 +23,8 @@ jobs: name: Scan Project-Built Docker Images runs-on: ubuntu-latest timeout-minutes: 15 + permissions: + contents: read strategy: fail-fast: false matrix: @@ -61,16 +63,20 @@ jobs: exit-code: "1" scanners: "vuln" # Only vulnerabilities, skip secrets (test containers have legitimate SSH keys) - - name: Upload Trivy results to GitHub Security - uses: github/codeql-action/upload-sarif@v4 + - name: Upload SARIF artifact + uses: actions/upload-artifact@v4 if: always() with: - sarif_file: "trivy-results-${{ matrix.image.name }}.sarif" + name: sarif-project-${{ matrix.image.name }} + path: "trivy-results-${{ matrix.image.name }}.sarif" + retention-days: 30 scan-third-party-images: name: Scan Third-Party Docker Images runs-on: ubuntu-latest timeout-minutes: 15 + permissions: + contents: read strategy: fail-fast: false matrix: @@ -100,8 +106,30 @@ jobs: exit-code: "1" scanners: "vuln" # Focus on CVEs, not secrets - - name: Upload Trivy results to GitHub Security - uses: github/codeql-action/upload-sarif@v4 + - name: Upload SARIF artifact + uses: actions/upload-artifact@v4 if: always() with: - sarif_file: "trivy-results.sarif" + name: sarif-third-party-${{ matrix.image }} + path: "trivy-results.sarif" + retention-days: 30 + + upload-sarif-results: + name: Upload SARIF Results to GitHub Security + runs-on: ubuntu-latest + needs: [scan-project-images, scan-third-party-images] + if: always() + permissions: + security-events: write + steps: + - name: Download all SARIF artifacts + uses: actions/download-artifact@v4 + with: + pattern: sarif-* + merge-multiple: false + + - name: Upload SARIF files to GitHub Security + uses: github/codeql-action/upload-sarif@v4 + with: + sarif_file: "." + category: "docker-security-scan" From 5b09357a6cf0a8036ad89aafbc231c710ba4c29f Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 23 Dec 2025 18:46:32 +0000 Subject: [PATCH 08/15] fix: [#251] sanitize Docker image names for artifact naming --- .github/workflows/docker-security-scan.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker-security-scan.yml b/.github/workflows/docker-security-scan.yml index a8851c4d..fdcaccf5 100644 --- a/.github/workflows/docker-security-scan.yml +++ b/.github/workflows/docker-security-scan.yml @@ -106,11 +106,15 @@ jobs: exit-code: "1" scanners: "vuln" # Focus on CVEs, not secrets + - name: Sanitize image name for artifact + id: sanitize + run: echo "name=$(echo '${{ matrix.image }}' | tr '/:' '-')" >> $GITHUB_OUTPUT + - name: Upload SARIF artifact uses: actions/upload-artifact@v4 if: always() with: - name: sarif-third-party-${{ matrix.image }} + name: sarif-third-party-${{ steps.sanitize.outputs.name }} path: "trivy-results.sarif" retention-days: 30 From cebf2a7ce31ddbf0e0440b865f31b4324c802fa8 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 23 Dec 2025 18:57:43 +0000 Subject: [PATCH 09/15] fix: [#251] prevent workflow failure and artifact name conflicts --- .github/workflows/docker-security-scan.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker-security-scan.yml b/.github/workflows/docker-security-scan.yml index fdcaccf5..83f84e81 100644 --- a/.github/workflows/docker-security-scan.yml +++ b/.github/workflows/docker-security-scan.yml @@ -55,6 +55,8 @@ jobs: - name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@0.33.1 + continue-on-error: true + id: trivy-scan with: image-ref: torrust-tracker-deployer/${{ matrix.image.name }}:latest format: "sarif" @@ -67,7 +69,7 @@ jobs: uses: actions/upload-artifact@v4 if: always() with: - name: sarif-project-${{ matrix.image.name }} + name: sarif-project-${{ matrix.image.name }}-${{ github.run_id }} path: "trivy-results-${{ matrix.image.name }}.sarif" retention-days: 30 @@ -98,6 +100,8 @@ jobs: - name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@0.33.1 + continue-on-error: true + id: trivy-scan with: image-ref: ${{ matrix.image }} format: "sarif" @@ -114,7 +118,7 @@ jobs: uses: actions/upload-artifact@v4 if: always() with: - name: sarif-third-party-${{ steps.sanitize.outputs.name }} + name: sarif-third-party-${{ steps.sanitize.outputs.name }}-${{ github.run_id }} path: "trivy-results.sarif" retention-days: 30 @@ -129,7 +133,7 @@ jobs: - name: Download all SARIF artifacts uses: actions/download-artifact@v4 with: - pattern: sarif-* + pattern: sarif-*-${{ github.run_id }} merge-multiple: false - name: Upload SARIF files to GitHub Security From 797addfb9ec0ff25c9807d50026b6a04cedda151 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 23 Dec 2025 19:02:44 +0000 Subject: [PATCH 10/15] fix: [#251] upload SARIF files with unique categories per image --- .github/workflows/docker-security-scan.yml | 25 ++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-security-scan.yml b/.github/workflows/docker-security-scan.yml index 83f84e81..2aa03810 100644 --- a/.github/workflows/docker-security-scan.yml +++ b/.github/workflows/docker-security-scan.yml @@ -137,7 +137,24 @@ jobs: merge-multiple: false - name: Upload SARIF files to GitHub Security - uses: github/codeql-action/upload-sarif@v4 - with: - sarif_file: "." - category: "docker-security-scan" + run: | + # Upload each SARIF file with a unique category + find . -name "*.sarif" -type f | while read -r sarif_file; do + # Extract image name from directory path for category + category=$(basename $(dirname "$sarif_file") | sed 's/^sarif-//' | sed 's/-[0-9]*$//') + echo "Uploading $sarif_file with category: docker-$category" + + # Use gh CLI to upload SARIF (simpler than action in loop) + cat "$sarif_file" | gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + /repos/${{ github.repository }}/code-scanning/sarifs \ + -f sarif=@- \ + -f ref="${{ github.ref }}" \ + -f commit_sha="${{ github.sha }}" \ + -f checkout_uri="${{ github.server_url }}/${{ github.repository }}" \ + -f category="docker-$category" || echo "Failed to upload $sarif_file" + done + env: + GH_TOKEN: ${{ github.token }} From 7ff69bec1ed7c5ba4f9d798887fffc0780e37aa4 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 23 Dec 2025 19:04:14 +0000 Subject: [PATCH 11/15] fix: [#251] upload SARIF files with unique categories per image --- .github/workflows/docker-security-scan.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-security-scan.yml b/.github/workflows/docker-security-scan.yml index 2aa03810..f48f5da8 100644 --- a/.github/workflows/docker-security-scan.yml +++ b/.github/workflows/docker-security-scan.yml @@ -143,7 +143,7 @@ jobs: # Extract image name from directory path for category category=$(basename $(dirname "$sarif_file") | sed 's/^sarif-//' | sed 's/-[0-9]*$//') echo "Uploading $sarif_file with category: docker-$category" - + # Use gh CLI to upload SARIF (simpler than action in loop) cat "$sarif_file" | gh api \ --method POST \ From 92b33253622175414f35f7e6e29726126b28c482 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 23 Dec 2025 19:11:56 +0000 Subject: [PATCH 12/15] refactor: [#251] apply security-first workflow philosophy with exit-code 0 --- .github/workflows/docker-security-scan.yml | 104 +++++++++++------- project-words.txt | 121 +++++++++++---------- 2 files changed, 123 insertions(+), 102 deletions(-) diff --git a/.github/workflows/docker-security-scan.yml b/.github/workflows/docker-security-scan.yml index f48f5da8..130a9882 100644 --- a/.github/workflows/docker-security-scan.yml +++ b/.github/workflows/docker-security-scan.yml @@ -2,21 +2,24 @@ name: Docker Security Scan on: push: - branches: - - main - - develop + branches: [main, develop] paths: - "docker/**" - "templates/docker-compose/**" - ".github/workflows/docker-security-scan.yml" + pull_request: paths: - "docker/**" - "templates/docker-compose/**" - ".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: # Allow manual triggering + + workflow_dispatch: jobs: scan-project-images: @@ -25,6 +28,7 @@ jobs: timeout-minutes: 15 permissions: contents: read + strategy: fail-fast: false matrix: @@ -35,42 +39,52 @@ jobs: - dockerfile: docker/ssh-server/Dockerfile context: docker/ssh-server name: ssh-server + steps: - name: Checkout code uses: actions/checkout@v4 + # 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 \ + docker build \ + -t torrust-tracker-deployer/${{ matrix.image.name }}:latest \ -f ${{ matrix.image.dockerfile }} \ . + # 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.33.1 with: image-ref: torrust-tracker-deployer/${{ matrix.image.name }}:latest format: "table" severity: "HIGH,CRITICAL" - exit-code: "0" # Don't fail here, just display + exit-code: "0" - - name: Run Trivy vulnerability scanner + # 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.33.1 - continue-on-error: true - id: trivy-scan with: image-ref: torrust-tracker-deployer/${{ matrix.image.name }}:latest format: "sarif" - output: "trivy-results-${{ matrix.image.name }}.sarif" + output: "trivy-${{ matrix.image.name }}.sarif" severity: "HIGH,CRITICAL" - exit-code: "1" - scanners: "vuln" # Only vulnerabilities, skip secrets (test containers have legitimate SSH keys) + exit-code: "0" + scanners: "vuln" - name: Upload SARIF artifact uses: actions/upload-artifact@v4 if: always() with: name: sarif-project-${{ matrix.image.name }}-${{ github.run_id }} - path: "trivy-results-${{ matrix.image.name }}.sarif" + path: trivy-${{ matrix.image.name }}.sarif retention-days: 30 scan-third-party-images: @@ -79,16 +93,18 @@ jobs: timeout-minutes: 15 permissions: contents: read + strategy: fail-fast: false matrix: - # NOTE: These images must match the ones used in templates/docker-compose/docker-compose.yml.tera - # TODO: Automate image detection from docker-compose templates - see https://github.com/torrust/torrust-tracker-deployer/issues/252 + # These must match docker-compose templates + # in templates/docker-compose/docker-compose.yml.tera image: - torrust/tracker:develop - mysql:8.0 - grafana/grafana:11.4.0 - prom/prometheus:v3.0.1 + steps: - name: Display vulnerabilities (table format) uses: aquasecurity/trivy-action@0.33.1 @@ -96,65 +112,69 @@ jobs: image-ref: ${{ matrix.image }} format: "table" severity: "HIGH,CRITICAL" - exit-code: "0" # Don't fail here, just display + exit-code: "0" - - name: Run Trivy vulnerability scanner + # Third-party images should NEVER block CI. + # We only report findings to GitHub Security. + - name: Generate SARIF (Code Scanning) uses: aquasecurity/trivy-action@0.33.1 - continue-on-error: true - id: trivy-scan with: image-ref: ${{ matrix.image }} format: "sarif" - output: "trivy-results.sarif" + output: "trivy.sarif" severity: "HIGH,CRITICAL" - exit-code: "1" - scanners: "vuln" # Focus on CVEs, not secrets + exit-code: "0" + scanners: "vuln" - - name: Sanitize image name for artifact + # Needed to produce stable artifact names + - name: Sanitize image name id: sanitize - run: echo "name=$(echo '${{ matrix.image }}' | tr '/:' '-')" >> $GITHUB_OUTPUT + run: | + echo "name=$(echo '${{ matrix.image }}' | tr '/:' '-')" >> "$GITHUB_OUTPUT" - name: Upload SARIF artifact uses: actions/upload-artifact@v4 if: always() with: name: sarif-third-party-${{ steps.sanitize.outputs.name }}-${{ github.run_id }} - path: "trivy-results.sarif" + path: trivy.sarif retention-days: 30 upload-sarif-results: name: Upload SARIF Results to GitHub Security runs-on: ubuntu-latest - needs: [scan-project-images, scan-third-party-images] + needs: + - scan-project-images + - scan-third-party-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@v4 with: pattern: sarif-*-${{ github.run_id }} - merge-multiple: false - - name: Upload SARIF files to GitHub Security + # We use gh CLI because it’s easier to loop + # and assign stable categories per image + - name: Upload SARIF files + env: + GH_TOKEN: ${{ github.token }} run: | - # Upload each SARIF file with a unique category - find . -name "*.sarif" -type f | while read -r sarif_file; do - # Extract image name from directory path for category - category=$(basename $(dirname "$sarif_file") | sed 's/^sarif-//' | sed 's/-[0-9]*$//') - echo "Uploading $sarif_file with category: docker-$category" - - # Use gh CLI to upload SARIF (simpler than action in loop) - cat "$sarif_file" | gh api \ + find . -name "*.sarif" -type f | while read -r sarif; do + category=$(basename "$(dirname "$sarif")" | sed 's/^sarif-//' | sed 's/-[0-9]*$//') + echo "Uploading $sarif as docker-$category" + + gh api \ --method POST \ -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ /repos/${{ github.repository }}/code-scanning/sarifs \ - -f sarif=@- \ + -f sarif=@-"$sarif" \ -f ref="${{ github.ref }}" \ -f commit_sha="${{ github.sha }}" \ - -f checkout_uri="${{ github.server_url }}/${{ github.repository }}" \ - -f category="docker-$category" || echo "Failed to upload $sarif_file" + -f category="docker-$category" || echo "Upload failed for $sarif" done - env: - GH_TOKEN: ${{ github.token }} diff --git a/project-words.txt b/project-words.txt index 8bad0a88..de5685eb 100644 --- a/project-words.txt +++ b/project-words.txt @@ -1,67 +1,20 @@ AAAAB AAAAC AAAAI -AGENTS -Alertmanager -aquasecurity -Ashburn -Avalonia -CIFS -Cockburn -Crossplane -Dockerfiles -EAAAADAQABAAABAQC -EPEL -Falkenstein -Gossman -Grafana -Grafonnet -Graça -Herberto -Hillsboro -Hostnames -Liskov -MAAACBA -MVVM -Mermaid -NOPASSWD -OAAAAN -Osherove -Preinstalling -Pulumi -RAII -RUSTDOCFLAGS -Repomix -Rustdoc -SCRIPTDIR -Scriptability -Silverlight -Subissue -Swatinem -Taplo -Tera -Testcontain -Testcontainers -Testinfra -Torrust -Traefik -VARCHAR -Zeroize addgroup adduser -BBDBE -bencoded -completei -downloadedi -filesd -incompletei -intervali -peerslee +AGENTS +Alertmanager appender appendonly +aquasecurity architecting +Ashburn autorestart +Avalonia backlinks +BBDBE +bencoded bootcmd browsable buildx @@ -70,17 +23,21 @@ checkmarks childlogdir chkdsk chrono +CIFS clig clippy clonable cloneable cloudinit +Cockburn +completei concepsts configurator connrefused containerd cpus creds +Crossplane custompass customuser dearmor @@ -92,40 +49,57 @@ devpass distro distroless distutils +Dockerfiles doctest doctests downcasted downcasting +downloadedi dpkg drwxr dtolnay +EAAAADAQABAAABAQC ehthumbs elif +Émojis endfor endraw entr epel +EPEL eprint eprintln equalto executability exfiltration exitcode +Falkenstein +filesd flatlined frontends fswc getent getopt +Gossman +Graça +Grafana +Grafonnet handleable hashset healthcheck +Herberto hetznercloud hexdigit hexdump +HIDS +Hillsboro +Hostnames hotfixes htdocs hugepages impls +incompletei +intervali isreg journalctl jsonlint @@ -141,6 +115,7 @@ leechers libc lifecycles lineinfile +Liskov listenfd listhost logfile @@ -149,7 +124,9 @@ logicaldisk loglevel lspconfig lxdbr +MAAACBA maxbytes +Mermaid mgmt millis mkdir @@ -164,6 +141,7 @@ mprotect mpsc mtorrust multiprocess +MVVM myapp myenv mysqladmin @@ -181,29 +159,38 @@ nocapture noconfirm nodaemon noninteractive +NOPASSWD nslookup nullglob +OAAAAN oneline +Osherove +OSSEC pacman parameterizing parseable passwordless pathbuf +peerslee pidfile pids pipefail pkill postconditions preconfigured +Preinstalling preinstalls prereq println promtool publickey +Pulumi pytest +RAII readlink realpath reentrancy +Repomix reprioritize reprovision reprovisioning @@ -217,20 +204,25 @@ runbooks runcmd runnability rustc +Rustdoc +RUSTDOCFLAGS rustflags rustls rustup sarif SARIF schemars +Scriptability +SCRIPTDIR secureboot selectattr serde serverurl shellcheck +Silverlight smorimoto -spki spëcial +spki sqlx sshpass startretries @@ -238,21 +230,29 @@ stringly subcontroller subcontrollers subhandlers +Subissue subissues subshell substates supervisorctl supervisord swappability +Swatinem sysfs sysv taiki +Taplo taskkill tasklist +Tera terraformrc +tést +Testcontain testcontainer testcontainers +Testcontainers testhost +Testinfra testkey testpass testuser @@ -268,10 +268,11 @@ tmpfiles tmpfs tmptu torrust +Torrust +Traefik tulnp tulpn turbofish -tést ulpn undertested unergonomic @@ -284,17 +285,17 @@ userpass userspace usize utmp +VARCHAR vbqajnc viewmodel +vulns +Wazuh webservers writeln wrongpassword youruser zeroize -HIDS -OSSEC -Wazuh -Émojis +Zeroize значение ключ конфиг From af93704e31e810fc37805aa51d009d31d4fe519a Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 23 Dec 2025 19:23:10 +0000 Subject: [PATCH 13/15] docs: [#251] add ADR for docker security scan exit code zero philosophy --- docs/decisions/README.md | 1 + .../docker-security-scan-exit-code-zero.md | 128 ++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 docs/decisions/docker-security-scan-exit-code-zero.md diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 9f224108..64ad8996 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -6,6 +6,7 @@ This directory contains architectural decision records for the Torrust Tracker D | Status | Date | Decision | Summary | | ------------- | ---------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | +| ✅ Accepted | 2025-12-23 | [Docker Security Scan Exit Code Zero](./docker-security-scan-exit-code-zero.md) | Use exit-code 0 for security scanning - Trivy detects, GitHub Security decides, CI green | | ✅ Accepted | 2025-12-20 | [Grafana Integration Pattern](./grafana-integration-pattern.md) | Enable Grafana by default with hard Prometheus dependency and environment variable config | | ✅ Accepted | 2025-12-17 | [Secrecy Crate for Sensitive Data Handling](./secrecy-crate-for-sensitive-data.md) | Use secrecy crate for type-safe secret handling with memory zeroing | | ✅ Accepted | 2025-12-14 | [Database Configuration Structure in Templates](./database-configuration-structure-in-templates.md) | Expose structured database fields in templates rather than pre-resolved connection strings | diff --git a/docs/decisions/docker-security-scan-exit-code-zero.md b/docs/decisions/docker-security-scan-exit-code-zero.md new file mode 100644 index 00000000..b8972204 --- /dev/null +++ b/docs/decisions/docker-security-scan-exit-code-zero.md @@ -0,0 +1,128 @@ +# Decision: Exit Code Zero for Docker Security Scanning + +## Status + +Accepted + +## Date + +2025-12-23 + +## Context + +When implementing automated Docker vulnerability scanning with Trivy in GitHub Actions, we faced a critical decision about how the CI/CD pipeline should respond to discovered vulnerabilities. + +Traditional approaches make CI fail when vulnerabilities are found, blocking all development until issues are resolved. However, this creates several problems: + +1. **False Positives**: Security scanners can report issues that don't apply to our context or are accepted risks +2. **Third-Party Dependencies**: We cannot immediately fix vulnerabilities in upstream images (mysql, prometheus, grafana) +3. **Scanner Quirks**: Trivy occasionally exits with code 1 even when no vulnerabilities are found +4. **Development Flow**: Security findings should not block unrelated development work +5. **Policy Enforcement**: Security decisions should be made by security teams, not automated tooling +6. **Partial Data Loss**: If CI fails early, later scans never run and we lose visibility into other images + +The initial implementation used `exit-code: "1"` which caused the workflow to fail on any HIGH or CRITICAL vulnerability, including when scanning third-party production images with known CVEs that we cannot immediately fix. + +## Decision + +Implement a **security-first philosophy** where: + +1. **Exit Code Zero Everywhere**: All Trivy scan steps use `exit-code: "0"` - the scanner never fails the CI pipeline +2. **Dual Output Strategy**: + - Human-readable table format in workflow logs for immediate visibility + - SARIF format uploaded to GitHub Security tab for tracking and alerting +3. **Separation of Concerns**: + - Trivy's role: **Detect** vulnerabilities and provide data + - GitHub Security's role: **Decide** enforcement policies and alert routing + - CI's role: **Stay green** and maintain development velocity +4. **Always Run Policy**: Upload job uses `if: always()` to ensure partial results are never lost +5. **Unique Categories**: Each image gets a unique SARIF category for proper alert tracking and deduplication +6. **Scheduled Scanning**: Daily cron ensures continuous monitoring without blocking code changes + +This philosophy is summarized as: **"Trivy detects, GitHub Security decides, CI stays green"** + +## Consequences + +### Positive + +- **No False Failures**: Development work never blocked by scanner quirks or edge cases +- **Continuous Visibility**: All scans complete even if one fails, providing complete security picture +- **Flexible Enforcement**: Security team can configure GitHub Security policies without changing code +- **Third-Party Tolerance**: Known vulnerabilities in upstream images don't block development +- **Developer Experience**: Green builds maintain team velocity while security team reviews findings +- **Policy Separation**: Security enforcement decoupled from CI/CD implementation +- **Audit Trail**: All findings recorded in GitHub Security tab for compliance and tracking +- **Incremental Improvement**: Can address vulnerabilities based on priority without CI pressure + +### Negative + +- **Potential Complacency**: Green CI might lead to ignoring security findings (mitigated by GitHub Security alerts) +- **Requires Monitoring**: Security team must actively monitor GitHub Security tab +- **Policy Configuration**: Requires additional GitHub Security policy setup for enforcement +- **Learning Curve**: Non-traditional approach may confuse developers expecting red builds for vulnerabilities + +### Risks Introduced + +- **Missed Critical Issues**: If GitHub Security is not properly configured or monitored, critical vulnerabilities might go unaddressed + - **Mitigation**: Daily scheduled scans ensure consistent monitoring; GitHub Security sends email notifications +- **Organizational Resistance**: Some organizations mandate CI failure on security issues + - **Mitigation**: GitHub Security can be configured to block PRs or deployments if needed + +## Alternatives Considered + +### 1. Exit Code 1 (Fail on Vulnerabilities) + +**Approach**: Use `exit-code: "1"` to fail CI when HIGH/CRITICAL vulnerabilities are found. + +**Rejected Because**: + +- Blocks development on third-party image vulnerabilities we cannot fix immediately +- Scanner quirks cause false CI failures even with zero vulnerabilities +- No flexibility for security team to make risk-based decisions +- Partial data loss when early scans fail + +### 2. Mixed Exit Codes (Project vs Third-Party) + +**Approach**: Use `exit-code: "1"` for project images but `exit-code: "0"` for third-party images. + +**Rejected Because**: + +- Inconsistent philosophy creates confusion +- Project images can have legitimate accepted risks +- Still susceptible to scanner quirks on project images +- Doesn't solve the fundamental policy enforcement problem + +### 3. Continue-on-Error Pattern + +**Approach**: Use `exit-code: "1"` but add `continue-on-error: true` to allow workflow to proceed. + +**Rejected Because**: + +- Shows misleading "failed" status even though workflow continues +- Scanner errors appear as failures in UI, creating noise +- Doesn't fundamentally change the enforcement model +- Confusing to developers seeing "failed" steps that don't actually fail + +### 4. CodeQL Action with Single Category + +**Approach**: Upload all SARIF files using github/codeql-action/upload-sarif with same category. + +**Rejected Because**: + +- CodeQL Action rejects multiple SARIF uploads with identical categories (as of July 2025) +- Results in "multiple SARIF runs with same category" error +- Cannot distinguish alerts between different images + +## Related Decisions + +- [GitHub Actions Workflow Structure](https://github.com/torrust/torrust-tracker-deployer/pull/256) - How the three-job structure enables this philosophy +- Future: Security Policy Configuration (to be documented when GitHub Security policies are configured) + +## References + +- [Issue #251: Implement basic Trivy scanning workflow](https://github.com/torrust/torrust-tracker-deployer/issues/251) +- [Pull Request #256: Implement Basic Trivy Scanning Workflow](https://github.com/torrust/torrust-tracker-deployer/pull/256) +- [Trivy Action Documentation](https://github.com/aquasecurity/trivy-action) +- [GitHub Code Scanning Documentation](https://docs.github.com/en/code-security/code-scanning) +- [GitHub Security Policy Enforcement](https://docs.github.com/en/code-security/code-scanning/managing-code-scanning-alerts) +- [Security-First Philosophy Discussion](https://github.com/torrust/torrust-tracker-deployer/pull/256#discussion) - External review recommending exit-code 0 approach From 098add24c74b4f22ec07e0aba0cc8f9d78af9a02 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 23 Dec 2025 20:11:13 +0000 Subject: [PATCH 14/15] fix: [#251] use CodeQL action for SARIF upload with category support --- .github/workflows/docker-security-scan.yml | 69 ++++++++++++++++------ 1 file changed, 50 insertions(+), 19 deletions(-) diff --git a/.github/workflows/docker-security-scan.yml b/.github/workflows/docker-security-scan.yml index 130a9882..2ee94ffe 100644 --- a/.github/workflows/docker-security-scan.yml +++ b/.github/workflows/docker-security-scan.yml @@ -159,22 +159,53 @@ jobs: with: pattern: sarif-*-${{ github.run_id }} - # We use gh CLI because it’s easier to loop - # and assign stable categories per image - - name: Upload SARIF files - env: - GH_TOKEN: ${{ github.token }} - run: | - find . -name "*.sarif" -type f | while read -r sarif; do - category=$(basename "$(dirname "$sarif")" | sed 's/^sarif-//' | sed 's/-[0-9]*$//') - echo "Uploading $sarif as docker-$category" - - gh api \ - --method POST \ - -H "Accept: application/vnd.github+json" \ - /repos/${{ github.repository }}/code-scanning/sarifs \ - -f sarif=@-"$sarif" \ - -f ref="${{ github.ref }}" \ - -f commit_sha="${{ github.sha }}" \ - -f category="docker-$category" || echo "Upload failed for $sarif" - done + # Upload each SARIF file with CodeQL Action using unique categories. + # The category parameter enables proper alert tracking per image. + # Must use CodeQL Action (not gh API) - API doesn't support category field. + - 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 + + - name: Upload third-party mysql SARIF + if: always() + uses: github/codeql-action/upload-sarif@v4 + with: + sarif_file: sarif-third-party-mysql-8.0-${{ github.run_id }}/trivy.sarif + category: docker-third-party-mysql-8.0 + continue-on-error: true + + - name: Upload third-party tracker SARIF + if: always() + uses: github/codeql-action/upload-sarif@v4 + with: + sarif_file: sarif-third-party-torrust-tracker-develop-${{ github.run_id }}/trivy.sarif + category: docker-third-party-torrust-tracker-develop + continue-on-error: true + + - name: Upload third-party grafana SARIF + if: always() + uses: github/codeql-action/upload-sarif@v4 + with: + sarif_file: sarif-third-party-grafana-grafana-11.4.0-${{ github.run_id }}/trivy.sarif + category: docker-third-party-grafana-grafana-11.4.0 + continue-on-error: true + + - name: Upload third-party prometheus SARIF + if: always() + uses: github/codeql-action/upload-sarif@v4 + with: + sarif_file: sarif-third-party-prom-prometheus-v3.0.1-${{ github.run_id }}/trivy.sarif + category: docker-third-party-prom-prometheus-v3.0.1 + continue-on-error: true From 40dd234c8fe9cd83841c4c69dbcd8054850138e6 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 23 Dec 2025 20:31:22 +0000 Subject: [PATCH 15/15] docs: [#251] document Security tab viewing behavior - Add comment in workflow explaining filter behavior - Add section in ADR explaining how to view results by branch/PR - Add PR comment with direct links to view scan results The default Security tab filters by 'is:open branch:main' which hides PR branch results. Users must use specific PR/branch filters to see results before merging to main. --- .github/workflows/docker-security-scan.yml | 7 +++++++ docs/decisions/docker-security-scan-exit-code-zero.md | 10 ++++++++++ 2 files changed, 17 insertions(+) diff --git a/.github/workflows/docker-security-scan.yml b/.github/workflows/docker-security-scan.yml index 2ee94ffe..2ddad760 100644 --- a/.github/workflows/docker-security-scan.yml +++ b/.github/workflows/docker-security-scan.yml @@ -162,6 +162,13 @@ jobs: # Upload each SARIF file with CodeQL Action using unique categories. # The category parameter enables proper alert tracking per image. # Must use CodeQL Action (not gh API) - API doesn't support category field. + # + # 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 diff --git a/docs/decisions/docker-security-scan-exit-code-zero.md b/docs/decisions/docker-security-scan-exit-code-zero.md index b8972204..4fa3c6c0 100644 --- a/docs/decisions/docker-security-scan-exit-code-zero.md +++ b/docs/decisions/docker-security-scan-exit-code-zero.md @@ -113,6 +113,16 @@ This philosophy is summarized as: **"Trivy detects, GitHub Security decides, CI - Results in "multiple SARIF runs with same category" error - Cannot distinguish alerts between different images +## Viewing Security Results + +Security scan results are uploaded to GitHub's Security tab, but the default view filters by `is:open branch:main`. This means: + +- **Pull Request Results**: Must use filter `pr:NUMBER is:open` (e.g., `/security/code-scanning?query=pr:256+is:open`) +- **Branch Results**: Must use filter `is:open branch:BRANCH-NAME` for non-main branches +- **Main Branch Results**: Visible in default view after merging to main + +Results uploaded from PR branches are not visible in the default Security tab view because the default filter excludes them. This is GitHub's standard behavior for code scanning across all analysis tools. + ## Related Decisions - [GitHub Actions Workflow Structure](https://github.com/torrust/torrust-tracker-deployer/pull/256) - How the three-job structure enables this philosophy