Skip to main content

Configuration Reference

Angos is configured via a TOML file (default: config.toml). The configuration is automatically reloaded when the file changes.

Multiple Configuration Files

-c is repeatable. Files are merged in the order given, and a later file wins:

angos -c /etc/angos/config.toml -c /etc/angos/secrets.toml server

This keeps credentials out of the file your deployment tooling renders. The base file carries everything else, and a second file, sourced from a Kubernetes Secret written by External Secrets Operator, Vault or sealed-secrets, supplies only the sensitive values:

# config.toml
[blob_store.s3]
endpoint = "https://s3.example.com"
bucket = "my-registry"
region = "us-east-1"
# secrets.toml
[blob_store.s3]
access_key_id = "AKIA..."
secret_key = "..."

Merge rules:

ValueBehaviour
TableMerged recursively, so a later file can add keys to a table an earlier file opened
ScalarReplaced by the last file that sets it
ArrayReplaced as a whole, never appended to

Because arrays are replaced, an array of tables such as [[repository."hub".upstream]] must be declared entirely in one file. To give an upstream a password from a separate file, repeat the whole entry there.

Only the merged result has to be a valid configuration, so an overriding file holds just the keys it changes. Every file is watched, so rotating any of them reloads the merged configuration without a restart.

A syntax error names the file it came from. An error detected after merging names the offending key path and the files that were merged, because a merged document has no single source line to quote.

Hot Reloading

Most configuration changes take effect immediately without restart. The following options require a restart:

  • server.bind_address
  • server.port
  • observability.tracing.sampling_rate
  • Enabling or disabling TLS
  • Changing storage backend type (filesystem ↔ S3)
  • Changing lock strategy
  • Adding or removing [global.job_queue]
  • max_concurrent_cache_jobs / max_concurrent_replication_jobs on a standalone angos worker: the worker's pool size is fixed at startup, so a running worker must be restarted to change it (the server's in-process drain applies the change on reload).

TLS certificate files are also automatically reloaded when they change.


Server (server)

OptionTypeDefaultDescription
bind_addressstringrequiredAddress to bind (e.g., "0.0.0.0", "127.0.0.1")
portu168000Port number
query_timeoutnon-zero u643600Query timeout in seconds
query_timeout_grace_periodnon-zero u6460Grace period for queries in seconds
handshake_timeoutnon-zero u6410Seconds a client may take to finish its handshake

Timeout values must be greater than zero.

TLS (server.tls)

When omitted, the server runs without TLS (insecure). A section that is present but incomplete or invalid fails startup rather than falling back to a plaintext listener.

OptionTypeDefaultDescription
server_certificate_bundlestringrequiredPath to server certificate (PEM)
server_private_keystringrequiredPath to server private key (PEM)
client_ca_bundlestring-Path to client CA bundle for mTLS
client_authstring"optional""optional" accepts both anonymous clients and clients whose certificate validates against client_ca_bundle; "required" rejects the TLS handshake without a valid client certificate and needs client_ca_bundle set. Ignored when client_ca_bundle is unset

Global Options (global)

OptionTypeDefaultDescription
max_concurrent_requestsusize64Tokio worker threads (see Performance Tuning)
max_concurrent_cache_jobsusize4Maximum concurrent cache jobs (minimum 1). With [global.job_queue] enabled, also bounds the number of jobs each angos worker processes in parallel.
max_concurrent_replication_jobsnon-zero usize4Concurrency for replication jobs (minimum 1). Bounds how many replication pushes are handled in parallel by each angos worker, the server's in-process drain, and the angos replicate end-of-run drain.
max_manifest_sizestring"5MiB"Maximum manifest body size accepted from clients or upstream registries
max_blob_sizestring"100GiB"Maximum total size of a single blob upload; a larger upload is rejected with BLOB_UPLOAD_INVALID (HTTP 413)
update_pull_timeboolfalseTrack pull times for retention policies
enable_blob_redirectbooltrueAllow HTTP 307 redirects for blob downloads.
enable_manifest_redirectbooltrueAllow HTTP 307 redirects for manifest downloads. Manifest bodies served via response-content-type to preserve the media type across redirects.
immutable_tagsboolfalseGlobal immutable tags default
immutable_tags_exclusions[string][]Regex patterns for mutable tags
allow_missing_manifest_referencesbooltrueWhen true (default), accept a manifest push whose referenced blobs or child manifests are not yet present/owned in the namespace; the missing references stay unreadable until their content is pushed. Set to false to reject such pushes with MANIFEST_BLOB_UNKNOWN. See note below.
authorization_webhookstring-Name of webhook for authorization
event_webhooks[string][]Event webhook names for all repositories
shutdown_drain_secsu6430Seconds to keep draining in-flight work on shutdown before forcing exit.
namespace_walk_concurrencyusize128Concurrent directory scans a catalog / upload-namespace walk keeps in flight, hiding per-request backend latency on S3.
trusted_proxies[string][]Proxy IPs or CIDR networks (e.g. "10.0.0.1", "10.0.0.0/8") whose X-Forwarded-For/X-Real-IP headers are honored as the client IP. From any other peer those headers are ignored and the socket address is used.

