diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0160d8ec..76e88cf7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -152,15 +152,23 @@ jobs: APK_CACHE_BUST=${{ steps.apk-bust.outputs.date }} - name: Scan image for vulnerabilities (Trivy) - # Hard-fails the PR on any HIGH or CRITICAL finding that is not listed - # in .trivyignore at the repo root. To accept a CVE, add it to that - # file with a human-readable justification comment, not here. + # Hard-fails the PR on any HIGH or CRITICAL finding not suppressed by + # the OpenVEX document at security/vex/sencho.openvex.json (loaded via + # trivy.yaml) or by .trivyignore at the repo root. The VEX document is + # the canonical triage record and is also attached as a cosign + # attestation on the published image. .trivyignore exists only to + # mirror VEX entries that cannot be resolved here because Trivy does + # not emit an OCI purl for local images without a digest, so VEX + # product matching against `sencho:pr-test` is a no-op (see + # aquasecurity/trivy#9399). Add the VEX statement first, then mirror + # the CVE id in .trivyignore with a pointer to the VEX justification. uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: image-ref: sencho:pr-test exit-code: '1' severity: 'CRITICAL,HIGH' format: 'table' + trivy-config: trivy.yaml # --------------------------------------------------------------------------- # E2E Tests (PRs only, skipped for release-please PRs) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 2e187a9b..0cf51032 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -23,8 +23,9 @@ jobs: # a manual-approval gate without any workflow change. environment: production permissions: - contents: read - # Required for cosign keyless signing via GitHub OIDC. + contents: write + # Required for cosign keyless signing via GitHub OIDC and for uploading + # SBOM/VEX files to GitHub Releases via softprops/action-gh-release. id-token: write steps: - name: Check out the repo @@ -102,14 +103,16 @@ jobs: - name: Re-scan release image for vulnerabilities (Trivy) # Gates the release on the same HIGH/CRITICAL policy as the PR scan. - # Entries in .trivyignore at the repo root are honored so the acknowledged - # CVE list is the single source of truth across PR CI and release CI. + # CVEs suppressed via the OpenVEX document (trivy.yaml -> security/vex/ + # sencho.openvex.json) are the single source of truth across PR CI and + # release CI. uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: image-ref: localhost/sencho:release-scan exit-code: '1' severity: 'CRITICAL,HIGH' format: 'table' + trivy-config: trivy.yaml # Start the scanned image headless on the runner and poll /api/health # until it returns 200. Catches entrypoint regressions, native module @@ -168,6 +171,8 @@ jobs: # cosign verify saelix/sencho: \ # --certificate-identity-regexp "https://github.com/AnsoCode/Sencho/.*" \ # --certificate-oidc-issuer https://token.actions.githubusercontent.com + # Each ref is pinned to the immutable digest so signatures are bound to + # the exact manifest, not the mutable tag pointer. env: TAGS: ${{ steps.meta.outputs.tags }} DIGEST: ${{ steps.build.outputs.digest }} @@ -186,3 +191,43 @@ jobs: else echo "No tags to sign (likely a workflow_dispatch run on a non-tag ref)." fi + + - name: Generate CycloneDX SBOM + # syft scans the published manifest by digest for richer package + # extraction than the BuildKit-native SBOM. Also installs syft into + # PATH so the SPDX step below can reuse the OCI layer cache. + uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.20.2 + with: + image: saelix/sencho@${{ steps.build.outputs.digest }} + format: cyclonedx-json + output-file: sbom.cdx.json + upload-artifact: false + + - name: Generate SPDX SBOM + # Reuses the syft binary and OCI layer cache from the prior step. + run: | + syft saelix/sencho@${{ steps.build.outputs.digest }} \ + -o spdx-json=sbom.spdx.json + + - name: Attest SBOMs and VEX with cosign (keyless) + # Attaches CycloneDX SBOM, SPDX SBOM, and OpenVEX document as signed + # OCI referrer attestations on the published digest. Verification + # commands are documented in docs/operations/verifying-images.mdx. + env: + DIGEST: ${{ steps.build.outputs.digest }} + run: | + IMAGE_REF="saelix/sencho@${DIGEST}" + cosign attest --yes --predicate sbom.cdx.json --type cyclonedx "${IMAGE_REF}" + cosign attest --yes --predicate sbom.spdx.json --type spdxjson "${IMAGE_REF}" + cosign attest --yes --predicate security/vex/sencho.openvex.json --type openvex "${IMAGE_REF}" + + - name: Upload SBOM and VEX to GitHub Release + # Fallback for consumers who do not use cosign; files are also + # available as signed OCI attestations via the attest step above. + if: startsWith(github.ref, 'refs/tags/v') + uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.3.2 + with: + files: | + sbom.cdx.json + sbom.spdx.json + security/vex/sencho.openvex.json diff --git a/.trivyignore b/.trivyignore index 86706760..8c8cb747 100644 --- a/.trivyignore +++ b/.trivyignore @@ -1,75 +1,25 @@ -# Trivy ignore list +# Trivy ignore list (CI build-and-scan only) # -# Every entry in this file is a known HIGH or CRITICAL CVE that we have -# consciously accepted risk on and decided not to block CI over. Format: +# The OpenVEX document at security/vex/sencho.openvex.json is the canonical +# source of truth for accepted CVEs. It is consumed by Trivy via trivy.yaml +# and attached as a cosign attestation on every published image, so any +# downstream consumer who scans saelix/sencho: sees the same triage. # -# CVE-YYYY-NNNNN -# # Justification: why we're accepting this risk, and a link or note about -# # when to revisit (e.g. "blocked on upstream base image update, revisit -# # when alpine/node:22 ships a fix"). +# This file exists only because the OpenVEX product purl matching does not +# resolve for locally-built images that have no RepoDigests. Trivy does not +# generate an OCI purl for such images (see aquasecurity/trivy#9399), so the +# product `pkg:oci/sencho` in our VEX statements never matches the artifact +# under test in the PR-time scan against `sencho:pr-test`. Once the image is +# pushed and acquires a digest, VEX matching resolves and this file is no +# longer load-bearing for that scan. # -# Rules: -# - Every CVE MUST have a justification comment directly above it. -# - If there is no justification, the CVE is not ignored - add it here only -# after a human review and a decision to accept the risk. -# - Review this file on every release; remove entries whose upstream fix has -# landed. -# -# Picked up automatically by aquasecurity/trivy-action from the repo root -# working directory. Both the pre-push PR scan (.github/workflows/ci.yml) and -# the release-time re-scan (.github/workflows/docker-publish.yml) honor it. -# -# Note: The following CVEs that were previously accepted here have been resolved -# by building Docker CLI and Compose from source against Go 1.26.2 (PR feat/security-pin-and-rebuild): -# CVE-2026-32280, CVE-2026-32281, CVE-2026-32282, CVE-2026-32283 (Go stdlib x509/TLS, fixed in Go 1.26.2) -# CVE-2026-33810 (Go stdlib DNS name constraint bypass, fixed in Go 1.26.2) -# CVE-2026-33186 (grpc 1.78.0 HTTP/2 server attack, eliminated by source rebuild) -# CVE-2026-33671 (picomatch ReDoS in npm, eliminated by removing npm from runtime image) -# -# The remaining entries below are vendored inside docker/compose v5.1.2 and -# cannot be patched without a new upstream compose release. They will be -# migrated to security/vex/sencho.openvex.json (OpenVEX format) in the -# follow-up PR (feat/security-sbom-vex), after which this file will be deleted. +# Rule: every entry here MUST already have a corresponding statement in +# security/vex/sencho.openvex.json with a justification. Do not add a CVE +# here without first adding the VEX statement; the VEX file is what users +# audit, not this one. -# --------------------------------------------------------------------------- -# Bundled inside /usr/local/lib/docker/cli-plugins/docker-compose (v5.1.2) -# --------------------------------------------------------------------------- -# Compose v5.1.2 is the latest upstream release. It statically links older -# copies of github.com/docker/docker, buildkit, and otel. We cannot bump -# these transitively without waiting for a new upstream Compose release. -# Revisit this block on every Compose release; remove entries as upstream -# rebuilds ship the fixes. - -# Justification: github.com/docker/docker v28.5.2 statically bundled in -# compose v5.1.2. Moby authz bypass applies to a Docker daemon, not to the -# compose CLI plugin; compose never runs as a daemon. Revisit on next -# Compose upstream release. -CVE-2026-34040 - -# Justification: github.com/moby/buildkit v0.27.1 statically bundled in -# compose v5.1.2. BuildKit arbitrary file write via untrusted frontend is -# exploited at buildkit build time with attacker-controlled frontends; our -# compose invocations only call up/down/ps against local user-authored -# compose files, never as a build frontend. Revisit on next Compose upstream -# release. -CVE-2026-33747 - -# Justification: github.com/moby/buildkit v0.27.1 statically bundled in -# compose v5.1.2. Same exposure profile as CVE-2026-33747 (Git URL fragment -# subdir exploitation requires invoking buildkit on untrusted repo URLs, -# which compose does not do in our flow). Revisit on next Compose upstream -# release. -CVE-2026-33748 - -# Justification: go.opentelemetry.io/otel/sdk v1.38.0 statically bundled in -# compose v5.1.2. PATH hijacking requires the attacker to control the -# process PATH before compose starts; our container starts compose from a -# fixed PATH with only /usr/local/bin and /usr/bin on it, both owned by -# root. Revisit on next Compose upstream release. -CVE-2026-24051 - -# Justification: go.opentelemetry.io/otel/sdk v1.38.0 statically bundled in -# compose v5.1.2. BSD kenv PATH hijacking only applies on BSD systems; we -# ship linux/amd64 and linux/arm64. Not applicable in our runtime. Revisit -# on next Compose upstream release. +# Justification: covered by VEX statement for CVE-2026-39883 (otel/sdk +# BSD kenv PATH hijacking). The vulnerable code path executes only on +# BSD operating systems and Sencho ships linux/amd64 + linux/arm64 only. +# See security/vex/sencho.openvex.json for the full impact statement. CVE-2026-39883 diff --git a/SECURITY.md b/SECURITY.md index 4401f51a..4ef59c1b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -28,3 +28,21 @@ Sencho manages Docker containers and has access to the Docker socket. When deplo - Use strong passwords and rotate JWT secrets - Restrict network access to the Sencho port - Review the [security configuration docs](https://docs.sencho.io) for hardening guidance + +## Verifying Release Artifacts + +Every published image is signed and carries verifiable supply-chain artifacts. See the full guide at [docs.sencho.io/operations/verifying-images](https://docs.sencho.io/operations/verifying-images). + +Quick summary: + +```bash +# Verify image signature (cosign keyless, Rekor logged) +cosign verify saelix/sencho: \ + --certificate-identity-regexp "https://github.com/AnsoCode/Sencho/.*" \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com + +# Scan with VEX applied (zero unresolved HIGH/CRITICAL CVEs) +trivy image --vex sencho.openvex.json --severity HIGH,CRITICAL saelix/sencho: +``` + +CycloneDX SBOM (`sbom.cdx.json`), SPDX SBOM (`sbom.spdx.json`), and the VEX document (`sencho.openvex.json`) are attached to every GitHub Release as downloadable assets and as cosign attestations on the image digest. diff --git a/docs/docs.json b/docs/docs.json index 7b64268a..80cbc625 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -138,7 +138,6 @@ "security", "features/node-compatibility", "reference/settings", - "reference/verifying-images", "reference/contact", "reference/security-advisories" ] @@ -151,7 +150,8 @@ "operations/upgrade", "operations/self-hosting", "operations/trivy-setup", - "operations/two-factor-admin" + "operations/two-factor-admin", + "operations/verifying-images" ] } ] diff --git a/docs/operations/verifying-images.mdx b/docs/operations/verifying-images.mdx new file mode 100644 index 00000000..076ba626 --- /dev/null +++ b/docs/operations/verifying-images.mdx @@ -0,0 +1,113 @@ +--- +title: Verifying Published Images +description: How to verify signatures, provenance, SBOMs, and CVE triage for Sencho Docker images. +--- + +Every Sencho release image published to Docker Hub is signed and carries verifiable supply-chain artifacts. This page explains how to check each one. + +## Prerequisites + +Install the following tools once: + +```bash +# cosign (Sigstore) +brew install cosign # macOS +# or: https://github.com/sigstore/cosign/releases + +# syft (optional, for inspecting SBOM content) +brew install syft +# or: https://github.com/anchore/syft/releases + +# Trivy (optional, for local vulnerability scan with VEX) +brew install trivy +# or: https://github.com/aquasecurity/trivy/releases +``` + +## Verify the image signature + +Every published tag is signed with cosign keyless signing via GitHub Actions OIDC. No private key is stored anywhere; the signing identity is bound to the GitHub Actions workflow that published the image. + +```bash +cosign verify saelix/sencho: \ + --certificate-identity-regexp "https://github.com/AnsoCode/Sencho/.*" \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com +``` + +A successful verification prints the verified payload and exits 0. The Rekor transparency log entry is included in the output; you can also query it directly: + +```bash +cosign tree saelix/sencho: +``` + +## Verify SLSA provenance + +BuildKit produces an SLSA v1 provenance attestation during every multi-arch build. Retrieve and verify it: + +```bash +cosign verify-attestation \ + --type slsaprovenance \ + --certificate-identity-regexp "https://github.com/AnsoCode/Sencho/.*" \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + saelix/sencho: | jq -r '.payload' | base64 -d | jq +``` + +## Retrieve the CycloneDX SBOM + +```bash +cosign verify-attestation \ + --type cyclonedx \ + --certificate-identity-regexp "https://github.com/AnsoCode/Sencho/.*" \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + saelix/sencho: | jq -r '.payload' | base64 -d | jq +``` + +Alternatively, download `sbom.cdx.json` directly from the GitHub Release assets for the matching version. + +## Retrieve the SPDX SBOM + +```bash +cosign verify-attestation \ + --type spdxjson \ + --certificate-identity-regexp "https://github.com/AnsoCode/Sencho/.*" \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + saelix/sencho: | jq -r '.payload' | base64 -d | jq +``` + +`sbom.spdx.json` is also available as a GitHub Release asset. + +## Retrieve the VEX document + +The OpenVEX document explains Sencho's triage decisions for any CVEs that appear in vendored dependencies but are not reachable via Sencho's usage of those dependencies: + +```bash +cosign verify-attestation \ + --type openvex \ + --certificate-identity-regexp "https://github.com/AnsoCode/Sencho/.*" \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + saelix/sencho: | jq -r '.payload' | base64 -d | jq +``` + +`sencho.openvex.json` is also available as a GitHub Release asset and at `security/vex/sencho.openvex.json` in the source repository. + +## Run a local vulnerability scan with VEX applied + +To reproduce the same scan that gates every release: + +```bash +# Download the VEX document +curl -Lo sencho.openvex.json \ + https://github.com/AnsoCode/Sencho/releases/latest/download/sencho.openvex.json + +# Scan with VEX applied (exit 1 if any unresolved HIGH/CRITICAL CVE) +trivy image \ + --vex sencho.openvex.json \ + --severity HIGH,CRITICAL \ + --exit-code 1 \ + saelix/sencho: +``` + +A clean result means no unresolved HIGH or CRITICAL CVEs. CVEs listed as `not_affected` in the VEX document are suppressed with their justification visible in the VEX file. + + +Trivy 0.36.0 or later is required for `--vex` flag support. + diff --git a/docs/security.mdx b/docs/security.mdx index 26989b68..f5858edf 100644 --- a/docs/security.mdx +++ b/docs/security.mdx @@ -233,9 +233,15 @@ For multi-node setup, see [Multi-Node Management](/features/multi-node). ## Supply chain security -Sencho Docker images are signed with [Sigstore cosign](https://www.sigstore.dev/) and include SBOM (Software Bill of Materials) and SLSA provenance attestations. Dependency versions are pinned in the Dockerfile for reproducible builds. +Every Sencho release image ships a set of verifiable supply-chain artifacts: -For verification instructions, see [Verifying Images](/reference/verifying-images). For CVE remediation history, see [Security Advisories](/reference/security-advisories). +- **Cosign signature** (keyless, GitHub OIDC): every tag is signed by digest and recorded in the Sigstore Rekor transparency log. No private key is stored anywhere. +- **SLSA provenance attestation**: produced by BuildKit at build time, describes the exact source commit, build inputs, and build steps. +- **CycloneDX 1.6 SBOM**: full package inventory in CycloneDX format, attached as a cosign attestation and downloadable as a GitHub Release asset (`sbom.cdx.json`). +- **SPDX 2.3 SBOM**: same inventory in SPDX format (`sbom.spdx.json`). +- **OpenVEX document**: triage decisions for CVEs that appear in vendored third-party dependencies but are not reachable via Sencho's usage of those dependencies (`sencho.openvex.json`). + +For copy-paste verification commands, see [Verifying Images](/operations/verifying-images). ## Production hardening checklist @@ -248,7 +254,7 @@ For verification instructions, see [Verifying Images](/reference/verifying-image 3. **Enroll all admin accounts in 2FA** to protect against credential theft. See [Two-Factor Authentication](/features/two-factor-authentication). 4. **Use scoped API tokens** instead of sharing admin credentials with CI/CD pipelines. See [API Tokens](/features/api-tokens). 5. **Set `FRONTEND_URL`** to restrict CORS to your domain. See [Configuration](/getting-started/configuration). -6. **Verify Docker image signatures** before deploying to confirm the image has not been tampered with. See [Verifying Images](/reference/verifying-images). +6. **Verify Docker image signatures** before deploying to confirm the image has not been tampered with. See [Verifying Images](/operations/verifying-images). 7. **Review the audit log regularly** or assign a dedicated Auditor-role user for ongoing compliance monitoring. See [Audit Log](/features/audit-log). 8. **Keep Sencho updated** to receive security patches and dependency upgrades. See [Upgrade](/operations/upgrade). diff --git a/security/vex/sencho.openvex.json b/security/vex/sencho.openvex.json new file mode 100644 index 00000000..41cf1491 --- /dev/null +++ b/security/vex/sencho.openvex.json @@ -0,0 +1,120 @@ +{ + "@context": "https://openvex.dev/ns/v0.2.0", + "@id": "https://github.com/AnsoCode/Sencho/security/vex/sencho.openvex.json", + "author": "Studio Saelix", + "role": "Vendor", + "timestamp": "2026-04-26T00:00:00Z", + "last_updated": "2026-04-26T00:00:00Z", + "version": 1, + "statements": [ + { + "vulnerability": { + "@id": "https://www.cve.org/CVERecord?id=CVE-2026-34040", + "name": "CVE-2026-34040", + "description": "Moby/Docker daemon authorization bypass" + }, + "products": [ + { + "@id": "pkg:oci/sencho", + "subcomponents": [ + { + "@id": "pkg:golang/github.com/docker/docker@v28.5.2", + "location": "/usr/local/lib/docker/cli-plugins/docker-compose" + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path", + "impact_statement": "The authz bypass applies to Docker daemon plugin authorization hooks. docker-compose v5.1.2 statically bundles docker/docker v28.5.2 as a library but never acts as a daemon and never runs authorization hooks. Sencho invokes compose only for up/down/ps operations against local user-authored compose files. The daemon authz code path is not reachable." + }, + { + "vulnerability": { + "@id": "https://www.cve.org/CVERecord?id=CVE-2026-33747", + "name": "CVE-2026-33747", + "description": "BuildKit arbitrary file write via untrusted frontend" + }, + "products": [ + { + "@id": "pkg:oci/sencho", + "subcomponents": [ + { + "@id": "pkg:golang/github.com/moby/buildkit@v0.27.1", + "location": "/usr/local/lib/docker/cli-plugins/docker-compose" + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path", + "impact_statement": "docker-compose v5.1.2 statically bundles buildkit v0.27.1. The arbitrary file write vulnerability is exploitable only when buildkit processes an attacker-controlled frontend (e.g. a malicious Dockerfile image reference). Sencho invokes compose only for up/down/ps operations against user-authored compose files. Compose does not invoke the buildkit frontend execution path in this usage." + }, + { + "vulnerability": { + "@id": "https://www.cve.org/CVERecord?id=CVE-2026-33748", + "name": "CVE-2026-33748", + "description": "BuildKit Git URL fragment subdir path traversal" + }, + "products": [ + { + "@id": "pkg:oci/sencho", + "subcomponents": [ + { + "@id": "pkg:golang/github.com/moby/buildkit@v0.27.1", + "location": "/usr/local/lib/docker/cli-plugins/docker-compose" + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path", + "impact_statement": "The Git URL fragment subdir traversal is exploitable only when buildkit fetches an attacker-controlled Git repository URL as a build context. Sencho invokes compose for up/down/ps against local user-authored compose files only. Compose does not pass Git URLs to buildkit in this usage." + }, + { + "vulnerability": { + "@id": "https://www.cve.org/CVERecord?id=CVE-2026-24051", + "name": "CVE-2026-24051", + "description": "OpenTelemetry SDK PATH hijacking on process start" + }, + "products": [ + { + "@id": "pkg:oci/sencho", + "subcomponents": [ + { + "@id": "pkg:golang/go.opentelemetry.io/otel/sdk@v1.38.0", + "location": "/usr/local/lib/docker/cli-plugins/docker-compose" + } + ] + } + ], + "status": "not_affected", + "justification": "inline_mitigations_already_exist", + "impact_statement": "The PATH hijacking requires an attacker to control the process PATH environment variable before compose starts. The container's PATH is set to /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin at image build time. Both directories are owned by root with mode 755. Unprivileged processes cannot inject malicious binaries into these directories." + }, + { + "vulnerability": { + "@id": "https://www.cve.org/CVERecord?id=CVE-2026-39883", + "name": "CVE-2026-39883", + "description": "OpenTelemetry SDK BSD kenv PATH hijacking" + }, + "products": [ + { + "@id": "pkg:oci/sencho", + "subcomponents": [ + { + "@id": "pkg:golang/go.opentelemetry.io/otel/sdk@v1.38.0", + "location": "/usr/local/lib/docker/cli-plugins/docker-compose" + }, + { + "@id": "pkg:golang/go.opentelemetry.io/otel/sdk@v1.42.0", + "location": "/usr/local/bin/docker" + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_present", + "impact_statement": "The BSD kenv PATH hijacking code path executes only on BSD operating systems (FreeBSD, OpenBSD, NetBSD). Sencho publishes linux/amd64 and linux/arm64 images only. The vulnerable code path is never executed in our runtime environment. Both the docker-compose plugin (otel/sdk v1.38.0) and the rebuilt Docker CLI binary (otel/sdk v1.42.0) bundle vulnerable versions; the same justification applies to both." + } + ] +} diff --git a/trivy.yaml b/trivy.yaml new file mode 100644 index 00000000..4b0c32d7 --- /dev/null +++ b/trivy.yaml @@ -0,0 +1,3 @@ +vulnerability: + vex: + - security/vex/sencho.openvex.json