Configure Event Webhooks
Notify external systems when registry events occur: manifest pushes and pulls, blob uploads and pulls, tag operations, and more.
Prerequisites
- Angos running
- External HTTP/HTTPS endpoint to receive events
Basic Configuration
Define a Webhook
[event_webhook.notifications]
url = "https://hooks.example.com/registry"
policy = "optional"
events = ["manifest.push", "tag.create"]
Enable Globally
[global]
event_webhooks = ["notifications"]
Or Per-Repository
[repository."production"]
event_webhooks = ["notifications"]
Delivery Policies
Required: Block on Failure
The registry waits for a successful response. If the webhook fails, the client operation fails too.
[event_webhook.audit]
url = "https://audit.example.com/events"
policy = "required"
events = ["manifest.push", "manifest.delete"]
timeout_ms = 3000
max_retries = 2
Use this when the external system must acknowledge the event before it is considered complete (e.g., audit logging, compliance).
Optional: Log on Failure
The registry waits for a response but does not fail the client operation if the webhook fails.
[event_webhook.monitoring]
url = "https://monitoring.example.com/events"
policy = "optional"
events = ["manifest.push", "blob.push", "tag.create"]
timeout_ms = 2000
max_retries = 1
Use this for best-effort notifications where delivery is important but not critical.
Async: Fire and Forget
The registry dispatches the event in the background and returns to the client immediately.
[event_webhook.analytics]
url = "https://analytics.example.com/ingest"
policy = "async"
events = ["manifest.push", "manifest.delete", "blob.push", "tag.create", "tag.delete"]
Use this when latency matters more than delivery guarantees (e.g., analytics, dashboards).
Pull Tracking
manifest.pull and blob.pull fire on every successful GET. They are
high-volume, so subscribe with the async policy: required and optional
put a webhook round-trip on the critical path of every pull.
[event_webhook.pull_tracking]
url = "https://analytics.example.com/pulls"
policy = "async"
events = ["manifest.pull", "blob.pull"]
Authentication
Bearer Token with HMAC Signing
When a token is configured, the registry sends it as a Bearer token and signs the payload with HMAC-SHA256:
[event_webhook.secure]
url = "https://hooks.example.com/events"
policy = "optional"
token = "my-webhook-secret"
events = ["manifest.push"]
The registry sends these headers:
| Header | Value |
|---|---|
Authorization | Bearer my-webhook-secret |
X-Registry-Signature-256 | sha256=<hmac-hex-digest> |
X-Registry-Event | Event type (e.g. manifest.push) |
Content-Type | application/json |
Verify HMAC Signatures
When you configure a token, verify the signature on your webhook endpoint to ensure the payload is authentic.
Python
import hashlib
import hmac
def verify_signature(secret: str, body: bytes, signature_header: str) -> bool:
"""Verify X-Registry-Signature-256 header."""
if not signature_header.startswith("sha256="):
return False
expected = hmac.new(
secret.encode(),
body,
hashlib.sha256
).hexdigest()
received = signature_header[len("sha256="):]
return hmac.compare_digest(expected, received)
# Usage in Flask:
from flask import Flask, request, abort
app = Flask(__name__)
@app.route('/events', methods=['POST'])
def handle_event():
signature = request.headers.get('X-Registry-Signature-256', '')
if not verify_signature('my-webhook-secret', request.data, signature):
abort(401)
event = request.json
print(f"Received {event['kind']} for {event['namespace']}")
return '', 200
Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"strings"
)
func verifySignature(secret string, body []byte, signatureHeader string) bool {
if !strings.HasPrefix(signatureHeader, "sha256=") {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
received := signatureHeader[len("sha256="):]
return hmac.Equal([]byte(expected), []byte(received))
}
Repository Filters
Scope webhooks to specific repositories using regex patterns:
[event_webhook.production-only]
url = "https://hooks.example.com/production"
policy = "required"
events = ["manifest.push", "manifest.delete"]
repository_filter = ["^production/.*"]
Multiple patterns are OR-matched (any pattern matching triggers delivery):
[event_webhook.important-repos]
url = "https://hooks.example.com/important"
policy = "optional"
events = ["manifest.push"]
repository_filter = ["^production/.*", "^staging/.*", "^library/.*"]
Without repository_filter, the webhook receives events from all repositories.
Retry Behavior
For required and optional policies, retries use exponential backoff:
Attempt 1: immediate
Attempt 2: 100ms delay
Attempt 3: 200ms delay
Attempt 4: 400ms delay
...
Formula: 100ms * 2^(attempt - 1), capped at 10 seconds
[event_webhook.reliable]
url = "https://hooks.example.com/events"
policy = "required"
events = ["manifest.push"]
max_retries = 3 # Up to 3 retries after the initial attempt (maximum supported value: 16; higher values are rejected at configuration load time to prevent retry storms and arithmetic overflow in the exponential-backoff calculation)
timeout_ms = 5000 # Per-request timeout
Multiple Webhooks
Configure different webhooks for different purposes:
# Audit log: must succeed
[event_webhook.audit]
url = "https://audit.example.com/events"
policy = "required"
token = "audit-secret"
events = ["manifest.push", "manifest.delete"]
max_retries = 3
# Slack notifications: best effort
[event_webhook.slack]
url = "https://hooks.slack.com/services/T.../B.../xxx"
policy = "optional"
events = ["manifest.push", "tag.create"]
timeout_ms = 3000
# Analytics: fire and forget
[event_webhook.analytics]
url = "https://analytics.internal/ingest"
policy = "async"
events = ["manifest.push", "manifest.delete", "blob.push", "tag.create", "tag.delete"]
# Enable all globally
[global]
event_webhooks = ["audit", "slack", "analytics"]
# Production repos get audit only
[repository."production"]
event_webhooks = ["audit"]
Hot Reloading
Event webhook configuration supports hot reloading. Changes to webhook definitions take effect without restarting the server:
- Adding new webhooks
- Removing webhooks
- Changing webhook URLs, tokens, policies, or filters
In-flight deliveries on the old configuration continue to completion.
Graceful Shutdown
On SIGTERM or SIGINT, the registry drains all in-flight async webhook deliveries to completion before shutting down. Each delivery is bounded by its own request timeout and retry budget, so the drain terminates on its own; no delivery is aborted.
Monitoring
Prometheus metrics for event webhooks:
# Delivery rate by webhook and result
sum by (webhook, result) (rate(event_webhook_deliveries_total[5m]))
# Error rate for a specific webhook
rate(event_webhook_deliveries_total{webhook="audit", result="error"}[5m])
# P95 delivery latency
histogram_quantile(0.95, rate(event_webhook_delivery_duration_seconds_bucket[5m]))
# Delivery latency by webhook
histogram_quantile(0.95, sum by (webhook, le) (rate(event_webhook_delivery_duration_seconds_bucket[5m])))
Troubleshooting
Webhook not receiving events:
- Verify the webhook name is listed in
event_webhooks(global or repository) - Check that the event type is in the webhook's
eventslist - If using
repository_filter, confirm the pattern matches the repository name
Required webhook failing client operations:
- Switch to
optionalpolicy while debugging - Increase
timeout_msif the endpoint is slow - Add
max_retriesfor transient failures
Events delayed or missing with async policy:
- Async deliveries run in the background; check server logs for errors
- Verify the endpoint is healthy and responding
Debug logging:
RUST_LOG=angos::event_webhook=debug ./angos server
Reference
- Event Webhooks Reference - Configuration options and payload schema
- Metrics Reference - Webhook delivery metrics