max_manifest_size and max_blob_size must be greater than zero.

allow_missing_manifest_references

This controls whether the live manifest-push path enforces the OCI distribution-spec option of rejecting a manifest whose descriptors reference content the registry does not have.

  • true (default). A push is accepted even if a referenced config, layer, or child manifest is absent from or not owned by the target namespace. The unowned references are not granted to the namespace: they resolve as unknown on a later pull (BLOB_UNKNOWN for a blob, MANIFEST_UNKNOWN for a child manifest) until their content is pushed. This maximizes compatibility with clients such as docker buildx/bake, which push multi-manifest image indexes and provenance/SBOM attestations whose children are not always namespace-local at validation time.
  • false. A push whose references are missing is rejected outright with MANIFEST_BLOB_UNKNOWN (HTTP 404). This is stricter and conformance-oriented.

Either setting preserves namespace isolation: a caller never gains read access to a blob digest it never uploaded. Inbound replicated manifest pushes follow the same rule; angos-to-angos replication pushes a manifest's children and blobs before the manifest itself, so its references are always owned. subject references (referrers) are always accepted regardless of this setting, per the spec. Pull-through cache-fill writes are trusted, independent of this flag.

Durable Job Queue (global.job_queue)

Optional. Controls where the job queue is drained. When absent (default), angos server drains the queue itself in-process. When present, angos server enqueues jobs and publishes the queue-depth gauge on /metrics, and one or more angos worker processes drain it. Either way, jobs persist under the [metadata_store] backend's _jobs/ prefix and survive restarts.

The queue does not have its own storage backend. Durable jobs are written to the same backend configured for [metadata_store] (filesystem or S3, whichever metadata uses), under a hardcoded top-level _jobs/ prefix; the lock strategy is likewise inherited from [metadata_store]. There is therefore no job-queue-level backend, credential, prefix, or lock-strategy setting: the section accepts only the tunables below.

A shared lock strategy is required. The durable queue is drained by separate processes, so the per-job execution lock must be shared across them. On an S3 metadata store whose provider supports conditional operations this holds by default: the unset lock strategy resolves to the shared S3 lock. The in-process memory lock cannot coordinate across processes, so [global.job_queue] combined with a memory lock strategy is rejected at startup. On providers without conditional operations and on filesystem metadata stores, configure [metadata_store.s3.lock_strategy.redis] / [metadata_store.fs.lock_strategy.redis] (Redis), or omit [global.job_queue] to use the single-process in-process queue.

OptionTypeDefaultDescription
pending_refresh_interval_secsu6415How often the server refreshes the angos_job_queue_pending gauge. Must be at least 5 (sub-5s ticks induce LIST storms on S3).
pending_ready_horizon_secsu64600Readiness horizon for the angos_job_queue_pending gauge. Only envelopes whose not_before falls within [..., now + horizon] are counted. Set comfortably larger than your worker pod startup time so KEDA has lead time to scale up before the work becomes claimable.
max_attemptsu325Times a failing job is retried before it is dead-lettered.
retry_backoff_min_msu64100First retry backoff delay; the exponential schedule grows from here.
retry_backoff_max_msu6410000Ceiling on the exponential retry backoff.

Per-lock_key execution TTL is governed by the lock backend. A worker that claims a job holds the lock configured under [metadata_store] for the duration of execution; the TTL on the lock object ([metadata_store.fs.lock_strategy.redis].ttl, [metadata_store.s3.lock_strategy.s3].ttl_secs) is what bounds how long another worker has to wait if the holder dies mid-job. Transient heartbeat failures (connect or refresh errors) tolerate a small budget (one heartbeat tick short of the TTL) before cancelling the job, so a brief network blip does not waste in-progress work; authoritative signals (ownership loss, max-hold expiry, missing lock object) cancel immediately.

