seal

Using Seal

Markdown

APIs and databases

What Seal can sit in front of today: HTTPS APIs, Postgres and Redis, and what each one allows.

The upstream is the service your key unlocks. You name it when you seal, and the enclave uses the key there and nowhere else. Every upstream must speak TLS.

HTTPS API, key in a header
Seal it with --tohttps://api.example.com/v1
Your app getsa local url and a stand-in key
Postgres
Seal it with --topostgres://[email protected]:5432/prod
Your app getsa local dsn
Redis
Seal it with --torediss://cache.example.com:6380/0
Your app getsa local dsn

Anyone can read a blob's destination without opening it.

HTTP APIs#

By default the enclave sends the key as Authorization: Bearer <key>, which OpenAI and Stripe expect. Your app sends the stand-in key, and the enclave swaps in the real one.

  • --header picks another header. A few, such as Host and Cookie, aren't allowed.
  • --format sets the value, with {key} exactly once. The default is Bearer {key} for Authorization and {key} for anything else.
Terminal
$seal secret --to https://api.openai.com/v1 --app checkout < openai-key.txt > openai.blob
$seal secret --to https://api.anthropic.com --app checkout --header x-api-key < anthropic-key.txt > anthropic.blob

Pass the session's url to your client as its base URL. It keeps the sealed path, so seal Anthropic's bare host, since its libraries add /v1. The SDKs take the same options:

blob = seal.seal(key, to="https://api.anthropic.com", apps=["checkout"], header="x-api-key")

Narrowing what a key can reach#

A session can make any request the key allows. Narrow the key at the provider, then the blob:

A path in --to, like /v1
EffectOnly /v1 and below, by whole segment: /v1/chat, not /v10. Anything else gets a 403
--methods GET,POST
EffectOnly these methods
--deny /v1/organization
EffectRefuses that prefix, ignoring case. Use it for key-management endpoints

Paths with percent-encoding, .. or // are always refused. With --methods, so are multipart/form-data bodies.

Response masking#

The enclave masks any copy of the key in a response with *. It finds plain text, one layer of JSON or URL escaping, and base64, including inside Basic auth and JWTs.

{

"error": "Invalid key sk-proj-7Hq2Lx9Vb9Xc4Ws1",plain text

"echo": "sk\u002dproj\u002d7Hq2Lx9Vb9Xc4Ws1",JSON-escaped

"header": "Bearer%20sk-proj-7Hq2Lx9Vb9Xc4Ws1",URL-encoded

"debug": "c2stcHJvai03SHEyTHg5VmI5WGM0V3Mx",base64

"key_hash": "9f86d081884c7d65…"a hash of the key

}

FigureThe enclave masks the key in plain text and in common encodings before the response leaves it. A hash, hex dump or fragment of the key isn't caught, so don't seal keys for endpoints that return those.

It misses hashes, hex, partial keys and double escaping. It's a backstop. It's also why Seal refuses secrets under 16 bytes: a short key could be guessed by watching which echo gets masked.

What the enclave changes about requests#

Any Host header
The enclaveSets it to the destination's host and port
A request that gets a redirect
The enclaveReturns the 3xx. It never follows redirects
A WebSocket or other Upgrade, or CONNECT
The enclaveRefuses it
A method override header or _method parameter
The enclaveRefuses it
Range or If-Range
The enclaveDrops it, so an echoed key arrives whole
Accept-Encoding: gzip
The enclaveAsks for an uncompressed response and refuses a compressed one

Streaming and server-sent events work over the local HTTP/1.1 connection.

Postgres#

Terminal
$seal secret --to postgres://[email protected]:5432/prod --app checkout < db-password.txt > db.blob

Seal only the password. Open a database session and give its dsn to your driver:

Python
with seal.session(os.environ["DATABASE_BLOB"], kind="database") as db:
    conn = psycopg.connect(db.dsn)
TLS required
DetailA server that won't do TLS is refused. No plain-text fallback
SCRAM-SHA-256 only
DetailMD5 and cleartext logins are refused. Before Postgres 14, set password_encryption = scram-sha-256 and reset the password
Fixed login
DetailAlways the sealed user and database, whatever your driver asks for. Startup options are dropped
Password
DetailPrintable ASCII, 16 to 1,024 bytes
Refused
DetailReplication connections and fast-path function calls

The local DSN says sslmode=disable because it points at the SDK's tunnel on 127.0.0.1. Both hops beyond it use TLS. The enclave doesn't read your SQL, so grant the sealed user only what your app needs.

Redis#

Terminal
$seal secret --to rediss://cache.example.com:6380/0 --app checkout < redis-password.txt > redis.blob

Use rediss://. Put an ACL user before the host if needed. Pipelining, transactions and pub/sub work. These commands get NOPERM, and the connection stays open:

Login
CommandsAUTH, HELLO … AUTH
Other databases
CommandsSELECT of another database, SWAPDB, MOVE, COPY … DB, FLUSHALL, MIGRATE
Connection changes
CommandsRESET, MONITOR, SYNC, PSYNC, REPLCONF, CLIENT REPLY OFF or SKIP
Administration
CommandsACL, CONFIG, MODULE, DEBUG, SHUTDOWN, REPLICAOF, FAILOVER, CLIENT KILL or PAUSE

The Redis user's ACL decides everything else, including scripts.

Certificates#

Each release trusts the Mozilla root list plus Amazon RDS's bundle. That covers public APIs, RDS, Aurora, Azure Database for PostgreSQL and Azure Cache for Redis. Google Cloud SQL works only in Google-managed shared CA mode. For a self-signed certificate or private CA, talk to us.

--pin sha256/<base64>, up to four times, requires that key in the chain. Pin your provider's CA key, since the server's can change at renewal. Compute it from ca.pem, then add sha256/ in front:

Terminal
$openssl x509 -in ca.pem -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl base64

What's not supported#

  • Keys in the request body or query string.
  • Plain http://, redis:// or Postgres without TLS.
  • MySQL, MongoDB, S3 and ClickHouse.
  • WebSockets and Redis Cluster.
  • Private-network databases from Seal's public replica. Run your own for those.

Need one? Write to [email protected]. Limits lists every size and rate.