From 4309135d1991fb8f957dea753013bb9713ecb78b Mon Sep 17 00:00:00 2001 From: Denis Policastro Date: Fri, 29 May 2026 10:20:08 -0300 Subject: [PATCH 1/4] docs(evidence): add JFrog Evidence image signing design (ANG-2396) Co-Authored-By: Claude Opus 4.8 (1M context) --- ...-29-jfrog-evidence-image-signing-design.md | 238 ++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-29-jfrog-evidence-image-signing-design.md diff --git a/docs/superpowers/specs/2026-05-29-jfrog-evidence-image-signing-design.md b/docs/superpowers/specs/2026-05-29-jfrog-evidence-image-signing-design.md new file mode 100644 index 0000000..1a16bda --- /dev/null +++ b/docs/superpowers/specs/2026-05-29-jfrog-evidence-image-signing-design.md @@ -0,0 +1,238 @@ +# JFrog Evidence Image Signing — Design (ANG-2396) + +## Summary + +Sign every Docker image produced by CI with JFrog Evidence using a managed +ECDSA key pair, so downstream Kubernetes admission control (ANG-2397, Kyverno) +can verify image provenance before allowing a workload to start. + +Signing is implemented with explicit `jf evd create` and a managed key — **not** +the keyless `actions/attest-build-provenance` path. The keyless path is avoided +because (a) it depends on RFC3161 timestamping by a TSA that may be absent from +the public Sigstore TUF root for private repos, currently mishandled by +Kyverno's newer `ImageValidatingPolicy` API (kyverno/kyverno#16054), and (b) +using GitHub's private Sigstore instance with Kyverno requires a cluster-wide +TUF override that constrains verification of other signing sources +(kyverno/kyverno#11618). A managed key gives a self-contained chain of trust +entirely within JFrog and a stable public key to wire into Kyverno. + +**Scope:** Docker images only. Language packages, generic artifacts, and release +bundles are explicitly out of scope for this iteration. + +## Motivation + +- ANG-2397 needs a verifiable, digest-bound provenance signal on every image. +- A managed key pair keeps the trust chain inside JFrog + GitHub, avoiding the + Sigstore/TUF coupling that breaks Kyverno verification for private repos. + +## Architecture + +All signing logic lives in the **reusable workflow** +`NethermindEth/github-workflows/.github/workflows/docker-build-push-jfrog.yaml`. +New steps run **after** the existing `Build and push` step, gated on +`inputs.push == true && inputs.sign_image == true`. Every repo consuming the +reusable workflow gets signing for free. + +`angkor-platform-aws-sts` (pushing to `angkor-oci-local`) is the pilot. + +### Why the reusable workflow, not the caller + +Signing must apply to *every* image produced by CI. Centralising it in the +reusable workflow means one implementation, one key convention, and no +copy-paste per repo. Consumers opt out with `sign_image: false`. + +### Why not modify the build action + +`NethermindEth/github-action-image-build-and-push` exposes **no outputs** (no +digest, no tags). Rather than fork that action, the reusable workflow resolves +the digest itself post-push via `docker buildx imagetools inspect`. Self-contained +and avoids a cross-repo dependency. + +## Evidence subject: how the digest pin works + +The ticket's acceptance criteria mandate the **package form** +(`--package-name` / `--package-version` / `--package-repo-name`). JFrog's docs +confirm this form attaches evidence to the **resolved manifest**, not to the +mutable tag reference. Therefore, if `--package-version` is an **immutable, +unique-per-commit tag** (`${{ github.sha }}`), the evidence is bound to that +exact manifest — effectively digest-pinned. + +To guarantee the tag exists and is unique, the workflow ensures the full +`${{ github.sha }}` is in the pushed tag set. The real sha256 manifest digest is +additionally resolved and recorded (in the predicate + job summary) so ANG-2397 +has the exact digest, even though the `jf evd create` subject is the SHA tag. + +A harder pin (`--subject-repo-path ///manifest.json +--subject-sha256 `) is available but deliberately not used in this +iteration: it diverges from the acceptance-criteria command and the package form +already binds to the resolved manifest. + +## New reusable-workflow inputs + +| Input | Type | Default | Description | +|---|---|---|---| +| `sign_image` | boolean | `true` | Opt-out toggle for Evidence signing | +| `signing_key_alias` | string | `jfrog-evidence-image-signing` | JFrog key alias used at create + verify time | +| `predicate_type` | string | `https://jfrog.com/evidence/signature/v1` | Custom predicate-type URI | + +## New reusable-workflow secrets + +| Secret | Required when signing | Description | +|---|---|---| +| `infisical_identity_id` | yes | Infisical machine identity ID (OIDC) for the signing-key project | +| `infisical_project_id` | yes | Infisical project ID holding the private signing key | + +The private key is stored at a **shared, access-restricted** Infisical path +`/github/workflows_critical/jfrog-evidence` (key name `IMAGE_SIGNING_PRIVATE_KEY`), +not the per-repo `/github/workflows/` path. The `workflows_critical/**` +prefix is denied to global devops/developer roles and scoped to specific groups, +which suits a signing key. Fetched via a `secret-path` override on +`get_infisical_secrets`. + +## Step sequence (added after `Build and push`) + +All steps gated on `inputs.push == true && inputs.sign_image == true`. + +1. **Ensure SHA tag pushed** — the full `${{ github.sha }}` is passed in the + build action's tag set so a deterministic, unique, immutable tag exists to + sign and to inspect. + +2. **Resolve image digest** + ```bash + IMAGE_DIGEST=$(docker buildx imagetools inspect \ + "${JFROG_URL}/${IMAGE_NAME}:${GITHUB_SHA}" \ + --format '{{json .Manifest.Digest}}' | jq -r .) + ``` + Returns the OCI index digest for multi-arch images, the image manifest digest + for single-arch. This is the immutable reference Kyverno resolves to. + +3. **Fetch private key** — `NethermindEth/github-workflows/get_infisical_secrets` + with a `secret-path` override pointing at the shared signing-key path. Exports + the PEM as a masked env var (`add-mask` line-by-line per the existing pattern). + +4. **Generate predicate** — injection-safe (`env:` block + `jq`, never inline + `${{ }}` in `run:`), writes `predicate.json`: + ```json + { + "actor": "", + "workflow": "", + "run_id": "", + "commit": "", + "repository": "", + "ref": "", + "timestamp": "", + "image_digest": "sha256:" + } + ``` + `image_digest` is added beyond the ticket's shape so ANG-2397 gets the exact + digest. Predicate contents are not policy-relevant yet (Kyverno verifies only + that signed Evidence exists); switching to SLSA Provenance later is cheap. + +5. **Sign** + ```bash + jf evd create \ + --package-name "${IMAGE_NAME}" \ + --package-version "${GITHUB_SHA}" \ + --package-repo-name "${REPO_NAME}" \ + --predicate ./predicate.json \ + --predicate-type "${PREDICATE_TYPE}" \ + --key "${PRIVATE_KEY}" \ + --key-alias "${SIGNING_KEY_ALIAS}" + ``` + JFrog auth is inherited from the existing `setup-jfrog-cli` OIDC step + (evidence create requires access-token/OIDC auth — basic auth is rejected). + +6. **Job summary** — write image, SHA tag, resolved digest, predicate-type, and + key alias to `$GITHUB_STEP_SUMMARY` for traceability. + +## Key management + +### Generation (once, ECDSA P-256) + +```bash +openssl ecparam -name prime256v1 -genkey -noout -out private.pem +openssl ec -in private.pem -pubout -out public.pem +``` + +ECDSA P-256 is the cosign/Kyverno standard and compact. RSA is acceptable but +larger; ed25519 is supported by `jf evd` but less universally consumed. + +### Distribution + +- **Private key** → Infisical `/github/workflows_critical/jfrog-evidence` + (`IMAGE_SIGNING_PRIVATE_KEY`), CI-accessible via OIDC machine identity. Never + committed. +- **Public key** → committed to + `github-workflows/keys/jfrog-evidence-image-signing.pub`. Stable, versioned URL + for Kyverno / ArgoCD to consume. + +### Key-alias convention + +`jfrog-evidence-image-signing` — matches the public-key filename and the +`--key-alias` passed to both `jf evd create` and `jf evd verify`. + +### Rotation procedure + +1. Generate a new ECDSA P-256 pair (commands above). +2. Replace the private key value in the Infisical shared path. +3. Commit the new public key to + `github-workflows/keys/jfrog-evidence-image-signing.pub` (optionally a + date-suffixed name during overlap). +4. Update the downstream Kyverno policy (ANG-2397) with the new public key. +5. Keep the old public key available until all previously-signed images have + aged out of admission-controlled clusters, then remove it. + +## Verification (acceptance) + +Offline, with the matching public key: + +```bash +jf evd verify \ + --package-name "${IMAGE_NAME}" \ + --package-version "${GITHUB_SHA}" \ + --package-repo-name "${REPO_NAME}" \ + --public-keys ./public.pem +``` + +No JFrog auth required for offline verify with `--public-keys` (purely +client-side). + +## Prerequisites (ops, not automated by this change) + +- Artifactory instance has the **Evidence** feature enabled (subscription gated). +- JFrog CLI ≥ 2.65.0 for `jf evd` (setup-jfrog-cli installs latest — satisfied). +- ECDSA key pair generated; private key uploaded to Infisical; public key + committed. +- Infisical machine identity + project for the signing key, bound to the calling + repos via OIDC. + +## Files touched + +- `github-workflows/.github/workflows/docker-build-push-jfrog.yaml` — new inputs, + secrets, and signing steps. +- `github-workflows/keys/jfrog-evidence-image-signing.pub` — public key. +- `github-workflows/docs/docker/` — signing usage + key rotation docs. +- `angkor-platform-aws-sts/.github/workflows/docker-build-push.yaml` — bump + reusable-workflow version ref once released; pass Infisical secrets if not + org-level. + +## Acceptance criteria mapping + +| Criterion | Satisfied by | +|---|---| +| ECDSA/RSA key pair generated; private in CI secret store; public consumable by Kyverno | ECDSA P-256; private → Infisical; public → committed `.pub` | +| Predicate JSON generated at runtime with CI provenance fields + custom predicate-type URI | Step 4 | +| Signs every image immediately after push via `jf evd create` package form | Steps 5, gated on `push && sign_image` | +| Evidence appears on Docker package in Artifactory Evidence tab | Step 5 result (manual verification at pilot) | +| Out-of-cluster `jf evd verify` with public key succeeds | Verification section | +| Predicate shape, predicate-type URI, key-alias convention documented | This doc | +| Key rotation procedure documented | Rotation section | +| At least one pilot image signed + verified end-to-end | `angkor-platform-aws-sts` pilot | +| Only Docker images signed this iteration | Scope section | + +## Out of scope + +- Non-Docker artifact types (packages, generic artifacts, release bundles). +- Kyverno admission policy (ANG-2397). +- Infisical / JFrog / key-pair infrastructure provisioning (documented as + prerequisites). From 5f000ec8e51e4cd2232606b32978fc5bb82a9304 Mon Sep 17 00:00:00 2001 From: Denis Policastro Date: Fri, 29 May 2026 11:47:23 -0300 Subject: [PATCH 2/4] feat(evidence): sign pushed Docker images with JFrog Evidence (ANG-2396) Add opt-out JFrog Evidence signing to the reusable jfrog build workflow: - new inputs sign_image, signing_key_alias, predicate_type - new secrets infisical_identity_id, infisical_project_id - push full commit SHA as an immutable tag (signed package-version) - resolve manifest digest, fetch key from Infisical, generate provenance predicate, run jf evd create, emit job summary Co-Authored-By: Claude Opus 4.8 (1M context) --- .../workflows/docker-build-push-jfrog.yaml | 123 +++++++++++++++++- 1 file changed, 122 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker-build-push-jfrog.yaml b/.github/workflows/docker-build-push-jfrog.yaml index b1aacae..39f4fb2 100644 --- a/.github/workflows/docker-build-push-jfrog.yaml +++ b/.github/workflows/docker-build-push-jfrog.yaml @@ -111,10 +111,31 @@ on: required: false type: boolean default: true + sign_image: + description: "Sign the pushed image with JFrog Evidence" + required: false + type: boolean + default: true + signing_key_alias: + description: "JFrog key alias for the Evidence signing key" + required: false + type: string + default: "jfrog-evidence-image-signing" + predicate_type: + description: "Predicate type URI for the signature Evidence" + required: false + type: string + default: "https://jfrog.com/evidence/signature/v1" secrets: git_token: description: "Git token to use for checkout" required: false + infisical_identity_id: + description: "Infisical machine identity ID for the image-signing key (required when sign_image is true)" + required: false + infisical_project_id: + description: "Infisical project ID holding the image-signing private key (required when sign_image is true)" + required: false permissions: id-token: write @@ -189,7 +210,9 @@ jobs: with: registry: "artifactory" image_name: ${{ steps.env-vars.outputs.IMAGE_NAME }} - image_tags: ${{ inputs.additional_tags }} + image_tags: | + ${{ github.sha }} + ${{ inputs.additional_tags }} push-to-registry: ${{ inputs.push }} platforms: ${{ inputs.platforms }} context: ${{ inputs.context }} @@ -202,3 +225,101 @@ jobs: build-args: ${{ inputs.docker_build_args }} ignore_trivy: ${{ inputs.ignore_trivy }} run_trivy: ${{ inputs.run_trivy }} + + - name: Resolve image digest + id: digest + if: ${{ inputs.push && inputs.sign_image }} + env: + IMAGE_REF: "${{ inputs.jfrog_url }}/${{ steps.env-vars.outputs.IMAGE_NAME }}:${{ github.sha }}" + run: | + set -euo pipefail + DIGEST="$(docker buildx imagetools inspect "$IMAGE_REF" --format '{{json .Manifest.Digest}}' | jq -r .)" + if [[ -z "$DIGEST" || "$DIGEST" != sha256:* ]]; then + echo "Failed to resolve a sha256 digest for $IMAGE_REF (got: '$DIGEST')." >&2 + exit 1 + fi + echo "IMAGE_DIGEST=$DIGEST" >> "$GITHUB_OUTPUT" + echo "Resolved $IMAGE_REF -> $DIGEST" + + - name: Fetch signing key from Infisical + if: ${{ inputs.push && inputs.sign_image }} + uses: NethermindEth/github-workflows/get_infisical_secrets@main + with: + identity-id: ${{ secrets.infisical_identity_id }} + project-id: ${{ secrets.infisical_project_id }} + env-slug: prod + secret-path: "/github/workflows_critical/jfrog-evidence" + + - name: Generate signature predicate + if: ${{ inputs.push && inputs.sign_image }} + env: + P_ACTOR: ${{ github.actor }} + P_WORKFLOW: ${{ github.workflow }} + P_RUN_ID: ${{ github.run_id }} + P_COMMIT: ${{ github.sha }} + P_REPOSITORY: ${{ github.repository }} + P_REF: ${{ github.ref }} + P_DIGEST: ${{ steps.digest.outputs.IMAGE_DIGEST }} + run: | + set -euo pipefail + jq -n \ + --arg actor "$P_ACTOR" \ + --arg workflow "$P_WORKFLOW" \ + --arg run_id "$P_RUN_ID" \ + --arg commit "$P_COMMIT" \ + --arg repository "$P_REPOSITORY" \ + --arg ref "$P_REF" \ + --arg image_digest "$P_DIGEST" \ + --arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + '{actor:$actor, workflow:$workflow, run_id:$run_id, commit:$commit, repository:$repository, ref:$ref, timestamp:$timestamp, image_digest:$image_digest}' \ + > predicate.json + cat predicate.json + + - name: Sign image with JFrog Evidence + if: ${{ inputs.push && inputs.sign_image }} + env: + IMAGE_NAME: ${{ steps.env-vars.outputs.IMAGE_NAME }} + PACKAGE_VERSION: ${{ github.sha }} + REPO_NAME_INPUT: ${{ inputs.repo_name }} + GROUP_NAME_INPUT: ${{ inputs.group_name }} + PREDICATE_TYPE: ${{ inputs.predicate_type }} + KEY_ALIAS: ${{ inputs.signing_key_alias }} + PRIVATE_KEY: ${{ env.IMAGE_SIGNING_PRIVATE_KEY }} + run: | + set -euo pipefail + if [[ -n "$REPO_NAME_INPUT" ]]; then + REPO_NAME="$REPO_NAME_INPUT" + else + REPO_NAME="${GROUP_NAME_INPUT}-oci-local-dev" + fi + # IMAGE_NAME is "/"; package-name is just "" + PACKAGE_NAME="${IMAGE_NAME#*/}" + jf evd create \ + --package-name "$PACKAGE_NAME" \ + --package-version "$PACKAGE_VERSION" \ + --package-repo-name "$REPO_NAME" \ + --predicate ./predicate.json \ + --predicate-type "$PREDICATE_TYPE" \ + --key "$PRIVATE_KEY" \ + --key-alias "$KEY_ALIAS" + + - name: Evidence summary + if: ${{ inputs.push && inputs.sign_image }} + env: + IMAGE_NAME: ${{ steps.env-vars.outputs.IMAGE_NAME }} + PACKAGE_VERSION: ${{ github.sha }} + IMAGE_DIGEST: ${{ steps.digest.outputs.IMAGE_DIGEST }} + PREDICATE_TYPE: ${{ inputs.predicate_type }} + KEY_ALIAS: ${{ inputs.signing_key_alias }} + run: | + { + echo "### JFrog Evidence signed" + echo "" + echo "| Field | Value |" + echo "|---|---|" + echo "| Image | \`${IMAGE_NAME}\` |" + echo "| Package version (tag) | \`${PACKAGE_VERSION}\` |" + echo "| Manifest digest | \`${IMAGE_DIGEST}\` |" + echo "| Predicate type | \`${PREDICATE_TYPE}\` |" + echo "| Key alias | \`${KEY_ALIAS}\` |" + } >> "$GITHUB_STEP_SUMMARY" From 7b2aae5c6715cac7f812f2bffd76291bfc24e657 Mon Sep 17 00:00:00 2001 From: Denis Policastro Date: Fri, 29 May 2026 11:48:04 -0300 Subject: [PATCH 3/4] docs(evidence): document image signing, verify, and rotation (ANG-2396) Co-Authored-By: Claude Opus 4.8 (1M context) --- examples/docker/README.md | 65 +++++++++++++++++++ examples/docker/build-push-jfrog-complete.yml | 8 +++ 2 files changed, 73 insertions(+) diff --git a/examples/docker/README.md b/examples/docker/README.md index 0d9452a..5d07988 100644 --- a/examples/docker/README.md +++ b/examples/docker/README.md @@ -53,6 +53,71 @@ Features: **Example:** [`examples/docker/promote-dockerhub.yml`](./promote-dockerhub.yml) +## Image Signing (JFrog Evidence) + +The JFrog build workflow signs every pushed image with JFrog Evidence using a +managed ECDSA P-256 key. Signing is on by default and runs only when `push` is +true. Set `sign_image: false` to opt out. + +### What gets signed + +- Subject: the package version `${{ github.sha }}` (pushed as an immutable tag). + JFrog attaches the Evidence to the resolved image manifest. +- Predicate type: `https://jfrog.com/evidence/signature/v1` (override with + `predicate_type`). +- Key alias: `jfrog-evidence-image-signing` (override with `signing_key_alias`). + +### Predicate shape + +```json +{ + "actor": "", + "workflow": "", + "run_id": "", + "commit": "", + "repository": "", + "ref": "", + "timestamp": "", + "image_digest": "sha256:" +} +``` + +### Required secrets + +| Secret | Description | +|---|---| +| `infisical_identity_id` | Infisical machine identity ID for the signing-key project | +| `infisical_project_id` | Infisical project ID holding `IMAGE_SIGNING_PRIVATE_KEY` | + +The private key is stored at Infisical `/github/workflows_critical/jfrog-evidence` +(secret name `IMAGE_SIGNING_PRIVATE_KEY`) and fetched at runtime. + +### Offline verification + +```bash +jf evd verify \ + --package-name \ + --package-version \ + --package-repo-name \ + --public-keys ./jfrog-evidence-image-signing.pub +``` + +Public key: [`keys/jfrog-evidence-image-signing.pub`](../../keys/jfrog-evidence-image-signing.pub). + +### Key rotation + +1. Generate a new ECDSA P-256 pair: + ```bash + openssl ecparam -name prime256v1 -genkey -noout -out private.pem + openssl ec -in private.pem -pubout -out public.pem + ``` +2. Replace `IMAGE_SIGNING_PRIVATE_KEY` at Infisical + `/github/workflows_critical/jfrog-evidence`. +3. Commit the new public key to `keys/jfrog-evidence-image-signing.pub`. +4. Update the Kyverno policy (ANG-2397) with the new public key. +5. Keep the old public key until all images signed with it have aged out of + admission-controlled clusters, then remove it. + ## Environment Flow - JFrog Artifactory Docker images follow this promotion path: diff --git a/examples/docker/build-push-jfrog-complete.yml b/examples/docker/build-push-jfrog-complete.yml index 75223d5..cdeea5d 100644 --- a/examples/docker/build-push-jfrog-complete.yml +++ b/examples/docker/build-push-jfrog-complete.yml @@ -56,3 +56,11 @@ jobs: docker_build_args: "" ignore_trivy: false run_trivy: true + + # Image signing (JFrog Evidence) — on by default + sign_image: true + signing_key_alias: "jfrog-evidence-image-signing" + predicate_type: "https://jfrog.com/evidence/signature/v1" + secrets: + infisical_identity_id: ${{ secrets.INFISICAL_IDENTITY_ID }} + infisical_project_id: ${{ secrets.INFISICAL_PROJECT_ID }} From 478c611a246b21d96663bdfc4b52ace831283e7a Mon Sep 17 00:00:00 2001 From: Denis Policastro Date: Fri, 29 May 2026 18:26:25 -0300 Subject: [PATCH 4/4] feat(evidence): use Angkor Platform shared Infisical key path (ANG-2396) Default signing key fetched from Infisical path /github/workflows/shared/github-workflows, secret DOCKER_IMAGE_SIGNING_PRIVATE_KEY. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/docker-build-push-jfrog.yaml | 4 ++-- ...05-29-jfrog-evidence-image-signing-design.md | 17 ++++++++--------- examples/docker/README.md | 10 +++++----- keys/README.md | 16 ++++++++++++++++ 4 files changed, 31 insertions(+), 16 deletions(-) create mode 100644 keys/README.md diff --git a/.github/workflows/docker-build-push-jfrog.yaml b/.github/workflows/docker-build-push-jfrog.yaml index 39f4fb2..516f13d 100644 --- a/.github/workflows/docker-build-push-jfrog.yaml +++ b/.github/workflows/docker-build-push-jfrog.yaml @@ -248,7 +248,7 @@ jobs: identity-id: ${{ secrets.infisical_identity_id }} project-id: ${{ secrets.infisical_project_id }} env-slug: prod - secret-path: "/github/workflows_critical/jfrog-evidence" + secret-path: "/github/workflows/shared/github-workflows" - name: Generate signature predicate if: ${{ inputs.push && inputs.sign_image }} @@ -284,7 +284,7 @@ jobs: GROUP_NAME_INPUT: ${{ inputs.group_name }} PREDICATE_TYPE: ${{ inputs.predicate_type }} KEY_ALIAS: ${{ inputs.signing_key_alias }} - PRIVATE_KEY: ${{ env.IMAGE_SIGNING_PRIVATE_KEY }} + PRIVATE_KEY: ${{ env.DOCKER_IMAGE_SIGNING_PRIVATE_KEY }} run: | set -euo pipefail if [[ -n "$REPO_NAME_INPUT" ]]; then diff --git a/docs/superpowers/specs/2026-05-29-jfrog-evidence-image-signing-design.md b/docs/superpowers/specs/2026-05-29-jfrog-evidence-image-signing-design.md index 1a16bda..3e1dbde 100644 --- a/docs/superpowers/specs/2026-05-29-jfrog-evidence-image-signing-design.md +++ b/docs/superpowers/specs/2026-05-29-jfrog-evidence-image-signing-design.md @@ -82,12 +82,11 @@ already binds to the resolved manifest. | `infisical_identity_id` | yes | Infisical machine identity ID (OIDC) for the signing-key project | | `infisical_project_id` | yes | Infisical project ID holding the private signing key | -The private key is stored at a **shared, access-restricted** Infisical path -`/github/workflows_critical/jfrog-evidence` (key name `IMAGE_SIGNING_PRIVATE_KEY`), -not the per-repo `/github/workflows/` path. The `workflows_critical/**` -prefix is denied to global devops/developer roles and scoped to specific groups, -which suits a signing key. Fetched via a `secret-path` override on -`get_infisical_secrets`. +The private key is stored at a **shared** Infisical path (Angkor Platform project) +`/github/workflows/shared/github-workflows` (key name +`DOCKER_IMAGE_SIGNING_PRIVATE_KEY`), not the per-repo `/github/workflows/` +path, so a single signing key serves all consumers. Fetched via a `secret-path` +override on `get_infisical_secrets`. ## Step sequence (added after `Build and push`) @@ -159,9 +158,9 @@ larger; ed25519 is supported by `jf evd` but less universally consumed. ### Distribution -- **Private key** → Infisical `/github/workflows_critical/jfrog-evidence` - (`IMAGE_SIGNING_PRIVATE_KEY`), CI-accessible via OIDC machine identity. Never - committed. +- **Private key** → Infisical `/github/workflows/shared/github-workflows` + (`DOCKER_IMAGE_SIGNING_PRIVATE_KEY`, Angkor Platform project), CI-accessible via + OIDC machine identity. Never committed. - **Public key** → committed to `github-workflows/keys/jfrog-evidence-image-signing.pub`. Stable, versioned URL for Kyverno / ArgoCD to consume. diff --git a/examples/docker/README.md b/examples/docker/README.md index 5d07988..a87356e 100644 --- a/examples/docker/README.md +++ b/examples/docker/README.md @@ -87,10 +87,10 @@ true. Set `sign_image: false` to opt out. | Secret | Description | |---|---| | `infisical_identity_id` | Infisical machine identity ID for the signing-key project | -| `infisical_project_id` | Infisical project ID holding `IMAGE_SIGNING_PRIVATE_KEY` | +| `infisical_project_id` | Infisical project ID (Angkor Platform) holding `DOCKER_IMAGE_SIGNING_PRIVATE_KEY` | -The private key is stored at Infisical `/github/workflows_critical/jfrog-evidence` -(secret name `IMAGE_SIGNING_PRIVATE_KEY`) and fetched at runtime. +The private key is stored at Infisical `/github/workflows/shared/github-workflows` +(secret name `DOCKER_IMAGE_SIGNING_PRIVATE_KEY`) and fetched at runtime. ### Offline verification @@ -111,8 +111,8 @@ Public key: [`keys/jfrog-evidence-image-signing.pub`](../../keys/jfrog-evidence- openssl ecparam -name prime256v1 -genkey -noout -out private.pem openssl ec -in private.pem -pubout -out public.pem ``` -2. Replace `IMAGE_SIGNING_PRIVATE_KEY` at Infisical - `/github/workflows_critical/jfrog-evidence`. +2. Replace `DOCKER_IMAGE_SIGNING_PRIVATE_KEY` at Infisical + `/github/workflows/shared/github-workflows` (Angkor Platform project). 3. Commit the new public key to `keys/jfrog-evidence-image-signing.pub`. 4. Update the Kyverno policy (ANG-2397) with the new public key. 5. Keep the old public key until all images signed with it have aged out of diff --git a/keys/README.md b/keys/README.md new file mode 100644 index 0000000..38cdefe --- /dev/null +++ b/keys/README.md @@ -0,0 +1,16 @@ +# Signing Keys + +## jfrog-evidence-image-signing.pub + +ECDSA P-256 public key for verifying JFrog Evidence on Docker images signed by +`.github/workflows/docker-build-push-jfrog.yaml`. + +- **Key alias (JFrog `--key-alias`):** `jfrog-evidence-image-signing` +- **Private key location:** Infisical project Angkor Platform, path + `/github/workflows/shared/github-workflows` (secret name + `DOCKER_IMAGE_SIGNING_PRIVATE_KEY`). Never committed. +- **Consumed by:** Kyverno admission policy (ANG-2397) and offline + `jf evd verify --public-keys`. + +Rotation: see [`examples/docker/README.md`](../examples/docker/README.md) → +"Image Signing (JFrog Evidence)" → "Key rotation".