See Enable Durable Cache Jobs for a full setup guide including angos worker invocation and KEDA autoscaling.

Global Access Policy (global.access_policy)

OptionTypeDefaultDescription
defaultstring"deny"Default action when no rules match ("allow" or "deny").
rules[string][]CEL expressions for access control

Global Retention Policy (global.retention_policy)

OptionTypeDefaultDescription
rules[string][]CEL expressions for retention

Cache (cache)

Token and key cache configuration. Defaults to in-memory (not suitable for multi-replica).

Redis Cache (cache.redis)

OptionTypeDefaultDescription
urlstringrequiredRedis URL (e.g., "redis://localhost:6379")
key_prefixstringrequiredPrefix for cache keys

Blob Storage (blob_store)

Required. Choose one: blob_store.fs or blob_store.s3. A configuration naming neither fails to load, and so does an fs backend whose root_dir is empty, which would otherwise resolve every object against the process working directory.

Filesystem (blob_store.fs)

OptionTypeDefaultDescription
root_dirstringrequiredDirectory for blob storage
sync_to_diskboolfalseForce fsync after writes

S3 (blob_store.s3)

OptionTypeDefaultDescription
access_key_idstringrequiredAWS access key ID
secret_keystringrequiredAWS secret key
endpointstringrequiredS3 endpoint URL
bucketstringrequiredS3 bucket name
regionstringrequiredAWS region
key_prefixstring-Prefix for S3 keys
multipart_part_sizestring"50MiB"Minimum multipart part size
multipart_copy_thresholdstring"5GB"Blob size above which S3 upload completion uses multipart copy
multipart_copy_chunk_sizestring"100MB"Server-side part size for multipart copy
multipart_copy_jobsusize4Max concurrent multipart copy jobs
multipart_uniform_partsboolfalseUse uniform multipart upload mode
max_attemptsu323Retry attempts for S3 operations
operation_timeout_secsu64900Total operation timeout
operation_attempt_timeout_secsu64300Per-attempt timeout
circuit_breaker_thresholdu325Consecutive failures that trip the circuit breaker open
circuit_breaker_cooldown_secsu6410Seconds the tripped breaker stays open before a half-open probe
children_scan_concurrencyusize16Concurrent range chains a truncated children/flat scan fans out to
presign_ttl_secsu641800Lifetime of a generated presigned download URL

S3 Blob Upload Modes

The registry supports two modes for uploading blobs to S3, controlled by multipart_uniform_parts:

Non-uniform mode (default, multipart_uniform_parts = false)

Each OCI PATCH request streams into a long-lived S3 multipart upload, with no intermediate objects or assembly phase. When the client completes the upload with a PUT request, the multipart upload is finalized and the blob is copied to its content-addressed path. This mode works with most S3-compatible providers.

A PATCH that carries a Content-Length is uploaded directly as an UploadPart with that known length. A chunked PATCH (no Content-Length, as docker push sends) is streamed to EOF. When multipart_part_size is above the 5 MiB floor, it is coalesced server-side into part_size parts via UploadPartCopy, buffering at most one 5 MiB sub-part and restaging the trailing remainder. When multipart_part_size is exactly 5 MiB, it streams plain 5 MiB parts directly with no coalescing.

Memory usage per upload: for a known-length PATCH, up to one ~1 MiB streaming read frame, with no data buffered beyond the current frame. For a coalesced chunked PATCH (multipart_part_size above the 5 MiB floor), at most one buffered 5 MiB sub-part, at the cost of moving each byte twice within S3 (into a scratch object, then UploadPartCopy into the upload).

Uniform mode (multipart_uniform_parts = true)

A long-lived S3 multipart upload is maintained across all PATCH requests. Both a known-length PATCH and a chunked PATCH (no Content-Length, as docker push sends) commit non-final parts of exactly multipart_part_size bytes; the final part may be smaller. The S3 protocol only requires non-final parts to be ≥ 5 MiB; uniform sizing is an additional constraint imposed by some S3 storage providers. Use this mode only if your provider rejects uploads with variable part sizes.

Memory usage per upload: streaming read frames for full parts, plus at most one trailing staged chunk smaller than multipart_part_size. A chunked PATCH buffers up to one multipart_part_size part, the same as the known-length remainder.

# Most S3 providers (AWS S3, Exoscale, etc.)
[blob_store.s3]
multipart_uniform_parts = false # Default

# Strict S3 providers (if non-uniform mode fails)
[blob_store.s3]
multipart_uniform_parts = true

