Skip to content

Authentication

Handled by nginx – zero-subrequest map-based auth, or auth_request to an external service.

Map-based auth (nginx)

Two headers per request:

X-Api-Key:    PUTFS_abc123     ← identifies the client
X-Api-Secret: <secret>         ← authenticates the client

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  → 401 if unknown/wrong
X-Api-Key + request_method + request_uri → $auth_ok map → 403 if out of scope

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 + URI in one regex
map "$http_x_api_key:$request_method:$request_uri" $auth_ok {
    default                                       0;
    "~^PUTFS_abc123:[^:]+:/acme/"                 1;   # acme, all methods
    "~^PUTFS_xyz789:(GET|HEAD):/acme/invoices/"   1;   # read-only, invoices
}

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:$request_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:

nginx -t && nginx -s reload

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.

S3 SigV4

The S3 API verifies signatures via awssig. Key files in PUTFS_S3_KEYS_DIR – filename is the access key ID, content is the secret(s) with a glob-style path allowlist:

# /keys/s3/AKID_JANE
HJSFHJSAFDJKAKDSJHJKSDF:test-bucket/*
HJSFHJSAFDJKAKDSJHJKSDF:other-bucket/**/*.json

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.

Key reload

Map-based keys and S3 key files are loaded at startup. Changes require nginx -s reload (map) or API restart (S3).

Further reading