Don't deploy what the pipeline didn't build.

Oath stops an image from reaching production unless your own pipeline built it. Every CI build records commit → digest → time in Solana, as a standard Memo transaction signed by the CI key. Before deploy, oath verify checks the image digest against those records and blocks the deploy if there is none.

Integrate in 3 steps
  • Standard Memo program, no custom smart contract
  • CLI and Kubernetes admission webhook
  • devnet by default, mainnet supported

How it works

  1. 01git commitsource revision
  2. 02CI buildimage digest
  3. 03Solana Memo txsigned by the CI key
  4. 04verify gateoath verify · admission webhook

exit 0 verified, the deploy proceeds

exit 1 / denied no record, the deploy is stopped

Record format

One Memo per build, fields separated by a vertical bar:

pod1|ghcr.io/me/app|3f9a1c0d2b7e|sha256:<64 hex>|2026-10-12T21:04:11Z

version | image name (the record is bound to it) | first 12 characters of the commit | image digest | ISO time

Live verifier

Runs in your browser: it queries the Solana RPC directly with the same logic as the CLI.

One-click demo
terminal output idle

Integrate

Three steps: create a CI key, anchor after the build, verify before the deploy.

Add the secret SOLANA_SECRET_KEY and the variable OATH_SIGNER (the public key) to the repository. For devnet SOL use faucet.solana.com.

# once: create the CI key (writes .oath/ci-key.json, funds it, prints what to add)
npx --yes --allow-git=all github:Tymofii-thc/oath init

# in your workflow, after the build: anchor the digest
- uses: Tymofii-thc/oath@main
  with:
    image: ghcr.io/${{ github.repository }}
    digest: ${{ steps.build.outputs.digest }}
    secret-key: ${{ secrets.SOLANA_SECRET_KEY }}

# before the deploy: pulls the image and fails the job if there is no record
- uses: Tymofii-thc/oath/verify@main
  with:
    signer: ${{ vars.OATH_SIGNER }}
    image: ghcr.io/${{ github.repository }}:latest

Signer records

Pipeline records anchored by the signer above, newest first.

Time Commit Image Digest Transaction

Threat model

Scenario Result Why
Image swapped in the registry under the same tagBlockedThe pulled digest has no record from a trusted signer.
Manual docker push or kubectl apply, bypassing CIBlockedoath verify exits with 1; the admission webhook denies the Pod.
Registry token leaked, CI signing key safeBlockedThe attacker can push an image but cannot anchor its digest.
Record anchored for a different image nameBlockedRecords are bound to the image name (disable with --no-bind).
Old but legitimate digest replayedLimitedUse --max-age to reject old records; without it the digest passes.
CI key compromised, then revokedMitigatedRemove the key from the signer list: its records stop counting at once. Records anchored before you noticed are not detected.
Registry, RPC or webhook unreachableFail closedNew Pods are not admitted, running Pods are untouched. The CLI exits with 2.

Not covered

  • Stolen CI signing key. Whoever holds SOLANA_SECRET_KEY can anchor a forged digest. Rotation, revocation, --max-age and image binding limit the damage. Hardware keys and keyless signing are planned, not implemented.
  • Malicious code built honestly. The chain proves that the signer recorded a digest at a given time, not that the build was honest or safe. That needs SLSA build attestations. The optional Trivy scan only checks fixable CRITICAL vulnerabilities.
  • Mainnet in CI. Mainnet is supported, but CI has not been run on live mainnet yet. devnet is the default and has no retention guarantees.
  • Availability and existing Pods. Registry or RPC outages block new Pods. Pods created before the webhook was installed are not checked; use oath verify --container for them.