Registry Authorization and Telemetry Reference
Scope: OCI (container), npm, PyPI, Go module, generic, and admin registry routes. Covers required permissions, 401/404 behavior for private repositories, and analytics events emitted per request.
Authorization Model
All registry routes share a single authorization helper (authorizeRegistry in
internal/registry/authz.go). Resolution order:
- Public-repo reads — allowed for everyone, including anonymous callers.
- Anonymous callers on writes/private reads — 401 Unauthorized.
- Instance admins (
claims.IsAdmin = true) — always allowed. - Repository owner — always allowed.
- Collaborator — allowed when their role (or, for
RoleCustom, their explicit permission list) grants the required permission.
401 / 404 behavior (private repos)
| Caller | Op | Response |
|---|---|---|
Anonymous (no Authorization header) |
Any | 401 with WWW-Authenticate: Bearer realm="Vetrix Registry" |
| Authenticated non-member | Read | 404 (existence-leak prevention — equivalent to OCI NAME_UNKNOWN) |
| Authenticated non-member | Write / Delete | 403 Forbidden |
| Authenticated member lacking write | Write | 403 Forbidden |
| Authenticated member lacking delete | Delete | 403 Forbidden |
The WWW-Authenticate challenge header is required by RFC 7235 §3.1 and the
OCI Distribution Spec. All 401 paths in the registry set this header.
Route Table
OCI Distribution Spec (/v2/*)
All OCI routes are served by OCIHandler (internal/registry/oci.go).
Authentication via Authorization: Bearer <JWT> or Authorization: Basic <base64(username:jwt)>.
| Method | Path | Required permission | 401/404 private behavior | Analytics event |
|---|---|---|---|---|
GET |
/v2/ |
None (version probe) | 401 w/ WWW-Authenticate if auth header is malformed |
— |
GET |
/v2/<name>/tags/list |
PermRegistryRead |
anon → 401; authed non-member → 404 NAME_UNKNOWN |
— |
GET, HEAD |
/v2/<name>/manifests/<ref> |
PermRegistryRead |
anon → 401; authed non-member → 404 NAME_UNKNOWN |
registry.pull (GET only, on success) |
PUT |
/v2/<name>/manifests/<ref> |
PermRegistryWrite |
anon → 401; authed denied → 403 DENIED |
registry.push (on success) |
DELETE |
/v2/<name>/manifests/<ref> |
PermRegistryDelete |
anon → 401; authed denied → 403 DENIED |
registry.delete (on success) |
GET, HEAD |
/v2/<name>/blobs/<digest> |
PermRegistryRead |
anon → 401; authed non-member → 404 NAME_UNKNOWN |
— |
DELETE |
/v2/<name>/blobs/<digest> |
PermRegistryDelete |
anon → 401; authed denied → 403 DENIED |
registry.delete (on success) |
POST |
/v2/<name>/blobs/uploads/ |
PermRegistryWrite |
anon → 401; authed denied → 403 DENIED |
— |
GET |
/v2/<name>/blobs/uploads/<uuid> |
PermRegistryWrite |
anon → 401; authed denied → 403 DENIED |
— |
PATCH |
/v2/<name>/blobs/uploads/<uuid> |
PermRegistryWrite |
anon → 401; authed denied → 403 DENIED |
— |
PUT |
/v2/<name>/blobs/uploads/<uuid>?digest= |
PermRegistryWrite |
anon → 401; authed denied → 403 DENIED |
— |
Job tokens: write requests may carry a typ=job-token bearer.
authorizeJobToken is evaluated first; a denied job token yields 403 DENIED
regardless of the standard ACL result.
npm Registry (/npm/{owner}/{repo}/)
Served by NPMHandler (internal/registry/npm.go).
| Method | Path | Required permission | 401/404 private behavior | Analytics event |
|---|---|---|---|---|
GET |
/npm/{owner}/{repo}/{package} |
PermRegistryRead |
anon → 401 w/ WWW-Authenticate; authed non-member → 404 |
— |
GET |
/npm/{owner}/{repo}/{package}/-/{tarball} |
PermRegistryRead |
anon → 401 w/ WWW-Authenticate; authed non-member → 404 |
— |
PUT |
/npm/{owner}/{repo}/{package} |
PermRegistryWrite |
missing auth → 401 w/ WWW-Authenticate; authed denied → 403 |
— |
PyPI Simple Index (/pypi/{owner}/{repo}/)
Served by PyPIHandler (internal/registry/pypi.go).
| Method | Path | Required permission | 401/404 private behavior | Analytics event |
|---|---|---|---|---|
GET |
/pypi/{owner}/{repo}/simple/ |
PermRegistryRead |
anon → 401 w/ WWW-Authenticate; authed non-member → 404 |
— |
GET |
/pypi/{owner}/{repo}/simple/{package}/ |
PermRegistryRead |
anon → 401 w/ WWW-Authenticate; authed non-member → 404 |
— |
GET |
/pypi/{owner}/{repo}/packages/{digest}/{filename} |
PermRegistryRead |
anon → 401 w/ WWW-Authenticate; authed non-member → 404 |
— |
POST |
/pypi/{owner}/{repo}/ |
PermRegistryWrite |
missing auth → 401 w/ WWW-Authenticate; authed denied → 403 |
— |
Go Module Proxy (/go/{owner}/{repo}/)
Served by GoModuleHandler (internal/registry/gomodule.go).
| Method | Path | Required permission | 401/404 private behavior | Analytics event |
|---|---|---|---|---|
GET |
/go/{owner}/{repo}/* |
PermRegistryRead |
anon → 401 w/ WWW-Authenticate; authed non-member → 404 |
— |
PUT |
/go/{owner}/{repo}/* |
PermRegistryWrite |
missing auth → 401 w/ WWW-Authenticate; authed denied → 403 |
— |
The * wildcard captures the GOPROXY path tail (e.g.
github.com/foo/bar/@v/v1.0.0.zip).
Generic Artifact Registry (/api/v1/repos/{owner}/{repo}/packages/generic/)
Served by GenericHandler (internal/registry/generic.go).
| Method | Path | Required permission | 401/404 private behavior | Analytics event |
|---|---|---|---|---|
GET |
.../packages/generic/{name} |
PermRegistryRead |
anon → 401 w/ WWW-Authenticate; authed non-member → 404 |
— |
GET |
.../packages/generic/{name}/{version} |
PermRegistryRead |
anon → 401 w/ WWW-Authenticate; authed non-member → 404 |
— |
GET |
.../packages/generic/{name}/{version}/{filename} |
PermRegistryRead |
anon → 401 w/ WWW-Authenticate; authed non-member → 404 |
— |
PUT |
.../packages/generic/{name}/{version}/{filename} |
PermRegistryWrite |
missing auth → 401 w/ WWW-Authenticate; authed denied → 403 |
— |
DELETE |
.../packages/generic/{name}/{version}/{filename} |
PermRegistryDelete |
missing auth → 401 w/ WWW-Authenticate; authed denied → 403 |
— |
Cross-Repo User Image Listing
| Method | Path | Required permission | 401/404 private behavior | Analytics event |
|---|---|---|---|---|
GET |
/api/v1/users/{username}/registry/images |
Authenticated; results scoped to caller's PermRegistryRead |
standard API auth middleware (401 on missing token) | — |
Results are visibility-filtered in SQL (ListImagesForUser) to repositories
where the caller is the owner, the repository is public, or the caller holds a
collaborator role that grants PermRegistryRead (built-in roles: repo_read,
repo_write, repo_admin; custom roles: requires explicit registry:read in
the permissions array).
Admin Registry Routes (/api/v1/admin/registry/)
All admin routes require admin:registry permission (instance-admin role).
Non-admins receive 403 from the admin middleware before reaching the handler.
| Method | Path | Required permission | 401/404 behavior | Analytics event |
|---|---|---|---|---|
GET |
/api/v1/admin/registry/images |
admin:registry (AdminBypass=true, sees all repos) |
admin middleware 401/403 | — |
DELETE |
/api/v1/admin/registry/images/{owner}/{repo}/{name}/tags/{tag} |
admin:registry |
admin middleware 401/403 | — |
DELETE |
/api/v1/admin/registry/images/{owner}/{repo}/{name} |
admin:registry |
admin middleware 401/403 | — |
GET |
/api/v1/admin/registry/gc/status |
admin:registry |
admin middleware 401/403 | — |
POST |
/api/v1/admin/registry/gc/run |
admin:registry |
admin middleware 401/403 | registry.gc.completed (on completion) |
GET |
/api/v1/admin/registry/retention/{owner}/{repo} |
admin:registry |
admin middleware 401/403 | — |
PUT |
/api/v1/admin/registry/retention/{owner}/{repo} |
admin:registry |
admin middleware 401/403 | registry.retention.completed (on success) |
GET |
/api/v1/admin/registry/storage |
admin:registry |
admin middleware 401/403 | — |
Analytics Events
All registry analytics events are sourced as SourceAPI. Non-OCI handlers do
not currently emit analytics events; OCI push/pull/delete telemetry is
implemented in OCIHandler.
| Event name | Trigger | Properties |
|---|---|---|
registry.push |
Successful OCI manifest PUT |
RegistryPushProperties (owner, repo, image, tag, digest, media_type, size_bytes) |
registry.pull |
Successful OCI manifest GET (not HEAD) |
RegistryPullProperties (owner, repo, image, tag, digest, media_type) |
registry.delete |
Successful OCI manifest or blob DELETE |
RegistryDeleteProperties (owner, repo, image, ref_or_digest, kind) |
registry.gc.completed |
GC run finishes | RegistryGCRunProperties (blobs_deleted, bytes_freed, duration_ms) |
registry.retention.completed |
Retention policy enforcement finishes | RegistryRetentionCompletedProperties (owner, repo, tags_deleted, kept) |
The event recorder is attached to OCIHandler via SetRecorder. A nil
recorder silently skips all emit calls — requests never fail due to analytics
errors.
Implementation Notes
nonOCIAuthzDeny(internal/registry/authz.go) is the single function that emits 401/403/404 for all non-OCI handlers. All 401 paths setWWW-Authenticate: Bearer realm="Vetrix Registry"per RFC 7235 §3.1.OCIHandler.ociUnauthorizedsetsWWW-Authenticate: Basic realm="Vetrix Registry"(OCI clients prefer Basic challenge for token service discovery).- The 404 on authenticated read denial is an existence-leak prevention measure: it prevents an authenticated non-member from enumerating private repository names by probing registry endpoints.
ListImagesForUserruns the visibility predicate in SQL so pagination counts are accurate without streaming all rows through Go.