Metadata Storage (metadata_store)

Optional. Defaults to same backend as blob store.

Lock Strategy Compatibility

The following table shows which lock strategies are supported with each metadata store backend:

Lock StrategyS3 metadata storeFS metadata store
memoryYesYes
redisYesYes
s3YesNo

Filesystem (metadata_store.fs)

OptionTypeDefaultDescription
root_dirstring-Directory for metadata (defaults to blob store)
sync_to_diskboolfalseForce fsync after writes
lock_strategystring/table"memory"Lock backend: "memory" (string), or [lock_strategy.redis] (table form). S3 locking not supported.

Note: The S3 lock strategy is not supported for filesystem metadata stores. Use "memory" for single-instance deployments or [lock_strategy.redis] for multi-replica deployments.

S3 (metadata_store.s3)

Same connection options as blob_store.s3, plus:

OptionTypeDefaultDescription
link_cache_ttlu6430Read-through cache TTL for link metadata, in seconds (0 to disable)
access_time_debounce_secsu6460Buffer access time writes and flush periodically, in seconds (0 to disable). Lock-coordinated deployments only; ignored with CAS
lock_strategystring/tablesee belowLock backend: "memory" (string), or [lock_strategy.s3]/[lock_strategy.redis] (table form, see below). Unset: s3 when the provider supports conditional operations, memory otherwise
conditional_operationsbool-Declares provider support for the conditional operations Angos coordinates with; omit to probe at startup (see below)

The link cache reduces S3 round-trips for repeated tag/layer reads. On lock-coordinated deployments, the access time debounce batches last_pulled_at timestamp writes in memory and flushes them periodically, reducing the critical-path operations per manifest pull from 4 (lock, read, write, unlock) to 1 (read). With CAS, every pull stamps the access time inline with one conditional write (a lost race is a no-op) and the debounce setting is ignored.

Conditional Operations (metadata_store.s3.conditional_operations)

When using S3 as the metadata store, you can declare whether your S3-compatible provider supports the conditional operations Angos coordinates with. The set is all-or-nothing: PutObject with If-None-Match: *, PutObject with If-Match: <etag>, and DeleteObject with If-Match: <etag>. Declaring it avoids a startup probe and, when true, enables optimistic (CAS) metadata updates.

Probe behavior:

  • When conditional_operations is omitted, the probe runs at startup (and on each config reload that has no cached value); the probed verdict is cached for subsequent reloads. The probe verifies each operation, including that the provider actually enforces the conditions and surfaces ETags.
  • When conditional_operations is set, the startup probe is skipped. The declared value is validated against the lock strategy's requirements (lock_strategy = "s3" requires true).
  • When lock_strategy is unset, the resolved value also selects the default lock backend: the shared S3 lock when true, the in-process memory lock when false.
  • With lock_strategy = "memory" or "redis", conditional_operations = true is used only for blob-index shard updates. Link updates still use the configured lock backend.
  • To avoid S3 CAS entirely, set conditional_operations = false; combine with lock_strategy = "redis" for multi-replica deployments.

Example with explicit declaration (AWS S3, Exoscale SOS):

[metadata_store.s3]
conditional_operations = true

Example disabling S3 CAS with memory locking:

[metadata_store.s3]
lock_strategy = "memory"
conditional_operations = false

Example with auto-probe:

[metadata_store.s3]
# No conditional_operations field: probe runs at startup to detect provider support

Performance impact:

  • lock_strategy selects the coordinator: "s3" selects the CAS coordinator (which uses S3 conditional requests for all coordination); "redis" and "memory" select the lock coordinator with the corresponding lock backend.
  • The CAS coordinator requires the full conditional set from the provider; startup fails if any operation is missing under lock_strategy = "s3". Conditional deletes keep lock release and lock reclaim race-free.
  • Under lock_strategy = "memory" or "redis", conditional_operations = true lets Angos update blob-index shards with optimistic concurrency while the lock coordinator still protects link metadata.
  • conditional_operations = false keeps blob-index updates on the configured lock backend. This is valid for lock_strategy = "memory" or "redis", but not for lock_strategy = "s3".

Warning: On lock-coordinated deployments, setting access_time_debounce_secs = 0 causes every manifest pull to perform a full lock-acquire → read → write → release cycle. At scale with many concurrent pulls, this adds significant latency and API costs. Keep the default value of 60 or higher there, or disable access time tracking entirely if not needed for retention policies. CAS deployments are unaffected: they ignore the knob and stamp inline with a single conditional write.

