-
Notifications
You must be signed in to change notification settings - Fork 1
feat: implement evidence-to-admissibility enforcement pipeline #22299
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| name: Admissibility Gate | ||
|
|
||
| on: | ||
| pull_request: | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
| id-token: write | ||
| attestations: write | ||
|
|
||
| jobs: | ||
| evidence-admissibility: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Setup Node | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: 20 | ||
|
|
||
| - name: Enable Corepack | ||
| run: corepack enable | ||
|
|
||
| - name: Install dependencies | ||
| run: pnpm install --frozen-lockfile | ||
|
|
||
| - name: Install Syft | ||
| uses: anchore/sbom-action/download-syft@v0.17.0 | ||
|
|
||
| - name: Install Cosign | ||
| uses: sigstore/cosign-installer@v3.8.1 | ||
|
|
||
| - name: Build deterministic artifact payload | ||
| run: | | ||
| mkdir -p dist | ||
| sha256sum package.json pnpm-lock.yaml | awk '{print $1}' | sort > dist/admissible-artifact.txt | ||
|
|
||
| - name: Generate SBOM (CycloneDX) | ||
| run: syft . -o cyclonedx-json=evidence/sbom.cdx.json | ||
|
|
||
| - name: Assert SBOM completeness | ||
| run: | | ||
| test -f evidence/sbom.cdx.json | ||
| jq -e '.components and (.components | length > 0)' evidence/sbom.cdx.json | ||
|
|
||
| - name: Generate provenance attestation (SLSA) | ||
| uses: actions/attest-build-provenance@v3 | ||
| with: | ||
| subject-path: dist/admissible-artifact.txt | ||
|
|
||
| - name: Materialize deterministic provenance snapshot | ||
| run: | | ||
| DIGEST="$(sha256sum dist/admissible-artifact.txt | awk '{print $1}')" | ||
| jq -n \ | ||
| --arg digest "sha256:${DIGEST}" \ | ||
| --arg repo "${{ github.repository }}" \ | ||
| '{ | ||
| _type: "https://in-toto.io/Statement/v1", | ||
| predicateType: "https://slsa.dev/provenance/v1", | ||
| subject: [{name: "dist/admissible-artifact.txt", digest: {sha256: ($digest | sub("^sha256:"; ""))}}], | ||
| builder: {id: "https://github.com/actions/runner"}, | ||
| invocation: {configSource: {uri: $repo}} | ||
| }' > evidence/provenance.json | ||
|
|
||
| - name: Sign and verify artifact signature | ||
| run: | | ||
| cosign generate-key-pair | ||
| cosign sign-blob --yes --key cosign.key --output-signature evidence/artifact.sig dist/admissible-artifact.txt | ||
| cosign verify-blob --key cosign.pub --signature evidence/artifact.sig dist/admissible-artifact.txt | ||
|
|
||
| - name: Build evidence report/metrics/stamp | ||
| env: | ||
| ARTIFACT_PATH: dist/admissible-artifact.txt | ||
| SBOM_PATH: evidence/sbom.cdx.json | ||
| PROVENANCE_PATH: evidence/provenance.json | ||
| SIGNATURE_VERIFIED: "true" | ||
| run: node scripts/ci/build_admissibility_evidence.mjs | ||
|
|
||
| - name: Evaluate admissibility gate | ||
| run: pnpm verify:admissibility --input evidence/report.json | ||
|
|
||
| - name: Upload evidence artifacts | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: admissibility-evidence-${{ github.run_id }} | ||
| path: | | ||
| evidence/report.json | ||
| evidence/metrics.json | ||
| evidence/stamp.json | ||
| evidence/sbom.cdx.json | ||
| evidence/provenance.json | ||
| evidence/artifact.sig | ||
| cosign.pub | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,33 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| apiVersion: v1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| kind: ConfigMap | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| metadata: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: admissibility-status | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| namespace: argocd | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| status: "PASS" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1
to
+7
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Including the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The PreSync job reads Useful? React with 👍 / 👎.
Comment on lines
+6
to
+7
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hardcoded "PASS" in ConfigMap defeats the PreSync gate purpose. Similar to the deployment label issue, the ConfigMap's 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| --- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| apiVersion: batch/v1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| kind: Job | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| metadata: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: admissibility-presync-check | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| namespace: argocd | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| annotations: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| argocd.argoproj.io/hook: PreSync | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| argocd.argoproj.io/hook-delete-policy: HookSucceeded | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| spec: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| template: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| spec: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| restartPolicy: Never | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+19
to
+20
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Job does not specify a spec:
serviceAccountName: admissibility-checker-sa
restartPolicy: Never |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| containers: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - name: gate | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| image: bitnami/kubectl:1.30 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| command: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - /bin/sh | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - -ec | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - | | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| STATUS="$(kubectl -n argocd get configmap admissibility-status -o jsonpath='{.data.status}')" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if [ "$STATUS" != "PASS" ]; then | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| echo "ArgoCD sync blocked: admissibility status is $STATUS" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| exit 1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| echo "ArgoCD sync permitted: admissibility status is PASS" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+19
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing ServiceAccount and RBAC for kubectl access. The Job uses 🛠️ Proposed fix template:
spec:
restartPolicy: Never
+ serviceAccountName: argocd-application-controller # or create a dedicated SA with ConfigMap read permissions
containers:
- name: gateAlternatively, create a dedicated ServiceAccount and RoleBinding: apiVersion: v1
kind: ServiceAccount
metadata:
name: admissibility-checker
namespace: argocd
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: admissibility-configmap-reader
namespace: argocd
rules:
- apiGroups: [""]
resources: ["configmaps"]
resourceNames: ["admissibility-status"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: admissibility-checker-binding
namespace: argocd
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: admissibility-configmap-reader
subjects:
- kind: ServiceAccount
name: admissibility-checker
namespace: argocd📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,59 @@ | ||||||||||||||
| apiVersion: kyverno.io/v1 | ||||||||||||||
| kind: ClusterPolicy | ||||||||||||||
| metadata: | ||||||||||||||
| name: enforce-admissibility-supply-chain | ||||||||||||||
| annotations: | ||||||||||||||
| policies.kyverno.io/title: Enforce Admissibility for Deployments | ||||||||||||||
| policies.kyverno.io/category: Supply Chain Security | ||||||||||||||
| policies.kyverno.io/severity: high | ||||||||||||||
| spec: | ||||||||||||||
| validationFailureAction: Enforce | ||||||||||||||
| background: true | ||||||||||||||
| rules: | ||||||||||||||
| - name: require-admissibility-pass-label | ||||||||||||||
| match: | ||||||||||||||
| any: | ||||||||||||||
| - resources: | ||||||||||||||
| kinds: | ||||||||||||||
| - Pod | ||||||||||||||
|
Comment on lines
+17
to
+18
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This rule currently matches all Useful? React with 👍 / 👎. |
||||||||||||||
| validate: | ||||||||||||||
| message: "security.summit.io/admissibility label must be PASS" | ||||||||||||||
| pattern: | ||||||||||||||
| metadata: | ||||||||||||||
| labels: | ||||||||||||||
| security.summit.io/admissibility: "PASS" | ||||||||||||||
|
|
||||||||||||||
| - name: require-signed-images | ||||||||||||||
| match: | ||||||||||||||
| any: | ||||||||||||||
| - resources: | ||||||||||||||
| kinds: | ||||||||||||||
| - Pod | ||||||||||||||
| verifyImages: | ||||||||||||||
| - imageReferences: | ||||||||||||||
| - "*" | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using - "ghcr.io/your-org/*"
Comment on lines
+33
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Check if there are other third-party images in k8s manifests that would be affected
rg -n 'image:' k8s/ --type yaml | grep -v 'summit/' | head -20Repository: BrianCLong/summit Length of output: 1551 🏁 Script executed: cat -n k8s/policies/kyverno-admissibility-enforcement.yaml | head -60Repository: BrianCLong/summit Length of output: 2071
The wildcard in both rules (lines 33-34 and 51-52) applies to every container image in the cluster. With
Third-party images in your manifests—alpine, node:18-alpine, bitnami/kubectl, neo4j:5, amazon/aws-cli:2, trufflesecurity/trufflehog, aquasec/kube-bench, and others—do not carry these attestations and will be rejected. To fix:
🤖 Prompt for AI Agents |
||||||||||||||
| mutateDigest: true | ||||||||||||||
| required: true | ||||||||||||||
| attestors: | ||||||||||||||
| - count: 1 | ||||||||||||||
| entries: | ||||||||||||||
| - keyless: | ||||||||||||||
| issuer: https://token.actions.githubusercontent.com | ||||||||||||||
| subject: https://github.com/*/* | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The keyless attestation subject is overly permissive. Setting it to subject: https://github.com/your-org/*
Comment on lines
+40
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Overly permissive keyless subject pattern. The subject - keyless:
issuer: https://token.actions.githubusercontent.com
- subject: https://github.com/*/*
+ subject: https://github.com/BrianCLong/summit/*📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
|
|
||||||||||||||
| - name: require-slsa-attestation | ||||||||||||||
| match: | ||||||||||||||
| any: | ||||||||||||||
| - resources: | ||||||||||||||
| kinds: | ||||||||||||||
| - Pod | ||||||||||||||
| verifyImages: | ||||||||||||||
| - imageReferences: | ||||||||||||||
| - "*" | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||||||||
| attestations: | ||||||||||||||
| - type: slsaprovenance | ||||||||||||||
| conditions: | ||||||||||||||
| all: | ||||||||||||||
| - key: "{{ payload.predicateType }}" | ||||||||||||||
| operator: Equals | ||||||||||||||
| value: https://slsa.dev/provenance/v1 | ||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,9 +10,10 @@ spec: | |
| metadata: | ||
| labels: | ||
| app: summit-server | ||
| security.summit.io/admissibility: "PASS" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hardcoding the admissibility label to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hardcoded "PASS" label undermines the admissibility enforcement model. The For the enforcement to be meaningful, this label should be injected dynamically by CI/CD based on the actual Consider using a Kustomize patch or Helm values injection in the deployment pipeline that sets this label only when CI verification passes. 🤖 Prompt for AI Agents |
||
| spec: | ||
| containers: | ||
| - name: summit-server | ||
| image: summit/server | ||
| ports: | ||
| - containerPort: 4000 | ||
| - name: summit-server | ||
| image: summit/server | ||
| ports: | ||
| - containerPort: 4000 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| export type AdmissibilityStatus = "PASS" | "FAIL"; | ||
|
|
||
| export interface SignatureEvidence { | ||
| valid: boolean; | ||
| keyId?: string; | ||
| } | ||
|
|
||
| export interface SbomEvidence { | ||
| present: boolean; | ||
| complete: boolean; | ||
| format?: string; | ||
| components?: string[]; | ||
| } | ||
|
|
||
| export interface ProvenanceEvidence { | ||
| present: boolean; | ||
| chainIntact: boolean; | ||
| builderId?: string; | ||
| subjects?: string[]; | ||
| } | ||
|
|
||
| export interface DependencyEvidence { | ||
| prohibitedFound: string[]; | ||
| } | ||
|
|
||
| export interface AdmissibilityEvidenceBundle { | ||
| artifact: { | ||
| digest: string; | ||
| name?: string; | ||
| }; | ||
| signature: SignatureEvidence; | ||
| sbom: SbomEvidence; | ||
| provenance: ProvenanceEvidence; | ||
| dependencies: DependencyEvidence; | ||
| } | ||
|
|
||
| export interface AdmissibilityVerdict { | ||
| status: AdmissibilityStatus; | ||
| reasons: string[]; | ||
| checks: { | ||
| signatureValid: boolean; | ||
| sbomComplete: boolean; | ||
| provenanceIntact: boolean; | ||
| prohibitedDependencies: boolean; | ||
| }; | ||
| artifact: { | ||
| digest: string; | ||
| name?: string; | ||
| }; | ||
| } | ||
|
|
||
| const sortUnique = (values: string[]): string[] => | ||
| Array.from(new Set(values)).sort((a, b) => a.localeCompare(b)); | ||
|
|
||
| export function evaluateAdmissibility( | ||
| evidenceBundle: AdmissibilityEvidenceBundle | ||
| ): AdmissibilityVerdict { | ||
| const reasons: string[] = []; | ||
|
|
||
| const signatureValid = evidenceBundle.signature.valid; | ||
| if (!signatureValid) { | ||
| reasons.push("INVALID_SIGNATURE"); | ||
| } | ||
|
|
||
| const sbomComplete = evidenceBundle.sbom.present && evidenceBundle.sbom.complete; | ||
| if (!sbomComplete) { | ||
| reasons.push("MISSING_OR_INCOMPLETE_SBOM"); | ||
| } | ||
|
|
||
| const provenanceIntact = | ||
| evidenceBundle.provenance.present && evidenceBundle.provenance.chainIntact; | ||
| if (!provenanceIntact) { | ||
| reasons.push("BROKEN_PROVENANCE_CHAIN"); | ||
| } | ||
|
|
||
| const prohibitedDependencies = evidenceBundle.dependencies.prohibitedFound.length === 0; | ||
| if (!prohibitedDependencies) { | ||
| const blocked = sortUnique(evidenceBundle.dependencies.prohibitedFound).join(","); | ||
| reasons.push(`PROHIBITED_DEPENDENCIES:${blocked}`); | ||
| } | ||
|
|
||
| return { | ||
| status: reasons.length === 0 ? "PASS" : "FAIL", | ||
| reasons, | ||
| checks: { | ||
| signatureValid, | ||
| sbomComplete, | ||
| provenanceIntact, | ||
| prohibitedDependencies, | ||
| }, | ||
| artifact: { | ||
| digest: evidenceBundle.artifact.digest, | ||
| name: evidenceBundle.artifact.name, | ||
| }, | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| import { createHash } from 'node:crypto'; | ||
| import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; | ||
|
|
||
| const artifactPath = process.env.ARTIFACT_PATH ?? 'dist/admissible-artifact.txt'; | ||
| const sbomPath = process.env.SBOM_PATH ?? 'evidence/sbom.cdx.json'; | ||
| const provenancePath = process.env.PROVENANCE_PATH ?? 'evidence/provenance.json'; | ||
| const signatureVerified = process.env.SIGNATURE_VERIFIED === 'true'; | ||
| const prohibitedDeps = (process.env.PROHIBITED_DEPS ?? '') | ||
| .split(',') | ||
| .map((item) => item.trim()) | ||
| .filter(Boolean) | ||
| .sort((a, b) => a.localeCompare(b)); | ||
|
|
||
| const artifactDigest = createHash('sha256') | ||
| .update(readFileSync(artifactPath)) | ||
| .digest('hex'); | ||
|
|
||
| const sbom = JSON.parse(readFileSync(sbomPath, 'utf8')); | ||
| const provenance = JSON.parse(readFileSync(provenancePath, 'utf8')); | ||
|
|
||
| const components = (sbom.components ?? []) | ||
| .map((c) => c?.name) | ||
| .filter(Boolean) | ||
| .sort((a, b) => a.localeCompare(b)); | ||
|
|
||
| const report = { | ||
| artifact: { | ||
| name: artifactPath, | ||
| digest: `sha256:${artifactDigest}`, | ||
| }, | ||
| signature: { | ||
| valid: signatureVerified, | ||
| keyId: 'cosign.pub', | ||
| }, | ||
| sbom: { | ||
| present: Boolean(sbom.bomFormat), | ||
| complete: components.length > 0, | ||
| format: sbom.bomFormat ?? 'unknown', | ||
| components, | ||
| }, | ||
| provenance: { | ||
| present: Boolean(provenance.predicateType), | ||
| chainIntact: Boolean(provenance.subject?.length && provenance.builder?.id), | ||
| builderId: provenance.builder?.id, | ||
| subjects: (provenance.subject ?? []) | ||
| .map((subject) => subject?.name) | ||
| .filter(Boolean) | ||
| .sort((a, b) => a.localeCompare(b)), | ||
| }, | ||
| dependencies: { | ||
| prohibitedFound: prohibitedDeps, | ||
| }, | ||
| }; | ||
|
|
||
| const metrics = { | ||
| checks_total: 4, | ||
| checks_passed: | ||
| Number(report.signature.valid) + | ||
| Number(report.sbom.present && report.sbom.complete) + | ||
| Number(report.provenance.present && report.provenance.chainIntact) + | ||
| Number(report.dependencies.prohibitedFound.length === 0), | ||
| prohibited_dependency_count: report.dependencies.prohibitedFound.length, | ||
| sbom_component_count: report.sbom.components.length, | ||
| }; | ||
|
|
||
| const stamp = { | ||
| run_id: process.env.GITHUB_RUN_ID ?? 'local', | ||
| git_sha: process.env.GITHUB_SHA ?? 'local', | ||
| generated_at: new Date().toISOString(), | ||
| }; | ||
|
|
||
| mkdirSync('evidence', { recursive: true }); | ||
| writeFileSync('evidence/report.json', `${JSON.stringify(report, null, 2)}\n`); | ||
| writeFileSync('evidence/metrics.json', `${JSON.stringify(metrics, null, 2)}\n`); | ||
| writeFileSync('evidence/stamp.json', `${JSON.stringify(stamp, null, 2)}\n`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ephemeral key pair compromises long-term signature verification.
Generating a new key pair on every workflow run means:
cosign.key) is created in the workspace and could be accidentally exposed.For a supply-chain security gate, consider using Sigstore's keyless signing (
cosign sign-blob --yeswithout--key) which leverages OIDC identity, or store a persistent signing key in GitHub Secrets.🔑 Suggested approach using keyless signing
- name: Sign and verify artifact signature run: | - cosign generate-key-pair - cosign sign-blob --yes --key cosign.key --output-signature evidence/artifact.sig dist/admissible-artifact.txt - cosign verify-blob --key cosign.pub --signature evidence/artifact.sig dist/admissible-artifact.txt + cosign sign-blob --yes --output-signature evidence/artifact.sig --output-certificate evidence/artifact.crt dist/admissible-artifact.txt + cosign verify-blob --signature evidence/artifact.sig --certificate evidence/artifact.crt \ + --certificate-identity-regexp ".*" --certificate-oidc-issuer-regexp ".*" dist/admissible-artifact.txtIf keyless signing isn't suitable, ensure the private key is stored securely in secrets and never written to disk in the workflow directory.
📝 Committable suggestion
🤖 Prompt for AI Agents