OPA Gatekeeper External Data Provider for ATProto Signatures#
This is a reference implementation of an OPA Gatekeeper External Data Provider that verifies ATProto signatures on ATCR container images.
Overview#
Gatekeeper'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.
Architecture#
Kubernetes Pod Creation
↓
OPA Gatekeeper (admission webhook)
↓
Rego Policy (constraint template)
↓
External Data Provider API call
↓
ATProto Verification Service ← This service
↓
1. Resolve image digest
2. Discover signature artifacts
3. Parse ATProto signature metadata
4. Resolve DID to public key
5. Fetch commit from PDS
6. Verify K-256 signature
7. Check trust policy
↓
Return: verified=true/false + metadata
Files#
main.go- HTTP server and provider endpointsverifier.go- ATProto signature verification logicresolver.go- DID and PDS resolutioncrypto.go- K-256 signature verificationtrust-policy.yaml- Trust policy configurationDockerfile- Build provider service imagedeployment.yaml- Kubernetes deployment manifestprovider-crd.yaml- Gatekeeper Provider custom resourceconstraint-template.yaml- Rego constraint templateconstraint.yaml- Policy constraint example
Prerequisites#
- Go 1.21+
- Kubernetes cluster with OPA Gatekeeper installed
- Access to ATCR registry
Building#
# Build binary
CGO_ENABLED=0 go build -o atcr-provider \
-ldflags="-w -s" \
./main.go
# Build Docker image
docker build -t atcr.io/atcr/gatekeeper-provider:latest .
# Push to registry
docker push atcr.io/atcr/gatekeeper-provider:latest
Deployment#
1. Create Trust Policy ConfigMap#
kubectl create namespace gatekeeper-system
kubectl create configmap atcr-trust-policy \
--from-file=trust-policy.yaml \
-n gatekeeper-system
2. Deploy Provider Service#
kubectl apply -f deployment.yaml
3. Configure Gatekeeper Provider#
kubectl apply -f provider-crd.yaml
4. Create Constraint Template#
kubectl apply -f constraint-template.yaml
5. Create Constraint#
kubectl apply -f constraint.yaml
6. Test#
# Try to create pod with signed image (should succeed)
kubectl run test-signed --image=atcr.io/alice/myapp:latest
# Try to create pod with unsigned image (should fail)
kubectl run test-unsigned --image=atcr.io/malicious/fake:latest
# Check constraint status
kubectl get constraint atcr-signatures-required -o yaml
API Specification#
Provider Endpoint#
POST /provide
Request:
{
"keys": ["image"],
"values": [
"atcr.io/alice/myapp:latest",
"atcr.io/bob/webapp:v1.0"
]
}
Response:
{
"responses": [
{
"image": "atcr.io/alice/myapp:latest",
"verified": true,
"did": "did:plc:alice123",
"handle": "alice.bsky.social",
"signedAt": "2025-10-31T12:34:56Z",
"commitCid": "bafyreih8..."
},
{
"image": "atcr.io/bob/webapp:v1.0",
"verified": false,
"error": "no signature found"
}
]
}
Health Check#
GET /health
Response:
{
"status": "ok",
"version": "1.0.0"
}
Configuration#
Trust Policy Format#
# trust-policy.yaml
version: 1.0
trustedDIDs:
did:plc:alice123:
name: "Alice (DevOps)"
validFrom: "2024-01-01T00:00:00Z"
expiresAt: null
did:plc:bob456:
name: "Bob (Security)"
validFrom: "2024-06-01T00:00:00Z"
expiresAt: "2025-12-31T23:59:59Z"
policies:
- name: production
scope: "atcr.io/*/prod-*"
require:
signature: true
trustedDIDs:
- did:plc:alice123
- did:plc:bob456
action: enforce
Provider Configuration#
Environment variables:
TRUST_POLICY_PATH- Path to trust policy file (default:/config/trust-policy.yaml)HTTP_PORT- HTTP server port (default:8080)LOG_LEVEL- Log level: debug, info, warn, error (default:info)CACHE_ENABLED- Enable caching (default:true)CACHE_TTL- Cache TTL in seconds (default:300)DID_RESOLVER_TIMEOUT- DID resolution timeout (default:10s)PDS_TIMEOUT- PDS XRPC timeout (default:10s)
Rego Policy Examples#
Simple Verification#
package atcrsignatures
import future.keywords.contains
import future.keywords.if
import future.keywords.in
provider := "atcr-verifier"
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
startswith(container.image, "atcr.io/")
# Call external provider
response := external_data({
"provider": provider,
"keys": ["image"],
"values": [container.image]
})
# Check verification result
not response[_].verified == true
msg := sprintf("Image %v has no valid ATProto signature", [container.image])
}
Advanced Verification with DID Trust#
package atcrsignatures
import future.keywords.contains
import future.keywords.if
import future.keywords.in
provider := "atcr-verifier"
trusted_dids := [
"did:plc:alice123",
"did:plc:bob456"
]
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
startswith(container.image, "atcr.io/")
# Call external provider
response := external_data({
"provider": provider,
"keys": ["image"],
"values": [container.image]
})
# Get response for this image
result := response[_]
result.image == container.image
# Check if verified
not result.verified == true
msg := sprintf("Image %v failed signature verification: %v", [container.image, result.error])
}
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
startswith(container.image, "atcr.io/")
# Call external provider
response := external_data({
"provider": provider,
"keys": ["image"],
"values": [container.image]
})
# Get response for this image
result := response[_]
result.image == container.image
result.verified == true
# Check DID is trusted
not result.did in trusted_dids
msg := sprintf("Image %v signed by untrusted DID: %v", [container.image, result.did])
}
Namespace-Specific Policies#
package atcrsignatures
import future.keywords.contains
import future.keywords.if
import future.keywords.in
provider := "atcr-verifier"
# Production namespaces require signatures
production_namespaces := ["production", "prod", "staging"]
violation[{"msg": msg}] {
# Only apply to production namespaces
input.review.object.metadata.namespace in production_namespaces
container := input.review.object.spec.containers[_]
startswith(container.image, "atcr.io/")
# Call external provider
response := external_data({
"provider": provider,
"keys": ["image"],
"values": [container.image]
})
# Check verification result
not response[_].verified == true
msg := sprintf("Production namespace requires signed images. Image %v is not signed", [container.image])
}
Performance Considerations#
Caching#
The provider caches:
- Signature verification results (TTL: 5 minutes)
- DID documents (TTL: 5 minutes)
- PDS endpoints (TTL: 5 minutes)
- Public keys (TTL: 5 minutes)
Enable/disable via CACHE_ENABLED environment variable.
Timeouts#
DID_RESOLVER_TIMEOUT- DID resolution timeout (default: 10s)PDS_TIMEOUT- PDS XRPC calls timeout (default: 10s)- HTTP client timeout: 30s total
Horizontal Scaling#
The provider is stateless and can be scaled horizontally:
apiVersion: apps/v1
kind: Deployment
spec:
replicas: 3 # Scale up for high traffic
Rate Limiting#
Consider implementing rate limiting for:
- Gatekeeper → Provider requests
- Provider → DID resolver
- Provider → PDS
Monitoring#
Metrics#
The provider exposes Prometheus metrics at /metrics:
# Request metrics
atcr_provider_requests_total{status="success|failure"}
atcr_provider_request_duration_seconds
# Verification metrics
atcr_provider_verifications_total{result="verified|failed|error"}
atcr_provider_verification_duration_seconds
# Cache metrics
atcr_provider_cache_hits_total
atcr_provider_cache_misses_total
Logging#
Structured JSON logging with fields:
image- Image being verifieddid- Signer DID (if found)duration- Verification durationerror- Error message (if failed)
Health Checks#
# Liveness probe
curl http://localhost:8080/health
# Readiness probe
curl http://localhost:8080/ready
Troubleshooting#
Provider Not Reachable#
# Check provider pod status
kubectl get pods -n gatekeeper-system -l app=atcr-provider
# Check service
kubectl get svc -n gatekeeper-system atcr-provider
# Test connectivity from Gatekeeper pod
kubectl exec -n gatekeeper-system deployment/gatekeeper-controller-manager -- \
curl http://atcr-provider.gatekeeper-system/health
Verification Failing#
# Check provider logs
kubectl logs -n gatekeeper-system deployment/atcr-provider
# Test verification manually
kubectl run test-curl --rm -it --image=curlimages/curl -- \
curl -X POST http://atcr-provider.gatekeeper-system/provide \
-H "Content-Type: application/json" \
-d '{"keys":["image"],"values":["atcr.io/alice/myapp:latest"]}'
Policy Not Enforcing#
# Check Gatekeeper logs
kubectl logs -n gatekeeper-system deployment/gatekeeper-controller-manager
# Check constraint status
kubectl get constraint atcr-signatures-required -o yaml
# Test policy manually with conftest
conftest test -p constraint-template.yaml pod.yaml
Security Considerations#
Network Policies#
Restrict network access:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: atcr-provider
namespace: gatekeeper-system
spec:
podSelector:
matchLabels:
app: atcr-provider
ingress:
- from:
- podSelector:
matchLabels:
control-plane: controller-manager # Gatekeeper
ports:
- port: 8080
egress:
- to: # PLC directory
- namespaceSelector: {}
ports:
- port: 443
Authentication#
The provider should only be accessible from Gatekeeper. Options:
- Network policies (recommended for Kubernetes)
- Mutual TLS
- API tokens
Trust Policy Management#
- Store trust policy in version control
- Use GitOps (Flux, ArgoCD) for updates
- Review DID changes carefully
- Audit policy modifications
See Also#
- Gatekeeper Documentation
- External Data Provider
- ATCR Signature Integration
- ATCR Integration Strategy
Support#
For issues or questions:
- GitHub Issues: https://github.com/atcr-io/atcr/issues
- Gatekeeper GitHub: https://github.com/open-policy-agent/gatekeeper