Distributed Locking

Multi-replica deployments require a distributed lock backend. The lock_strategy field on the metadata store selects the backend. Three options are available; see Lock Strategy Compatibility for the backend support matrix.

Note: lock_strategy = "s3" selects the CAS-based coordinator and requires the provider to support the full conditional set (If-None-Match and If-Match on PUT, If-Match on DELETE); startup fails fast if any is missing. lock_strategy = "memory" and "redis" select the lock coordinator, but Angos still uses conditional writes for blob-index shard updates when available.

Memory uses in-process locks, suitable for single-instance deployments only. It is the default for filesystem metadata stores and for S3 providers without conditional-operation support:

[metadata_store.s3]
lock_strategy = "memory"

S3 uses S3 conditional requests for distributed locking without extra infrastructure: If-None-Match: * to acquire, If-Match on PUT to refresh, and If-Match on DELETE to release, so an instance can only ever remove its own lock object. It is the default when the provider supports the full conditional set; Angos verifies the set at startup and fails fast if the strategy is selected explicitly and any operation is missing:

# With defaults (empty table body; all fields use defaults)
[metadata_store.s3.lock_strategy.s3]

# With custom settings
[metadata_store.s3.lock_strategy.s3]
ttl_secs = 30
max_retries = 100
retry_delay_ms = 50

Note: The bare-string form lock_strategy = "s3" is not supported; use the table form [metadata_store.s3.lock_strategy.s3] to accept defaults or override individual fields.

OptionTypeDefaultDescription
ttl_secsu6430Lock TTL in seconds (9 to 3600). Heartbeat renews at intervals of ttl_secs / 3
max_retriesu32100Max lock acquisition retries
retry_delay_msu6450Delay between retries (minimum: 1)
max_hold_secsu64300Maximum lock hold duration in seconds (must be >= ttl_secs). Guard is invalidated if held beyond this duration
operation_timeout_secsu6415Total timeout for lock S3 operations
operation_attempt_timeout_secsu644Per-attempt timeout for lock S3 operations
max_attemptsu322Maximum retry attempts for lock S3 operations
conditional_max_attemptsu323Attempts (initial write plus reconciling retries) for a conditional lock write whose transport outcome is ambiguous

Lock operation timeouts: Lock operations use their own S3 client with significantly tighter timeouts than blob/metadata operations. This is intentional: lock operations are small JSON payloads and should fail fast rather than blocking for minutes on a stuck request. The defaults (operation_timeout_secs = 15, operation_attempt_timeout_secs = 4, max_attempts = 2) ensure that a single stuck request cannot consume an entire heartbeat interval (10s with default TTL). Each heartbeat tick is also capped to the heartbeat interval to prevent the slow path (two sequential SDK calls) from exceeding it. For high-latency S3 scenarios, increase these values but keep attempt_timeout × max_attempts below the heartbeat interval.

Heartbeat Mechanism:

The S3 lock implementation uses a heartbeat to keep locks alive. Once acquired, a background task automatically renews the lock at regular intervals of ttl_secs / 3. For example, with the default ttl_secs = 30, the heartbeat runs every 10 seconds. This allows the lock to remain valid beyond the initial TTL as long as the lock-holder remains alive. If a lock-holder crashes, other instances must wait for the full ttl_secs duration before the lock becomes available for recovery.

Transient heartbeat failures (connect errors, refresh timeouts, network blips) accumulate up to a small budget (one heartbeat tick short of the TTL) before cancelling the in-flight operation. Authoritative signals cancel immediately: S3 reports ownership_lost, file_disappeared, or max_hold_exceeded; Redis reports ownership_lost (refresh script detected the key was overwritten). When the budget is exhausted, the heartbeat emits heartbeat_failure for both backends to flag the cancellation as transient-failure-driven rather than authoritative.

Locks are released as part of the operation flow: a successful operation releases its lock before returning. If the surrounding request or task is cancelled mid-operation, a best-effort background release fires on the current Tokio runtime so the remote lock is freed promptly without waiting on TTL. The fallback applies only when a runtime is still available; during process shutdown the lock expires via ttl_secs.

Contention note: The first lock acquisition attempt uses parallel PUTs for low latency. If any key is contended, the system falls back to sequential sorted acquisition for all subsequent retries, which eliminates circular wait and prevents livelock. When CAS blob index updates are active (S3 lock strategy), blob digest keys are excluded from locking, avoiding cross-namespace contention on shared layers. Randomized jitter on retry delays desynchronises retrying instances.

