Authentication
Handled by nginx – zero-subrequest map-based auth, or auth_request to an external service.
Map-based auth (nginx)
Two headers per request:
Two map lookups, both O(1) hash – pure in-process nginx memory, no syscall, no subrequest:
X-Api-Key + X-Api-Secret → $key_ok map (1 = valid pair)
X-Api-Key + request_method + uri → $auth_ok map (1 = in scope)
A third map ($deny) folds both checks plus the /_/dl/ presign carve-out into a single deny gate – every failure mode returns the same 403. There is no 401; a distinct status for "wrong key" vs "wrong scope" would let an attacker probe for valid keys by watching status codes.
$uri is nginx's normalised, percent-decoded path (no query string, .. already collapsed). Matching auth on $uri rather than the raw $request_uri means the auth check, the try_files lookup, and the upstream all see the same canonical path – encoding tricks like %2F cannot diverge auth from the served file.
Keys file
Both maps live in a single file included by nginx:
# keys.conf
# Regenerate on key changes, nginx -s reload to apply
# Authentication: key + secret must match exactly
map "$http_x_api_key:$http_x_api_secret" $key_ok {
default 0;
"PUTFS_abc123:secret1" 1;
"PUTFS_xyz789:secret2" 1;
}
# Authorisation: key + method + normalised URI in one regex
map "$http_x_api_key:$request_method:$uri" $auth_ok {
default 0;
"~^PUTFS_abc123:[^:]+:/acme/" 1; # acme, all methods
"~^PUTFS_xyz789:(GET|HEAD):/acme/invoices/" 1; # read-only, invoices
}
# Single deny gate – 1 = block (return 403), 0 = allow.
# /_/dl/ bypasses (HMAC validates inside the presign location).
# Otherwise both $key_ok and $auth_ok must be 1.
map "$uri:$key_ok:$auth_ok" $deny {
default 1;
"~^/_/dl/" 0;
"~:1:1$" 0;
}
In the server block:
Method patterns
| Pattern | Meaning |
|---|---|
[^:]+ |
Any method (wildcard) |
(GET|HEAD) |
Read only |
(PUT|DELETE) |
Write only |
GET |
GET only |
Scope examples
map "$http_x_api_key:$request_method:$uri" $auth_ok {
# deny any other access
default 0;
# full dataset, all methods
"~^PUTFS_acme_rw:[^:]+:/acme/" 1;
# full dataset, read only
"~^PUTFS_acme_ro:(GET|HEAD):/acme/" 1;
# subfolder within dataset, all methods
"~^PUTFS_invoices:[^:]+:/acme/invoices/" 1;
# upload-only – PUT and DELETE, specific path
"~^PUTFS_upload:(PUT|DELETE):/acme/uploads/" 1;
# global admin – all methods, all paths
"~^PUTFS_admin:[^:]+:/.+" 1;
}
Key management
Issue a key:
KEY_ID="PUTFS_$(openssl rand -hex 8 | tr '[:lower:]' '[:upper:]')"
SECRET="$(openssl rand -hex 32)"
echo "Key ID: $KEY_ID"
echo "Secret: $SECRET"
# Add to keys.conf, then:
nginx -t && nginx -s reload
Revoke a key – remove the entries from keys.conf, then:
Revocation is immediate after reload. Reload is graceful – no connections dropped.
External auth service via auth_request
For dynamic auth (OAuth, LDAP, token validation):
auth_request /auth;
location = /auth {
internal;
proxy_pass http://auth-service/validate;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Original-URI $request_uri;
proxy_set_header X-Original-Method $request_method;
}
Returns 200 (allow) or 401/403 (deny). Fires a subrequest per connection – slower than map auth at high concurrency.
Security considerations
No upload size limit
Default client_max_body_size 0 – no limit. Protect with ZFS quotas (zfs set quota=500G), filesystem quotas, or set client_max_body_size in nginx.
No rate limiting by default
Add limit_req for public deployments. See Nginx config.
Resource caps
Two app-level caps mitigate authenticated DoS:
PUTFS_MAX_PATH_DEPTH(default32) – PUT to a key with more path segments returns400. Bounds the inode amplification frommkdir -p(one file at depth N creates N empty parent dirs).PUTFS_MAX_LIST_KEYS(default0= disabled) – when set, listings raiseListingTooLargemid-stream after yielding that many keys, closing the connection abruptly. Never silent truncation – callers that rely on exact iteration are not corrupted, they fail. Enable on shared/public deployments where you'd rather break large listings than tie up workers indefinitely; clients can then narrow withprefix/glob/depth.
Raise / enable as your trust model requires; defaults preserve correctness.
Key reload
Map-based keys are loaded at startup. Changes require nginx -s reload.
Further reading
- nginx
mapmodule – hash table lookups from request variables - nginx
auth_requestmodule – subrequest-based auth delegation