* chore(metadb): add writer version to interface
Signed-off-by: Jacob McSwain <jacob@mcswain.dev>
* chore(metadb): add writer version to db mock
Signed-off-by: Jacob McSwain <jacob@mcswain.dev>
* chore(metadb): implement writer version for bolt, redis, and dynamodb
Signed-off-by: Jacob McSwain <jacob@mcswain.dev>
* feat(metadb): add optional fast restart path that skips storage walk when binary identity matches metaDB stamp
binary identity is determined by the current release tag/commit and stored in metaDB after a successful storage parse. When fast restart is enabled, the next startup will skip the parse if the stored identity matches the current binary
Signed-off-by: Jacob McSwain <jacob@mcswain.dev>
* chore(cli): serve: add a way to force reparse storage
Signed-off-by: Jacob McSwain <jacob@mcswain.dev>
* refactor(meta): version: split to avoid global state mutation in tests
Signed-off-by: Jacob McSwain <jacob@mcswain.dev>
* fix(meta): version: include commit in writerVersion to distinguish retags
Signed-off-by: Jacob McSwain <jacob@mcswain.dev>
* chore(config): add IsFastRestartEnabled() test
Signed-off-by: Jacob McSwain <jacob@mcswain.dev>
* fix(meta): skip writer-version stamp when storage parse is incomplete
ParseStorage returns nil even when individual repos fail to parse or are only partially parsed (a missing manifest blob), so MaybeParseStorage would stamp a partially-populated metaDB as good. On the next restart fastRestart trusts the stamp, skips the storage walk, and never recovers.
Track per-repo outcomes via parseStats and stamp only when the walk fully populated the metaDB, otherwise log and continue so the next restart reparses
Signed-off-by: Jacob McSwain <jacob@mcswain.dev>
* fix(docs): readme: remove trailing comma from JSON config
Signed-off-by: Jacob McSwain <jacob@mcswain.dev>
* fix(meta): dynamodb: use context.Background instead of context.TODO
Signed-off-by: Jacob McSwain <jacob@mcswain.dev>
* fix(meta): invalidate fast restart on storage config changes
Signed-off-by: Jacob McSwain <jacob@mcswain.dev>
* chore(meta): dynamodb: use context.Background() instead of context.TODO()
Signed-off-by: Jacob McSwain <jacob@mcswain.dev>
* docs(meta): dynamodb: add comment about nil AttributeValue handling in GetWriterVersion
Signed-off-by: Jacob McSwain <jacob@mcswain.dev>
* chore: rename writer-version stamp to fast-restart stamp
also replaces the version/commit tracking to use BinaryVersion instead of WriterVersion
This should make things more clear
Signed-off-by: Jacob McSwain <jacob@mcswain.dev>
* fix(config): ensure FastRestart is on GlobalStorageConfig
This is not a per-subpath setting
Signed-off-by: Jacob McSwain <jacob@mcswain.dev>
* fix(metadb): redis: tests: ensure clients are closed
Signed-off-by: Jacob McSwain <jacob@mcswain.dev>
---------
Signed-off-by: Jacob McSwain <jacob@mcswain.dev>
* feat(metrics): add Prometheus GC metrics
Track garbage collection activity with three new metrics:
- zot_gc_runs_total (counter, label: error) — GC run count
- zot_gc_duration_seconds (summary) — GC run duration
- zot_gc_deleted_total (counter, label: type) — items deleted
by type: blob, manifest, upload
MetricServer is added to GarbageCollect and wired through
all callers (controller, verify-feature retention, tests).
Signed-off-by: Benoit Tigeot <benoit.tigeot@lifen.fr>
* fix(test): add missing metrics var in GCS GC tests
TestGCSGarbageCollectImageIndex and
TestGCSGarbageCollectChainedImageIndexes were missing the
metrics variable required by NewGarbageCollect after the
MetricServer parameter was added.
Signed-off-by: Benoit Tigeot <benoit.tigeot@lifen.fr>
* fix(test): add defer metrics.Stop() in GC tests
Prevent goroutine/port leaks by stopping MetricsServer in
storage_test.go (3 functions) and gcs_test.go (also add
missing metrics declaration in TestGCSGarbageCollectImageManifest).
Signed-off-by: Benoit Tigeot <benoit.tigeot@lifen.fr>
* fix(test): cover `CleanRepo` error path
Add test that exercises the error branch in
`CleanRepo` where `cleanRepo` fails, covering
the metrics calls and log lines flagged by Codecov.
Signed-off-by: Benoit Tigeot <benoit.tigeot@lifen.fr>
* test: Cover GC error paths for codecov
Add three tests in gc_internal_test.go to cover previously
untested error branches in `removeBlobUploads` and
`removeUnreferencedBlobs`: `ListBlobUploads` failure,
`addIndexBlobsToReferences` failure, and `PathNotFoundError`
from `GetAllBlobs`.
Signed-off-by: Benoit Tigeot <benoit.tigeot@lifen.fr>
* test(gc): cover remaining error paths
Cover `StatBlobUpload`, `digest.Validate()`,
`isBlobOlderThan`, and `CleanupRepo` error branches
in `removeBlobUploads` and `removeUnreferencedBlobs`.
`removeUnreferencedBlobs` now at 100% coverage,
`removeBlobUploads` from 78.3% to 91.3%.
Signed-off-by: Benoit Tigeot <benoit.tigeot@lifen.fr>
* test: cover `sanityChecks` label name mismatch
Try to avoid -0.09% coverage regression on `minimal.go`
by exercising the uncovered branch in `sanityChecks`
where label names have correct count but wrong values.
Signed-off-by: Benoit Tigeot <benoit.tigeot@lifen.fr>
* test(gc): exercise real GC path in metrics test
TestGCMetrics was calling metric helpers directly instead of
running actual garbage collection, so it couldn't catch wiring
regressions where `CleanRepo` stops recording metrics.
Now uploads an orphaned blob and runs `gc.CleanRepo` end-to-end,
verifying metrics appear on the Prometheus endpoint.
Suggestion from Copilot: https://github.com/project-zot/zot/pull/3863#discussion_r3129324719
Signed-off-by: Benoit Tigeot <benoit.tigeot@lifen.fr>
* fix(gc): skip deletion metrics when DryRun is enabled
https://github.com/project-zot/zot/pull/3863#discussion_r3129324684
Signed-off-by: Benoit Tigeot <benoit.tigeot@lifen.fr>
* fix(test): stop leaked MetricsServer goroutines in GCS tests
https://github.com/project-zot/zot/pull/3863#discussion_r3129324657
Signed-off-by: Benoit Tigeot <benoit.tigeot@lifen.fr>
* refactor(test): drop unnecessary zlog import alias
Signed-off-by: Benoit Tigeot <benoit.tigeot@lifen.fr>
* fix(monitoring): expose metric types outside build tag
`MetricsCopy` and related types were only visible under `\!metrics`,
causing a typecheck failure when golangci-lint runs with `-tags metrics`.
Moving the type definitions to `common.go` makes them unconditionally available.
Signed-off-by: Benoit Tigeot <benoit.tigeot@lifen.fr>
* fix(monitoring): remove extra blank line for gci
Signed-off-by: Benoit Tigeot <benoit.tigeot@lifen.fr>
* test(gc): cover both dry-run and real deletion metrics
And fix issue with build tag with metrics
Signed-off-by: Benoit Tigeot <benoit.tigeot@lifen.fr>
* Satisfy testpackage linter for gc metrics test
The `testpackage` linter allows `package gc` only in files named
`*_internal_test.go`; rename to follow that convention.
Signed-off-by: Benoit Tigeot <benoit.tigeot@lifen.fr>
---------
Signed-off-by: Benoit Tigeot <benoit.tigeot@lifen.fr>
Keep CLI binaries from importing pkg/api/config just for version strings by
centralizing Commit/ReleaseTag/BinaryType/GoVersion in a tiny buildinfo package.
Update ldflags targets and callers accordingly.
Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
Read identity from request context after handlers run so Basic, Bearer,
OIDC, mTLS, etc. are covered; use subject "anonymous" when unset.
Redact Authorization in SessionLogger without decoding credentials.
Add session_test.go for SessionLogger and SessionAuditLogger.
Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
Rename getOpenIDUsername to getOpenIDIdentity and thread "identity"
through bearer OIDC, Basic-auth parsing, OAuth2Callback, and log fields.
Only fall back (and warn) to the default email claim when the configured
username claim is non-default but missing or empty.
Stop emitting Info logs when groups are absent on only UserInfo or only ID
token claims; log once at Debug when no groups remain after merging both.
Update ClaimMapping docs to mention username and groups claims; fix mTLS
extractIdentity comment typo; clarify GetAuthUserFromRequestSession doc.
Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
countingReader was not respecting the single responsibility principle
and the implementation was hard to understand
Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
* feat(auth): map OpenID groups claim
Signed-off-by: Akash Kumar <meakash7902@gmail.com>
* fix(auth): refine OIDC claim mapping logs
Signed-off-by: Akash Kumar <meakash7902@gmail.com>
* refactor(auth): collapse OIDC username fallback into nested if
Reuse the empty-username branch for the email fallback so the value is
checked once and the failure path lives next to the recovery attempt.
Signed-off-by: Akash Kumar <meakash7902@gmail.com>
* refactor(auth): consolidate OIDC claim extraction into authn.go
Move getOpenIDClaimMapping, getOpenIDUsername, and appendOpenIDGroups
out of routes.go into authn.go alongside a new extractOpenIDIdentity
helper that owns the username/groups extraction flow. This keeps the
HTTP callback in routes.go thin and groups OIDC plumbing with the rest
of the authentication code.
Also:
- Filter nil and empty entries consistently across the []any, []string,
and string branches of appendOpenIDGroups, with new test cases
covering []any{nil, ""} and []string{"admin","",...}.
- Surface a Warn log when an operator-configured username claim is
missing/empty so the fallback to email isn't silent.
- Rename openid_claim_mapping_internal_test.go to authn_internal_test.go
and drop the build tags that aren't needed for the internal tests.
Signed-off-by: Akash Kumar <meakash7902@gmail.com>
---------
Signed-off-by: Akash Kumar <meakash7902@gmail.com>
* fix(api): set blob response Content-Type from OCI descriptor
Blob HEAD responses had no Content-Type and GET responses echoed the
request's Accept header verbatim, which produced missing or malformed
media types and left multipart/byteranges parts without a per-part
Content-Type. This breaks OCI distribution-spec conformance and
consumers like stargz-snapshotter that need a well-formed layer media
type.
Add a blobResponseMediaType helper that resolves the descriptor's
MediaType via GetBlobDescriptorFromRepo and falls back to
application/octet-stream. Use it in CheckBlob (HEAD), GetBlob full
(200), GetBlob single-range (206), and per-part in
writeMultipartRanges (206 multipart). Lookup is deferred until after
the blob is known to exist.
Cover the new behaviour with mock-based unit tests in routes_test.go
and end-to-end assertions in TestPullRange.
Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
* perf(api): stream multipart blob ranges lazily with precomputed Content-Length
writeMultipartRanges previously opened every range reader up front
and emitted no Content-Length, so an N-range request held N
concurrent storage readers (and their fds / read buffers) per
response window and forced chunked encoding on HTTP/1.1 — neither
friendly to proxies nor to fan-out scenarios like stargz lazy pulls.
Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
---------
Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
* fix(api): support multipart range blob pulls
Signed-off-by: Akash Kumar <meakash7902@gmail.com>
* fix(api): tighten multipart range response
- Drop the redundant deferred closeRangeReaders; the deferred cleanup
registered when the slice is created already covers all paths.
- Stop copying the request Accept header into each multipart part's
Content-Type. Accept can be a list of media ranges (e.g.
"application/octet-stream,*/*"), which is not a valid Content-Type and
may confuse multipart parsers. RFC 9110 lets us omit it entirely.
- Set Docker-Content-Digest on the partial-content response so range
pulls expose the same header as a full GET.
- Drop the over-broad build tag on routes_internal_test.go; the parser
unit test does not need any extension build tags.
Signed-off-by: Akash Kumar <meakash7902@gmail.com>
---------
Signed-off-by: Akash Kumar <meakash7902@gmail.com>
POST /zot/auth/logout now returns an endSessionUrl in the JSON
response body when the session was established via an OIDC provider
whose discovery document advertises an endSessionEndpoint, so the
UI can navigate the browser to it and terminate the session at the
IdP in addition to clearing the local cookie.
- The OIDC callback records the provider name in the session after
login; the github OAuth2 path is untouched.
- end_session_endpoint is read from the zitadel/oidc RelyingParty
and validated as an absolute http(s) URL.
- post_logout_redirect_uri prefers http.externalUrl when set and
falls back to deriving the origin from the incoming request.
- No id_token_hint is sent; client_id identifies the RP, so the
ID token does not need to be persisted.
- Non-OIDC sessions (local/basic/LDAP/GitHub) retain the existing
200 OK, no body behavior.
Operators must register the URI zot sends as a valid post-logout
redirect URI on the IdP client.
Ref: https://openid.net/specs/openid-connect-rpinitiated-1_0.html
Signed-off-by: Nikita Vakula <programmistov.programmist@gmail.com>
Docker Compose and Buildx proxy through the Docker daemon, which sends
a User-Agent starting with "docker/<version>" rather than the
"Docker-Client/<version>" string sent by direct Docker CLI pulls.
This caused compose/buildx pulls to skip the 401 challenge on
registries with mixed anonymous/authenticated access policies,
resulting in 'unauthorized' errors.
Add strings.HasPrefix(ua, "docker/") alongside the existing
Docker-Client check so daemon-proxied requests from any upstream
tool (compose, buildx, etc.) are handled correctly.
Fixes#3991
Align closing blob upload (PUT) with the OCI Distribution Spec: invalid /
out-of-order upload ranges (ErrBadUploadRange) return 416 Requested Range Not
Satisfiable instead of 400, for both the final-chunk PutBlobChunk path and
FinishBlobUpload.
GetBlobUpload (GET upload status): fix the Range response when zero bytes have
been received—send Range: 0-0 instead of Range: 0--1, consistent with a new
session and the spec’s Location + Range upload status shape. Only map
ErrBadBlobDigest to 400 here; do not handle ErrBadUploadRange on GET (that
request carries no range; ImageStore.GetBlobUpload does not return it).
Document PUT upload failures 400 and 416 in swagger; regenerate swagger
artifacts. Update route tests (expect 416 on UpdateBlobUpload for
ErrBadUploadRange), drop the mock-only GetBlobUpload+ErrBadUploadRange case,
and assert Range: 0-0 in TestPullRange after GET on a new upload location.
Fix potential panic when parsing Content-Range (index out of range)
when accessing `tokens[0]`.
Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
fix(security): suppress Allow-Credentials on wildcard CORS origin (CORS-1)
Per CORS spec §3.2, Access-Control-Allow-Credentials must not be
"true" when Access-Control-Allow-Origin is the wildcard "*".
ACHeadersMiddleware (pkg/common/http_server.go) and
getUIHeadersHandler (pkg/api/routes.go) now only emit the
credentials header when an explicit, non-empty AllowOrigin is
configured. Deployments that leave AllowOrigin blank (default
wildcard) no longer produce a contradictory header pair.
Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com>
Wrap req.Body with http.MaxBytesReader before io.ReadAll in
CreateAPIKey. Requests with bodies larger than MaxAPIKeyBodySize
(4 KiB) now return HTTP 413 instead of buffering arbitrary data.
Add the MaxAPIKeyBodySize constant, update the Swagger @Failure
annotation to document 413, and add a unit test.
Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com>
Wrap request.Body with http.MaxBytesReader before io.ReadAll in
UpdateManifest. Bodies exceeding MaxManifestBodySize (4 MiB) now
return HTTP 413 with a MANIFEST_INVALID error body instead of
buffering unlimited data into memory.
Add the MaxManifestBodySize constant and a unit test that sends an
oversized body and asserts the 413 status.
Agent-Logs-Url: https://github.com/project-zot/zot/sessions/5eca86eb-9749-4cf8-9fb8-7b9ace2ba87f
Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com>
* fix(auth): add workaround for Docker client auth with mixed anonymous policies
Docker client fails to authenticate to protected repositories when basic auth
(htpasswd/LDAP) is used with mixed access policies (some repos anonymous,
some requiring auth). This happens because Docker determines whether to send
credentials based on the /v2/ response - if it returns 200, Docker assumes
no auth is needed anywhere.
Add `forceDockerClientAuth` config option that, when enabled, forces 401 on
/v2/ for Docker clients, triggering Docker's authentication flow.
This workaround only affects Docker clients (detected via User-Agent).
Podman and other OCI-compliant clients are unaffected.
Refs: https://github.com/opencontainers/wg-auth/blob/main/docs/implementations/moby.md
Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
* feat: remove ForceDockerClientAuth flag and use only authz policies to determine the docker specific behavior
Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
---------
Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
Adds a configurable maximum repository count per registry instance.
When maxRepos is set on StorageConfig, manifest pushes that would create
a new repository beyond the limit are rejected with HTTP 429
TOOMANYREQUESTS. Pushes to existing repositories are always allowed.
Implemented as an always-available feature in pkg/api (not a build-tag
extension). MaxRepos is a field on StorageConfig, enabled when > 0.
- repoQuotaMiddleware on the dist-spec router intercepts manifest PUTs.
New-repo pushes are serialized with a sync.Mutex to prevent concurrent
requests from exceeding the limit.
- Adds CountRepos(ctx) to the MetaDB interface with efficient
implementations: BoltDB (Stats().KeyN), Redis (HLen), DynamoDB
(Scan with Select=COUNT).
- Config.IsQuotaEnabled() added, wired into controller.go metaDB init.
- Four integration tests (enforcement, concurrency, disabled,
unconfigured) and backend-specific CountRepos tests for BoltDB, Redis,
and DynamoDB.
Signed-off-by: Bachir Khiati <bachir.khiati@gmail.com>
Validate callback_ui and default invalid values to /.
Allow absolute callback_ui only when its origin is allowlisted via http.auth.openid.callbackAllowOrigins (and externalUrl).
Add/adjust unit + controller tests and update examples/docs for relative vs allowlisted absolute redirect
Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
feat(storage): add a GCS driver
test(storage): add unit tests for GCS driver
test(storage): add missing unit tests for GCS driver & resolve lint issues
fix: configuration validation for GCS Storage
test(storage): resolve panic by test due to setupGCS ignoring returned error
test(storage): add dummy gcs credentials
test: add darwin support for macos to run tests
ci: update workflows to pin gcs emulator version
lint: resolve long line lengths & formatting issues
test: move error for gcs mock earlier with an error
test: stop test using local google credentials and use mock instead
test: add missing dummy creds
test(storage): use storage-testbench for GCS, isolate GCS tests, fix driver Delete
- Switch GCS emulator from fake-gcs-server to storage-testbench in CI.
Run the GCS emulator only in the privileged-test job; remove it from
minimal and extended test jobs.
- Consolidate GCS tests under pkg/storage/gcs (needprivileges,linux).
Add TestMain with HTTPS proxy and /etc/hosts so tests talk to
storage-testbench; move GCS-specific cases from storage_test.go and
scrub_test.go into gcs_test.go. Run GCS tests via a second privileged-test
invocation and collect coverage in coverage-needprivileges-gcs.txt.
- Make GCS driver Delete idempotent and normalize errors. Treat
PathNotFoundError from Delete as success so that deleting an already-gone
path (e.g. after GC under eventual consistency) does not fail. Add
formatErr to map 404/not found to PathNotFoundError and use it for all
driver methods so callers get consistent storage driver errors.
- Drop GCS branches and helpers from storage_test.go and scrub_test.go so
non-privileged tests only use local/S3; GCS is tested only in
pkg/storage/gcs with storage-testbench.
- Set GCSMOCK_ENDPOINT without /storage/v1/, as the rest of the URL is set in tests.
- Show errors in case of failure to create bucket.
- Consolidate StorageDriverMock structs inside the pkg/test/mocks package.
Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
Co-authored-by: Steven Marks <steve.marks@qomodo.io>
Add support for configurable identity attributes in mTLS authentication,
allowing identity extraction from CommonName, Subject DN, Email SAN,
URI SAN, or DNSName SAN with fallback chain support. Includes regex
pattern matching for URI SANs (e.g., SPIFFE workload IDs).
- Add MTLSConfig with identity attributes, URISANPattern, and index fields
- Implement extractMTLSIdentity with fallback chain logic
- Move the mtls tests in the api package to pkg/api/mtls_test.go
Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
- Refactored HTTP client from global cache to struct-based approach (global state was shared between tests, including what certificates to use)
- Enhanced pkg/test/tls to support ECDSA and ED25519 key types
- Replaced static certificate files with dynamic generation in golang tests
- Fixed test cleanup issues and improved resource management
This eliminates dependency on external cert generation scripts and
improves test maintainability.
Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
* feat: support mTLS-only authn/authz with AccessControl and allow combining mTLS with other auth mechanisms
Signed-off-by: Ivan Arkhipov <me@endevir.ru>
* refactor: improve authentication logic and TLS certificate generation
- Fix mTLS authentication to use only leaf certificate instead of iterating
through all certificates in the chain
- Reject Authorization headers when corresponding auth method is disabled,
regardless of mTLS status (security improvement)
- Simplify authentication switch statement ordering and logic
- Move ErrUserDataNotFound error handling into sessionAuthn method
- Refactor TLS certificate generation to use Options pattern with
CertificateOptions struct for better extensibility
- Consolidate duplicate certificate generation code into helper functions
(generateCertificate, parseCA, initializeTemplate, applyOptions)
- Rename certificate generation functions for clarity:
- GenerateCertWithCN -> GenerateClientCert
- GenerateSelfSignedCertWithCN -> GenerateClientSelfSignedCert
- Add support for SAN settings including email addresses in certificates
- Update tests to reflect new authentication behavior and certificate API
This commit improves both the security posture (rejecting disabled auth
methods) and code maintainability (consolidated certificate generation).
Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
* fix: guard against multiple Authorization headers
Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
---------
Signed-off-by: Ivan Arkhipov <me@endevir.ru>
Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
Co-authored-by: Ivan Arkhipov <me@endevir.ru>
Replace MakeTempFile usage with MakeTempFilePath and MakeTempFileWithContent
helpers that automatically handle file lifecycle. This prevents resource
leaks by ensuring temporary files are properly closed.
Shoudld also make the tests easier to read.
Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
feat: add support for sha256 and sha512 htpasswd formats
Fixes issue #3495
We currently support only bcrypt htpasswd hashes, however bcrypt is not
FIPS-140 approved since it uses Blowfish.
This PR adds support for sha256 and sha512 formats and enforces that
bcrypt be disabled when fips140 mode is enabled.
Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com>
- Use custom authURL/tokenURL from config instead of hardcoded github.com endpoints
- Properly configure GitHub Enterprise API base URL from auth endpoints
Fixes OAuth2 authentication with GitHub Enterprise Server and other
self-hosted OAuth2 providers.
Signed-off-by: Mathias Bogaert <mathias.bogaert@gmail.com>