Vault

Your project's keys and secrets, in one place Appentic can't read. Encrypt and sign with keys that never leave it, and give your services API keys without putting them in the environment.

Every app has things it must keep safe. Some are keys: the key that encrypts your users' documents, or the key that signs your login tokens. Some are secrets: your Anthropic API key, your Stripe key, your email provider's password.

The usual place for all of them is an environment variable. That works until something reads the environment that shouldn't: a leaked backup, a deploy log, a compromised platform.

A Vault gives your project a better home for both. It has two halves:

  • Keys never leave the Vault. Your app sends data in and gets back ciphertext, a signature or an HMAC. Nobody can copy the key itself, not even Appentic.
  • Secrets are values your app needs in plain form, like an API key. They live only in the Vault. Appentic can save them but never read them back, and only the services you choose can read them.

If you're coming from AWS or Google Cloud, Keys is what you'd use KMS for, and Secrets is what you'd use Secrets Manager or Secret Manager for. On Azure, it's Key Vault. Here they're one service, with one price and one off switch.

Add a Vault

  1. On the project's Services page, click Add next to Vault.
  2. Click Add Vault.

The form shows the monthly fee under Price. Your Vault is ready in a few seconds, and every web service, worker and cron job in the project restarts once to pick up its settings. A project has one Vault. It's available for projects whose machines are all in the EU for now.

You can also ask a coding agent to add one. It needs your go-ahead for the fee.

Secrets

Start here. It's the quickest win: move your API keys out of the environment and into the Vault.

  1. Open the Vault's Secrets page and click Add Secret.
  2. Enter a Name, such as ANTHROPIC_API_KEY, and its Value. Click Add another for more.
  3. Under Available to, keep All services or choose the services that need them.
  4. Click Save.

The services that read the secrets restart once, within a minute, however many you saved. A secret is never shown again, so keep your own copy somewhere safe. To change one, save it again with the same name. The old value is gone for good, so a key you replace after a leak can't be read back.

A name that Appentic or the system relies on, such as PATH or one starting APPENTIC_, can't be used for a secret.

Reading secrets in your app

Your services load their secrets when they start:

  • Feather apps get each secret as an ordinary environment variable. os.environ["ANTHROPIC_API_KEY"] just works, with no code change. You need Feather 0.9.38 or later.
  • Any other app reads the file /run/appentic/secrets/ANTHROPIC_API_KEY. The folder is in APPENTIC_SECRETS_DIR, and it lives in memory, never on a disk.

Each service signs in to the Vault with its own identity and can read only the secrets made available to it. If a worker doesn't need your Stripe key, it can't read it. Unlike environment variables, a secret doesn't follow a web service to the workers and cron jobs that run its code: choose each one that needs it.

Moving a secret out of Environment

  1. Save it on the Secrets page with the same name.
  2. Delete it from Environment.

A service that gets a name from both uses the secret, so your app keeps working while you move things across.

Keys

Every Vault comes with a main key, for encrypting. Most apps never need more. Add another key from the Keys page when you have a job the main key isn't right for:

TypeUse it to
EncryptionKeep a kind of data apart, with its own rotation. For example, one key for documents and another for payments.
Signing, Ed25519Sign tokens, webhooks or licence files
Signing, ECDSA P-256Sign JWTs as ES256
Signing, RSA 2048Sign JWTs as RS256
HMACSign webhooks, or look up a value without storing it in plain text, such as finding a user by an HMAC of their email

Give the key a name, choose its type and click Add key. Your app calls a key by the name shown under it on the Keys page: your main key's name, a dash, then the key's own name. For a signing key, the Keys page lets you copy its public key, so other systems can check your app's signatures.

Using keys in your app

Appentic sets these variables on every web service, worker and cron job in the project. None of them is a secret.

VariableWhat it is
APPENTIC_KMS_ADDRThe Vault's address. It answers only inside Appentic.
APPENTIC_KMS_KEYYour main key's name
APPENTIC_KMS_ROLEThe role your app signs in as
APPENTIC_KMS_MOUNTtransit
APPENTIC_KMS_AUTH_PATHauth/kubernetes/login
APPENTIC_KMS_TOKEN_FILEA file holding your app's sign-in token. It renews itself.
APPENTIC_KMS_CA_FILEThe certificate your app trusts the Vault by

Your app signs in with the token file, then calls the key. Here's a small Python helper you can drop into your app:

import base64, os, requests

ADDR = os.environ["APPENTIC_KMS_ADDR"]
CA = os.environ["APPENTIC_KMS_CA_FILE"]


def _token() -> str:
    with open(os.environ["APPENTIC_KMS_TOKEN_FILE"]) as f:
        jwt = f.read()
    r = requests.post(f"{ADDR}/v1/{os.environ['APPENTIC_KMS_AUTH_PATH']}",
                      json={"role": os.environ["APPENTIC_KMS_ROLE"], "jwt": jwt}, verify=CA, timeout=10)
    r.raise_for_status()
    return r.json()["auth"]["client_token"]


