Skip to main content
Version: 0.1.0-alpha.4 (alpha)

Bounded OCI Bearer authentication

lsf-oci-bearer-v1 provides scoped token caching, coalesced on-demand refresh, credential rotation and authenticated writes. The complete lsf-oci-bearer-v1 transport adds the separately configured DNS and redirect policy; authentication alone does not authorize either implicitly.

The permanent authority rules in OCI registry adapter continue to apply. A registry-provided challenge is untrusted protocol data, not authority to contact a new service or forward credentials.

Configure one approved token authority​

Use RegistryCredentials::BearerChallenge only when the operator already knows and approves the exact token service:

use latent_core::TenantId;
use latent_oci::{BearerIdentity, RegistryActions, RegistryConfig, RegistryCredentials, RegistryLimits};

let config = RegistryConfig {
origin: "https://registry.example".into(),
repository: "tenant/site".into(),
credentials: RegistryCredentials::BearerChallenge {
realm: "https://auth.example/token".into(),
service: "registry.example".into(),
identity: BearerIdentity {
tenant: TenantId("tenant".into()),
principal: "registry-reader".into(),
credential_epoch: 1,
},
actions: RegistryActions::Pull,
username: "robot-reader".into(),
password: load_secret(),
addresses: vec!["192.0.2.40:443".parse().unwrap()],
},
addresses: vec!["192.0.2.20:443".parse().unwrap()],
additional_root_certificates: Vec::new(),
allow_insecure_loopback: false,
limits: RegistryLimits::default(),
};

The token realm must be an exact HTTPS URL with a host and without userinfo, query or fragment. With the ordinary constructors, a hostname realm requires at least one explicit SocketAddr and accepts at most 16; every address must use the realm's effective port and must not be unspecified. Token requests use a separate HTTP client and resolver mapping from registry requests. Both clients disable ambient proxies, redirects, automatic retries and response decompression and use the configured TLS trust roots.

username, password and acquired token bytes are kept in sensitive HTTP header values and are redacted by configuration diagnostics. The Basic credential is sent only to the configured token realm. It is never forwarded merely because a registry advertises another URL.

The identity and action set are trusted operator configuration, never values learned from a challenge or guest. Identity fields must be nonempty bounded printable ASCII; epochs are positive u64. The BearerChallenge Rust constructor requires explicit identity and actions. Share one HttpOciRegistry owner or its clones; do not construct a provider/client per dormant deployment or per request.

Challenge and scope rules​

A challenge continuation is considered only after a 401 Unauthorized response to a bodyless GET or HEAD. The adapter requires exactly one WWW-Authenticate field no larger than 4096 bytes. It accepts only a strict Bearer challenge containing exactly one quoted realm and service, and an optional quoted scope (the Registry v2 ping may omit it). Unknown, duplicated, malformed or escaped parameters fail closed.

Realm and service must match local configuration. Any challenge scope must name the configured repository and only locally permitted actions. Quoted comma lists are parsed as values, not split into additional challenge parameters. The requested token scope is constructed locally as:

repository:<configured repository>:pull
repository:<configured repository>:pull,push

RegistryActions::PullPush explicitly enables the second form. A pull-only owner rejects writes before network work; a challenge cannot expand it. Another repository, service, realm or action is rejected. Registry and token realm remain two separately configured authorities, even when both use the same hostname.

Token exchange and deadlines​

The token request uses the same absolute Operation.deadline created for the original OCI operation. Connect and per-request timeouts remain inner ceilings; a challenge does not start a fresh operation budget. Each continuation joins at most one bounded acquisition and makes one authenticated follow-up. A second 401 is returned as an authentication failure rather than entering another challenge loop.

Token-service response headers retain the normal OCI response ceiling of 100 headers and 16 KiB aggregate header bytes. The JSON body is limited to 16 KiB. token or access_token is accepted; empty strings are absent aliases, as emitted by Harbor 2.15.2. At least one nonempty token is required, and two nonempty aliases must be identical. Explicit nulls remain malformed. The token itself is limited to 8192 printable ASCII bytes. Optional fields may be absent, but present-null, malformed and duplicate fields are rejected. A returned scope, when present, must exactly equal the locally requested action set.

expires_in defaults to 60 seconds only when absent. Accepted lifetimes are 6–3600 seconds, with five seconds removed for skew. Bounded RFC3339 issued_at values cannot be expired or more than five seconds ahead of the receiving clock. The monotonic expiry cannot exceed the original acquisition-start lifetime; issued time can shorten it, never extend it. Long token exchanges consume that same lifetime and operation deadline.

