Skip to content

Security scanning

The Security workflow, the dependency update policies, and the Dockerfile pinning policy. Security scans are advisory, but the go-fuzz job fails when it finds a failing input. The CI workflow is the only publishing condition (see Releases). The vulnerability disclosure process is in SECURITY.md at the repository root.

security.yml

Purpose:

  • run continuous security checks alongside the main CI workflows

Triggers:

  • push to main
  • push to develop
  • pull request targeting main, including the labeled event type (see Fork pull request policy)
  • workflow_dispatch

This workflow runs only on GitHub-hosted runners. The CodeQL CLI does not support linux/arm64, so act on an arm64 host cannot run it.

Jobs:

  1. dependency-review
  2. runs only for pull requests
  3. checks dependency changes with actions/dependency-review-action in warn-only mode
  4. keeps the pull request free of comments (comment-summary-in-pr: never); findings appear in the job summary only
  5. reports findings of low severity and above as warnings and does not fail on them
  6. fails if the review does not complete
  7. summarizes the review outcome and advisory policy
  8. codeql
  9. runs CodeQL analysis for Python and Go
  10. uploads findings to code scanning and links to them in the job summary
  11. summarizes the analysis outcome and code scanning link
  12. dependency-audit-python, dependency-audit-go, and dependency-audit-npm
  13. run the Python, Go, and Hub frontend (npm lockfile) audits in separate jobs
  14. report known vulnerabilities as warnings and in the job summary, and do not fail on them
  15. fail if a tool cannot run or returns an invalid result
  16. summarize the audit target and final job status
  17. go-fuzz
  18. runs make fuzz-go FUZZTIME=30s: each Go fuzz target of the Site API runs with generated inputs for 30 seconds
  19. FuzzParseContentRange checks that an accepted Content-Range header gives a valid range inside the total size
  20. FuzzResolveRootPath checks that a path that resolveRootPath accepts stays inside the content root, also through planted symlinks (inside the root, to the sibling of the root, absolute, loops, and dangling chains), and that the site os.Root finds it
  21. fails when it finds a failing input, because the input shows a real bug
  22. uploads the failing inputs from api/testdata/fuzz/ as the artifact go-fuzz-failing-inputs (kept for 14 days)
  23. summarizes the target and the final job status
  24. secret-scan
  25. checks out the full Git history (fetch-depth: 0)
  26. fetches the pull request refs (refs/pull/*/head) into refs/remotes/pull/*, with the GITHUB_TOKEN in a masked, one-time HTTP header, so that the next steps read them. Pull request refs become public with the repository, and an administrator cannot delete them
  27. downloads the pinned gitleaks release and stops if its SHA-256 is not the pinned GITLEAKS_SHA256
  28. runs gitleaks git with .gitleaks.toml (the default gitleaks rules and an allowlist for synthetic test values), and prints each finding with the secret value redacted
  29. reports findings as warnings, and fails if the scan cannot run
  30. runs security_scan.py refs, which lists the root commits of all refs. A repository that was imported more than once has more than one root commit, and the refs of the earlier import can hold data that the later import removed. More than one root commit is a warning. A shallow clone fails the step, because the result would not be known. If the repository has more than one root commit on purpose, for example after a subtree merge, the warning is expected
  31. summarizes the gitleaks version and the outcome of both checks
  32. workflow-lint
  33. runs actionlint on the workflow files
  34. runs zizmor on .github/, reports findings as warnings, and fails if the scan cannot run
  35. summarizes both lint tools and the zizmor outcome
  36. repository-tests
  37. tests credential selection, validation, and masked outputs
  38. tests the develop.yml publishing gate
  39. tests the CHANGELOG.md release notes extraction (utilities/tests/changelog-notes.test.sh)
  40. tests the dependency audit Makefile targets, the audit fix helper, the advisory scan helper, and the Security workflow policy (including the go-fuzz job and the fuzz-go target)
  41. tests the documentation build and publish script
  42. tests Helm routing and configurable application Secret references
  43. tests the proxy checksum update script (utilities/tests/update-proxy-checksums.test.sh)
  44. tests the Dockerfile policy (utilities/tests/dockerfile-policy.test.sh)
  45. tests the Hub frontend vendor copy (node --test hub/frontend/tests/copy-vendor.test.mjs)
  46. tests and summarizes workflow job summary reporting

Dependabot (.github/dependabot.yml) proposes weekly updates for Python (uv), Go, the Hub frontend npm packages (hub/frontend), GitHub Actions, and Docker base images. An npm update that adds a package also needs an entry in hub/frontend/vendor-files.json; until then the Hub tests and the image build fail. It waits 7 days after a release before it proposes the new version.

Dependabot and Renovate do not update gitleaks. To update it, set GITLEAKS_VERSION and GITLEAKS_SHA256 in the secret-scan job together. Use the SHA-256 of gitleaks_<version>_linux_x64.tar.gz from the checksums.txt file of the gitleaks release.

Renovate (renovate.json5) updates site/proxy.Dockerfile only: the openresty/openresty image, OPENRESTY_VERSION, and FANCYINDEX_VERSION. A post-upgrade task runs utilities/release/update-proxy-checksums.sh, which updates the source checksums in the same branch; post-upgrade tasks need a self-hosted Renovate runner. Renovate waits 7 days after a release and runs before 6 am Monday. Dependabot ignores the openresty/openresty image. Renovate does not update the OPM Lua packages in that file; update their =version pins by hand. See the site applications README.

All Dockerfiles, including the local-development Dockerfiles, use UID/GID 65534:65534 for the final non-root user and for copied-file ownership. Dockerfiles pin every public image reference, including images used by COPY --from, with a tag and a digest (image:tag@sha256:...). Dependabot or Renovate updates the digests. Images in eccr.ecmwf.int use fixed tags. The Dockerfile policy test (utilities/tests/dockerfile-policy.test.sh) enforces what it can check mechanically: every USER is 65534:65534, --chown values are numeric, the TAG build argument exists and is validated, APT use follows hygiene rules (--no-install-recommends, removed lists), every opm install or opm get package carries an explicit =version, no chmod makes a path under /usr/local/openresty or /etc/nginx writable by the group or by other users, external COPY --from images carry a digest, and site/mirror.Dockerfile keeps its sitesctl repository overridable, pins SITESCTL_VERSION and a 64-character SITESCTL_SHA256, and verifies the download with sha256sum -c. The publish.yml workflow must pin the same version and checksum and also run sha256sum -c. Both Hub Dockerfiles must have a frontend stage from a node image with a digest, install with npm ci --ignore-scripts, copy the vendor files to the Hub static/vendor directory before uv sync, and copy the licence files to /usr/share/licenses/sites-hub/. The tag-and-digest form of FROM references is a convention the tests do not check.

Security findings are advisory. The CI workflow is the only publishing condition. Security findings appear as warning annotations, in the job summary and logs, and in code scanning. A Security job still fails when a scan cannot run, so that a broken scan is not mistaken for a clean result. The workflows do not change repository or organization rulesets. An authorized maintainer must remove any required Security status check from the rulesets that protect main and develop.

Permissions:

  • top-level contents: read
  • job-level permissions are narrowed further where needed
  • the CodeQL job uses security-events: write to upload analysis results