A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go
at refactor 1210 lines 32 kB view raw view rendered
1# Integrating ATProto Signatures with OCI Tools 2 3This guide shows how to work with ATProto signatures using standard OCI/ORAS tools and integrate signature verification into your workflows. 4 5## Quick Reference: Tool Compatibility 6 7| Tool | Discover Signatures | Fetch Signatures | Verify Signatures | 8|------|-------------------|------------------|-------------------| 9| `oras discover` | ✅ Yes | - | - | 10| `oras pull` | - | ✅ Yes | ❌ No (custom tool needed) | 11| `oras manifest fetch` | - | ✅ Yes | - | 12| `cosign tree` | ✅ Yes (as artifacts) | - | - | 13| `cosign verify` | - | - | ❌ No (different format) | 14| `crane manifest` | - | ✅ Yes | - | 15| `skopeo inspect` | ✅ Yes (in referrers) | - | - | 16| `docker` | ❌ No (not visible) | - | - | 17| **`atcr-verify`** | ✅ Yes | ✅ Yes | ✅ Yes | 18 19**Key Takeaway:** Standard OCI tools can **discover and fetch** ATProto signatures, but **verification requires custom tooling** because ATProto uses a different trust model than Cosign/Notary. 20 21## Understanding What Tools See 22 23### ORAS CLI: Full Support for Discovery 24 25ORAS understands referrers and can discover ATProto signature artifacts: 26 27```bash 28# Discover all artifacts attached to an image 29$ oras discover atcr.io/alice/myapp:latest 30 31Discovered 2 artifacts referencing alice/myapp@sha256:abc123456789...: 32Digest: sha256:abc123456789... 33 34Artifact Type: application/spdx+json 35Digest: sha256:sbom123... 36Size: 45678 37 38Artifact Type: application/vnd.atproto.signature.v1+json 39Digest: sha256:sig789... 40Size: 512 41``` 42 43**What ORAS shows:** 44- ✅ Artifact type (identifies it as an ATProto signature) 45- ✅ Digest (can fetch the artifact) 46- ✅ Size 47 48**To filter for signatures only:** 49 50```bash 51$ oras discover atcr.io/alice/myapp:latest \ 52 --artifact-type application/vnd.atproto.signature.v1+json 53 54Discovered 1 artifact referencing alice/myapp@sha256:abc123...: 55Artifact Type: application/vnd.atproto.signature.v1+json 56Digest: sha256:sig789... 57``` 58 59### Fetching Signature Metadata with ORAS 60 61Pull the signature artifact to examine it: 62 63```bash 64# Pull signature artifact to current directory 65$ oras pull atcr.io/alice/myapp@sha256:sig789... 66 67Downloaded atproto-signature.json 68Pulled atcr.io/alice/myapp@sha256:sig789... 69Digest: sha256:sig789... 70 71# Examine the signature metadata 72$ cat atproto-signature.json | jq . 73{ 74 "$type": "io.atcr.atproto.signature", 75 "version": "1.0", 76 "subject": { 77 "digest": "sha256:abc123...", 78 "mediaType": "application/vnd.oci.image.manifest.v1+json" 79 }, 80 "atproto": { 81 "did": "did:plc:alice123", 82 "handle": "alice.bsky.social", 83 "pdsEndpoint": "https://bsky.social", 84 "recordUri": "at://did:plc:alice123/io.atcr.manifest/abc123", 85 "commitCid": "bafyreih8...", 86 "signedAt": "2025-10-31T12:34:56.789Z" 87 }, 88 "signature": { 89 "algorithm": "ECDSA-K256-SHA256", 90 "keyId": "did:plc:alice123#atproto", 91 "publicKeyMultibase": "zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDdo1Ko4Z" 92 } 93} 94``` 95 96### Cosign: Discovers but Cannot Verify 97 98Cosign can see ATProto signatures as artifacts but can't verify them: 99 100```bash 101# Cosign tree shows attached artifacts 102$ cosign tree atcr.io/alice/myapp:latest 103 104📦 Supply Chain Security Related artifacts for an image: atcr.io/alice/myapp:latest 105└── 💾 Attestations for an image tag: atcr.io/alice/myapp:sha256-abc123.att 106 ├── 🍒 sha256:sbom123... (application/spdx+json) 107 └── 🍒 sha256:sig789... (application/vnd.atproto.signature.v1+json) 108 109# Cosign verify doesn't work (expected) 110$ cosign verify atcr.io/alice/myapp:latest 111 112Error: no matching signatures: 113main.go:62: error during command execution: no matching signatures: 114``` 115 116**Why cosign verify fails:** 117- Cosign expects signatures in its own format (`dev.cosignproject.cosign/signature` annotation) 118- ATProto signatures use a different format and trust model 119- This is **intentional** - we're not trying to be Cosign-compatible 120 121### Crane: Fetch Manifests 122 123Crane can fetch the signature manifest: 124 125```bash 126# Get signature artifact manifest 127$ crane manifest atcr.io/alice/myapp@sha256:sig789... | jq . 128{ 129 "schemaVersion": 2, 130 "mediaType": "application/vnd.oci.image.manifest.v1+json", 131 "artifactType": "application/vnd.atproto.signature.v1+json", 132 "config": { 133 "mediaType": "application/vnd.oci.empty.v1+json", 134 "digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", 135 "size": 2 136 }, 137 "subject": { 138 "mediaType": "application/vnd.oci.image.manifest.v1+json", 139 "digest": "sha256:abc123...", 140 "size": 1234 141 }, 142 "layers": [{ 143 "mediaType": "application/vnd.atproto.signature.v1+json", 144 "digest": "sha256:meta456...", 145 "size": 512 146 }], 147 "annotations": { 148 "io.atcr.atproto.did": "did:plc:alice123", 149 "io.atcr.atproto.pds": "https://bsky.social", 150 "io.atcr.atproto.recordUri": "at://did:plc:alice123/io.atcr.manifest/abc123" 151 } 152} 153``` 154 155### Skopeo: Inspect Images 156 157Skopeo shows referrers in image inspection: 158 159```bash 160$ skopeo inspect --raw docker://atcr.io/alice/myapp:latest | jq . 161 162# Standard manifest (no signature info visible in manifest itself) 163 164# To see referrers (if registry supports Referrers API): 165$ curl -H "Accept: application/vnd.oci.image.index.v1+json" \ 166 "https://atcr.io/v2/alice/myapp/referrers/sha256:abc123" 167``` 168 169## Manual Verification with Shell Scripts 170 171Until `atcr-verify` is built, you can verify signatures manually: 172 173### Simple Verification Script 174 175```bash 176#!/bin/bash 177# verify-atproto-signature.sh 178# Usage: ./verify-atproto-signature.sh atcr.io/alice/myapp:latest 179 180set -e 181 182IMAGE="$1" 183 184echo "[1/6] Resolving image digest..." 185DIGEST=$(crane digest "$IMAGE") 186echo "$DIGEST" 187 188echo "[2/6] Discovering ATProto signature..." 189REGISTRY=$(echo "$IMAGE" | cut -d/ -f1) 190REPO=$(echo "$IMAGE" | cut -d/ -f2-) 191REPO_PATH=$(echo "$REPO" | cut -d: -f1) 192 193SIG_ARTIFACTS=$(curl -s -H "Accept: application/vnd.oci.image.index.v1+json" \ 194 "https://${REGISTRY}/v2/${REPO_PATH}/referrers/${DIGEST}?artifactType=application/vnd.atproto.signature.v1+json") 195 196SIG_DIGEST=$(echo "$SIG_ARTIFACTS" | jq -r '.manifests[0].digest') 197if [ "$SIG_DIGEST" = "null" ]; then 198 echo " ✗ No ATProto signature found" 199 exit 1 200fi 201echo " → Found signature: $SIG_DIGEST" 202 203echo "[3/6] Fetching signature metadata..." 204oras pull "${REGISTRY}/${REPO_PATH}@${SIG_DIGEST}" -o /tmp/sig --quiet 205 206DID=$(jq -r '.atproto.did' /tmp/sig/atproto-signature.json) 207PDS=$(jq -r '.atproto.pdsEndpoint' /tmp/sig/atproto-signature.json) 208RECORD_URI=$(jq -r '.atproto.recordUri' /tmp/sig/atproto-signature.json) 209echo " → DID: $DID" 210echo " → PDS: $PDS" 211echo " → Record: $RECORD_URI" 212 213echo "[4/6] Resolving DID to public key..." 214DID_DOC=$(curl -s "https://plc.directory/$DID") 215PUB_KEY_MB=$(echo "$DID_DOC" | jq -r '.verificationMethod[0].publicKeyMultibase') 216echo " → Public key: $PUB_KEY_MB" 217 218echo "[5/6] Querying PDS for signed commit..." 219# Extract collection and rkey from record URI 220COLLECTION=$(echo "$RECORD_URI" | sed 's|at://[^/]*/\([^/]*\)/.*|\1|') 221RKEY=$(echo "$RECORD_URI" | sed 's|at://.*/||') 222 223RECORD=$(curl -s "${PDS}/xrpc/com.atproto.repo.getRecord?repo=${DID}&collection=${COLLECTION}&rkey=${RKEY}") 224RECORD_CID=$(echo "$RECORD" | jq -r '.cid') 225echo " → Record CID: $RECORD_CID" 226 227echo "[6/6] Verifying signature..." 228echo " ⚠ Note: Full cryptographic verification requires ATProto crypto library" 229echo " ⚠ This script verifies record existence and DID resolution only" 230echo "" 231echo " ✓ Record exists in PDS" 232echo " ✓ DID resolved successfully" 233echo " ✓ Public key retrieved" 234echo "" 235echo "To fully verify the cryptographic signature, use: atcr-verify $IMAGE" 236``` 237 238### Full Verification (Requires Go + indigo) 239 240```go 241// verify.go - Full cryptographic verification 242package main 243 244import ( 245 "encoding/json" 246 "fmt" 247 "net/http" 248 249 "github.com/bluesky-social/indigo/atproto/crypto" 250 "github.com/multiformats/go-multibase" 251) 252 253func verifyATProtoSignature(did, pds, recordURI string) error { 254 // 1. Resolve DID to public key 255 didDoc, err := fetchDIDDocument(did) 256 if err != nil { 257 return fmt.Errorf("failed to resolve DID: %w", err) 258 } 259 260 pubKeyMB := didDoc.VerificationMethod[0].PublicKeyMultibase 261 262 // 2. Decode multibase public key 263 _, pubKeyBytes, err := multibase.Decode(pubKeyMB) 264 if err != nil { 265 return fmt.Errorf("failed to decode public key: %w", err) 266 } 267 268 // Remove multicodec prefix (first 2 bytes for K-256) 269 pubKeyBytes = pubKeyBytes[2:] 270 271 // 3. Parse as K-256 public key 272 pubKey, err := crypto.ParsePublicKeyK256(pubKeyBytes) 273 if err != nil { 274 return fmt.Errorf("failed to parse public key: %w", err) 275 } 276 277 // 4. Fetch repository commit from PDS 278 commit, err := fetchRepoCommit(pds, did) 279 if err != nil { 280 return fmt.Errorf("failed to fetch commit: %w", err) 281 } 282 283 // 5. Verify signature 284 bytesToVerify := commit.Unsigned().BytesForSigning() 285 err = pubKey.Verify(bytesToVerify, commit.Sig) 286 if err != nil { 287 return fmt.Errorf("signature verification failed: %w", err) 288 } 289 290 fmt.Println("✓ Signature verified successfully!") 291 return nil 292} 293 294func fetchDIDDocument(did string) (*DIDDocument, error) { 295 resp, err := http.Get(fmt.Sprintf("https://plc.directory/%s", did)) 296 if err != nil { 297 return nil, err 298 } 299 defer resp.Body.Close() 300 301 var didDoc DIDDocument 302 err = json.NewDecoder(resp.Body).Decode(&didDoc) 303 return &didDoc, err 304} 305 306// ... additional helper functions 307``` 308 309## Kubernetes Integration 310 311### Option 1: Admission Webhook (Recommended) 312 313Create a validating webhook that verifies ATProto signatures: 314 315```yaml 316# atcr-verify-webhook.yaml 317apiVersion: v1 318kind: Service 319metadata: 320 name: atcr-verify 321 namespace: kube-system 322spec: 323 selector: 324 app: atcr-verify 325 ports: 326 - port: 443 327 targetPort: 8443 328--- 329apiVersion: apps/v1 330kind: Deployment 331metadata: 332 name: atcr-verify 333 namespace: kube-system 334spec: 335 replicas: 2 336 selector: 337 matchLabels: 338 app: atcr-verify 339 template: 340 metadata: 341 labels: 342 app: atcr-verify 343 spec: 344 containers: 345 - name: webhook 346 image: atcr.io/atcr/verify-webhook:latest 347 ports: 348 - containerPort: 8443 349 env: 350 - name: REQUIRE_SIGNATURE 351 value: "true" 352 - name: TRUSTED_DIDS 353 value: "did:plc:alice123,did:plc:bob456" 354--- 355apiVersion: admissionregistration.k8s.io/v1 356kind: ValidatingWebhookConfiguration 357metadata: 358 name: atcr-verify 359webhooks: 360- name: verify.atcr.io 361 clientConfig: 362 service: 363 name: atcr-verify 364 namespace: kube-system 365 path: /validate 366 caBundle: <base64-encoded-ca-cert> 367 rules: 368 - operations: ["CREATE", "UPDATE"] 369 apiGroups: [""] 370 apiVersions: ["v1"] 371 resources: ["pods"] 372 admissionReviewVersions: ["v1", "v1beta1"] 373 sideEffects: None 374 failurePolicy: Fail # Reject pods if verification fails 375 namespaceSelector: 376 matchExpressions: 377 - key: atcr-verify 378 operator: In 379 values: ["enabled"] 380``` 381 382**Webhook Server Logic** (pseudocode): 383 384```go 385func (h *WebhookHandler) ValidatePod(req *admissionv1.AdmissionRequest) *admissionv1.AdmissionResponse { 386 pod := &corev1.Pod{} 387 json.Unmarshal(req.Object.Raw, pod) 388 389 for _, container := range pod.Spec.Containers { 390 if !strings.HasPrefix(container.Image, "atcr.io/") { 391 continue // Only verify ATCR images 392 } 393 394 // Verify ATProto signature 395 err := verifyImageSignature(container.Image) 396 if err != nil { 397 return &admissionv1.AdmissionResponse{ 398 Allowed: false, 399 Result: &metav1.Status{ 400 Message: fmt.Sprintf("Image %s failed ATProto verification: %v", 401 container.Image, err), 402 }, 403 } 404 } 405 } 406 407 return &admissionv1.AdmissionResponse{Allowed: true} 408} 409``` 410 411**Enable verification for specific namespaces:** 412 413```bash 414# Label namespace to enable verification 415kubectl label namespace production atcr-verify=enabled 416 417# Pods in this namespace must have valid ATProto signatures 418kubectl apply -f pod.yaml -n production 419``` 420 421### Option 2: Kyverno Policy 422 423Use Kyverno for policy-based validation: 424 425```yaml 426# kyverno-atcr-policy.yaml 427apiVersion: kyverno.io/v1 428kind: ClusterPolicy 429metadata: 430 name: verify-atcr-signatures 431spec: 432 validationFailureAction: enforce 433 background: false 434 rules: 435 - name: atcr-images-must-be-signed 436 match: 437 any: 438 - resources: 439 kinds: 440 - Pod 441 validate: 442 message: "ATCR images must have valid ATProto signatures" 443 foreach: 444 - list: "request.object.spec.containers" 445 deny: 446 conditions: 447 all: 448 - key: "{{ element.image }}" 449 operator: In 450 value: "atcr.io/*" 451 - key: "{{ atcrVerifySignature(element.image) }}" 452 operator: NotEquals 453 value: true 454``` 455 456**Note:** Requires custom Kyverno extension for `atcrVerifySignature()` function or external service integration. 457 458### Option 3: Ratify Verifier Plugin (Recommended) ⭐ 459 460Ratify is a verification engine that integrates with OPA Gatekeeper. Build a custom verifier plugin for ATProto signatures: 461 462**Ratify Plugin Architecture:** 463```go 464// pkg/verifier/atproto/verifier.go 465package atproto 466 467import ( 468 "context" 469 "encoding/json" 470 471 "github.com/ratify-project/ratify/pkg/common" 472 "github.com/ratify-project/ratify/pkg/ocispecs" 473 "github.com/ratify-project/ratify/pkg/referrerstore" 474 "github.com/ratify-project/ratify/pkg/verifier" 475) 476 477type ATProtoVerifier struct { 478 name string 479 config ATProtoConfig 480 resolver *Resolver 481} 482 483type ATProtoConfig struct { 484 TrustedDIDs []string `json:"trustedDIDs"` 485} 486 487func (v *ATProtoVerifier) Name() string { 488 return v.name 489} 490 491func (v *ATProtoVerifier) Type() string { 492 return "atproto" 493} 494 495func (v *ATProtoVerifier) CanVerify(artifactType string) bool { 496 return artifactType == "application/vnd.atproto.signature.v1+json" 497} 498 499func (v *ATProtoVerifier) VerifyReference( 500 ctx context.Context, 501 subjectRef common.Reference, 502 referenceDesc ocispecs.ReferenceDescriptor, 503 store referrerstore.ReferrerStore, 504) (verifier.VerifierResult, error) { 505 // 1. Fetch signature blob from store 506 sigBlob, err := store.GetBlobContent(ctx, subjectRef, referenceDesc.Digest) 507 if err != nil { 508 return verifier.VerifierResult{IsSuccess: false}, err 509 } 510 511 // 2. Parse ATProto signature metadata 512 var sigData ATProtoSignature 513 if err := json.Unmarshal(sigBlob, &sigData); err != nil { 514 return verifier.VerifierResult{IsSuccess: false}, err 515 } 516 517 // 3. Resolve DID to public key 518 pubKey, err := v.resolver.ResolveDIDToPublicKey(ctx, sigData.ATProto.DID) 519 if err != nil { 520 return verifier.VerifierResult{IsSuccess: false}, err 521 } 522 523 // 4. Fetch repository commit from PDS 524 commit, err := v.resolver.FetchCommit(ctx, sigData.ATProto.PDSEndpoint, 525 sigData.ATProto.DID, sigData.ATProto.CommitCID) 526 if err != nil { 527 return verifier.VerifierResult{IsSuccess: false}, err 528 } 529 530 // 5. Verify K-256 signature 531 valid := verifyK256Signature(pubKey, commit.Unsigned(), commit.Sig) 532 if !valid { 533 return verifier.VerifierResult{IsSuccess: false}, 534 fmt.Errorf("signature verification failed") 535 } 536 537 // 6. Check trust policy 538 if !v.isTrusted(sigData.ATProto.DID) { 539 return verifier.VerifierResult{IsSuccess: false}, 540 fmt.Errorf("DID %s not in trusted list", sigData.ATProto.DID) 541 } 542 543 return verifier.VerifierResult{ 544 IsSuccess: true, 545 Name: v.name, 546 Type: v.Type(), 547 Message: fmt.Sprintf("Verified for DID %s", sigData.ATProto.DID), 548 Extensions: map[string]interface{}{ 549 "did": sigData.ATProto.DID, 550 "handle": sigData.ATProto.Handle, 551 "signedAt": sigData.ATProto.SignedAt, 552 "commitCid": sigData.ATProto.CommitCID, 553 }, 554 }, nil 555} 556 557func (v *ATProtoVerifier) isTrusted(did string) bool { 558 for _, trustedDID := range v.config.TrustedDIDs { 559 if did == trustedDID { 560 return true 561 } 562 } 563 return false 564} 565``` 566 567**Deploy Ratify with ATProto Plugin:** 568 5691. **Build plugin:** 570```bash 571CGO_ENABLED=0 go build -o atproto-verifier ./cmd/ratify-atproto-plugin 572``` 573 5742. **Create custom Ratify image:** 575```dockerfile 576FROM ghcr.io/ratify-project/ratify:latest 577COPY atproto-verifier /.ratify/plugins/atproto-verifier 578``` 579 5803. **Deploy Ratify:** 581```yaml 582apiVersion: apps/v1 583kind: Deployment 584metadata: 585 name: ratify 586 namespace: gatekeeper-system 587spec: 588 replicas: 1 589 selector: 590 matchLabels: 591 app: ratify 592 template: 593 metadata: 594 labels: 595 app: ratify 596 spec: 597 containers: 598 - name: ratify 599 image: atcr.io/atcr/ratify-with-atproto:latest 600 args: 601 - serve 602 - --config=/config/ratify-config.yaml 603 volumeMounts: 604 - name: config 605 mountPath: /config 606 volumes: 607 - name: config 608 configMap: 609 name: ratify-config 610``` 611 6124. **Configure Verifier:** 613```yaml 614apiVersion: config.ratify.deislabs.io/v1beta1 615kind: Verifier 616metadata: 617 name: atproto-verifier 618spec: 619 name: atproto 620 artifactType: application/vnd.atproto.signature.v1+json 621 address: /.ratify/plugins/atproto-verifier 622 parameters: 623 trustedDIDs: 624 - did:plc:alice123 625 - did:plc:bob456 626``` 627 6285. **Use with Gatekeeper:** 629```yaml 630apiVersion: constraints.gatekeeper.sh/v1beta1 631kind: RatifyVerification 632metadata: 633 name: atcr-signatures-required 634spec: 635 enforcementAction: deny 636 match: 637 kinds: 638 - apiGroups: [""] 639 kinds: ["Pod"] 640``` 641 642**Benefits:** 643- ✅ Standard plugin interface 644- ✅ Works with existing Ratify deployments 645- ✅ Can combine with other verifiers (Notation, Cosign) 646- ✅ Policy-based enforcement via Gatekeeper 647 648**See Also:** [Integration Strategy - Ratify Plugin](./INTEGRATION_STRATEGY.md#ratify-verifier-plugin) 649 650--- 651 652### Option 4: OPA Gatekeeper External Data Provider ⭐ 653 654Use Gatekeeper's External Data Provider feature to verify ATProto signatures: 655 656**Provider Service:** 657```go 658// cmd/gatekeeper-provider/main.go 659package main 660 661import ( 662 "context" 663 "encoding/json" 664 "net/http" 665 666 "github.com/atcr-io/atcr/pkg/verify" 667) 668 669type ProviderRequest struct { 670 Keys []string `json:"keys"` 671 Values []string `json:"values"` 672} 673 674type ProviderResponse struct { 675 SystemError string `json:"system_error,omitempty"` 676 Responses []map[string]interface{} `json:"responses"` 677} 678 679func handleProvide(w http.ResponseWriter, r *http.Request) { 680 var req ProviderRequest 681 if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 682 http.Error(w, err.Error(), http.StatusBadRequest) 683 return 684 } 685 686 // Verify each image 687 responses := make([]map[string]interface{}, 0, len(req.Values)) 688 for _, image := range req.Values { 689 result, err := verifier.Verify(context.Background(), image) 690 691 response := map[string]interface{}{ 692 "image": image, 693 "verified": false, 694 } 695 696 if err == nil && result.Verified { 697 response["verified"] = true 698 response["did"] = result.Signature.DID 699 response["signedAt"] = result.Signature.SignedAt 700 } 701 702 responses = append(responses, response) 703 } 704 705 w.Header().Set("Content-Type", "application/json") 706 json.NewEncoder(w).Encode(ProviderResponse{ 707 Responses: responses, 708 }) 709} 710 711func main() { 712 http.HandleFunc("/provide", handleProvide) 713 http.ListenAndServe(":8080", nil) 714} 715``` 716 717**Deploy Provider:** 718```yaml 719apiVersion: apps/v1 720kind: Deployment 721metadata: 722 name: atcr-provider 723 namespace: gatekeeper-system 724spec: 725 replicas: 2 726 selector: 727 matchLabels: 728 app: atcr-provider 729 template: 730 metadata: 731 labels: 732 app: atcr-provider 733 spec: 734 containers: 735 - name: provider 736 image: atcr.io/atcr/gatekeeper-provider:latest 737 ports: 738 - containerPort: 8080 739 env: 740 - name: ATCR_POLICY_FILE 741 value: /config/trust-policy.yaml 742 volumeMounts: 743 - name: config 744 mountPath: /config 745 volumes: 746 - name: config 747 configMap: 748 name: atcr-trust-policy 749--- 750apiVersion: v1 751kind: Service 752metadata: 753 name: atcr-provider 754 namespace: gatekeeper-system 755spec: 756 selector: 757 app: atcr-provider 758 ports: 759 - port: 80 760 targetPort: 8080 761``` 762 763**Configure Gatekeeper:** 764```yaml 765apiVersion: config.gatekeeper.sh/v1alpha1 766kind: Config 767metadata: 768 name: config 769 namespace: gatekeeper-system 770spec: 771 sync: 772 syncOnly: 773 - group: "" 774 version: "v1" 775 kind: "Pod" 776 validation: 777 traces: 778 - user: "gatekeeper" 779 dump: "All" 780--- 781apiVersion: externaldata.gatekeeper.sh/v1alpha1 782kind: Provider 783metadata: 784 name: atcr-verifier 785spec: 786 url: http://atcr-provider.gatekeeper-system/provide 787 timeout: 10 788``` 789 790**Policy (Rego):** 791```rego 792package verify 793 794import future.keywords.contains 795import future.keywords.if 796import future.keywords.in 797 798# External data call 799provider := "atcr-verifier" 800 801violation[{"msg": msg}] { 802 container := input.review.object.spec.containers[_] 803 startswith(container.image, "atcr.io/") 804 805 # Call external provider 806 response := external_data({ 807 "provider": provider, 808 "keys": ["image"], 809 "values": [container.image] 810 }) 811 812 # Check verification result 813 not response[_].verified == true 814 815 msg := sprintf("Image %v has no valid ATProto signature", [container.image]) 816} 817``` 818 819**Benefits:** 820- ✅ Uses standard Gatekeeper external data API 821- ✅ Flexible Rego policies 822- ✅ Can add caching, rate limiting 823- ✅ Easy to deploy and update 824 825**See Also:** [Integration Strategy - Gatekeeper Provider](./INTEGRATION_STRATEGY.md#opa-gatekeeper-external-provider) 826 827--- 828 829### Option 5: OPA Gatekeeper 830 831Use OPA for policy enforcement: 832 833```yaml 834# gatekeeper-constraint-template.yaml 835apiVersion: templates.gatekeeper.sh/v1 836kind: ConstraintTemplate 837metadata: 838 name: atcrverify 839spec: 840 crd: 841 spec: 842 names: 843 kind: ATCRVerify 844 targets: 845 - target: admission.k8s.gatekeeper.sh 846 rego: | 847 package atcrverify 848 849 violation[{"msg": msg}] { 850 container := input.review.object.spec.containers[_] 851 startswith(container.image, "atcr.io/") 852 not verified(container.image) 853 msg := sprintf("Image %v has no valid ATProto signature", [container.image]) 854 } 855 856 verified(image) { 857 # Call external verification service 858 response := http.send({ 859 "method": "GET", 860 "url": sprintf("http://atcr-verify.kube-system.svc/verify?image=%v", [image]), 861 }) 862 response.status_code == 200 863 } 864--- 865apiVersion: constraints.gatekeeper.sh/v1beta1 866kind: ATCRVerify 867metadata: 868 name: atcr-signatures-required 869spec: 870 match: 871 kinds: 872 - apiGroups: [""] 873 kinds: ["Pod"] 874``` 875 876## CI/CD Integration 877 878### GitHub Actions 879 880```yaml 881# .github/workflows/verify-and-deploy.yml 882name: Verify and Deploy 883 884on: 885 push: 886 branches: [main] 887 888jobs: 889 verify-image: 890 runs-on: ubuntu-latest 891 steps: 892 - name: Install ORAS 893 run: | 894 curl -LO https://github.com/oras-project/oras/releases/download/v1.0.0/oras_1.0.0_linux_amd64.tar.gz 895 tar -xzf oras_1.0.0_linux_amd64.tar.gz 896 sudo mv oras /usr/local/bin/ 897 898 - name: Install crane 899 run: | 900 curl -sL "https://github.com/google/go-containerregistry/releases/download/v0.15.2/go-containerregistry_Linux_x86_64.tar.gz" > crane.tar.gz 901 tar -xzf crane.tar.gz 902 sudo mv crane /usr/local/bin/ 903 904 - name: Verify image signature 905 run: | 906 IMAGE="atcr.io/alice/myapp:${{ github.sha }}" 907 908 # Get image digest 909 DIGEST=$(crane digest "$IMAGE") 910 911 # Check for ATProto signature 912 REFERRERS=$(curl -s "https://atcr.io/v2/alice/myapp/referrers/${DIGEST}?artifactType=application/vnd.atproto.signature.v1+json") 913 914 SIG_COUNT=$(echo "$REFERRERS" | jq '.manifests | length') 915 if [ "$SIG_COUNT" -eq 0 ]; then 916 echo "❌ No ATProto signature found" 917 exit 1 918 fi 919 920 echo "✓ Found $SIG_COUNT signature(s)" 921 922 # TODO: Full verification when atcr-verify is available 923 # atcr-verify "$IMAGE" --policy policy.yaml 924 925 - name: Deploy to Kubernetes 926 if: success() 927 run: | 928 kubectl set image deployment/myapp myapp=atcr.io/alice/myapp:${{ github.sha }} 929``` 930 931### GitLab CI 932 933```yaml 934# .gitlab-ci.yml 935verify_image: 936 stage: verify 937 image: alpine:latest 938 before_script: 939 - apk add --no-cache curl jq 940 script: 941 - | 942 IMAGE="atcr.io/alice/myapp:${CI_COMMIT_SHA}" 943 944 # Install crane 945 wget https://github.com/google/go-containerregistry/releases/download/v0.15.2/go-containerregistry_Linux_x86_64.tar.gz 946 tar -xzf go-containerregistry_Linux_x86_64.tar.gz crane 947 948 # Get digest 949 DIGEST=$(./crane digest "$IMAGE") 950 951 # Check signature 952 REFERRERS=$(curl -s "https://atcr.io/v2/alice/myapp/referrers/${DIGEST}?artifactType=application/vnd.atproto.signature.v1+json") 953 954 if [ $(echo "$REFERRERS" | jq '.manifests | length') -eq 0 ]; then 955 echo "❌ No signature found" 956 exit 1 957 fi 958 959 echo "✓ Signature verified" 960 961deploy: 962 stage: deploy 963 dependencies: 964 - verify_image 965 script: 966 - kubectl set image deployment/myapp myapp=atcr.io/alice/myapp:${CI_COMMIT_SHA} 967``` 968 969## Integration with Containerd 970 971Containerd can be extended with verification plugins: 972 973```go 974// containerd-atcr-verifier plugin 975package main 976 977import ( 978 "context" 979 "fmt" 980 981 "github.com/containerd/containerd" 982 "github.com/containerd/containerd/remotes" 983) 984 985type ATCRVerifier struct { 986 // Configuration 987} 988 989func (v *ATCRVerifier) Verify(ctx context.Context, ref string) error { 990 // 1. Query referrers API for signatures 991 sigs, err := v.discoverSignatures(ctx, ref) 992 if err != nil { 993 return err 994 } 995 996 if len(sigs) == 0 { 997 return fmt.Errorf("no ATProto signature found for %s", ref) 998 } 999 1000 // 2. Fetch and verify signature 1001 for _, sig := range sigs { 1002 err := v.verifySignature(ctx, sig) 1003 if err == nil { 1004 return nil // At least one valid signature 1005 } 1006 } 1007 1008 return fmt.Errorf("all signatures failed verification") 1009} 1010 1011// Use as containerd resolver wrapper 1012func NewVerifyingResolver(base remotes.Resolver, verifier *ATCRVerifier) remotes.Resolver { 1013 return &verifyingResolver{ 1014 Resolver: base, 1015 verifier: verifier, 1016 } 1017} 1018``` 1019 1020**Containerd config** (`/etc/containerd/config.toml`): 1021 1022```toml 1023[plugins."io.containerd.grpc.v1.cri".registry] 1024 [plugins."io.containerd.grpc.v1.cri".registry.configs] 1025 [plugins."io.containerd.grpc.v1.cri".registry.configs."atcr.io"] 1026 [plugins."io.containerd.grpc.v1.cri".registry.configs."atcr.io".auth] 1027 username = "alice" 1028 password = "..." 1029 # Custom verifier hook (requires plugin) 1030 verify_signatures = true 1031 signature_type = "atproto" 1032``` 1033 1034## Trust Policies 1035 1036Define what signatures you trust: 1037 1038```yaml 1039# trust-policy.yaml 1040version: 1.0 1041 1042policies: 1043 # Production images must be signed 1044 - name: production-images 1045 scope: "atcr.io/*/prod-*" 1046 require: 1047 - signature: true 1048 trustedDIDs: 1049 - did:plc:alice123 1050 - did:plc:bob456 1051 minSignatures: 1 1052 action: enforce # reject if policy fails 1053 1054 # Development images don't require signatures 1055 - name: dev-images 1056 scope: "atcr.io/*/dev-*" 1057 require: 1058 - signature: false 1059 action: audit # log but don't reject 1060 1061 # Staging requires at least 1 signature from any trusted DID 1062 - name: staging-images 1063 scope: "atcr.io/*/staging-*" 1064 require: 1065 - signature: true 1066 trustedDIDs: 1067 - did:plc:alice123 1068 - did:plc:bob456 1069 - did:plc:charlie789 1070 action: enforce 1071 1072# DID trust configuration 1073trustedDIDs: 1074 did:plc:alice123: 1075 name: "Alice (DevOps Lead)" 1076 validFrom: "2024-01-01T00:00:00Z" 1077 expiresAt: null 1078 1079 did:plc:bob456: 1080 name: "Bob (Security Team)" 1081 validFrom: "2024-06-01T00:00:00Z" 1082 expiresAt: "2025-12-31T23:59:59Z" 1083``` 1084 1085## Troubleshooting 1086 1087### No Signature Found 1088 1089```bash 1090$ oras discover atcr.io/alice/myapp:latest --artifact-type application/vnd.atproto.signature.v1+json 1091 1092Discovered 0 artifacts 1093``` 1094 1095**Possible causes:** 10961. Image was pushed before signature creation was implemented 10972. Signature artifact creation failed 10983. Registry doesn't support Referrers API 1099 1100**Solutions:** 1101- Re-push the image to generate signature 1102- Check AppView logs for signature creation errors 1103- Verify Referrers API endpoint: `GET /v2/{repo}/referrers/{digest}` 1104 1105### Signature Verification Fails 1106 1107**Check DID resolution:** 1108```bash 1109curl -s "https://plc.directory/did:plc:alice123" | jq . 1110# Should return DID document with verificationMethod 1111``` 1112 1113**Check PDS connectivity:** 1114```bash 1115curl -s "https://bsky.social/xrpc/com.atproto.repo.describeRepo?repo=did:plc:alice123" | jq . 1116# Should return repository metadata 1117``` 1118 1119**Check record exists:** 1120```bash 1121curl -s "https://bsky.social/xrpc/com.atproto.repo.getRecord?\ 1122 repo=did:plc:alice123&\ 1123 collection=io.atcr.manifest&\ 1124 rkey=abc123" | jq . 1125# Should return manifest record 1126``` 1127 1128### Registry Returns 404 for Referrers 1129 1130Some registries don't support OCI Referrers API yet. Fallback to tag-based discovery: 1131 1132```bash 1133# Look for signature tags (if implemented) 1134crane ls atcr.io/alice/myapp | grep sig 1135``` 1136 1137## Best Practices 1138 1139### 1. Always Verify in Production 1140 1141Enable signature verification for production namespaces: 1142 1143```bash 1144kubectl label namespace production atcr-verify=enabled 1145``` 1146 1147### 2. Use Trust Policies 1148 1149Don't blindly trust all signatures - define which DIDs you trust: 1150 1151```yaml 1152trustedDIDs: 1153 - did:plc:your-org-team 1154 - did:plc:your-ci-system 1155``` 1156 1157### 3. Monitor Signature Coverage 1158 1159Track which images have signatures: 1160 1161```bash 1162# Check all images in a namespace 1163kubectl get pods -n production -o json | \ 1164 jq -r '.items[].spec.containers[].image' | \ 1165 while read image; do 1166 echo -n "$image: " 1167 oras discover "$image" --artifact-type application/vnd.atproto.signature.v1+json | \ 1168 grep -q "Discovered 0" && echo "❌ No signature" || echo "✓ Signed" 1169 done 1170``` 1171 1172### 4. Automate Verification in CI/CD 1173 1174Never deploy unsigned images to production: 1175 1176```yaml 1177# GitHub Actions 1178- name: Verify signature 1179 run: | 1180 if ! atcr-verify $IMAGE; then 1181 echo "❌ Image is not signed" 1182 exit 1 1183 fi 1184``` 1185 1186### 5. Plan for Offline Scenarios 1187 1188For air-gapped environments, cache signature metadata and DID documents: 1189 1190```bash 1191# Export signatures and DID docs for offline use 1192./export-verification-bundle.sh atcr.io/alice/myapp:latest > bundle.json 1193 1194# In air-gapped environment 1195atcr-verify --offline --bundle bundle.json atcr.io/alice/myapp:latest 1196``` 1197 1198## Next Steps 1199 12001. **Try manual verification** using the shell scripts above 12012. **Set up admission webhook** for your Kubernetes cluster 12023. **Define trust policies** for your organization 12034. **Integrate into CI/CD** pipelines 12045. **Monitor signature coverage** across your images 1205 1206## See Also 1207 1208- [ATProto Signatures](./ATPROTO_SIGNATURES.md) - Technical deep-dive 1209- [SBOM Scanning](./SBOM_SCANNING.md) - Similar ORAS artifact pattern 1210- [Example Scripts](../examples/verification/) - Working verification examples