Skip to content

fix(security): revert broken padding auth — use explicit length + ct_eq #200

fix(security): revert broken padding auth — use explicit length + ct_eq

fix(security): revert broken padding auth — use explicit length + ct_eq #200

Workflow file for this run

name: Release
# Builds and publishes a multi-arch container image to GHCR.
#
# Triggers and resulting tags (CalVer-on-SemVer scheme, YYYY.0M0D.PATCH):
# - push to main → :main, :sha-<short>
# - pre-release tag → :X.Y.Z-rc.N, :testing, :sha-<short>
# (e.g. 2026.510.0-rc.1; :latest and floating-version tags stay
# on the prior final release)
# - final tag (no -) → :X.Y.Z, :X.Y, :X, :latest, :stable, :sha-<short>
# - pull_request → build-only verification on both arches (no push)
# - workflow_dispatch → manual republish
#
# Architecture: separate native-runner jobs per platform (amd64 on
# ubuntu-latest, arm64 on ubuntu-24.04-arm) push by digest, then a
# merge job assembles the multi-arch manifest, signs it keyless with
# cosign, and attaches a SLSA build-provenance attestation. This is
# materially faster than QEMU emulation for a Rust workload.
on:
push:
branches: [main]
tags: ['[0-9]*']
pull_request:
branches: [main]
workflow_dispatch:
# Don't cancel a release in flight if a newer commit lands — the
# in-flight build might be the one a user is about to pull. PRs are
# safe to cancel on rebase.
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository_owner }}/gluco-hub
# Deny-all by default. Each job below opts into the narrowest token scope
# it needs — `build` only `packages: write` for digest push, `merge` adds
# `id-token: write` for cosign keyless OIDC and `attestations: write` for
# the SLSA build-provenance attestation. PR runs gate the actual push via
# `if:` on the step, so even though the build job has `packages: write`,
# nothing exercises it on `pull_request`.
permissions: {}
jobs:
build:
name: build (${{ matrix.platform }})
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write # push image layers by digest (no-op on PRs)
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
- platform: linux/arm64
runner: ubuntu-24.04-arm
steps:
- uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Compute env
run: |
echo "BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_ENV"
# platform "linux/arm64" → "linux-arm64", used as artifact suffix
echo "PLATFORM_PAIR=${PLATFORM//\//-}" >> "$GITHUB_ENV"
env:
PLATFORM: ${{ matrix.platform }}
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Log in to GHCR
if: github.event_name != 'pull_request'
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Only labels here — tags are computed in the merge job and applied
# via `buildx imagetools create -t <tag> ...`. Per-arch builds push
# by digest and have no human-readable tag of their own.
- name: Compute image labels
id: meta
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
labels: |
org.opencontainers.image.licenses=AGPL-3.0-or-later
- name: Build & push by digest
id: build
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GLUCO_HUB_GIT_SHA=${{ github.sha }}
BUILD_DATE=${{ env.BUILD_DATE }}
# Per-platform cache scope so the two matrix jobs don't evict
# each other.
cache-from: type=gha,scope=${{ env.PLATFORM_PAIR }}
cache-to: type=gha,mode=max,scope=${{ env.PLATFORM_PAIR }}
# PRs build but don't push. Non-PR runs push by digest only —
# the merge job assembles the multi-arch manifest with proper
# tags. `name-canonical=true` ensures the digest is recorded
# against the canonical image name.
outputs: |
${{ github.event_name == 'pull_request'
&& 'type=cacheonly'
|| format('type=image,name={0}/{1},push-by-digest=true,name-canonical=true,push=true', env.REGISTRY, env.IMAGE_NAME) }}
# Provenance default for public repos is mode=max since
# build-push-action v6 — leaving implicit. SBOM must be opted
# into explicitly.
sbom: true
- name: Export digest
if: github.event_name != 'pull_request'
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
if: github.event_name != 'pull_request'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: digest-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
scan:
name: Container CVE scan
needs: [build]
if: github.event_name == 'push' || github.event_name == 'release'
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Build local amd64 image for scanning
run: |
docker build \
--build-arg GLUCO_HUB_GIT_SHA=${{ github.sha }} \
--build-arg BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) \
--platform linux/amd64 \
-t gluco-hub:scan \
.
- name: Scan image with Grype
id: grype
uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 # v7.4.0
with:
image: gluco-hub:scan
fail-build: true
severity-cutoff: critical
output-format: sarif
- name: Upload SARIF to GitHub Security
if: always() && steps.grype.outputs.sarif != ''
uses: github/codeql-action/upload-sarif@dd903d2e4f5405488e5ef1422510ee31c8b32357 # v3.36.2
with:
sarif_file: ${{ steps.grype.outputs.sarif }}
category: grype
merge:
name: merge & publish manifest
if: github.event_name != 'pull_request'
needs: build
runs-on: ubuntu-latest
permissions:
contents: read
packages: write # push manifest list to GHCR
id-token: write # cosign keyless OIDC + attestation signing
attestations: write # GitHub Artifact Attestations
steps:
- uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit
- name: Download digests
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
path: /tmp/digests
pattern: digest-*
merge-multiple: true
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Log in to GHCR
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Channel model:
# :main — stable; main-only pushes (release-cuts)
# :testing — pre-release tags (v*-rc/alpha/beta)
# :latest, :stable, floating — final tags only (v2026.510.0, no suffix)
# `latest=false` disables auto-latest so :latest is set exclusively by
# the explicit suffix-filtered raw rule below — no RC ever moves
# :latest, :stable, :2026, or :2026.510.
- name: Compute image metadata
id: meta
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
# `buildx imagetools create` operates on the manifest list / OCI
# index and rejects the default `manifest:` annotation prefix
# ("manifest annotations are not supported yet"). Tell metadata-
# action to emit `index:`-prefixed annotations instead, which is
# the level imagetools is actually editing.
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: index
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
flavor: |
latest=false
labels: |
org.opencontainers.image.description=LibreLink Up → HTTP / Nightscout bridge
org.opencontainers.image.licenses=AGPL-3.0-or-later
tags: |
type=raw,value=main,enable={{is_default_branch}}
type=raw,value=testing,enable=${{ startsWith(github.ref, 'refs/tags/v') && contains(github.ref, '-') }}
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-') }}
type=raw,value=stable,enable=${{ startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-') }}
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}},enable=${{ !contains(github.ref, '-') }}
type=semver,pattern={{major}},enable=${{ !contains(github.ref, '-') }}
type=sha,format=short
- name: Create manifest list & push
working-directory: /tmp/digests
# Build the argv as a bash array so values containing spaces or
# shell-meaningful chars (e.g. "->", "--flag=") cannot be word-split
# into stray flags — closes a CWE-88 argument-injection footgun.
run: |
set -euo pipefail
args=()
while IFS= read -r tag; do
args+=(-t "$tag")
done < <(jq -r '.tags[]' <<< "$DOCKER_METADATA_OUTPUT_JSON")
while IFS= read -r ann; do
args+=(--annotation "$ann")
done < <(jq -r '.annotations[]? // empty' <<< "$DOCKER_METADATA_OUTPUT_JSON")
for digest in *; do
args+=("${REGISTRY}/${IMAGE_NAME}@sha256:${digest}")
done
docker buildx imagetools create "${args[@]}"
- name: Inspect manifest
run: |
docker buildx imagetools inspect \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}
- name: Resolve manifest digest
id: manifest
run: |
digest=$(docker buildx imagetools inspect \
"${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}" \
--format '{{json .Manifest.Digest}}' | tr -d '"')
echo "digest=${digest}" >> "$GITHUB_OUTPUT"
- name: Install cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
# Sign the manifest-list digest. One signature covers every tag
# pointing at the same digest — re-tagging doesn't invalidate it.
- name: Sign image (keyless)
env:
IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
DIGEST: ${{ steps.manifest.outputs.digest }}
run: |
cosign sign --yes \
-a "repo=${{ github.repository }}" \
-a "workflow=${{ github.workflow }}" \
-a "ref=${{ github.sha }}" \
"${IMAGE}@${DIGEST}"
# Pushes a Sigstore-signed SLSA provenance bundle to the registry,
# discoverable in the GitHub UI and verifiable via:
# gh attestation verify oci://ghcr.io/<owner>/gluco-hub:<tag> \
# --owner <owner>
- name: Attest build provenance
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.manifest.outputs.digest }}
push-to-registry: true