CLI documentation

OpenCloud security checks in CI pipelines

Run the OpenCloud Security Scanner in GitHub Actions and GitLab CI.

Use a scheduled CI pipeline for regular scans or to check an instance from another network. Monitor whether the pipeline runs as well as what it reports: a missed job produces no scan result.

Whatever the platform, three things decide whether it works:

  • The runner has to reach the instance. A hosted runner cannot scan something behind your firewall - use a self-hosted runner for that.
  • The scan is from the runner's vantage point. What it measures about TLS, enforced HTTPS and reachable debug ports is what an outsider on that network sees, which is usually the interesting answer.
  • The exit code is the result: 0 OK, 1 WARNING, 2 CRITICAL, 3 UNKNOWN. A pipeline fails on anything non-zero, so --warning and --critical decide how strict it is.

GitHub Actions

The action

name: OpenCloud security check

on:
  schedule:
    - cron: "0 6 * * *"
  workflow_dispatch:

permissions:
  contents: read

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: sowoi/[email protected]
        with:
          target: opencloud.example.com
          # Raises GitHub's anonymous rate limit for the release feed. The
          # token the job already has is enough; it needs no scopes.
          releases-token: ${{ github.token }}

That is the whole thing. The step installs the pinned release, scans the instance, writes opencloud-security.json, puts the result in the job summary, and fails the job on WARNING, CRITICAL or UNKNOWN.

Pin the tag. The release schedule and the newest known OpenCloud version ship inside the package, so which release runs is part of the verdict. @v1.16.0 installs exactly 1.16.0; a branch or SHA ref installs the newest release and says so in a warning annotation.

Input Default What it does
target required The instance, as a hostname or URL
version the pinned tag Which release of the check to install
format json json, sarif, junit or nagios
output-file opencloud-security.json Where the output is written
fail-on warning warning, critical or never
warning / critical plugin defaults Rating thresholds
check-hardening true Count hardening measures towards the result
ignore-hardening none Identifiers to waive, comma-separated
release-track auto auto, rolling, production or lts
releases-token none A token for the release feed's rate limit
summary true Write the result to the job summary
extra-args none Any other flag, passed verbatim

The outputs are exit-code, status, rating, rating-label, message and result-file. All but the first two are empty unless format is json, because they are read out of that document:

      - uses: sowoi/[email protected]
        id: scan
        with:
          target: opencloud.example.com
          fail-on: never

      - name: Open an issue when the grade drops below A
        if: steps.scan.outputs.rating < 4
        run: gh issue create --title "OpenCloud is rated ${{ steps.scan.outputs.rating-label }}"
        env:
          GH_TOKEN: ${{ github.token }}

fail-on: never is what makes that possible: the step succeeds, and the decision moves to a later step that can do something more useful than turning the run red.

Note that the runner has to be able to reach the instance. A hosted runner cannot see anything behind your firewall - use a self-hosted one for that, or scan from the network the instance actually publishes to.

Feeding the code-scanning dashboard

format: sarif writes SARIF 2.1.0, which is what GitHub's Security tab ingests. The findings then live where the rest of your security findings do, with their own history, rather than in a log nobody opens:

permissions:
  contents: read
  security-events: write

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: sowoi/[email protected]
        with:
          target: opencloud.example.com
          format: sarif
          output-file: opencloud-security.sarif
          # Upload the findings even when they are bad enough to fail a build.
          fail-on: never

      - uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: opencloud-security.sarif
          category: opencloud-security

Keep fail-on: never here and gate on the uploaded findings instead. A step that fails before the upload throws away the very findings that failed it.

Installing it yourself instead

Nothing about the action is privileged - it installs the same package and runs the same command. Do it by hand when you need a step the action does not model, or when you would rather not depend on an action at all.

name: OpenCloud security check

on:
  schedule:
    - cron: "0 6 * * *"
  workflow_dispatch:

permissions:
  contents: read

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - name: Install the check
        run: pipx install check-opencloud-security==1.1.0

      - name: Scan the instance
        env:
          COS_HOST: opencloud.example.com
          COS_CHECK_HARDENING: "true"
          COS_UPDATE_WARNING: "true"
          # Raises GitHub's anonymous rate limit for the release feed. The
          # token GITHUB_TOKEN gives the job is enough; it needs no scopes.
          COS_RELEASES_TOKEN: ${{ github.token }}
        run: check-opencloud-security

Pin the version rather than tracking latest. The release schedule and the newest known OpenCloud version ship inside the package, so which version you install is part of the verdict - and an unpinned install turns an upstream release into an unexplained pipeline failure.

workflow_dispatch is worth keeping: it is how you re-run the check after fixing something without waiting for tomorrow.

Reporting rather than failing