Clock synchronisation: The lock implementation uses S3's server-side timestamps for expiry checks, so lock correctness does not depend on synchronised instance clocks. Registry instances should still maintain synchronised clocks (NTP) for logging and other operational reasons.

Redis provides distributed locking via Redis, suitable for multi-instance deployments:

[metadata_store.s3.lock_strategy.redis]
url = "redis://localhost:6379"
ttl = 10
OptionTypeDefaultDescription
urlstringrequiredRedis URL
ttlusizerequiredLock TTL in seconds
key_prefixstring-Prefix for lock keys
max_retriesu32100Max lock acquisition retries
retry_delay_msu6410Initial retry delay in milliseconds. Retries use exponential backoff capped at 1s, plus jitter

Authentication (auth)

Basic Auth (auth.identity.<name>)

OptionTypeDefaultDescription
usernamestringrequiredUsername
passwordstringrequiredArgon2 password hash

Password hashes are validated when the configuration is parsed. An invalid Argon2 hash causes the server to fail to start with a clear error. Use angos argon to generate a valid hash.

Usernames must be unique across all auth.identity entries, and none may match an auth.oidc provider name: a Basic credential naming a provider is read as that provider's token. Either collision causes the server to fail to start.

OIDC (auth.oidc.<name>)

Every provider takes the same options: a provider is an issuer plus how its tokens are validated, so there is no provider type to select.

OptionTypeDefaultDescription
issuerstringrequiredOIDC issuer URL
jwks_uristring-Custom JWKS URI (auto-discovered if not set)
server_ca_bundlestring-PEM CA bundle trusted for this provider's HTTPS fetches
client_certificate_bundlestring-PEM client certificate presented on those fetches, requires client_private_key
client_private_keystring-PEM key for client_certificate_bundle
required_claimsarray[]Claims a token must carry; a missing or null one is rejected
jwks_refresh_intervalu643600JWKS refresh interval (seconds)
required_audiencestring-Required audience claim
clock_skew_toleranceu6460Clock skew tolerance (seconds)
allowed_algorithmsarray["RS256"]Allowed JWT signing algorithms
http_request_timeout_secsu6430Timeout for a JWKS or discovery HTTP fetch (seconds)
jwks_refresh_timeout_secsu645Timeout for the forced JWKS refetch on key rotation (seconds)

GitHub Actions, for example, is one such entry:

[auth.oidc.github-actions]
issuer = "https://token.actions.githubusercontent.com"
jwks_uri = "https://token.actions.githubusercontent.com/.well-known/jwks"
required_claims = ["repository", "actor"]

Set server_ca_bundle for an issuer whose certificate the system roots do not cover, such as a kube-apiserver signed by the cluster CA. It applies to the discovery and JWKS fetches for that provider alone; other providers keep the system roots.

Set client_certificate_bundle and client_private_key for an issuer that refuses an anonymous caller on those endpoints, a kube-apiserver serving discovery to authenticated users only being the usual case. Configuring one without the other fails startup rather than fetching anonymously.

required_claims checks presence only. Predicates over claim values belong in the access policy, which sees the whole claim map.

allowed_algorithms accepts JWT algorithm names such as "RS256", "RS384", "RS512", "ES256", and "ES384". Angos rejects tokens whose header claims an algorithm outside the provider allowlist before signature verification to prevent algorithm-confusion attacks.

Token Service (auth.token_service)

Issues registry-signed bearer tokens at GET /token, so a client holding a short-lived credential can exchange it once and keep pushing after that credential expires. Present the section to enable it.

OptionTypeDefaultDescription
secret_keystringrequiredBase64 HMAC signing key, at least 32 bytes decoded
realmstring-Absolute token URL advertised to clients, path must end with /token
ttl_secsu643600Token lifetime in seconds, at most 86400

With the section present, a 401 carries WWW-Authenticate: Bearer instead of Basic, for every client rather than only OIDC ones. Left unset, the challenge is built from each request's own Host, which is what a registry serving several hostnames wants; behind a TLS-terminating proxy, list the proxy in global.trusted_proxies so its X-Forwarded-Proto decides the scheme. Set realm when anything in front of the registry caches responses, so the challenge cannot follow a Host a client chose, and when a proxy strips a path prefix, so the advertised URL is the prefixed one clients must call.

