From 082ba3d0373335865fa4d325951fa6e040bdf5ae Mon Sep 17 00:00:00 2001 From: Joe Harrison <53116754+sudo-kraken@users.noreply.github.com> Date: Sun, 19 Oct 2025 04:22:28 +0100 Subject: [PATCH] ci: added helm cosign verification and renovate app workflow to bump chart versions (#2064) * ci: added helm cosign verification and renovate app workflow to bump chart versions * docs: add helm artifacts verification Signed-off-by: Ludovic Ortega * fix: update app id Signed-off-by: Ludovic Ortega * docs: add documentation link in helm chart and seerr docs Signed-off-by: Ludovic Ortega --------- Signed-off-by: Ludovic Ortega Co-authored-by: Ludovic Ortega --- .github/workflows/helm.yml | 56 +++++- .github/workflows/release.yml | 1 + .../workflows/renovate-helm-custom-hooks.yml | 181 ++++++++++++++++++ charts/seerr-chart/README.md | 4 + charts/seerr-chart/README.md.gotmpl | 6 +- docs/getting-started/docker.mdx | 11 +- docs/getting-started/kubernetes.mdx | 6 + .../advanced/verifying-signed-artifacts.mdx | 145 ++++++++++++-- 8 files changed, 376 insertions(+), 34 deletions(-) create mode 100644 .github/workflows/renovate-helm-custom-hooks.yml diff --git a/.github/workflows/helm.yml b/.github/workflows/helm.yml index 4b1b4a751..4af45586b 100644 --- a/.github/workflows/helm.yml +++ b/.github/workflows/helm.yml @@ -55,7 +55,7 @@ jobs: # get current version current_version=$(grep '^version:' "$chart_path/Chart.yaml" | awk '{print $2}') # try to get current release version - if oras manifest fetch "ghcr.io/${GITHUB_REPOSITORY@L}/${chart_name}:${current_version}" >/dev/null 2>&1; then + if oras manifest fetch "ghcr.io/${{ github.repository }}/${chart_name}:${current_version}" >/dev/null 2>&1; then echo "No version change for $chart_name. Skipping." else helm dependency build "$chart_path" @@ -87,8 +87,8 @@ jobs: name: Publish to ghcr.io runs-on: ubuntu-24.04 permissions: - packages: write # needed for pushing to github registry - id-token: write # needed for signing the images with GitHub OIDC Token + packages: write + id-token: write needs: [package-helm-chart] if: needs.package-helm-chart.outputs.has_artifacts == 'true' steps: @@ -128,17 +128,59 @@ jobs: # push chart to OCI chart_release_file=$(basename "$chart_path") chart_name=${chart_release_file%-*} - helm push ${chart_path} oci://ghcr.io/${GITHUB_REPOSITORY@L} |& tee helm-push-output.log + helm push ${chart_path} oci://ghcr.io/${{ github.repository }} |& tee helm-push-output.log chart_digest=$(awk -F "[, ]+" '/Digest/{print $NF}' < helm-push-output.log) # sign chart - cosign sign "ghcr.io/${GITHUB_REPOSITORY@L}/${chart_name}@${chart_digest}" + cosign sign "ghcr.io/${{ github.repository }}/${chart_name}@${chart_digest}" # push artifacthub-repo.yml to OCI oras push \ - ghcr.io/${GITHUB_REPOSITORY@L}/${chart_name}:artifacthub.io \ + ghcr.io/${{ github.repository }}/${chart_name}:artifacthub.io \ --config /dev/null:application/vnd.cncf.artifacthub.config.v1+yaml \ charts/$chart_name/artifacthub-repo.yml:application/vnd.cncf.artifacthub.repository-metadata.layer.v1.yaml \ |& tee oras-push-output.log artifacthub_digest=$(grep "Digest:" oras-push-output.log | awk '{print $2}') # sign artifacthub-repo.yml - cosign sign "ghcr.io/${GITHUB_REPOSITORY@L}/${chart_name}:artifacthub.io@${artifacthub_digest}" + cosign sign "ghcr.io/${{ github.repository }}/${chart_name}:artifacthub.io@${artifacthub_digest}" + done + + verify: + name: Verify signatures for each chart tag + needs: [publish] + runs-on: ubuntu-24.04 + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Install Cosign + uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0 + + - name: Downloads artifacts + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + with: + name: artifacts + path: .cr-release-packages/ + + - name: Login to GitHub Container Registry + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Verify signatures for each chart tag + run: | + for chart_path in $(find .cr-release-packages -name '*.tgz' -print); do + chart_release_file=$(basename "$chart_path") + chart_name=${chart_release_file%-*} + version=${chart_release_file#$chart_name-} + version=${version%.tgz} + + cosign verify "ghcr.io/${{ github.repository }}/${chart_name}:${version}" \ + --certificate-identity "https://github.com/${{ github.workflow_ref }}" \ + --certificate-oidc-issuer "https://token.actions.githubusercontent.com" done diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 35188a3ce..53b806f3d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,3 +1,4 @@ +--- # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json name: Seerr Release diff --git a/.github/workflows/renovate-helm-custom-hooks.yml b/.github/workflows/renovate-helm-custom-hooks.yml new file mode 100644 index 000000000..e833403a2 --- /dev/null +++ b/.github/workflows/renovate-helm-custom-hooks.yml @@ -0,0 +1,181 @@ +--- +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json +name: Renovate Helm Hooks + +on: + pull_request: + branches: + - develop + paths: + - 'charts/**' + +permissions: {} + +concurrency: + group: renovate-helm-hooks-${{ github.ref }} + cancel-in-progress: true + +jobs: + renovate-post-run: + name: Renovate Bump Chart Version + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + if: github.actor == 'renovate[bot]' + steps: + - name: Checkout code + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + fetch-depth: 0 + persist-credentials: false + + - uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4 + id: app-token + with: + app-id: 2138788 + private-key: ${{ secrets.APP_SEERR_HELM_PRIVATE_KEY }} + + - name: Set up chart-testing + uses: helm/chart-testing-action@0d28d3144d3a25ea2cc349d6e59901c4ff469b3b # v2.7.0 + + - name: Run chart-testing (list-changed) + id: list-changed + run: | + changed="$(ct list-changed --target-branch ${TARGET_BRANCH})" + if [[ -n "$changed" ]]; then + echo "changed=true" >> "$GITHUB_OUTPUT" + echo "changed_list=${changed//$'\n'/ }" >> "$GITHUB_OUTPUT" + fi + env: + TARGET_BRANCH: ${{ github.event.repository.default_branch }} + + - name: Bump chart version + if: steps.list-changed.outputs.changed == 'true' + env: + CHART: ${{ steps.list-changed.outputs.changed_list }} + run: | + if [[ ! -d "${CHART}" ]]; then + echo "${CHART} directory not found" + exit 0 + fi + + # Extract current appVersion and chart version from Chart.yaml + APP_VERSION=$(grep -e "^appVersion:" "$CHART/Chart.yaml" | cut -d ":" -f 2 | tr -d '[:space:]' | tr -d '"') + CHART_VERSION=$(grep -e "^version:" "$CHART/Chart.yaml" | cut -d ":" -f 2 | tr -d '[:space:]' | tr -d '"') + + # Extract major, minor and patch versions of appVersion + APP_MAJOR_VERSION=$(printf '%s' "$APP_VERSION" | cut -d "." -f 1) + APP_MINOR_VERSION=$(printf '%s' "$APP_VERSION" | cut -d "." -f 2) + APP_PATCH_VERSION=$(printf '%s' "$APP_VERSION" | cut -d "." -f 3) + + # Extract major, minor and patch versions of chart version + CHART_MAJOR_VERSION=$(printf '%s' "$CHART_VERSION" | cut -d "." -f 1) + CHART_MINOR_VERSION=$(printf '%s' "$CHART_VERSION" | cut -d "." -f 2) + CHART_PATCH_VERSION=$(printf '%s' "$CHART_VERSION" | cut -d "." -f 3) + + # Get previous appVersion from the base commit of the pull request + BASE_COMMIT=$(git merge-base origin/main HEAD) + PREV_APP_VERSION=$(git show "$BASE_COMMIT":"$CHART/Chart.yaml" | grep -e "^appVersion:" | cut -d ":" -f 2 | tr -d '[:space:]' | tr -d '"') + + # Extract major, minor and patch versions of previous appVersion + PREV_APP_MAJOR_VERSION=$(printf '%s' "$PREV_APP_VERSION" | cut -d "." -f 1) + PREV_APP_MINOR_VERSION=$(printf '%s' "$PREV_APP_VERSION" | cut -d "." -f 2) + PREV_APP_PATCH_VERSION=$(printf '%s' "$PREV_APP_VERSION" | cut -d "." -f 3) + + # Check if the major, minor, or patch version of appVersion has changed + if [[ "$APP_MAJOR_VERSION" != "$PREV_APP_MAJOR_VERSION" ]]; then + # Bump major version of the chart and reset minor and patch versions to 0 + CHART_MAJOR_VERSION=$((CHART_MAJOR_VERSION+1)) + CHART_MINOR_VERSION=0 + CHART_PATCH_VERSION=0 + elif [[ "$APP_MINOR_VERSION" != "$PREV_APP_MINOR_VERSION" ]]; then + # Bump minor version of the chart and reset patch version to 0 + CHART_MINOR_VERSION=$((CHART_MINOR_VERSION+1)) + CHART_PATCH_VERSION=0 + elif [[ "$APP_PATCH_VERSION" != "$PREV_APP_PATCH_VERSION" ]]; then + # Bump patch version of the chart + CHART_PATCH_VERSION=$((CHART_PATCH_VERSION+1)) + fi + + # Update the chart version in Chart.yaml + CHART_NEW_VERSION="${CHART_MAJOR_VERSION}.${CHART_MINOR_VERSION}.${CHART_PATCH_VERSION}" + sed -i "s/^version:.*/version: ${CHART_NEW_VERSION}/" "$CHART/Chart.yaml" + + - name: Ensure documentation is updated + if: steps.list-changed.outputs.changed == 'true' + uses: docker://jnorwood/helm-docs:v1.14.2@sha256:7e562b49ab6b1dbc50c3da8f2dd6ffa8a5c6bba327b1c6335cc15ce29267979c + + - name: Commit changes + if: steps.list-changed.outputs.changed == 'true' + env: + CHART: ${{ steps.list-changed.outputs.changed_list }} + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} + GITHUB_HEAD_REF: ${{ github.head_ref }} + run: | + # Define the target directory + TARGET_DIR="$CHART" + + # Fetch deleted files in the target directory + DELETED_FILES=$(git diff --diff-filter=D --name-only HEAD -- "$TARGET_DIR") + + # Fetch added/modified files in the target directory + MODIFIED_FILES=$(git diff --diff-filter=ACM --name-only HEAD -- "$TARGET_DIR") + + # Create a temporary file for JSON output + FILE_CHANGES_JSON_FILE=$(mktemp) + + # Initialize JSON structure in the file + echo '{ "deletions": [], "additions": [] }' > "$FILE_CHANGES_JSON_FILE" + + # Add deletions + for file in $DELETED_FILES; do + jq --arg path "$file" '.deletions += [{"path": $path}]' "$FILE_CHANGES_JSON_FILE" > "$FILE_CHANGES_JSON_FILE.tmp" + mv "$FILE_CHANGES_JSON_FILE.tmp" "$FILE_CHANGES_JSON_FILE" + done + + # Add additions (new or modified files) + for file in $MODIFIED_FILES; do + BASE64_CONTENT=$(base64 -w 0 <"$file") # Encode file content + jq --arg path "$file" --arg content "$BASE64_CONTENT" \ + '.additions += [{"path": $path, "contents": $content}]' "$FILE_CHANGES_JSON_FILE" > "$FILE_CHANGES_JSON_FILE.tmp" + mv "$FILE_CHANGES_JSON_FILE.tmp" "$FILE_CHANGES_JSON_FILE" + done + + # Create a temporary file for the final JSON payload + JSON_PAYLOAD_FILE=$(mktemp) + + # Construct the final JSON using jq and store it in a file + jq -n --arg repo "$GITHUB_REPOSITORY" \ + --arg branch "$GITHUB_HEAD_REF" \ + --arg message "fix: post upgrade changes from renovate" \ + --arg expectedOid "$GITHUB_SHA" \ + --slurpfile fileChanges "$FILE_CHANGES_JSON_FILE" \ + '{ + query: "mutation ($input: CreateCommitOnBranchInput!) { + createCommitOnBranch(input: $input) { + commit { + url + } + } + }", + variables: { + input: { + branch: { + repositoryNameWithOwner: $repo, + branchName: $branch + }, + message: { headline: $message }, + fileChanges: $fileChanges[0], + expectedHeadOid: $expectedOid + } + } + }' > "$JSON_PAYLOAD_FILE" + + # Call GitHub API + curl https://api.github.com/graphql -f \ + -sSf -H "Authorization: Bearer $GITHUB_TOKEN" \ + --data "@$JSON_PAYLOAD_FILE" + + # Clean up temporary files + rm "$FILE_CHANGES_JSON_FILE" "$JSON_PAYLOAD_FILE" diff --git a/charts/seerr-chart/README.md b/charts/seerr-chart/README.md index 318f90b3c..2b63be031 100644 --- a/charts/seerr-chart/README.md +++ b/charts/seerr-chart/README.md @@ -20,6 +20,10 @@ Seerr helm chart for Kubernetes Kubernetes: `>=1.23.0-0` +## Installation + +Refer to [https://docs.seerr.dev/getting-started/kubernetes](Seerr kubernetes documentation) + ## Update Notes ### Updating to 3.0.0 diff --git a/charts/seerr-chart/README.md.gotmpl b/charts/seerr-chart/README.md.gotmpl index cdf693740..15a45b064 100644 --- a/charts/seerr-chart/README.md.gotmpl +++ b/charts/seerr-chart/README.md.gotmpl @@ -14,11 +14,15 @@ {{ template "chart.requirementsSection" . }} +## Installation + +Refer to [https://docs.seerr.dev/getting-started/kubernetes](Seerr kubernetes documentation) + ## Update Notes ### Updating to 3.0.0 -Nothing change we just rebranded `jellyseerr` helm-chart to `seerr` :) +Nothing has changed; we just rebranded the `jellyseerr` Helm chart to `seerr` 🥳. ### Updating to 2.7.0 diff --git a/docs/getting-started/docker.mdx b/docs/getting-started/docker.mdx index 7efb5a2de..664882f8b 100644 --- a/docs/getting-started/docker.mdx +++ b/docs/getting-started/docker.mdx @@ -15,6 +15,12 @@ Refer to [Configuring Databases](/extending-jellyseerr/database-config#postgresq An alternative Docker image is available on Docker Hub for this project. You can find it at [Docker Hub Repository Link](https://hub.docker.com/r/seerr/seerr) ::: +:::info +All official Seerr images are cryptographically signed and include a verified [Software Bill of Materials (SBOM)](https://cyclonedx.org/). + +To confirm that the container image you are using is authentic and unmodified, please refer to the [Verifying Signed Artifacts](/using-jellyseerr/advanced/verifying-signed-artifacts#verifying-signed-images) guide. +::: + ## Unix (Linux, macOS) :::warning Be sure to replace `/path/to/appdata/config` in the below examples with a valid host directory path. If this volume mount is not configured correctly, your Jellyseerr settings/data will not be persisted when the container is recreated (e.g., when updating the image or rebooting your machine). @@ -72,11 +78,6 @@ Finally, run the container with the same parameters originally used to create th ```bash docker run -d ... ``` -:::info -All official Seerr images are cryptographically signed and include a verified [Software Bill of Materials (SBOM)](https://cyclonedx.org/). - -To confirm that the container image you are using is authentic and unmodified, please refer to the [Verifying Signed Artifacts](/using-jellyseerr/advanced/verifying-signed-artifacts) guide. -::: :::tip You may alternatively use a third-party updating mechanism, such as [Watchtower](https://github.com/containrrr/watchtower) or [Ouroboros](https://github.com/pyouroboros/ouroboros), to keep Jellyseerr up-to-date automatically. diff --git a/docs/getting-started/kubernetes.mdx b/docs/getting-started/kubernetes.mdx index 4720abed9..1d5cb6ff0 100644 --- a/docs/getting-started/kubernetes.mdx +++ b/docs/getting-started/kubernetes.mdx @@ -8,6 +8,12 @@ sidebar_position: 5 This method is not recommended for most users. It is intended for advanced users who are using Kubernetes. ::: +:::info +All official Seerr charts are cryptographically signed and include a verified [Software Bill of Materials (SBOM)](https://cyclonedx.org/). + +To confirm that the chart you are using is authentic and unmodified, please refer to the [Verifying Signed Artifacts](/using-jellyseerr/advanced/verifying-signed-artifacts#verifying-signed-helm-charts) guide. +::: + ## Installation ```console helm install jellyseerr oci://ghcr.io/fallenbagel/jellyseerr/jellyseerr-chart diff --git a/docs/using-jellyseerr/advanced/verifying-signed-artifacts.mdx b/docs/using-jellyseerr/advanced/verifying-signed-artifacts.mdx index c706fcab9..3ed328676 100644 --- a/docs/using-jellyseerr/advanced/verifying-signed-artifacts.mdx +++ b/docs/using-jellyseerr/advanced/verifying-signed-artifacts.mdx @@ -12,6 +12,7 @@ import TabItem from '@theme/TabItem'; These artifacts are cryptographically signed using [Sigstore Cosign](https://docs.sigstore.dev/quickstart/quickstart-cosign/): - Container images +- Helm charts This ensures that the images you pull are authentic, tamper-proof, and built by the official Seerr release pipeline. @@ -27,19 +28,11 @@ You will need the following tools installed: To verify images: -- [Docker](https://docs.docker.com/get-docker/) **or** -- [Podman](https://podman.io/getting-started/installation) (including [Skopeo](https://github.com/containers/skopeo/blob/main/install.md)) +- [Docker](https://docs.docker.com/get-docker/) **or** [Podman](https://podman.io/getting-started/installation) (including [Skopeo](https://github.com/containers/skopeo/blob/main/install.md)) --- -# Verifying Signed Images - -All Seerr container images published to GitHub Container Registry (GHCR) are cryptographically signed using [Sigstore Cosign](https://docs.sigstore.dev/quickstart/quickstart-cosign/). -This ensures that the images you pull are authentic, tamper-proof, and built by the official Seerr release pipeline. - -Each image also includes a CycloneDX SBOM (Software Bill of Materials) attestation, generated with [Trivy](https://aquasecurity.github.io/trivy/), providing transparency about all dependencies included in the image. - ---- +## Verifying Signed Images ### Image Locations @@ -227,17 +220,6 @@ This confirms that the image was: --- -### Troubleshooting - -| Issue | Likely Cause | Suggested Fix | -|-------|---------------|----------------| -| `no matching signatures` | Incorrect digest or tag | Retrieve the digest again using Docker or Skopeo | -| `certificate identity does not match expected` | Workflow reference changed | Ensure your `--certificate-identity` matches this documentation | -| `cosign: command not found` | Cosign not installed | Install Cosign from the official release | -| `certificate expired` | Old release | Verify a newer tag or digest | - ---- - ### Example: Full Verification Flow @@ -269,6 +251,127 @@ cosign verify ghcr.io/seerr-team/seerr@"$DIGEST" \ +## Verifying Signed Helm charts + +### Helm Chart Locations + +Official Seerr helm charts are available from: + +- GitHub Container Registry (GHCR): `ghcr.io/seerr-team/seerr/seerr-chart/seerr-chart:` + +You can view all available tags on the [Seerr Releases page](https://github.com/seerr-team/seerr/pkgs/container/seerr%2Fseerr-chart). + +--- + +### Verifying a Specific Release Tag + +Each tagged release (for example `3.0.0`) is immutable and cryptographically signed. +Verification should always be performed using the image digest (SHA256). + +#### Retrieve the Helm Chart Digest + + + + +```bash +docker buildx imagetools inspect ghcr.io/seerr-team/seerr/seerr-chart:3.0.0 --format '{{json .Manifest.Digest}}' | tr -d '"' +``` + + + + +```bash +skopeo inspect docker://ghcr.io/seerr-team/seerr/seerr-chart:3.0.0 --format '{{.Digest}}' +``` + + + +Example output: + +``` +sha256:abcd1234... +``` + +--- + +#### Verify the Helm Chart Signature + +```bash +cosign verify ghcr.io/seerr-team/seerr/seerr-chart@sha256:abcd1234... \ + --certificate-identity "https://github.com/seerr-team/seerr/.github/workflows/helm.yml@refs/heads/main" \ + --certificate-oidc-issuer "https://token.actions.githubusercontent.com" +``` + +:::info Successful Verification Example +Verification for `ghcr.io/seerr-team/seerr/seerr-chart@sha256:abcd1234...` + +The following checks were performed: + +- Cosign claims validated +- Signatures verified against the transparency log +- Certificate issued by Fulcio to the expected workflow identity +::: + +--- + +### Expected Certificate Identity + +The expected certificate identity for all signed Seerr images is: + +``` +https://github.com/seerr-team/seerr/.github/workflows/helm.yml@refs/heads/main +``` + +This confirms that the image was: + +- Built by the official Seerr Release workflow +- Produced from the seerr-team/seerr repository +- Signed using GitHub’s OIDC identity via Sigstore Fulcio + +--- + +### Example: Full Verification Flow + + + + +```bash +DIGEST=$(docker buildx imagetools inspect ghcr.io/seerr-team/seerr/seerr-chart:3.0.0 --format '{{json .Manifest.Digest}}' | tr -d '"') + +cosign verify ghcr.io/seerr-team/seerr/seerr-chart@"$DIGEST" \ + --certificate-identity-regexp "https://github.com/seerr-team/seerr/.github/workflows/helm.yml@refs/heads/main" \ + --certificate-oidc-issuer "https://token.actions.githubusercontent.com" + +cosign verify-attestation ghcr.io/seerr-team/seerr/seerr-chart@"$DIGEST" \ + --type cyclonedx \ + --certificate-identity-regexp "https://github.com/seerr-team/seerr/.github/workflows/helm.yml@refs/heads/main" \ + --certificate-oidc-issuer "https://token.actions.githubusercontent.com" +``` + + + + +```bash +DIGEST=$(skopeo inspect docker://ghcr.io/seerr-team/seerr/seerr-chart:3.0.0 --format '{{.Digest}}') + +cosign verify ghcr.io/seerr-team/seerr/seerr-chart@"$DIGEST" \ + --certificate-identity-regexp "https://github.com/seerr-team/seerr/.github/workflows/helm.yml@refs/heads/main" \ + --certificate-oidc-issuer "https://token.actions.githubusercontent.com" +``` + + + +--- + +## Troubleshooting + +| Issue | Likely Cause | Suggested Fix | +|-------|---------------|----------------| +| `no matching signatures` | Incorrect digest or tag | Retrieve the digest again using Docker or Skopeo | +| `certificate identity does not match expected` | Workflow reference changed | Ensure your `--certificate-identity` matches this documentation | +| `cosign: command not found` | Cosign not installed | Install Cosign from the official release | +| `certificate expired` | Old release | Verify a newer tag or digest | + --- ## Further Reading