Zero-knowledge AES-256-GCM encryption at rest, what it actually protects when SaltingIO holds your API key, and the auth pitfalls to avoid.

How do you trust a service to hold your Stripe secret key when that same service runs the database the key sits in? The honest answer for most hosted secret stores is that you don't, not fully. The provider can read the plaintext, so a breach on their side becomes a breach on yours.
Zero-knowledge encryption changes the shape of that risk. The provider stores your data, serves it back when an authorized request comes in, and still cannot read it at rest. SaltingIO encrypts every Credential with AES-256-GCM before it touches disk, and the decryption only happens at request time for a caller that proves it is allowed. This post walks through what that property actually buys you, and where the sharp edges are.
The term gets stretched, so it helps to pin down the concrete claim. A SaltingIO Credential is encrypted data stored at rest. The bytes on disk are ciphertext. When a valid request hits the record's endpoint, the platform decrypts the payload and returns it as JSON:
{ "data": "sk_live_51H...your-decrypted-secret" }
AES-256-GCM is an authenticated cipher, which matters for two reasons. The 256-bit key gives you the symmetric strength you'd expect, and the GCM authentication tag means tampered ciphertext fails to decrypt rather than returning garbage. You either get the original plaintext back or you get an error. There is no quiet corruption in between.
The practical consequence: a stolen database snapshot is a pile of ciphertext and authentication tags. Without the key material that lives outside that snapshot, the Stripe secret in record r/9f2c... is not recoverable from the dump alone. That is the whole point of storing it in a Credential instead of an environment variable on a box you also have to defend.
A private Credential is decrypted only for a caller that presents the right key. For programmatic access that key goes in the X-API-Key header. Here is a minimal fetch from a Node service or an edge function:
const res = await fetch("https://api.salting.io/r/9f2c1a7e-...", {
headers: { "X-API-Key": process.env.SALTING_KEY }
});
const { data } = await res.json();
// data is the decrypted secret, e.g. "sk_live_51H..."
The same call as curl, useful for a quick check from a terminal:
curl https://api.salting.io/r/9f2c1a7e-... \
-H "X-API-Key: $SALTING_KEY"
Notice what is not here. There is no decryption library in your code, no key-derivation step, no IV handling. The cryptography happens server side at the moment of the request, and your application receives plaintext over TLS. For a public Credential you drop the header entirely and call the endpoint directly, though you'd only make a record public when the stored value is not actually sensitive.
If the value you are protecting is a third-party API key rather than a static string, a Bridge is usually the better primitive. The key stays inside SaltingIO, the request gets forwarded to the upstream (OpenAI, Stripe, Anthropic) with the secret injected server side, and the browser never sees it at all. The encryption-at-rest story is the same, but the secret is never returned to the caller, which is a stronger position than handing back plaintext.
A few things bite developers wiring this up for the first time.
The key is shown exactly once. When you create a private Credential, the X-API-Key appears on the success modal with a Download .txt button, and it is never displayed again. Copy it immediately. If you click away before saving it, the key is gone and you have to rotate the record. Treat that modal the way you'd treat a freshly generated SSH key.
Bearer tokens do not work on /r/ endpoints. A common reflex is to reach for Authorization: Bearer <token>, because that is how most APIs authenticate. On a private Credential that returns a 401 with this body:
{
"error": "Authentication required",
"message": "Please provide your token in the X-API-Key header to decrypt the content."
}
The error is telling you the literal fix: the credential goes in X-API-Key, not in an Authorization header. Bearer tokens are dashboard JWTs and are not valid on the public request endpoints.
Origin locking fails closed, which is by design but surprising. If you restrict a record to a set of allowed origins and then call it from a domain that is not on the list, you get a 403:
{
"error": "Origin not allowed",
"message": "Your origin is not authorized to access this endpoint."
}
This trips people during local development, when http://localhost:3000 is not in the allowlist they configured for production. Add your dev origin explicitly, or test against a record without origin restrictions, rather than assuming the key is wrong.
Zero-knowledge encryption at rest defends against one specific threat: someone reading your secret out of storage they should not have access to. It is a strong defense for exactly that. It does not make a leaked X-API-Key harmless, and it does not stop an attacker who already runs code in your authorized environment. If the key that decrypts the Credential is sitting in a public client bundle, the encryption at rest did its job and the access control still failed.
That is the reason the Bridge pattern matters for anything truly sensitive. Returning decrypted { "data": ... } to the browser puts the plaintext in the client, which is fine for a value meant to be there and wrong for a value that is not. Keep genuinely secret upstream keys inside a Bridge so they are used server side and never serialized back to the caller.
If you want the field-by-field details on creating a record and where the one-time key appears, the docs cover the full dashboard flow.
Professional API security without the "Backend Tax."