Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions .github/workflows/sign-agent-card.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
name: Sign AgentCard with Sigstore

on:
workflow_dispatch: # Manual trigger for demo
push:
branches: ['**'] # Any branch (for demo purposes)
paths:
- 'demo/sigstore/unsigned-agent-card.json'

permissions:
id-token: write # Required for OIDC token (keyless signing)
contents: read
actions: read

jobs:
sign:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

Check failure on line 21 in .github/workflows/sign-agent-card.yml

View workflow job for this annotation

GitHub Actions / YAML Lint

21:1 [trailing-spaces] trailing spaces
- name: Install cosign
uses: sigstore/cosign-installer@v3

Check failure on line 24 in .github/workflows/sign-agent-card.yml

View workflow job for this annotation

GitHub Actions / YAML Lint

24:1 [trailing-spaces] trailing spaces
- name: Create canonical AgentCard JSON
run: |
# Remove signatures field and sort keys for canonical form
jq -S 'del(.signatures)' demo/sigstore/unsigned-agent-card.json > canonical-card.json
echo "=== Canonical AgentCard JSON ==="
cat canonical-card.json

Check failure on line 31 in .github/workflows/sign-agent-card.yml

View workflow job for this annotation

GitHub Actions / YAML Lint

31:1 [trailing-spaces] trailing spaces
- name: Sign with Sigstore (keyless)
run: |
echo "=== Signing with Sigstore (keyless via GitHub OIDC) ==="
cosign sign-blob \
--yes \
--output-signature signature.sig \
--output-certificate certificate.pem \
canonical-card.json

Check failure on line 40 in .github/workflows/sign-agent-card.yml

View workflow job for this annotation

GitHub Actions / YAML Lint

40:1 [trailing-spaces] trailing spaces
echo ""
echo "=== Fulcio Certificate ==="
openssl x509 -in certificate.pem -text -noout | head -30

Check failure on line 44 in .github/workflows/sign-agent-card.yml

View workflow job for this annotation

GitHub Actions / YAML Lint

44:1 [trailing-spaces] trailing spaces
- name: Assemble signed AgentCard
run: |
echo "=== Assembling Signed AgentCard ==="

Check failure on line 48 in .github/workflows/sign-agent-card.yml

View workflow job for this annotation

GitHub Actions / YAML Lint

48:1 [trailing-spaces] trailing spaces
# Create base64url-encoded protected header
PROTECTED=$(echo -n '{"alg":"ES256","typ":"JWT"}' | base64 -w0 | tr '+/' '-_' | tr -d '=')

Check failure on line 51 in .github/workflows/sign-agent-card.yml

View workflow job for this annotation

GitHub Actions / YAML Lint

51:1 [trailing-spaces] trailing spaces
# Read signature and convert to base64url
SIGNATURE=$(cat signature.sig | tr '+/' '-_' | tr -d '=' | tr -d '\n')

Check failure on line 54 in .github/workflows/sign-agent-card.yml

View workflow job for this annotation

GitHub Actions / YAML Lint

54:1 [trailing-spaces] trailing spaces
# Convert certificate to base64 DER for x5c
X5C=$(openssl x509 -in certificate.pem -outform DER | base64 -w0)

Check failure on line 57 in .github/workflows/sign-agent-card.yml

View workflow job for this annotation

GitHub Actions / YAML Lint

57:1 [trailing-spaces] trailing spaces
# Get current timestamp
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")

Check failure on line 60 in .github/workflows/sign-agent-card.yml

View workflow job for this annotation

GitHub Actions / YAML Lint

60:1 [trailing-spaces] trailing spaces
# For now use a placeholder index (cosign doesn't easily expose this)
# The actual index can be found in Rekor after the fact
INDEX=0

