440 lines
11 KiB
Markdown
440 lines
11 KiB
Markdown
# Provcache OCI Attestation Verification Guide
|
|
|
|
This document describes how to verify Provcache decision attestations attached to OCI container images.
|
|
|
|
## Overview
|
|
|
|
StellaOps can attach provenance cache decisions as OCI-attached attestations to container images. These attestations enable:
|
|
|
|
- **Supply chain verification** — Verify security decisions were made by trusted evaluators
|
|
- **Audit trails** — Retrieve the exact decision state at image push time
|
|
- **Policy gates** — Admission controllers can verify attestations before deployment
|
|
- **Offline verification** — Decisions verifiable without calling StellaOps services
|
|
|
|
## Attestation Format
|
|
|
|
### Predicate Type
|
|
|
|
```
|
|
stella.ops/provcache@v1
|
|
```
|
|
|
|
### Predicate Schema
|
|
|
|
```json
|
|
{
|
|
"_type": "stella.ops/provcache@v1",
|
|
"veriKey": "sha256:abc123...",
|
|
"decision": {
|
|
"digestVersion": "v1",
|
|
"verdictHash": "sha256:def456...",
|
|
"proofRoot": "sha256:789abc...",
|
|
"trustScore": 85,
|
|
"createdAt": "2025-12-24T12:00:00Z",
|
|
"expiresAt": "2025-12-25T12:00:00Z"
|
|
},
|
|
"inputs": {
|
|
"sourceDigest": "sha256:image...",
|
|
"sbomDigest": "sha256:sbom...",
|
|
"policyDigest": "sha256:policy...",
|
|
"feedEpoch": "2024-W52"
|
|
},
|
|
"verdicts": {
|
|
"CVE-2024-1234": "mitigated",
|
|
"CVE-2024-5678": "affected"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Field Descriptions
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `_type` | string | Predicate type URI |
|
|
| `veriKey` | string | VeriKey hash identifying this decision context |
|
|
| `decision.digestVersion` | string | Decision digest schema version |
|
|
| `decision.verdictHash` | string | Hash of all verdicts |
|
|
| `decision.proofRoot` | string | Merkle proof root hash |
|
|
| `decision.trustScore` | number | Overall trust score (0-100) |
|
|
| `decision.createdAt` | string | ISO-8601 creation timestamp |
|
|
| `decision.expiresAt` | string | ISO-8601 expiry timestamp |
|
|
| `inputs.sourceDigest` | string | Container image digest |
|
|
| `inputs.sbomDigest` | string | SBOM document digest |
|
|
| `inputs.policyDigest` | string | Policy bundle digest |
|
|
| `inputs.feedEpoch` | string | Feed epoch identifier |
|
|
| `verdicts` | object | Map of CVE IDs to verdict status |
|
|
|
|
---
|
|
|
|
## Verification with Cosign
|
|
|
|
### Prerequisites
|
|
|
|
```bash
|
|
# Install cosign
|
|
brew install cosign # macOS
|
|
# or
|
|
go install github.com/sigstore/cosign/v2/cmd/cosign@latest
|
|
```
|
|
|
|
### Basic Verification
|
|
|
|
```bash
|
|
# Verify attestation exists and is signed
|
|
cosign verify-attestation \
|
|
--type stella.ops/provcache@v1 \
|
|
registry.example.com/app:v1.2.3
|
|
```
|
|
|
|
### Verify with Identity Constraints
|
|
|
|
```bash
|
|
# Verify with signer identity (Fulcio)
|
|
cosign verify-attestation \
|
|
--type stella.ops/provcache@v1 \
|
|
--certificate-identity-regexp '.*@stellaops\.example\.com' \
|
|
--certificate-oidc-issuer https://auth.stellaops.example.com \
|
|
registry.example.com/app:v1.2.3
|
|
```
|
|
|
|
### Verify with Custom Trust Root
|
|
|
|
```bash
|
|
# Using enterprise CA
|
|
cosign verify-attestation \
|
|
--type stella.ops/provcache@v1 \
|
|
--certificate /path/to/enterprise-ca.crt \
|
|
--certificate-chain /path/to/ca-chain.crt \
|
|
registry.example.com/app:v1.2.3
|
|
```
|
|
|
|
### Extract Attestation Payload
|
|
|
|
```bash
|
|
# Get raw attestation JSON
|
|
cosign verify-attestation \
|
|
--type stella.ops/provcache@v1 \
|
|
--certificate-identity-regexp '.*@stellaops\.example\.com' \
|
|
--certificate-oidc-issuer https://auth.stellaops.example.com \
|
|
registry.example.com/app:v1.2.3 | jq '.payload' | base64 -d | jq .
|
|
```
|
|
|
|
---
|
|
|
|
## Verification with StellaOps CLI
|
|
|
|
### Verify Attestation
|
|
|
|
```bash
|
|
# Verify using StellaOps CLI
|
|
stella verify attestation \
|
|
--image registry.example.com/app:v1.2.3 \
|
|
--type provcache
|
|
|
|
# Output:
|
|
# ✓ Attestation found: stella.ops/provcache@v1
|
|
# ✓ Signature valid (Fulcio)
|
|
# ✓ Trust score: 85
|
|
# ✓ Decision created: 2025-12-24T12:00:00Z
|
|
# ✓ Decision expires: 2025-12-25T12:00:00Z
|
|
```
|
|
|
|
### Verify with Policy Requirements
|
|
|
|
```bash
|
|
# Verify with minimum trust score
|
|
stella verify attestation \
|
|
--image registry.example.com/app:v1.2.3 \
|
|
--type provcache \
|
|
--min-trust-score 80
|
|
|
|
# Verify with freshness requirement
|
|
stella verify attestation \
|
|
--image registry.example.com/app:v1.2.3 \
|
|
--type provcache \
|
|
--max-age 24h
|
|
```
|
|
|
|
### Extract Decision Details
|
|
|
|
```bash
|
|
# Get full decision details
|
|
stella verify attestation \
|
|
--image registry.example.com/app:v1.2.3 \
|
|
--type provcache \
|
|
--output json | jq .
|
|
|
|
# Get specific fields
|
|
stella verify attestation \
|
|
--image registry.example.com/app:v1.2.3 \
|
|
--type provcache \
|
|
--output json | jq '.predicate.verdicts'
|
|
```
|
|
|
|
---
|
|
|
|
## Kubernetes Admission Control
|
|
|
|
### Gatekeeper Policy
|
|
|
|
```yaml
|
|
# constraint-template.yaml
|
|
apiVersion: templates.gatekeeper.sh/v1
|
|
kind: ConstraintTemplate
|
|
metadata:
|
|
name: provcacheattestation
|
|
spec:
|
|
crd:
|
|
spec:
|
|
names:
|
|
kind: ProvcacheAttestation
|
|
validation:
|
|
openAPIV3Schema:
|
|
type: object
|
|
properties:
|
|
minTrustScore:
|
|
type: integer
|
|
minimum: 0
|
|
maximum: 100
|
|
maxAgeHours:
|
|
type: integer
|
|
minimum: 1
|
|
targets:
|
|
- target: admission.k8s.gatekeeper.sh
|
|
rego: |
|
|
package provcacheattestation
|
|
|
|
violation[{"msg": msg}] {
|
|
container := input.review.object.spec.containers[_]
|
|
image := container.image
|
|
not has_valid_attestation(image)
|
|
msg := sprintf("Image %v missing valid provcache attestation", [image])
|
|
}
|
|
|
|
has_valid_attestation(image) {
|
|
attestation := get_attestation(image, "stella.ops/provcache@v1")
|
|
attestation.predicate.decision.trustScore >= input.parameters.minTrustScore
|
|
not is_expired(attestation.predicate.decision.expiresAt)
|
|
}
|
|
|
|
is_expired(expiry) {
|
|
time.parse_rfc3339_ns(expiry) < time.now_ns()
|
|
}
|
|
```
|
|
|
|
```yaml
|
|
# constraint.yaml
|
|
apiVersion: constraints.gatekeeper.sh/v1beta1
|
|
kind: ProvcacheAttestation
|
|
metadata:
|
|
name: require-provcache-attestation
|
|
spec:
|
|
match:
|
|
kinds:
|
|
- apiGroups: [""]
|
|
kinds: ["Pod"]
|
|
namespaces:
|
|
- production
|
|
parameters:
|
|
minTrustScore: 80
|
|
maxAgeHours: 48
|
|
```
|
|
|
|
### Kyverno Policy
|
|
|
|
```yaml
|
|
apiVersion: kyverno.io/v1
|
|
kind: ClusterPolicy
|
|
metadata:
|
|
name: verify-provcache-attestation
|
|
spec:
|
|
validationFailureAction: enforce
|
|
background: true
|
|
rules:
|
|
- name: check-provcache-attestation
|
|
match:
|
|
any:
|
|
- resources:
|
|
kinds:
|
|
- Pod
|
|
verifyImages:
|
|
- imageReferences:
|
|
- "*"
|
|
attestations:
|
|
- predicateType: stella.ops/provcache@v1
|
|
conditions:
|
|
- all:
|
|
- key: "{{ decision.trustScore }}"
|
|
operator: GreaterThanOrEquals
|
|
value: 80
|
|
- key: "{{ decision.expiresAt }}"
|
|
operator: GreaterThan
|
|
value: "{{ time.Now() }}"
|
|
attestors:
|
|
- entries:
|
|
- keyless:
|
|
issuer: https://auth.stellaops.example.com
|
|
subject: ".*@stellaops\\.example\\.com"
|
|
```
|
|
|
|
---
|
|
|
|
## CI/CD Integration
|
|
|
|
### GitHub Actions
|
|
|
|
```yaml
|
|
# .github/workflows/verify-attestation.yml
|
|
name: Verify Provcache Attestation
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
image:
|
|
description: 'Image to verify'
|
|
required: true
|
|
|
|
jobs:
|
|
verify:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Install cosign
|
|
uses: sigstore/cosign-installer@v3
|
|
|
|
- name: Verify attestation
|
|
run: |
|
|
cosign verify-attestation \
|
|
--type stella.ops/provcache@v1 \
|
|
--certificate-identity-regexp '.*@stellaops\.example\.com' \
|
|
--certificate-oidc-issuer https://auth.stellaops.example.com \
|
|
${{ inputs.image }}
|
|
|
|
- name: Check trust score
|
|
run: |
|
|
TRUST_SCORE=$(cosign verify-attestation \
|
|
--type stella.ops/provcache@v1 \
|
|
--certificate-identity-regexp '.*@stellaops\.example\.com' \
|
|
--certificate-oidc-issuer https://auth.stellaops.example.com \
|
|
${{ inputs.image }} | jq -r '.payload' | base64 -d | jq '.predicate.decision.trustScore')
|
|
|
|
if [ "$TRUST_SCORE" -lt 80 ]; then
|
|
echo "Trust score $TRUST_SCORE is below threshold (80)"
|
|
exit 1
|
|
fi
|
|
```
|
|
|
|
### GitLab CI
|
|
|
|
```yaml
|
|
# .gitlab-ci.yml
|
|
verify-attestation:
|
|
stage: verify
|
|
image: gcr.io/projectsigstore/cosign:latest
|
|
script:
|
|
- cosign verify-attestation
|
|
--type stella.ops/provcache@v1
|
|
--certificate-identity-regexp '.*@stellaops\.example\.com'
|
|
--certificate-oidc-issuer https://auth.stellaops.example.com
|
|
${CI_REGISTRY_IMAGE}:${CI_COMMIT_TAG}
|
|
rules:
|
|
- if: $CI_COMMIT_TAG
|
|
```
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### No Attestation Found
|
|
|
|
```bash
|
|
# List all attestations on image
|
|
cosign tree registry.example.com/app:v1.2.3
|
|
|
|
# Check if attestation was pushed
|
|
crane manifest registry.example.com/app:sha256-<digest>.att
|
|
```
|
|
|
|
### Signature Verification Failed
|
|
|
|
```bash
|
|
# Check certificate details
|
|
cosign verify-attestation \
|
|
--type stella.ops/provcache@v1 \
|
|
--output text \
|
|
registry.example.com/app:v1.2.3 2>&1 | grep -A5 "Certificate"
|
|
|
|
# Verify with verbose output
|
|
COSIGN_EXPERIMENTAL=1 cosign verify-attestation \
|
|
--type stella.ops/provcache@v1 \
|
|
registry.example.com/app:v1.2.3 -v
|
|
```
|
|
|
|
### Attestation Expired
|
|
|
|
```bash
|
|
# Check expiry timestamp
|
|
cosign verify-attestation \
|
|
--type stella.ops/provcache@v1 \
|
|
--certificate-identity-regexp '.*@stellaops\.example\.com' \
|
|
--certificate-oidc-issuer https://auth.stellaops.example.com \
|
|
registry.example.com/app:v1.2.3 | \
|
|
jq -r '.payload' | base64 -d | jq '.predicate.decision.expiresAt'
|
|
```
|
|
|
|
### Trust Score Below Threshold
|
|
|
|
```bash
|
|
# Check trust score breakdown
|
|
stella verify attestation \
|
|
--image registry.example.com/app:v1.2.3 \
|
|
--type provcache \
|
|
--output json | jq '.predicate.decision.trustScore'
|
|
|
|
# If score is low, check individual components:
|
|
# - SBOM completeness
|
|
# - VEX coverage
|
|
# - Reachability analysis
|
|
# - Policy freshness
|
|
# - Signer trust
|
|
```
|
|
|
|
---
|
|
|
|
## Security Considerations
|
|
|
|
### Key Management
|
|
|
|
- **Fulcio** — Ephemeral certificates tied to OIDC identity; recommended for public workflows
|
|
- **Enterprise CA** — Long-lived certificates for air-gapped environments
|
|
- **Self-signed** — Only for development/testing; not recommended for production
|
|
|
|
### Attestation Integrity
|
|
|
|
- Attestations are signed at push time
|
|
- Signature covers the entire predicate payload
|
|
- Modifying any field invalidates the signature
|
|
|
|
### Expiry Handling
|
|
|
|
- Attestations have `expiresAt` timestamps
|
|
- Expired attestations should be rejected by admission controllers
|
|
- Consider re-scanning images before deployment to get fresh attestations
|
|
|
|
### Verdict Reconciliation
|
|
|
|
- Verdicts in attestation reflect state at push time
|
|
- New vulnerabilities discovered after push won't appear
|
|
- Use `stella verify attestation --check-freshness` to compare against current feeds
|
|
|
|
---
|
|
|
|
## Related Documentation
|
|
|
|
- [Provcache Module README](./README.md) — Core concepts
|
|
- [Provcache Metrics and Alerting](./metrics-alerting.md) — Observability
|
|
- [Signer Module](../signer/architecture.md) — Signing infrastructure
|
|
- [Attestor Module](../attestor/architecture.md) — Attestation generation
|
|
- [OCI Artifact Spec](https://github.com/opencontainers/image-spec) — OCI standards
|
|
- [In-toto Attestation Spec](https://github.com/in-toto/attestation) — Attestation format
|
|
- [Sigstore Documentation](https://docs.sigstore.dev/) — Cosign and Fulcio
|