docs/Use cases/Verify images in CI/CD

Verify images in CI/CD

Taking Flareo's verification out of "thing I run manually on my laptop" and into "thing the CI/CD pipeline enforces on every deploy" is one line of shell and one small YAML change. This page shows the wiring for GitHub Actions, GitLab CI, and Argo CD. The pattern generalizes.

The point: if a deploy fails because an image isn't Flareo-signed or its signature has been revoked, the deploy fails before anything changes in production. No manual gate, no post-deploy scramble, no "let's check this next time" technical debt.

What "verify" means here

The flareo verify CLI checks three things against any image reference:

  • Signature presence and validity — the image was signed by a workflow identity we trust (pinned by default to Flareo's signer workflows, overridable)
  • Transparency log entry — the signature is in Rekor (Sigstore's public append-only log)
  • Scan status — the current CVE status from Flareo's latest scan

A passing flareo verify is a claim that the image hasn't been tampered with, was built by the expected pipeline, and has been checked for known vulnerabilities recently. It's a fast check — typically under 2 seconds — because the verification is done locally against publicly-cached signing data.

Exit code 0 on success, non-zero on any failure. That's the seam CI/CD hooks into.

Pattern 1: GitHub Actions

A two-step addition to any deploy workflow. First, install the CLI; second, verify each image you're about to deploy.

- name: Build Flareo CLI from source
  run: |
    git clone --depth 1 https://github.com/Yolo1105/flareo.git
    cargo build --release --manifest-path flareo/packages/cli/Cargo.toml
    echo "$PWD/flareo/packages/cli/target/release" >> "$GITHUB_PATH"

- name: Verify image signature
  run: |
    flareo verify public.ecr.aws/flareo/vaultwarden@sha256:${{ env.VAULTWARDEN_DIGEST }}

If verify exits non-zero, the job fails and the deploy step downstream never runs. You get the failure in the GitHub Actions UI with a precise reason.

For a matrix of images, pull them into a step input:

- name: Verify all images
  env:
    IMAGES: |
      public.ecr.aws/flareo/vaultwarden@sha256:7f2a...
      public.ecr.aws/flareo/gitea@sha256:a3b1...
      public.ecr.aws/flareo/uptime-kuma@sha256:d8e2...
  run: |
    set -e
    while read -r img; do
      [ -z "$img" ] && continue
      echo "Verifying $img"
      flareo verify "$img"
    done <<< "$IMAGES"

The set -e plus the non-zero exit from flareo verify means the first failure aborts the whole batch.

Pinning the signer identity

By default flareo verify accepts any image signed by Flareo's signer workflows. If you want to pin tighter — e.g. only accept images signed by a specific workflow tag — pass --signer:

flareo verify \
  --signer "https://github.com/Yolo1105/flareo/.github/workflows/sign.yml@refs/tags/v1" \
  public.ecr.aws/flareo/vaultwarden@sha256:7f2a...

This is stricter — a Flareo signing-workflow update (v1 → v2) would stop satisfying your pin until you review and update the pin yourself. Appropriate for production. Slightly overkill for a homelab.

Pattern 2: GitLab CI

Same pattern, slightly different shell syntax:

verify_images:
  stage: verify
  image: rust:1.80
  before_script:
    - git clone --depth 1 https://github.com/Yolo1105/flareo.git
    - cargo build --release --manifest-path flareo/packages/cli/Cargo.toml
    - export PATH="$PWD/flareo/packages/cli/target/release:$PATH"
  script:
    - flareo verify "$VAULTWARDEN_IMAGE"

deploy:
  stage: deploy
  needs: [verify_images]
  # ... actual deploy steps

The needs: directive ensures deploy only runs if verify_images passes. Failing the verify job blocks the deploy.

Pattern 3: Argo CD — pre-sync hook

For Kubernetes deploys via Argo CD, the cleanest place to verify is a pre-sync Job that runs before the real workload rollout:

apiVersion: batch/v1
kind: Job
metadata:
  name: verify-images
  annotations:
    argocd.argoproj.io/hook: PreSync
    argocd.argoproj.io/hook-delete-policy: HookSucceeded
spec:
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: verify
          image: ghcr.io/flareo/flareo-cli:latest
          command:
            - /bin/sh
            - -c
            - |
              set -e
              flareo verify public.ecr.aws/flareo/vaultwarden@sha256:$DIGEST
          env:
            - name: DIGEST
              valueFrom:
                configMapKeyRef:
                  name: deploy-digests
                  key: vaultwarden

Argo CD runs this Job before sync, waits for it to complete, and aborts the sync if the Job fails. The actual deployment manifests only apply after verification passes.

For cluster-wide enforcement rather than per-app, use an admission controller — see Admission policies.

Pattern 4: pre-commit hook for compose files

Less "CI/CD" more "keep CI/CD from ever needing to reject", but worth mentioning. Drop a pre-commit hook in .git/hooks/pre-commit:

#!/usr/bin/env sh
set -e
# Find all flareo-hosted image refs in docker-compose files and verify each
grep -hoE 'public\.ecr\.aws/flareo/[a-z0-9-]+@sha256:[a-f0-9]+' docker-compose*.yaml | sort -u | while read -r img; do
  echo "Verifying $img"
  flareo verify "$img"
done

Now a compose-file change that pins to an unverified digest fails at commit time, before the CI pipeline ever runs.

Failure modes worth knowing

Three things can make flareo verify fail:

  • Signature missing or invalid — the image either wasn't signed by our pipeline, or the signature on Sigstore is gone (e.g. the image was tampered with after publication). Never deploy. Investigate.
  • Scan outdated or failing — the image's CVE scan is stale (check lastRebuiltAt on the module page; "stale" is whatever your risk tolerance allows) or the most recent scan flagged critical/high CVEs. Decide based on risk tolerance; the CLI surfaces the CVE list so you can look at them.
  • Network error — flareo verify couldn't reach Sigstore or the Flareo API. Retry once; if it happens twice, something's wrong with your CI's outbound connectivity.

We intentionally don't let "image not recognized" fail silently or pass. An unknown image reference exits non-zero with an explanatory error.

Why this matters

A CI/CD pipeline that doesn't verify is trusting:

  • That the image registry wasn't compromised between your last pull and this deploy
  • That no one swapped the image behind a mutable tag
  • That the image you tested in staging is byte-identical to what prod pulls
  • That the scanner you ran three weeks ago still reflects current CVE knowledge

Digest pinning addresses the first three. Flareo signatures plus scan freshness address the fourth. Wiring flareo verify into the deploy is roughly 10 lines of YAML in exchange for eliminating a whole class of silent-tampering risk.

Next steps