Generate secret_key with openssl rand -base64 32: it is decoded before use, so its strength is the randomness of those bytes and not the length of a passphrase. Rotating it invalidates every outstanding token; clients recover by fetching a new one. An issued token freezes the identity it was minted from but not its permissions: access policies are still evaluated per request. A token cannot otherwise be revoked before it expires, so ttl_secs is the window a stolen one stays usable. Removing or renaming an auth.oidc entry invalidates outstanding tokens minted from that provider. The section reloads without a restart, secret_key included, so rotating the key during an incident costs no downtime.

GET /token is subject to the access policy like any other route. Under default = "deny", add a rule for it:

[global.access_policy]
default = "deny"
rules = [
"request.action == 'get-token' && identity.oidc != null",
]

Webhooks (auth.webhook.<name>)

OptionTypeDefaultDescription
urlstringrequiredWebhook URL
timeout_msu64requiredRequest timeout in milliseconds
bearer_tokenstring-Bearer token for authentication
basic_auth.usernamestring-Basic auth username
basic_auth.passwordstring-Basic auth password
client_certificate_bundlestring-Client cert for mTLS
client_private_keystring-Client key for mTLS
server_ca_bundlestring-CA bundle for server verification
forward_headers[string][]Headers to forward from client
cache_ttlu6460Response cache duration (0 to disable)

url and forward_headers are validated when the configuration is loaded. If either client_certificate_bundle or client_private_key is set, both must be set.


Repository (repository."<namespace>")

Repository namespace keys must not overlap: a key like team and a key like team/app are considered overlapping because one is a namespace-prefix of the other. The registry rejects this configuration at startup, as it does two repositories declaring the same namespace.

OptionTypeDefaultDescription
namespacestringnoneRegistry namespace this repository mirrors (docker.io), as a client names it in the ?ns= proxy parameter. A request naming it is served from this repository whatever path it asks for; see Upstream Selection
immutable_tagsboolfalseEnable immutable tags for this repository. The effective flag is this value OR global.immutable_tags, so a repository can add immutability but never opt out of a global true
immutable_tags_exclusions[string]inheritsReplaces the global exclusion list when non-empty
authorization_webhookstringinheritsWebhook name (empty to disable)
event_webhooks[string]inheritsEvent webhook names

Upstream (repository."<namespace>".upstream)

Array of upstream registries for pull-through cache.

