A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
atcr.io
docker
container
atproto
go
1# OPA Gatekeeper External Data Provider for ATProto Signatures
2
3This is a reference implementation of an OPA Gatekeeper External Data Provider that verifies ATProto signatures on ATCR container images.
4
5## Overview
6
7Gatekeeper's External Data Provider feature allows Rego policies to call external HTTP services for data validation. This provider implements signature verification as an HTTP service that Gatekeeper can query.
8
9## Architecture
10
11```
12Kubernetes Pod Creation
13 ↓
14OPA Gatekeeper (admission webhook)
15 ↓
16Rego Policy (constraint template)
17 ↓
18External Data Provider API call
19 ↓
20ATProto Verification Service ← This service
21 ↓
22 1. Resolve image digest
23 2. Discover signature artifacts
24 3. Parse ATProto signature metadata
25 4. Resolve DID to public key
26 5. Fetch commit from PDS
27 6. Verify K-256 signature
28 7. Check trust policy
29 ↓
30 Return: verified=true/false + metadata
31```
32
33## Files
34
35- `main.go` - HTTP server and provider endpoints
36- `verifier.go` - ATProto signature verification logic
37- `resolver.go` - DID and PDS resolution
38- `crypto.go` - K-256 signature verification
39- `trust-policy.yaml` - Trust policy configuration
40- `Dockerfile` - Build provider service image
41- `deployment.yaml` - Kubernetes deployment manifest
42- `provider-crd.yaml` - Gatekeeper Provider custom resource
43- `constraint-template.yaml` - Rego constraint template
44- `constraint.yaml` - Policy constraint example
45
46## Prerequisites
47
48- Go 1.21+
49- Kubernetes cluster with OPA Gatekeeper installed
50- Access to ATCR registry
51
52## Building
53
54```bash
55# Build binary
56CGO_ENABLED=0 go build -o atcr-provider \
57 -ldflags="-w -s" \
58 ./main.go
59
60# Build Docker image
61docker build -t atcr.io/atcr/gatekeeper-provider:latest .
62
63# Push to registry
64docker push atcr.io/atcr/gatekeeper-provider:latest
65```
66
67## Deployment
68
69### 1. Create Trust Policy ConfigMap
70
71```bash
72kubectl create namespace gatekeeper-system
73kubectl create configmap atcr-trust-policy \
74 --from-file=trust-policy.yaml \
75 -n gatekeeper-system
76```
77
78### 2. Deploy Provider Service
79
80```bash
81kubectl apply -f deployment.yaml
82```
83
84### 3. Configure Gatekeeper Provider
85
86```bash
87kubectl apply -f provider-crd.yaml
88```
89
90### 4. Create Constraint Template
91
92```bash
93kubectl apply -f constraint-template.yaml
94```
95
96### 5. Create Constraint
97
98```bash
99kubectl apply -f constraint.yaml
100```
101
102### 6. Test
103
104```bash
105# Try to create pod with signed image (should succeed)
106kubectl run test-signed --image=atcr.io/alice/myapp:latest
107
108# Try to create pod with unsigned image (should fail)
109kubectl run test-unsigned --image=atcr.io/malicious/fake:latest
110
111# Check constraint status
112kubectl get constraint atcr-signatures-required -o yaml
113```
114
115## API Specification
116
117### Provider Endpoint
118
119**POST /provide**
120
121Request:
122```json
123{
124 "keys": ["image"],
125 "values": [
126 "atcr.io/alice/myapp:latest",
127 "atcr.io/bob/webapp:v1.0"
128 ]
129}
130```
131
132Response:
133```json
134{
135 "responses": [
136 {
137 "image": "atcr.io/alice/myapp:latest",
138 "verified": true,
139 "did": "did:plc:alice123",
140 "handle": "alice.bsky.social",
141 "signedAt": "2025-10-31T12:34:56Z",
142 "commitCid": "bafyreih8..."
143 },
144 {
145 "image": "atcr.io/bob/webapp:v1.0",
146 "verified": false,
147 "error": "no signature found"
148 }
149 ]
150}
151```
152
153### Health Check
154
155**GET /health**
156
157Response:
158```json
159{
160 "status": "ok",
161 "version": "1.0.0"
162}
163```
164
165## Configuration
166
167### Trust Policy Format
168
169```yaml
170# trust-policy.yaml
171version: 1.0
172
173trustedDIDs:
174 did:plc:alice123:
175 name: "Alice (DevOps)"
176 validFrom: "2024-01-01T00:00:00Z"
177 expiresAt: null
178
179 did:plc:bob456:
180 name: "Bob (Security)"
181 validFrom: "2024-06-01T00:00:00Z"
182 expiresAt: "2025-12-31T23:59:59Z"
183
184policies:
185 - name: production
186 scope: "atcr.io/*/prod-*"
187 require:
188 signature: true
189 trustedDIDs:
190 - did:plc:alice123
191 - did:plc:bob456
192 action: enforce
193```
194
195### Provider Configuration
196
197Environment variables:
198- `TRUST_POLICY_PATH` - Path to trust policy file (default: `/config/trust-policy.yaml`)
199- `HTTP_PORT` - HTTP server port (default: `8080`)
200- `LOG_LEVEL` - Log level: debug, info, warn, error (default: `info`)
201- `CACHE_ENABLED` - Enable caching (default: `true`)
202- `CACHE_TTL` - Cache TTL in seconds (default: `300`)
203- `DID_RESOLVER_TIMEOUT` - DID resolution timeout (default: `10s`)
204- `PDS_TIMEOUT` - PDS XRPC timeout (default: `10s`)
205
206## Rego Policy Examples
207
208### Simple Verification
209
210```rego
211package atcrsignatures
212
213import future.keywords.contains
214import future.keywords.if
215import future.keywords.in
216
217provider := "atcr-verifier"
218
219violation[{"msg": msg}] {
220 container := input.review.object.spec.containers[_]
221 startswith(container.image, "atcr.io/")
222
223 # Call external provider
224 response := external_data({
225 "provider": provider,
226 "keys": ["image"],
227 "values": [container.image]
228 })
229
230 # Check verification result
231 not response[_].verified == true
232
233 msg := sprintf("Image %v has no valid ATProto signature", [container.image])
234}
235```
236
237### Advanced Verification with DID Trust
238
239```rego
240package atcrsignatures
241
242import future.keywords.contains
243import future.keywords.if
244import future.keywords.in
245
246provider := "atcr-verifier"
247
248trusted_dids := [
249 "did:plc:alice123",
250 "did:plc:bob456"
251]
252
253violation[{"msg": msg}] {
254 container := input.review.object.spec.containers[_]
255 startswith(container.image, "atcr.io/")
256
257 # Call external provider
258 response := external_data({
259 "provider": provider,
260 "keys": ["image"],
261 "values": [container.image]
262 })
263
264 # Get response for this image
265 result := response[_]
266 result.image == container.image
267
268 # Check if verified
269 not result.verified == true
270 msg := sprintf("Image %v failed signature verification: %v", [container.image, result.error])
271}
272
273violation[{"msg": msg}] {
274 container := input.review.object.spec.containers[_]
275 startswith(container.image, "atcr.io/")
276
277 # Call external provider
278 response := external_data({
279 "provider": provider,
280 "keys": ["image"],
281 "values": [container.image]
282 })
283
284 # Get response for this image
285 result := response[_]
286 result.image == container.image
287 result.verified == true
288
289 # Check DID is trusted
290 not result.did in trusted_dids
291 msg := sprintf("Image %v signed by untrusted DID: %v", [container.image, result.did])
292}
293```
294
295### Namespace-Specific Policies
296
297```rego
298package atcrsignatures
299
300import future.keywords.contains
301import future.keywords.if
302import future.keywords.in
303
304provider := "atcr-verifier"
305
306# Production namespaces require signatures
307production_namespaces := ["production", "prod", "staging"]
308
309violation[{"msg": msg}] {
310 # Only apply to production namespaces
311 input.review.object.metadata.namespace in production_namespaces
312
313 container := input.review.object.spec.containers[_]
314 startswith(container.image, "atcr.io/")
315
316 # Call external provider
317 response := external_data({
318 "provider": provider,
319 "keys": ["image"],
320 "values": [container.image]
321 })
322
323 # Check verification result
324 not response[_].verified == true
325
326 msg := sprintf("Production namespace requires signed images. Image %v is not signed", [container.image])
327}
328```
329
330## Performance Considerations
331
332### Caching
333
334The provider caches:
335- Signature verification results (TTL: 5 minutes)
336- DID documents (TTL: 5 minutes)
337- PDS endpoints (TTL: 5 minutes)
338- Public keys (TTL: 5 minutes)
339
340Enable/disable via `CACHE_ENABLED` environment variable.
341
342### Timeouts
343
344- `DID_RESOLVER_TIMEOUT` - DID resolution timeout (default: 10s)
345- `PDS_TIMEOUT` - PDS XRPC calls timeout (default: 10s)
346- HTTP client timeout: 30s total
347
348### Horizontal Scaling
349
350The provider is stateless and can be scaled horizontally:
351
352```yaml
353apiVersion: apps/v1
354kind: Deployment
355spec:
356 replicas: 3 # Scale up for high traffic
357```
358
359### Rate Limiting
360
361Consider implementing rate limiting for:
362- Gatekeeper → Provider requests
363- Provider → DID resolver
364- Provider → PDS
365
366## Monitoring
367
368### Metrics
369
370The provider exposes Prometheus metrics at `/metrics`:
371
372```
373# Request metrics
374atcr_provider_requests_total{status="success|failure"}
375atcr_provider_request_duration_seconds
376
377# Verification metrics
378atcr_provider_verifications_total{result="verified|failed|error"}
379atcr_provider_verification_duration_seconds
380
381# Cache metrics
382atcr_provider_cache_hits_total
383atcr_provider_cache_misses_total
384```
385
386### Logging
387
388Structured JSON logging with fields:
389- `image` - Image being verified
390- `did` - Signer DID (if found)
391- `duration` - Verification duration
392- `error` - Error message (if failed)
393
394### Health Checks
395
396```bash
397# Liveness probe
398curl http://localhost:8080/health
399
400# Readiness probe
401curl http://localhost:8080/ready
402```
403
404## Troubleshooting
405
406### Provider Not Reachable
407
408```bash
409# Check provider pod status
410kubectl get pods -n gatekeeper-system -l app=atcr-provider
411
412# Check service
413kubectl get svc -n gatekeeper-system atcr-provider
414
415# Test connectivity from Gatekeeper pod
416kubectl exec -n gatekeeper-system deployment/gatekeeper-controller-manager -- \
417 curl http://atcr-provider.gatekeeper-system/health
418```
419
420### Verification Failing
421
422```bash
423# Check provider logs
424kubectl logs -n gatekeeper-system deployment/atcr-provider
425
426# Test verification manually
427kubectl run test-curl --rm -it --image=curlimages/curl -- \
428 curl -X POST http://atcr-provider.gatekeeper-system/provide \
429 -H "Content-Type: application/json" \
430 -d '{"keys":["image"],"values":["atcr.io/alice/myapp:latest"]}'
431```
432
433### Policy Not Enforcing
434
435```bash
436# Check Gatekeeper logs
437kubectl logs -n gatekeeper-system deployment/gatekeeper-controller-manager
438
439# Check constraint status
440kubectl get constraint atcr-signatures-required -o yaml
441
442# Test policy manually with conftest
443conftest test -p constraint-template.yaml pod.yaml
444```
445
446## Security Considerations
447
448### Network Policies
449
450Restrict network access:
451
452```yaml
453apiVersion: networking.k8s.io/v1
454kind: NetworkPolicy
455metadata:
456 name: atcr-provider
457 namespace: gatekeeper-system
458spec:
459 podSelector:
460 matchLabels:
461 app: atcr-provider
462 ingress:
463 - from:
464 - podSelector:
465 matchLabels:
466 control-plane: controller-manager # Gatekeeper
467 ports:
468 - port: 8080
469 egress:
470 - to: # PLC directory
471 - namespaceSelector: {}
472 ports:
473 - port: 443
474```
475
476### Authentication
477
478The provider should only be accessible from Gatekeeper. Options:
479- Network policies (recommended for Kubernetes)
480- Mutual TLS
481- API tokens
482
483### Trust Policy Management
484
485- Store trust policy in version control
486- Use GitOps (Flux, ArgoCD) for updates
487- Review DID changes carefully
488- Audit policy modifications
489
490## See Also
491
492- [Gatekeeper Documentation](https://open-policy-agent.github.io/gatekeeper/)
493- [External Data Provider](https://open-policy-agent.github.io/gatekeeper/website/docs/externaldata/)
494- [ATCR Signature Integration](../../../docs/SIGNATURE_INTEGRATION.md)
495- [ATCR Integration Strategy](../../../docs/INTEGRATION_STRATEGY.md)
496
497## Support
498
499For issues or questions:
500- GitHub Issues: https://github.com/atcr-io/atcr/issues
501- Gatekeeper GitHub: https://github.com/open-policy-agent/gatekeeper