There is one cached token per owner, bound structurally to the configured profile, registry, realm, service, repository/actions, tenant, principal and credential epoch. Clones share this state; independent clients never share tokens. One async acquisition owner coalesces equivalent calls under the existing operation limit (hard maximum 32); excess admission is rejected, not queued indefinitely. Acquisition failures have a one-second on-demand negative-cache window to avoid a serialized failure stampede. Deadline/cancellation does not poison other callers' remaining budgets. No refresh worker or per-service timer is created.

Expired tokens are retired on demand. Refresh reauthenticates with the approved Basic credential; offline refresh tokens are neither requested nor stored. Unexpected refresh_token material is rejected rather than becoming a second unscoped credential source. Token JSON and parsed secret strings are zeroized when retired; HTTP headers remain sensitive and diagnostics omit credential data.

Authenticated writes are never replayed​

POST, PUT, PATCH, DELETE, and any body-bearing request are never repeated automatically after an authentication response. The adapter cannot generally know whether a remote registry observed an upload or finalization request before the response was lost or replaced. A PullPush owner therefore obtains a token from the explicitly approved realm before sending the first write, without first probing with an unauthenticated mutation. A cached valid token can be reused.

POST initiation, upload/finalization and manifest publication each preserve the existing upload ownership and uncertainty rules. A write's 401, lost response, timeout or disconnect is returned, not retried. Callers must inspect the original digest/session outcome before considering another mutation. A write's 401 invalidates that cached token for a later independently submitted operation; it does not acquire another token or resubmit the failed write. Static Basic and preissued Bearer behavior is unchanged.

Rotation and physical ownership​

HttpOciRegistry::rotate_bearer_credentials(identity, username, password) requires the same tenant and principal and a strictly newer positive epoch. It immediately invalidates later cached use and negative-cache entries. An old acquisition may finish physically, but cannot install or send its token after rotation. New callers wait on the same finite acquisition owner, not a second refresh worker. Changing tenant, principal, realm or action authority requires a separate client.

RegistryUsage.bearer exposes the epoch, cached tokens, active and waiting acquisitions, retained token bytes, their hard ceiling and a conservative 64 KiB reservation per active acquisition. Token headers are at most 8199 bytes including the scheme; retained headers are capped at (max_in_flight + 2) * 8199. Retired tokens pinned by responses remain charged through response retirement; eviction and rotation cannot manufacture capacity. Socket pools retain their existing separate fixed ceilings.

Cancelling an acquisition leader drops its actual HTTP future and releases its ownership only as that future retires; a remaining follower can acquire under its own original deadline. Shutdown closes operation admission and waits for actual transfers/upload cleanup before clearing credentials and token cache. A shutdown deadline failure leaves live ownership visible instead of claiming reclamation. Rotation does not undo a mutation already submitted to a registry.

Validation and remaining transport boundary​

cargo test -p latent-oci --all-targets --locked
cargo clippy -p latent-oci --all-targets --locked --no-deps -- -D warnings
python -m unittest tools.tests.test_harbor_registry_runner tools.tests.test_oci_registry_runner
python tools/run_harbor_registry_tests.py --output target/harbor-receipt.json

Run the owned integration fixtures on Linux x86_64 with Docker. For the static Zot profile, follow the prepared registry check; that runner requires an explicit Cargo test inventory.

The ordinary Rust suite includes TLS fault peers for malicious authority, malformed/oversized/expired tokens, read/write permission, coalescing, retained buffers, rotation, lost writes, cancellation, deadlines and failed shutdown. Controlled peers are not substituted for real-registry conformance.

The separate Harbor runner uses the digest-verified 2.15.2 installer template and pinned images, eight finite containers, an isolated backend network, a loopback-only TLS frontend, fresh certificates and a disposable private project's pull/push-only robot credential. It waits for dependency health without restart loops. Cleanup verifies ownership labels and removes immutable container/network IDs and owned volumes; fixture files stay under the repository's target/phase3-harbor directory. This is not a production Harbor installer or a server-hardening claim.

The real test pushes tiny packages and detached evidence, pulls by digest, discovers native referrers, checks pull-only write denial and clean client shutdown. The receipt records image/installer identities, source revision, tracked-tree state and cleanup. A working-tree receipt is diagnostic, not an immutable release claim. The separate network conformance path adds explicit DNS and controlled-peer redirect validation without claiming untested hosted-storage topology support. No mutable referrers-tag fallback is introduced.