A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
atcr.io
docker
container
atproto
go
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