def _transit(op: str, body: dict, key: str = "") -> dict:
    """One call with the main key, or with another of your keys by its own name."""
    name = os.environ["APPENTIC_KMS_KEY"] + (f"-{key}" if key else "")
    url = f"{ADDR}/v1/{os.environ['APPENTIC_KMS_MOUNT']}/{op}/{name}"
    r = requests.post(url, json=body, headers={"X-Vault-Token": _token()}, verify=CA, timeout=10)
    r.raise_for_status()
    return r.json()["data"]

Protect a master key

The classic pattern, called envelope encryption: your app has its own master key that encrypts its data, and the Vault keeps that master key safe.

def wrap(master_key: bytes) -> str:
    """Run once. Store what it returns (vault:v1:...) as a variable."""
    return _transit("encrypt", {"plaintext": base64.b64encode(master_key).decode()})["ciphertext"]


def unwrap(wrapped: str) -> bytes:
    """Run at start. Keep the master key in memory only."""
    return base64.b64decode(_transit("decrypt", {"ciphertext": wrapped})["plaintext"])
  1. Make a master key and wrap it from inside your project, for example in a web service's Shell: python -c "import os; from app.kms import wrap; print(wrap(os.urandom(32)))".
  2. Add the result, which starts vault:v1:, as a variable such as MASTER_KEY_WRAPPED. On its own it's useless to anyone.
  3. At start, your app calls unwrap(os.environ["MASTER_KEY_WRAPPED"]) and keeps the master key in memory. Never write it to the database, a file or a log.

Unwrap once per process, not per request. Your app only calls the Vault when it starts, so running traffic never waits for it. If unwrapping fails, stop at start with a clear message. Never fall back to a key in plain text.

Encrypt large data

Wrapping suits small things. For a file or many rows, ask for a fresh data key, encrypt locally with it, and keep only its wrapped copy beside the data:

from cryptography.hazmat.primitives.ciphers.aead import AESGCM


def encrypt_large(data: bytes) -> tuple[str, bytes]:
    """Returns (wrapped data key, nonce + ciphertext). Store both."""
    made = _transit("datakey/plaintext", {})
    key = base64.b64decode(made["plaintext"])
    nonce = os.urandom(12)
    return made["ciphertext"], nonce + AESGCM(key).encrypt(nonce, data, None)


def decrypt_large(wrapped_key: str, blob: bytes) -> bytes:
    key = base64.b64decode(_transit("decrypt", {"ciphertext": wrapped_key})["plaintext"])
    return AESGCM(key).decrypt(blob[:12], blob[12:], None)

Sign and verify

def sign(message: bytes, key: str = "signing") -> str:
    return _transit("sign", {"input": base64.b64encode(message).decode()}, key)["signature"]


def verify(message: bytes, signature: str, key: str = "signing") -> bool:
    body = {"input": base64.b64encode(message).decode(), "signature": signature}
    return _transit("verify", body, key)["valid"]


def hmac(message: bytes, key: str = "webhooks") -> str:
    return _transit("hmac", {"input": base64.b64encode(message).decode()}, key)["hmac"]

For a JWT signed with an ECDSA key, pass "marshaling_algorithm": "jws" with the input. Each signature is a call to the Vault, so sign at the rate requests arrive at most, and cache what you've already checked.

What else it protects

A Vault protects more than your app's own data. Appentic encrypts these under keys of their own, and your Vault controls those keys too:

The Vault's Overview lists what it protects in your project.

Activity

The Vault's Activity page lists every call in the last 30 days. For each call it shows:

  • when it was made;
  • which service, worker or cron job made it;
  • which key or secret it used;
  • what it did, such as encrypt, sign or read;
  • whether it was allowed. Hover a refused call to see why.

Nothing your app encrypted, signed or read is ever logged. Appentic's own uses show too, such as saving a secret or reading your variables to deploy. Last used on the Overview counts only your app's own calls.

Rotate

On the Vault's Settings, click Rotate key. This adds a new version of your main key and of the keys that protect your variables, file storage, backups and volumes. Everything encrypted before still decrypts, so your app needs no change. To move a stored value onto the new version without unwrapping it, call _transit("rewrap", {"ciphertext": old}).

Rotate any of your other keys from the Keys page. Signatures made before a rotation still verify.

Emergency stop

If you think someone is using your Vault who shouldn't be, click Disable key on its Settings. Every key and every secret is refused at once. Your services and workers restart, and a cron job's run under way ends, so none keeps anything in memory. Until you enable it again:

  • your services can't start;
  • deploys and changes to your variables fail;
  • backups can't be restored, though backups keep running;
  • your files can't be read or written, within a minute;
  • a database on a volume keeps running, but can't restart.

Your data is safe. Click Enable key to undo it. Your services and workers restart and sign in again.

Remove a Vault

On the Vault's Settings, click Remove Vault. Nothing in the project can use its keys from then on, its secrets are deleted, and billing stops.

The keys are kept for 30 days, then deleted for good. Data encrypted with them can't be decrypted after that. Add a Vault to the same project within those 30 days to get the same keys back. Removing one of your other keys from the Keys page works the same way.

Deleting the project removes its Vault the same way.