Authentication & Security β
Zedgi uses a defence-in-depth approach: two-value API keys for identity, HMAC-SHA256 request signing for integrity, and ECIES credential encryption for confidentiality.
Two-value API key model β
Every API key consists of two independent values, returned once at creation:
| Value | Length | Purpose | Stored on server |
|---|---|---|---|
key (starts zk_) | 64 hex chars | Public identifier β sent as x-zedgi-key | SHA-256 hashed (irrecoverable) |
signing_secret | 64 hex chars | HMAC-SHA256 signing key | AES-256-GCM encrypted (Master KEK) |
{
"id": "a1b2c3d4e5f6...",
"key": "zk_live_1a2b3c4d5e6f7890abcdef1234567890abcdef12",
"signing_secret": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"label": "production"
}Neither value is ever shown again. Store both securely β the key is irrecoverable from our servers.
Request signing β
Every RPC request must carry four headers:
| Header | Format | Description |
|---|---|---|
x-zedgi-key | zk_.. (68 chars) | Public key identifier |
x-zedgi-ts | epoch milliseconds | Within Β±5 min of server clock |
x-zedgi-nonce | 32 hex chars (128-bit) | Single-use, prevents replay |
x-zedgi-sig | 64 hex chars | HMAC-SHA256 of canonical message |
Canonical message β
message = `${ts}:${nonce}:${sha256hex(body)}`
body = JSON.stringify(rpcPayload) // raw JSON, not an object
sig = HMAC-SHA256(message, signing_secret)The nonce must be a fresh 128-bit random value for every request. The server rejects replayed nonces (stored in KV until the timestamp window expires).
Verification flow β
x-zedgi-sig = HMAC-SHA256("1749600000000:a1b2...:3c4d...", signing_secret)- Server checks
x-zedgi-tsis within Β±5 minutes - Server checks
x-zedgi-noncehas not been seen before - Server decrypts
signing_secret_encfrom the database - Server recomputes the HMAC and compares constant-time
- Nonce is stored in KV (global replay protection across PoPs)
Credential encryption (ECIES) β
Credentials (host, port, user, password, database) never travel in plaintext. They are supplied by your application and turned into the x-zedgi-cred "link" (encrypted client-side).
Account keypair β
Every account has an X25519 keypair, generated on first sign-in:
- Public key β safe to share; used for client-side encryption
- Private key β wrapped with the Master KEK (AES-256-GCM); stored in D1
Two-hop encryption β
ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
β Client β β Gateway β β Proxy β
β β β (Worker) β β (Node.js) β
β credentials β β β β β
β β β β β β β
β βΌ β β β β β
β ECIES encryptβββββββΆβ ECIES decryptβ β β
β (account pub)β β β β β
β β β ECIES encryptβββββββΆβ ECIES decryptβ
β β β (RPC node pk)β β β
β β β β β connect + β
β β β β β execute β
ββββββββββββββββ ββββββββββββββββ ββββββββββββββββClient β Gateway hop:
Key derivation: X25519(ephemeral_priv, account_pub) β HKDF(secret, "zedgi-cred-client-gateway-v1") β AES-256-GCM key
Blob format (base64url-encoded binary):
0x01 (1 byte β version)
accountId (16 bytes β raw hex)
keyVersion (2 bytes β uint16 BE)
ephemeralPub (32 bytes β X25519 raw)
iv (12 bytes β AES-GCM nonce)
ciphertext (variable)
tag (16 bytes β appended by GCM)Gateway β RPC node hop:
Same ECIES scheme, different HKDF info: "zedgi-cred-gateway-rpc-v1"
The double-encrypted blob is cached in L1 (memory, rotated every 5 min) and L2 (KV, TTL 1 hour). No plaintext credential ever enters the cache.
Decrypt cache β
Cache key: SHA-256(encryptedCredBlob) β ciphertext in, ciphertext out. On miss: full ECDH decrypt β re-encrypt β populate cache. On hit: zero crypto, zero network.
API key management endpoints β
List API keys β
GET /api/services/:serviceId/keysReturns metadata only (id, label, created_at, last_used_at). The key and signing_secret are never returned again.
Create API key β
POST /api/services/:serviceId/keys
Content-Type: application/json
{ "label": "production" }Returns the key and signing_secret β save them immediately.
Delete API key β
DELETE /api/keys/:keyIdInvalidates the key globally. The KV cache entry is deleted so the key stops working immediately.
Account keypair endpoints β
Get current public key (for the credential link) β
SDKs can auto-pull this. Manually:
GET /api/account/keys/current
x-zedgi-key: zk_live_...(Works with just the public identifier β no signing secret needed. Public data only.)
{
"key_version": 1,
"public_key": "lRk7...base64url...",
"created_at": 1749600000000
}Use public_key (and the key_version embedded in the blob) when you or the SDK encrypt credentials into the x-zedgi-cred "link".
List all keypairs β
GET /api/account/keysReturns all keypairs (active and retired), ordered by version descending.
Rotate keypair β
POST /api/account/keys/rotateCreates a new active keypair and retires the current one. A notification email is sent.
What you do:
- The SDK (when
publicKeyis omitted) or your code fetches the new public key viaGET /api/account/keys/current(using yourx-zedgi-key). - Re-supply your
credentialtocreateZedgiClient(it will re-encrypt under the new key) or manually produce a freshx-zedgi-credblob. - Deploy the update.
Old credential blobs encrypted under retired key versions will stop working after the grace period.
x-zedgi-cred link (re-encrypt credentials with the new public key). Rate limiting β
Rate limits are enforced per-user, based on balance:
- Free tier: 100 req/day
- Paid tier: proportional to balance
When exceeded, the server returns 429 RATE_LIMIT_EXCEEDED.
IP whitelisting β
When creating a service, you can optionally specify a whitelisted IP. Only requests forwarded to the proxy from that IP will be accepted. The whitelist is checked by the proxy at connection time.