# Assemble the signed card
jq --arg protected "$PROTECTED" \
--arg signature "$SIGNATURE" \
--arg x5c "$X5C" \
--argjson rekor_index "$INDEX" \
--arg timestamp "$TIMESTAMP" \
'. + {signatures: [{protected: $protected, signature: $signature, header: {x5c: [$x5c], rekor_log_index: $rekor_index, timestamp: $timestamp}}]}' \

Check warning on line 71 in .github/workflows/sign-agent-card.yml

View workflow job for this annotation

GitHub Actions / YAML Lint

71:151 [line-length] line too long (160 > 150 characters)
demo/sigstore/unsigned-agent-card.json > signed-agent-card.json

echo ""
echo "=== Signed AgentCard ==="
cat signed-agent-card.json | jq .

- name: Upload signed card as artifact
uses: actions/upload-artifact@v4
with:
name: signed-weather-agent-card
path: |
signed-agent-card.json
certificate.pem
signature.sig
retention-days: 30

- name: Summary
run: |
echo "## Sigstore Signing Complete! :white_check_mark:" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**AgentCard:** WeatherAgent v1.0.0" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Next Steps" >> $GITHUB_STEP_SUMMARY
echo "1. Download the \`signed-weather-agent-card\` artifact" >> $GITHUB_STEP_SUMMARY
echo "2. Deploy to your Kubernetes cluster with kagenti-operator" >> $GITHUB_STEP_SUMMARY
echo "3. The operator will verify the Sigstore signature" >> $GITHUB_STEP_SUMMARY
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ secrets.*
# IDE and editor files
.idea/
.vscode/
.cursor/
*.swp
*.swo
*~
Expand All @@ -47,6 +48,7 @@ Thumbs.db
# Build artifacts
**/dist/
**/bin/
kagenti-operator/main