A failed scheduled workflow only notifies the person who last touched it. To get the result somewhere people look, keep the job green and post the outcome:

      - name: Scan the instance
        id: scan
        continue-on-error: true
        env:
          COS_HOST: opencloud.example.com
          COS_CHECK_HARDENING: "true"
        run: |
          set +e
          check-opencloud-security > result.txt
          state=$?
          set -e
          cat result.txt
          echo "state=$state" >> "$GITHUB_OUTPUT"

      - name: Summarise
        run: |
          {
            echo "### OpenCloud security check"
            echo '```'
            cat result.txt
            echo '```'
          } >> "$GITHUB_STEP_SUMMARY"

Alternatively, set --webhook-url to deliver the result directly; see Webhook recipes. Delivery still depends on the workflow running. If scheduled runs are disabled, neither summaries nor webhooks are produced.

The JSON document instead

For anything that has to make a decision - a policy gate, a dashboard, an issue filed automatically - use the scanner rather than the plugin. It prints the full result document, and every field in it is documented in the library README.

      - name: Scan and keep the result
        run: |
          check-opencloud-scanner scan --compact opencloud.example.com > scan.json
          jq -e '.EOL == false' scan.json \
            || { echo "::error::The installed release no longer receives security fixes"; exit 1; }

      - uses: actions/upload-artifact@v4
        with:
          name: opencloud-scan
          path: scan.json

jq -e exits non-zero when the expression is false, which is what turns a field of the document into a pipeline gate. .EOL, .rating, .updates.available and .lifecycle.daysRemaining are the four worth gating on.

OpenCloud compatibility evidence

The repository's real OpenCloud container workflow keeps one reviewed, immutable rolling-image digest as its baseline and runs the scanner against it weekly. It verifies that the container still initializes, exposes the expected public status endpoint, identifies itself as OpenCloud, reports a version, and produces a bounded rating. The image reports its exact OpenCloud version during the test; the workflow intentionally does not claim compatibility for a new release until that evidence has been reviewed.

Evidence Baseline Compatible when Review path
Vendor container integration opencloudeu/opencloud-rolling@sha256:0bb9038f4c01ab187a014e97550435f5d45630731aed9341d87a0b40fe72fe3d The complete integration test passes and its reported version and externally observable behaviour are reviewed Dispatch the workflow with candidate_image; update the baseline only in a reviewed pull request
Release lifecycle Bundled schedule plus the daily conservative refresh New or changed lines retain existing support facts and pass lifecycle regressions Review the release-schedule refresh PR
Advisories Bundled database plus the daily conservative refresh New advisories add evidence without removing known affected ranges Review the advisory-database refresh PR

The automation never rewrites fixtures, grades, or security expectations to turn a candidate green. A changed response, header, endpoint, or security property must have release evidence and a reviewable test change naming that evidence.

The repository's Supply-chain checks workflow runs on pull requests, pushes to main and weekly. It exports the fully resolved uv.lock dependency set, runs pip-audit over core, web and MCP dependencies, and publishes a CycloneDX SBOM as a workflow artifact. Pushes and scheduled runs also receive a GitHub Sigstore attestation, so the SBOM can be verified with gh attestation verify. The release workflow repeats this for the exact runtime environment shipped with each package and attests the package files and web bundle.

GitLab CI

opencloud-security:
  image: python:3.13-slim
  rules:
    - if: $CI_PIPELINE_SOURCE == "schedule"
  variables:
    COS_HOST: opencloud.example.com
    COS_CHECK_HARDENING: "true"
  before_script:
    - pip install --no-cache-dir check-opencloud-security==1.1.0
  script:
    - check-opencloud-security
  # WARNING (1) is worth seeing without failing the pipeline outright.
  allow_failure:
    exit_codes: [1]

allow_failure.exit_codes maps the plugin's states onto GitLab's directly: list 1 to tolerate WARNING, add 3 to tolerate a scan that could not complete - though a scan that cannot complete is usually the thing you most want to know about.

Add the schedule under Build → Pipeline schedules; the rules block above keeps the job out of ordinary commit pipelines.

Using the container image instead of installing

This example builds the plugin image from the checkout. The published okxo/opencloud-scanner image is another option when you select the plugin with --entrypoint check-opencloud-security.

docker build -f docker/Dockerfile \
  -t registry.example.com/check-opencloud-security:1.1.0 .
docker push registry.example.com/check-opencloud-security:1.1.0

docker run --rm \
  -e COS_HOST=opencloud.example.com \
  -e COS_CHECK_HARDENING=true \
  registry.example.com/check-opencloud-security:1.1.0

Do not put the token on the command line

CI logs are readable by more people than you think, and --release-token on a command line ends up in them. Pass secrets as environment variables (COS_RELEASES_TOKEN, COS_WEBHOOK_URL) or as a secret reference. The plugin redacts tokens from its own debug output; it cannot redact your shell trace.


Back to the documentation index | Back to the main README