Auth#
The auth layer lives under internal/auth/. Every request enters through
auth.Middleware, which verifies an AWS SigV4 signature, derives a stable
identity, and stamps the result onto the request context. The router and
handlers downstream never re-derive identity — they read it from
auth.FromContext(ctx).Owner.
SigV4 verification#
internal/auth/sigv4.go implements the standard four-step canonicalisation:
- Parse
Authorization(orX-Amz-*query parameters for presigned URLs) to extractAccessKey,Scope,SignedHeaders, andSignature. - Look up the secret for the access key via the configured static
credentials store (
internal/auth/static.go) — no IdP federation in this cycle. - Build the canonical request string from
Method,URL.Path, the sortedSignedHeaders, and the body hash (x-amz-content-sha256, which may be the literal sentinelSTREAMING-AWS4-HMAC-SHA256-PAYLOADfor chunked uploads — see below). - Recompute the signature with the derived signing key and constant-time
compare. Mismatch returns
ErrSignatureInvalid(HTTP 403, AWS codeSignatureDoesNotMatch).
The middleware MUST run before any URL rewriting. The signed canonical
string includes the original Host header and the original
URL.Path — if the router rewrites either before verification, the
signature breaks. See the Router page
for the order.
STRATA_AUTH_MODE controls the gate:
| Mode | Behaviour |
|---|---|
required (default) | Reject unsigned requests with AccessDenied; every request must carry a valid SigV4 signature. Secure-by-default — an unconfigured deployment is locked down. |
optional | Verify if a signature is present; allow unsigned requests through with the anonymous principal (which is then gated by bucket policy/ACL — anonymous can only reach public resources). |
off / disabled / "" | Dev-only. Bypasses SigV4 and the bucket policy/ACL stack entirely — every request is full-access. Never use in production; the gateway logs a WARN at boot when off is active. Set explicitly by make run-memory and the e2e harness. |
Secure-by-default note. The default flipped from
offtorequired. A gateway started with noSTRATA_AUTH_MODEnow demands valid signatures. Local dev paths opt intooffexplicitly; production should runrequired(oroptionalfor public-bucket workloads).
Presigned URLs#
internal/auth/presigned.go parses the X-Amz-Algorithm,
X-Amz-Credential, X-Amz-Date, X-Amz-Expires, X-Amz-SignedHeaders,
and X-Amz-Signature query parameters and re-runs the same canonicalisation
without an Authorization header. The expiry check is wall-clock and
strict — the caller is expected to refresh the URL before
Date + Expires.
Strata mints presigned URLs through the presign_mint.go helper for
internal flows (admin console downloads, replication HTTPDispatcher
fallbacks). The mint side does not differ from the verify side — the same
canonical string and signing key derivation are reused.
Streaming chunk decoder#
Multipart and large PUTs use the STREAMING-AWS4-HMAC-SHA256-PAYLOAD
encoding: the body is split into chunks and each chunk carries a per-chunk
signature in the form
<chunk-size-hex>;chunk-signature=<hex>\r\n<bytes>\r\n. The decoder lives
in internal/auth/streaming.go. The contract:
- The first chunk’s signature uses the
Authorization-header signature asprevSig. - Each subsequent chunk’s signature is
HMAC(signingKey, string-to-sign)where the string-to-sign explicitly includes the previous chunk’s signature (prevSig). This is the chain-HMAC the spec calls out. - A mismatch on any chunk returns
ErrSignatureInvalidand the gateway drops the connection mid-body. Already-processed bytes are NOT committed to the data backend — the upload aborts.
This per-chunk validation was added under US-022. Without it, an
attacker who could intercept chunk boundaries could splice a body. The
streaming decoder is the only path that touches prevSig; non-chunked
PUTs verify the whole body’s x-amz-content-sha256 against the
canonical-request hash.
Identity in context#
After verification the middleware calls auth.WithIdentity(ctx, id). The
identity carries:
Owner— canonical user (used as the bucket / object owner on writes).AccessKey— the SigV4 access key id (audited on every write).IsAnonymous bool— set when the request was unsigned underoptionalmode.FullAccess bool— set underoffmode; bypasses the policy/ACL gates.
Handlers reach the identity via auth.FromContext(r.Context()). The
audit log middleware (internal/s3api/audit.go) reads the same value to
fill the principal column.
Virtual-hosted-style routing#
internal/s3api/vhost.go extracts the bucket from the host header for
clients that prefer <bucket>.s3.local/key over the path-style
s3.local/<bucket>/key. The pattern list comes from STRATA_VHOST_PATTERN
(comma-separated *.<suffix>; default *.s3.local; - disables).
The crucial ordering invariant is: the auth middleware runs first and
signs the original Host + URL.Path. Server.ServeHTTP then rewrites
r.URL.Path to prepend /<bucket> so the path-style router shape works
unchanged. Rewriting before SigV4 verification breaks every signed
request — the canonical string would no longer match what the client
signed.
Source#
internal/auth/sigv4.go— header path.internal/auth/presigned.go— query path.internal/auth/streaming.go— chunked body decoder.internal/auth/middleware.go— context plumbing + mode gate.internal/auth/static.go—STRATA_STATIC_CREDENTIALSparser.internal/s3api/vhost.go+internal/s3api/server.go::ServeHTTP— vhost rewrite (after auth).