User Guide

Security Model

What actually protects a secret between "create" and "get" — encryption, key hashing, access scoping, and what does (and doesn't) leave the service in logs and traces.

Encryption at rest

Every secret value is encrypted with Fernet (AES-128-CBC, HMAC-authenticated) before it's written to the secrets table — the database only ever sees ciphertext. By default the service's FERNET_KEY is used; a caller can instead supply a fernet-key header per secret (BYOK) for additional isolation. A BYOK key is used for that single request and is never stored by Airlock — lose it, and that secret is unrecoverable.

Access key hashing

Access keys are never stored in a reversible form. Each key is SHA-256 hashed, then that digest is bcrypt-hashed (cost factor 12) for storage. A second, plain SHA-256 hash of the key (acckey_lookup_hash) is stored purely as an index, so validation is an indexed lookup plus one bcrypt comparison — not a bcrypt check against every row in the table.

IP allowlisting

Each access key carries a semicolon-separated acckey_allowed_ips list. 0.0.0.0 disables IP checking for that key; anything else is an explicit allowlist. Airlock resolves the caller's IP from cf-connecting-ip, then x-forwarded-for, then the raw socket address — CIDR ranges aren't supported natively, so put a reverse proxy or firewall in front if you need them.

Rate limiting

SlowAPI enforces per-IP limits before authentication even runs — RATE_LIMIT_READ (60/minute default) and RATE_LIMIT_WRITE (5/minute default) — which caps how fast an attacker can grind through access-key guesses.

Error sanitization

Every error message is checked against a small allowlist of known-safe strings (e.g. "Secret not found", "Fernet key does not match") before it's returned to the client. Anything that doesn't match is logged server-side in full and replaced with a generic message plus a request ID — internal exception text, stack traces, and DB errors never reach the caller.

Observability & secret scrubbing

Every request is traced via Pydantic Logfire. Logfire's default scrubbing already redacts attribute paths containing words like secret, password, or token — but that's a keyword match on the field name, not the value, so it doesn't catch everything.

Fixed: the access_key and fernet_key fields don't contain a default-scrubbed keyword, so they were showing up in plaintext in traces. Airlock now sets explicit scrub patterns for all three sensitive fields:
logfire.configure(
    service_name="airlock",
    distributed_tracing=False,
    scrubbing=logfire.ScrubbingOptions(
        extra_patterns=["access_key", "fernet_key", "secret_name"]
    ),
)

Any matching field is replaced with [Scrubbed due to '<field>'] before export — the raw value never leaves the process.

Best practices

Master key custody: for non-BYOK secrets, Network14.cloud generates, secures, and backs up the service's Fernet master key as part of the managed offering — it's not something you need to manage yourself.