# Spurious controller-gen CRD artifact
kagenti-operator/config/crd/bases/_.yaml
Expand Down
39 changes: 39 additions & 0 deletions charts/kagenti-operator/crds/agent.kagenti.dev_agentcards.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@
jsonPath: .status.conditions[?(@.type=='Verified')].status
name: Verified
type: string
- description: Sigstore Bundle Verified
jsonPath: .status.sigstoreBundleVerified
name: Sigstore
type: boolean
- description: Identity Bound
jsonPath: .status.bindingStatus.bound
name: Bound
Expand Down Expand Up @@ -105,6 +109,21 @@
pattern: ^[a-zA-Z0-9]([a-zA-Z0-9\-\.]*[a-zA-Z0-9])?$
type: string
type: object
sigstoreVerification:
description: |-
SigstoreVerification optionally overrides operator-level Sigstore identity
constraints for supply-chain bundle verification on SignedAgentCard documents.
properties:
certificateIdentity:
description: CertificateIdentity is the expected OIDC subject
in the Fulcio certificate SAN (exact match unless paired with
regex elsewhere).
type: string
certificateOIDCIssuer:
description: CertificateOIDCIssuer is the expected OIDC issuer
URL (e.g. https://token.actions.githubusercontent.com).
type: string
type: object
syncPeriod:
default: 30s
description: SyncPeriod is how often to re-fetch the agent card (e.g.,
Expand Down Expand Up @@ -365,7 +384,7 @@
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.

Check warning on line 387 in charts/kagenti-operator/crds/agent.kagenti.dev_agentcards.yaml

View workflow job for this annotation

GitHub Actions / YAML Lint

387:151 [line-length] line too long (162 > 150 characters)
format: date-time
type: string
message:
Expand All @@ -377,7 +396,7 @@
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date

Check warning on line 399 in charts/kagenti-operator/crds/agent.kagenti.dev_agentcards.yaml

View workflow job for this annotation

GitHub Actions / YAML Lint

399:151 [line-length] line too long (162 > 150 characters)
with respect to the current state of the instance.
format: int64
minimum: 0
Expand Down Expand Up @@ -424,6 +443,10 @@
protocol:
description: Protocol is the detected agent protocol (e.g., "a2a")
type: string
rekorLogIndex:
description: RekorLogIndex is the transparency log index when available
from the bundle.
type: string
signatureIdentityMatch:
description: SignatureIdentityMatch is true when both signature and
identity binding pass.
Expand All @@ -440,6 +463,22 @@
description: SignatureVerificationDetails contains details about the
last signature verification
type: string
sigstoreBundleVerified:
description: SigstoreBundleVerified is true when SignedAgentCard attestations.signatureBundle
verifies successfully.
type: boolean
sigstoreIdentity:
description: SigstoreIdentity is the verified Fulcio signing identity
when Sigstore verification succeeds.
type: string
slsaCommitSHA:
description: SLSACommitSHA from provenanceBundle when present and
parsed.
type: string
slsaRepository:
description: SLSARepository from provenanceBundle when present and
parsed.
type: string
targetRef:
description: |-
TargetRef contains the resolved reference to the backing workload.
Expand Down
24 changes: 24 additions & 0 deletions charts/kagenti-operator/templates/manager/manager.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,30 @@ spec:
- "--credential-wait-timeout={{ .Values.authbridgeConfig.credentialWaitTimeout }}"
{{- end }}
{{- end }}
{{- if .Values.sigstore.cardVerification.enabled }}
- "--enable-sigstore-verification=true"
{{- if .Values.sigstore.cardVerification.auditMode }}
- "--sigstore-audit-mode=true"
{{- end }}
{{- if .Values.sigstore.cardVerification.certificateIdentity }}
- "--sigstore-certificate-identity={{ .Values.sigstore.cardVerification.certificateIdentity }}"
{{- end }}
{{- if .Values.sigstore.cardVerification.certificateOIDCIssuer }}
- "--sigstore-certificate-oidc-issuer={{ .Values.sigstore.cardVerification.certificateOIDCIssuer }}"
{{- end }}
{{- if .Values.sigstore.cardVerification.staging }}
- "--sigstore-staging=true"
{{- end }}
{{- if .Values.sigstore.cardVerification.trustedRoot.configMapName }}
- "--sigstore-trusted-root-configmap={{ .Values.sigstore.cardVerification.trustedRoot.configMapName }}"
{{- end }}
{{- if .Values.sigstore.cardVerification.trustedRoot.configMapNamespace }}
- "--sigstore-trusted-root-configmap-namespace={{ .Values.sigstore.cardVerification.trustedRoot.configMapNamespace }}"
{{- end }}
{{- if .Values.sigstore.cardVerification.trustedRoot.configMapKey }}
- "--sigstore-trusted-root-configmap-key={{ .Values.sigstore.cardVerification.trustedRoot.configMapKey }}"
{{- end }}
{{- end }}
command:
- {{ .Values.controllerManager.container.cmd }}
image: {{ .Values.controllerManager.container.image.repository }}:{{ .Values.controllerManager.container.image.tag }}
Expand Down
13 changes: 13 additions & 0 deletions charts/kagenti-operator/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,19 @@ signatureVerification:
# How far before SVID expiry to trigger proactive workload restart
svidExpiryGracePeriod: "30m"

# Supply-chain: sigstore-a2a SignedAgentCard bundle verification (Rekor / Fulcio)
sigstore:
cardVerification:
enabled: false
auditMode: false
certificateIdentity: ""
certificateOIDCIssuer: ""
staging: false
trustedRoot:
configMapName: ""
configMapNamespace: ""
configMapKey: "trusted-root.json"

# Feature gates — highest-priority layer in the injection precedence chain.
# Set globalEnabled to false to disable ALL sidecar injection (kill switch).
# Set individual gates to false to disable specific sidecars cluster-wide.
Expand Down
37 changes: 37 additions & 0 deletions kagenti-operator/api/v1alpha1/agentcard_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,22 @@ type AgentCardSpec struct {
// IdentityBinding specifies SPIFFE identity binding configuration
// +optional
IdentityBinding *IdentityBinding `json:"identityBinding,omitempty"`

// SigstoreVerification optionally overrides operator-level Sigstore identity
// constraints for supply-chain bundle verification on SignedAgentCard documents.
// +optional
SigstoreVerification *SigstoreVerification `json:"sigstoreVerification,omitempty"`
}

// SigstoreVerification configures expected Fulcio certificate identity for Sigstore bundle verification.
type SigstoreVerification struct {
// CertificateIdentity is the expected OIDC subject in the Fulcio certificate SAN (exact match unless paired with regex elsewhere).
// +optional
CertificateIdentity string `json:"certificateIdentity,omitempty"`

// CertificateOIDCIssuer is the expected OIDC issuer URL (e.g. https://token.actions.githubusercontent.com).
// +optional
CertificateOIDCIssuer string `json:"certificateOIDCIssuer,omitempty"`
}

// IdentityBinding configures workload identity binding for an AgentCard.
Expand Down Expand Up @@ -131,6 +147,26 @@ type AgentCardStatus struct {
// BindingStatus contains the result of identity binding evaluation
// +optional
BindingStatus *BindingStatus `json:"bindingStatus,omitempty"`

// SigstoreBundleVerified is true when SignedAgentCard attestations.signatureBundle verifies successfully.
// +optional
SigstoreBundleVerified *bool `json:"sigstoreBundleVerified,omitempty"`

// SigstoreIdentity is the verified Fulcio signing identity when Sigstore verification succeeds.
// +optional
SigstoreIdentity string `json:"sigstoreIdentity,omitempty"`

// RekorLogIndex is the transparency log index when available from the bundle.
// +optional
RekorLogIndex string `json:"rekorLogIndex,omitempty"`

// SLSARepository from provenanceBundle when present and parsed.
// +optional
SLSARepository string `json:"slsaRepository,omitempty"`

// SLSACommitSHA from provenanceBundle when present and parsed.
// +optional
SLSACommitSHA string `json:"slsaCommitSHA,omitempty"`
}

// BindingStatus represents the result of identity binding evaluation
Expand Down Expand Up @@ -348,6 +384,7 @@ type SkillParameter struct {
// +kubebuilder:printcolumn:name="Target",type="string",JSONPath=".status.targetRef.name",description="Target Workload"
// +kubebuilder:printcolumn:name="Agent",type="string",JSONPath=".status.card.name",description="Agent Name"
// +kubebuilder:printcolumn:name="Verified",type="string",JSONPath=".status.conditions[?(@.type=='Verified')].status",description="Identity Verified"
// +kubebuilder:printcolumn:name="Sigstore",type="boolean",JSONPath=".status.sigstoreBundleVerified",description="Sigstore Bundle Verified"
// +kubebuilder:printcolumn:name="Bound",type="boolean",JSONPath=".status.bindingStatus.bound",description="Identity Bound"
// +kubebuilder:printcolumn:name="Synced",type="string",JSONPath=".status.conditions[?(@.type=='Synced')].status",description="Sync Status"
// +kubebuilder:printcolumn:name="LastSync",type="date",JSONPath=".status.lastSyncTime",description="Last Sync Time"
Expand Down
25 changes: 25 additions & 0 deletions kagenti-operator/api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion kagenti-operator/cmd/configmap_cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ func TestConfigMapCacheVisibility(t *testing.T) {

// 4. Build the scoped cache config (function under test) and start a
// manager whose ConfigMap informers are restricted to those selectors.
cmCacheNamespaces := buildConfigMapCacheNamespaces(true, spireBundleName, spireNS)
cmCacheNamespaces := buildConfigMapCacheNamespaces(true, spireBundleName, spireNS, false, "", "")

mgr, err := ctrl.NewManager(cfg, ctrl.Options{
Scheme: clientgoscheme.Scheme,
Expand Down
Loading
Loading