Skip to main content

Configure Webhook Authorization

Delegate access control decisions to an external HTTP service for maximum flexibility.

Prerequisites

  • Angos running
  • External authorization service accessible via HTTP/HTTPS

How Webhooks Work

  1. Client makes a request to the registry
  2. Registry sends request context to webhook via HTTP GET
  3. Webhook response is classified into three buckets (see table below)
  4. Registry proceeds or rejects based on classification

Response Classification

StatusDecisionCached?
2xxAllowYes
401, 403Explicit denyYes
429, 5xx, other 4xxUnavailableNo
Transport errorUnavailableNo

Unavailable responses fail the in-flight request closed but do not write to the cache, so the next request re-probes the webhook. As soon as the webhook returns 2xx or 401/403, normal caching resumes.


Basic Configuration

Define a Webhook

[auth.webhook.my-auth]
url = "https://auth.example.com/authorize"
timeout_ms = 1000

Enable Globally

[global]
authorization_webhook = "my-auth"

Or Per-Repository

[repository."sensitive"]
authorization_webhook = "my-auth"

Authentication Options

Bearer Token

[auth.webhook.api-auth]
url = "https://api.example.com/authorize"
timeout_ms = 1000
bearer_token = "secret-api-key"

Basic Authentication

[auth.webhook.basic-auth]
url = "https://service.example.com/authorize"
timeout_ms = 1000
basic_auth = { username = "webhook", password = "secret" }

Mutual TLS

[auth.webhook.mtls-auth]
url = "https://secure.example.com/authorize"
timeout_ms = 1000
client_certificate_bundle = "/certs/client.pem"
client_private_key = "/certs/client-key.pem"
server_ca_bundle = "/certs/ca.pem"

Combined

[auth.webhook.secure-api]
url = "https://secure-api.example.com/authorize"
timeout_ms = 1000
bearer_token = "api-key"
client_certificate_bundle = "/certs/client.pem"
client_private_key = "/certs/client-key.pem"

Webhook Protocol

The registry sends GET requests with headers containing request context.

Always Included

HeaderDescription
X-Forwarded-MethodOriginal HTTP method
X-Forwarded-ProtoProtocol (http/https)
X-Forwarded-HostOriginal Host header
X-Forwarded-UriComplete request URI
X-Forwarded-ForClient IP address

Registry-Specific

HeaderDescription
X-Registry-ActionOperation type (get-manifest, put-manifest, etc.)
X-Registry-NamespaceRepository namespace
X-Registry-ReferenceTag or digest reference
X-Registry-DigestBlob digest

Identity (when authenticated)

HeaderDescription
X-Registry-UsernameBasic auth or OIDC subject
X-Registry-Identity-IDIdentity identifier
X-Registry-Certificate-CNCertificate Common Name
X-Registry-Certificate-OCertificate Organization

Forward Client Headers

Pass specific client headers to the webhook:

[auth.webhook.header-aware]
url = "https://auth.example.com/authorize"
timeout_ms = 1000
forward_headers = [
"X-Custom-Token",
"X-Request-ID",
"Authorization"
]

Response Caching

Webhook responses are cached to reduce load:

[auth.webhook.cached]
url = "https://auth.example.com/authorize"
timeout_ms = 1000
cache_ttl = 60 # Cache for 60 seconds (default)

Disable caching:

cache_ttl = 0

Multiple Webhooks

Configure different webhooks for different purposes:

# Standard authorization
[auth.webhook.standard]
url = "https://auth.example.com/authorize"
timeout_ms = 1000

# Enhanced for sensitive repos
[auth.webhook.enhanced]
url = "https://secure-auth.example.com/authorize"
timeout_ms = 2000
client_certificate_bundle = "/certs/client.pem"
client_private_key = "/certs/client-key.pem"

# Quota checking
[auth.webhook.quotas]
url = "http://quotas.internal:8080/check"
timeout_ms = 500

# Apply to repositories
[global]
authorization_webhook = "standard"

[repository."public"]
authorization_webhook = "" # Disable for public

[repository."sensitive"]
authorization_webhook = "enhanced"

[repository."limited"]
authorization_webhook = "quotas"

Example Webhook Service

Simple Python webhook:

from flask import Flask, request

app = Flask(__name__)

@app.route('/authorize', methods=['GET'])
def authorize():
action = request.headers.get('X-Registry-Action', '')
namespace = request.headers.get('X-Registry-Namespace', '')
username = request.headers.get('X-Registry-Username', '')

# Allow read operations
if action.startswith('get-') or action == 'list-tags':
return '', 200

# Require authentication for writes
if not username:
return 'Unauthorized', 401

# Check quota, permissions, etc.
if check_quota(username, namespace):
return '', 200

return 'Quota exceeded', 403

if __name__ == '__main__':
app.run(port=8080)

Integration with CEL Policies

Webhooks run after CEL policies. Use CEL for fast common rules:

# CEL handles basic checks
[global.access_policy]
default = "deny"
rules = [
"identity.username != null"
]

# Webhook handles complex logic
[global]
authorization_webhook = "business-rules"

[auth.webhook.business-rules]
url = "https://auth.example.com/check"
timeout_ms = 2000

Monitoring

Prometheus metrics:

# Request rate by result
rate(webhook_authorization_requests_total[5m])

# Cache hit rate
sum(rate(webhook_authorization_requests_total{result=~"cached_.*"}[5m])) /
sum(rate(webhook_authorization_requests_total[5m]))

# Webhook latency
histogram_quantile(0.95, rate(webhook_authorization_duration_seconds_bucket[5m]))

# Webhook unavailability rate (429, 5xx, and other non-2xx/non-401/403 responses)
rate(webhook_authorization_requests_total{result="unavailable"}[5m])

The result label values on webhook_authorization_requests_total are:

ValueMeaning
allowWebhook returned 2xx; decision cached
denyWebhook returned 401 or 403; explicit denial cached
unavailableWebhook returned 429, 5xx, or other non-decision status; not cached
transport_errorNetwork/TLS failure reaching the webhook; not cached
cached_allowDecision served from cache (allow)
cached_denyDecision served from cache (deny)

Troubleshooting

All requests denied:

  • Check webhook is returning 2xx
  • Verify network connectivity
  • Check timeout is sufficient

Webhook not called:

  • Verify webhook name matches configuration
  • Check if CEL policies deny first
  • Ensure repository doesn't override with empty webhook

Debug logging:

RUST_LOG=angos::auth::webhook=debug \
./angos server

Reference