mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-08-29 04:06:58 +00:00
Credential-less pulls of public images. /auth/token issues a pull-only
token with an empty subject when no Basic auth is present; the
destination hold still enforces captain.Public, and push or delete always
challenges.
- token.IsPullOnlyScope and AuthMethodAnonymous;
Handler.issueAnonymousToken skips the authorizer gate and the
service-auth pre-mint, since there is no identity to reconcile and no
AppView-to-hold service token to bind. The token is still stamped
with the resolved registry domain, so anonymous pull works on
secondary front doors whose access controller demands their own
audience.
- auth.allow_anonymous_pull (default true) turns it fully off, restoring
the previous always-challenge behavior. Mirrored into the deploy
template, since the default means existing deploys pick this up.
- RegistryContext.Anonymous is plumbed from the middleware.
- ProxyBlobStore sends no Authorization header when the service token is
empty, and returns 401 rather than 403 for anonymous denials so Docker
prompts for credentials, including when a stale captain cache lets the
request through and the hold says private.
- BearerChallenge wraps the /v2/ subtree so a 401 raised deep in the
stack via errcode.ServeJSON still carries WWW-Authenticate.
Distribution's own scoped challenges are left alone.
IsPullOnlyScope allowlists the pull action instead of denylisting push and
delete. Distribution's actionSet.contains treats "*" as *every* action, so
a scope of `repository:victim/img:*` names neither denied string and would
have handed an unauthenticated caller a token valid for push and delete on
someone else's repository — clearing the authgate entirely, since anonymous
tokens deliberately skip it. Writes would still have failed further down
(no PDS credential), but the gate itself was bypassable. Now every
requested action must be exactly "pull". Covered by new claims tests.
Unresolvable identities return NAME_UNKNOWN instead of a bare error that
distribution renders as 500. This path was previously unreachable without
credentials; anonymous pull opens it to the internet, and a 5xx on
arbitrary input both misreports a bad request as a server fault and sends
clients that retry 5xx into a retry loop. That loop was real: in the auth
matrix, regclient spent 83s on a single case before this fix, and the
suite now runs in 5s.
Stat preserves an authorization verdict from getPresignedURL rather than
flattening it to ErrBlobUnknown. Distribution calls Stat before ServeBlob
on GET and HEAD, so without this an anonymous pull from a private hold
answered 404 and BearerChallenge had no 401 to annotate — the 401 path
above could never actually reach a client.
The auth matrix is updated to match: anonymous pull of the seeded public
repo now succeeds, anonymous push is denied against a real identity's
namespace (rather than an unresolvable one, which was testing name
resolution rather than authorization), and a new case pins the
NAME_UNKNOWN behavior for an unknown identity.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
167 lines
7.6 KiB
YAML
167 lines
7.6 KiB
YAML
# ATCR AppView Configuration
|
|
# Generated with defaults — edit as needed.
|
|
|
|
# Configuration format version.
|
|
version: "0.1"
|
|
# Log level: debug, info, warn, error.
|
|
log_level: info
|
|
# Remote log shipping settings.
|
|
log_shipper:
|
|
# Log shipping backend: "victoria", "opensearch", or "loki". Empty disables shipping.
|
|
backend: ""
|
|
# Remote log service endpoint, e.g. "http://victorialogs:9428".
|
|
url: ""
|
|
# Number of log entries to buffer before flushing to the remote service.
|
|
batch_size: 100
|
|
# Maximum time between flushes, even if batch is not full.
|
|
flush_interval: 5s
|
|
# Basic auth username for the log service (optional).
|
|
username: ""
|
|
# Basic auth password for the log service (optional).
|
|
password: ""
|
|
# HTTP server and identity settings.
|
|
server:
|
|
# Listen address, e.g. ":5000" or "127.0.0.1:5000".
|
|
addr: :5000
|
|
# Public-facing URL for OAuth callbacks and JWT realm. Auto-detected if empty.
|
|
base_url: ""
|
|
# Allows HTTP (not HTTPS) for DID resolution and uses transition:generic OAuth scope.
|
|
test_mode: false
|
|
# Display name shown on OAuth authorization screens.
|
|
client_name: AT Container Registry
|
|
# Short name used in page titles and browser tabs.
|
|
client_short_name: ATCR
|
|
# Separate domains for OCI registry API (e.g. ["buoy.cr"]). First is primary. Browser visits redirect to BaseURL.
|
|
registry_domains: [127.0.0.1:5000, atcr.io]
|
|
# DIDs of holds this appview manages billing for (REQUIRED). The first entry is the default blob-storage hold. Tier updates are pushed to these holds.
|
|
managed_holds:
|
|
- did:web:172.28.0.3%3A8080
|
|
# Web UI settings.
|
|
ui:
|
|
# SQLite/libSQL database for OAuth sessions, stars, pull counts, and device approvals.
|
|
database_path: /var/lib/atcr/ui.db
|
|
# Visual theme name (e.g. "seamark"). Empty uses default atcr.io branding.
|
|
theme: "seamark"
|
|
# libSQL sync URL (libsql://...). Works with Turso cloud or self-hosted libsql-server. Leave empty for local-only SQLite.
|
|
libsql_sync_url: ""
|
|
# Auth token for libSQL sync. Required if libsql_sync_url is set.
|
|
libsql_auth_token: ""
|
|
# How often to sync with remote libSQL server. Default: 60s.
|
|
libsql_sync_interval: 1m0s
|
|
# Source code URL displayed in the footer "Source" link. Defaults to the upstream ATCR project.
|
|
source_url: https://tangled.org/evan.jarrett.net/at-container-registry
|
|
# Health check and cache settings.
|
|
health:
|
|
# How long to cache hold health check results.
|
|
cache_ttl: 15m0s
|
|
# How often to refresh hold health checks.
|
|
check_interval: 15m0s
|
|
# ATProto Jetstream event stream settings.
|
|
jetstream:
|
|
# Jetstream WebSocket endpoints, tried in order on failure.
|
|
urls:
|
|
- wss://jetstream2.us-west.bsky.network/subscribe
|
|
- wss://jetstream1.us-west.bsky.network/subscribe
|
|
- wss://jetstream2.us-east.bsky.network/subscribe
|
|
- wss://jetstream1.us-east.bsky.network/subscribe
|
|
# Sync existing records from PDS on startup.
|
|
backfill_enabled: true
|
|
# How often to re-run backfill to catch missed events. Set to 0 to only backfill on startup.
|
|
backfill_interval: 24h0m0s
|
|
# Endpoints used for backfill. MUST support com.atproto.sync.listReposByCollection. Tried in order on failure.
|
|
relay_endpoints:
|
|
- https://relay1.us-east.bsky.network
|
|
- https://relay1.us-west.bsky.network
|
|
# JWT authentication settings.
|
|
auth:
|
|
# X.509 certificate matching the JWT signing key (auto-generated on each boot from the JWT key in the database).
|
|
cert_path: /var/lib/atcr/auth/private-key.crt
|
|
# Allow unauthenticated Docker pulls from public holds. Per-hold privacy (captain.Public) still applies. Default true.
|
|
allow_anonymous_pull: true
|
|
# Credential helper download settings.
|
|
credential_helper:
|
|
# Tangled repository URL for credential helper downloads.
|
|
tangled_repo: ""
|
|
# Legal page customization for self-hosted instances.
|
|
legal:
|
|
# Organization name for Terms of Service and Privacy Policy. Defaults to server.client_name.
|
|
company_name: ""
|
|
# Governing law jurisdiction for legal terms.
|
|
jurisdiction: ""
|
|
# AI-powered image advisor settings.
|
|
ai:
|
|
# Anthropic API key for AI Image Advisor. Also reads CLAUDE_API_KEY env var as fallback.
|
|
api_key: ""
|
|
# ATProto labeler for content moderation (DMCA takedowns).
|
|
labeler:
|
|
# DID of the ATProto labeler (did:plc:... or did:web:...). Empty disables label filtering.
|
|
did: ""
|
|
# Stripe billing integration (requires -tags billing build).
|
|
billing:
|
|
# Stripe secret key. Can also be set via STRIPE_SECRET_KEY env var (takes precedence). Billing is enabled automatically when set.
|
|
stripe_secret_key: ""
|
|
# Stripe webhook signing secret. Can also be set via STRIPE_WEBHOOK_SECRET env var (takes precedence).
|
|
webhook_secret: ""
|
|
# ISO 4217 currency code (e.g. "usd").
|
|
currency: usd
|
|
# Redirect URL after successful checkout. Use {base_url} placeholder.
|
|
success_url: '{base_url}/settings/billing'
|
|
# Redirect URL after cancelled checkout. Use {base_url} placeholder.
|
|
cancel_url: '{base_url}/settings/billing'
|
|
# Subscription tiers ordered by rank (lowest to highest).
|
|
tiers:
|
|
- # Tier name. Position in list determines rank (0-based).
|
|
name: Free
|
|
# Short description shown on the plan card.
|
|
description: Get started with basic storage
|
|
# List of features included in this tier.
|
|
features: []
|
|
# Stripe price ID for monthly billing. Empty = free tier.
|
|
stripe_price_monthly: ""
|
|
# Stripe price ID for yearly billing.
|
|
stripe_price_yearly: ""
|
|
# Maximum webhooks for this tier (-1 = unlimited).
|
|
max_webhooks: 1
|
|
# Allow all webhook trigger types (not just first-scan).
|
|
webhook_all_triggers: false
|
|
# Enable AI Image Advisor for this tier.
|
|
ai_advisor: false
|
|
# Show supporter badge on user profiles for subscribers at this tier.
|
|
supporter_badge: false
|
|
- # Tier name. Position in list determines rank (0-based).
|
|
name: Supporter
|
|
# Short description shown on the plan card.
|
|
description: Get started with basic storage
|
|
# List of features included in this tier.
|
|
features: []
|
|
# Stripe price ID for monthly billing. Empty = free tier.
|
|
stripe_price_monthly: ""
|
|
# Stripe price ID for yearly billing.
|
|
stripe_price_yearly: "price_1SmK1mRROAC4bYmSwhTQ7RY9"
|
|
# Maximum webhooks for this tier (-1 = unlimited).
|
|
max_webhooks: 1
|
|
# Allow all webhook trigger types (not just first-scan).
|
|
webhook_all_triggers: true
|
|
# Enable AI Image Advisor for this tier.
|
|
ai_advisor: true
|
|
# Show supporter badge on user profiles for subscribers at this tier.
|
|
supporter_badge: true
|
|
- # Tier name. Position in list determines rank (0-based).
|
|
name: Pro
|
|
# Short description shown on the plan card.
|
|
description: More storage with scan-on-push
|
|
# List of features included in this tier.
|
|
features: []
|
|
# Stripe price ID for monthly billing. Empty = free tier.
|
|
stripe_price_monthly: "price_1SmK4QRROAC4bYmSxpr35HUl"
|
|
# Stripe price ID for yearly billing.
|
|
stripe_price_yearly: "price_1SmJuLRROAC4bYmSUgVCwZWo"
|
|
# Maximum webhooks for this tier (-1 = unlimited).
|
|
max_webhooks: 10
|
|
# Allow all webhook trigger types (not just first-scan).
|
|
webhook_all_triggers: true
|
|
# Enable AI Image Advisor for this tier.
|
|
ai_advisor: true
|
|
# Show supporter badge on user profiles for subscribers at this tier.
|
|
supporter_badge: true
|