OptionTypeDefaultDescription
urlstringrequiredUpstream registry URL. A bare host pulls from the registry root after stripping the repository name (<repo>/x → upstream x); a path (https://host/team) becomes the upstream namespace prefix instead (<repo>/x → upstream team/x). Pulling the repository root <repo> itself maps to <repo> for a bare host and to <path> for a path URL. Angos talks to the OCI /v2/ root, so the path is mapped into the namespace, not the HTTP path.
max_redirectu85Maximum redirects to follow
connect_timeout_secsu6430Timeout for establishing the connection (TCP + TLS handshake)
read_timeout_secsu64300Per-read inactivity timeout during a transfer; not a whole-transfer cap, so a large blob is never limited by total time
server_ca_bundlestring-CA bundle for server verification
client_certificatestring-Client certificate for mTLS
client_private_keystring-Client key for mTLS
usernamestring-Basic auth username
passwordstring-Basic auth password

Downstream (repository."<namespace>".downstream)

Array of downstream registries to which this repository's mutations are replicated. See Configure Replication.

OptionTypeDefaultDescription
namestringrequiredLocal identifier for this downstream (logs, downstream metric label)
urlstringrequiredDownstream registry URL. A bare host (http://host:8000) mirrors the namespace verbatim; a path (http://host:8000/team) becomes the namespace prefix the content lands under, replacing the source repository prefix (<repo>/xteam/x). The repository root <repo> itself maps to <repo> for a bare host and to <path> for a path URL. Angos serves the OCI API at the root, so the path is mapped into the namespace, not the HTTP path.
modestring"event+reconcile""event+reconcile", "event-only", or "reconcile-only"
namespace_filter[string][] (all)Regex patterns; a namespace replicates here only if it matches one
max_concurrent_pushesusize4Concurrent blob pushes per manifest for this downstream (positive integer, >= 1)
pruneboolfalseWhen true, reconciliation also deletes downstream-only tags (authoritative one-way mirror; unsafe for active-active peers)
max_redirectu85Maximum redirects to follow
connect_timeout_secsu6430Timeout for establishing the connection (TCP + TLS handshake)
read_timeout_secsu64300Per-read inactivity timeout during a transfer; not a whole-transfer cap, so a large blob push is never limited by total time
usernamestring-Basic auth username
passwordstring-Basic auth password
server_ca_bundlestring-CA bundle for downstream TLS verification
client_certificatestring-Client certificate for mTLS (requires client_private_key)
client_private_keystring-Client key for mTLS (requires client_certificate)

mode values:

  • event+reconcile (default): push on every local mutation and include in angos replicate.
  • event-only: push on local mutations; excluded from angos replicate reconciliation.
  • reconcile-only: excluded from live pushes; mirrored only via angos replicate.

If either client_certificate or client_private_key is set, both must be set.

Access Policy (repository."<namespace>".access_policy)

Same as global.access_policy.

Retention Policy (repository."<namespace>".retention_policy)

Same as global.retention_policy.


Event Webhooks (event_webhook.<name>)

HTTP POST notifications for registry operations. See Event Webhooks Reference for full details.

OptionTypeDefaultDescription
urlstringrequiredHTTP/HTTPS endpoint URL
policystringrequiredDelivery policy: required, optional, async
events[string]requiredEvent types to deliver (at least one)
tokenstring-Bearer token and HMAC signing secret
timeout_msu645000HTTP request timeout in milliseconds
max_retriesu32policyMaximum retry attempts after initial failure (max 16); defaults to 3 for required, 0 otherwise
repository_filter[string]-Regex patterns to match repository names

url, events, token, and repository_filter are validated when the configuration is loaded. If token is set, it must not be empty.

Webhooks are enabled by referencing their names:

LocationOptionTypeDescription
globalevent_webhooks[string]Webhook names for all repositories
repository."<namespace>"event_webhooks[string]Webhook names for this repository

Observability

Tracing (observability.tracing)

OptionTypeDefaultDescription
endpointstringrequiredOpenTelemetry endpoint
sampling_ratef64requiredSampling rate (0.0 - 1.0)

Prometheus Metrics

Angos emits Prometheus metrics on the /metrics endpoint, including a family of lock metrics for the distributed lock backends. See Lock Metrics for the metric names and label values.


Web UI (ui)

OptionTypeDefaultDescription
enabledboolfalseEnable web interface
namestring"Angos"Registry name in UI header

Performance Tuning

max_concurrent_requests

Controls the number of Tokio worker threads handling HTTP requests. Default: 64.

Registry operations are likely I/O-bound (network transfers, storage I/O), so more threads than CPU cores typically improves throughput.

Rule of thumb: Start with 8-16x your CPU core count and adjust based on monitoring.


Example Configuration

[server]
bind_address = "0.0.0.0"
port = 8000

[server.tls]
server_certificate_bundle = "/tls/server.crt"
server_private_key = "/tls/server.key"

[global]
update_pull_time = true
immutable_tags = true
immutable_tags_exclusions = ["^latest$"]

[blob_store.fs]
root_dir = "/var/registry/blobs"

[metadata_store.fs]
root_dir = "/var/registry/metadata"

[metadata_store.fs.lock_strategy.redis]
url = "redis://localhost:6379"
ttl = 10

[cache.redis]
url = "redis://localhost:6379"
key_prefix = "angos"

[auth.identity.admin]
username = "admin"
password = "$argon2id$v=19$m=19456,t=2,p=1$..."

[auth.oidc.github-actions]
issuer = "https://token.actions.githubusercontent.com"
required_claims = ["repository", "actor"]

[global.access_policy]
default = "deny"
rules = ["identity.username != null"]

[repository."docker-io"]
[[repository."docker-io".upstream]]
url = "https://registry-1.docker.io"

[ui]
enabled = true
name = "My Registry"

S3-Only Multi-Instance Deployment

This example uses S3 for both blob and metadata storage with S3-based distributed locking, eliminating the need for Redis:

[server]
bind_address = "0.0.0.0"
port = 8000

[global]
update_pull_time = true

[blob_store.s3]
# Example credentials - replace for production
access_key_id = "your-access-key-id"
secret_key = "your-secret-key"
endpoint = "https://s3.example.com"
bucket = "registry"
region = "us-east-1"

[metadata_store.s3]
# Example credentials - replace for production
access_key_id = "your-access-key-id"
secret_key = "your-secret-key"
endpoint = "https://s3.example.com"
bucket = "registry-metadata"
region = "us-east-1"

[metadata_store.s3.lock_strategy.s3]

[auth.identity.admin]
username = "admin"
password = "$argon2id$v=19$m=19456,t=2,p=1$..."