Fallback cache

How the CLI and SDKs keep an encrypted copy of each secret a machine reads, when a copy answers a read, when copies are deleted, and the cryptography that protects them.

Updated Oct 19, 2026

The fallback cache keeps an encrypted copy of each secret a machine reads, on the machine itself, so it can still read them when your Burrow can't be reached. It runs entirely on the machine, and nothing changes on your Burrow. The Ratel CLI and every Burrow SDK keep the same files in the same format, so each can answer from copies the others saved. This page sets out where the copies are kept, when they're written, used and deleted, and how they're encrypted. It describes how the current release behaves.

Turning it on#

  • The cache is off until you turn it on, and each Burrow identity on the machine has its own.

  • ratel cache enable turns it on by writing cache.json, holding {"enabled": true}, into the identity's folder, and ratel cache disable removes that file and deletes every copy.

  • An app reading as the same identity with an SDK (Go, Kotlin, Node.js, Python, .NET) uses the cache when that file turns it on, unless its code turns the cache on or off itself. Turning it off in code stops that app using it and leaves the copies in place.

  • An app that supplies its own key, instead of an identity from ~/.burrow, has the cache only when its code turns it on and names a folder to keep the copies in.

  • A machine that reads with OIDC, and one enrolled in memory from a machine token's code, keeps no key on disk, so it has no cache.

Where the copies are kept#

  • Each identity keeps its copies in its own folder, one file per secret: ~/.burrow/burrows/<slug>/cache/<project>/<environment>/<NAME>. BURROW_HOME takes the place of ~/.burrow when it's set, and on Windows ~ is your user profile folder.

  • An app that supplies its own key keeps the same layout in a cache folder inside the folder it names.

  • On Linux and macOS, each folder is made readable only by its owner (0700) and each file is 0600. On Windows, they take the permissions of the identity's folder.

  • Project, environment and secret names become folder and file names as they are. Burrow names are made only of letters, digits, hyphens and underscores, so no name can reach outside its folder, and the cache refuses any name that isn't. A name Windows keeps for a device, such as CON or NUL, is written with a ~ after it.

  • Each environment's folder also holds .environment, naming the secrets the environment held the last time it was read in full.

  • A copy is written to a temporary file beside it, then renamed over the old one, so a reader never finds half a copy. The CLI and any number of apps can share the folder, and the latest write wins.

When a copy is written#

  • Every successful read of a whole secret saves its copy, replacing the one before: ratel secret get, an SDK reading a secret, and each read a live secret makes as it follows changes.

  • A read of a whole environment, by ratel run or a live environment, saves a copy of each secret in it, rewrites .environment, and deletes the copies of any secret the environment no longer holds.

  • Reading one field of a structured secret with ratel secret get NAME field saves no copy of its own.

  • Certificates from an SSH user secret, and the lists of projects and secrets a machine can read, are never cached.

  • A copy holds the secret exactly as your Burrow returned it: a single value with its type and version, or a structured secret with every field. For a dynamic secret, that's the login your Burrow issued this machine.

  • A copy that can't be written never fails the read.

When a copy answers#

A read is answered from its copy only when it fails for a reason other than a refusal:

  • Your Burrow can't be reached: its address doesn't resolve, or the connection is refused, times out or drops.

  • Its certificate fails the check this machine makes. The check runs while connecting, before the request is sent, so nothing reaches whoever answered.

  • It answers with an error of its own instead of the secret, such as 500 or 503 while it restarts, 502 when a dynamic secret can't issue a login, or 429 when its rate limit is reached.

When one does, ratel secret get prints a notice on standard error with your Burrow's error and how old the copy is, and the value alone on standard output:

bash
ratel: can't reach the burrow at https://burrow.example.com: burrow.example.com:443 didn't answer in time.
ratel: using the copy of myapp/prod/DATABASE_URL cached 12m ago
  • One field is answered from the whole secret's copy.

  • An SDK returns the secret with the time its copy was saved, so your app can tell.

  • ratel run answers an environment from its copies only when .environment opens and every secret it names has a copy that opens, so a command never starts with part of its environment. Its notice gives the age of the oldest copy.

  • With ratel run --watch, only the first load can come from the cache. A change is read from your Burrow, and if that read fails the command keeps running with what it has.

  • A live secret or environment in an SDK never answers from the cache. While your Burrow can't be reached it keeps the value it last read, and reads again until it succeeds.

  • --offline answers ratel secret get and ratel run from the copies alone, without contacting your Burrow, whether or not the cache is on.

  • A copy has no expiry. It answers until it's replaced or deleted, however old it is.

  • A read answered from a copy never reaches your Burrow, so it isn't in your Burrow's audit log. The read that saved the copy is.

When copies are deleted#

When your Burrow refuses a read, the read fails with its answer, and the copies it refused are deleted first:

Your Burrow answers

Because

Deleted

401

It refused the machine: it was removed, disabled or has expired, it's reading from an address outside the machine allowlist, or its request couldn't be verified, for example because its clock is too far from the Burrow's

Every copy the identity keeps

403

The machine has no grant for the environment

That environment's copies

404

The project, environment or secret doesn't exist

That secret's copy, or the environment's for a read of the whole environment

409

The secret's own settings refuse the read, such as a dynamic secret that isn't issuing this machine a login

As for 404

  • A refused read of one field deletes nothing on a 404 or 409, since the refusal may be about the field.

  • ratel cache clear deletes the copies of a whole identity, a project, an environment or one secret, and ratel cache disable deletes them all.

  • Otherwise a copy stays until a read replaces it, or an environment read finds its secret gone.

Encryption#

  • Every copy is encrypted with AES-256-GCM, with a 96-bit nonce drawn from the operating system's random source for each write, and a 128-bit authentication tag.

  • The key is derived with HKDF-SHA256 (RFC 5869) from the identity's Ed25519 private key: its 32-byte seed is the input key material, the machine ID is the salt, and ratel-cache-v1 is the info. The output is the 256-bit AES key.

  • The key is derived in memory when a client opens the cache, and is never written anywhere. Each identity has its own key, so one identity's copies never open with another's.

  • The private key itself never encrypts anything, and HKDF can't be reversed, so the cache key gives no way back to the key the machine signs its requests with.

  • Each copy is bound, as additional authenticated data, to the text ratel-cache-v1, the machine ID, the secret's project/environment/NAME and the time the copy was saved, separated by zero bytes. A copy moved onto another secret's file, carried to another machine, or given a different time fails its tag check and is treated as missing.

  • .environment is encrypted the same way, bound to its environment with an empty name, so one environment's list can't be moved to another.

  • A copy that doesn't open, because it was damaged, changed or written for another identity, never answers. ratel cache list shows it as one that doesn't open with this machine's key.

  • NIST SP 800-38D allows 2³² encryptions under one key with random nonces, and each copy written is one. A machine rewriting a copy every second would take more than a century to reach it.

File format#

Each copy is a small JSON file:

json
{"v":1,"cachedAt":1760000000000,"nonce":"oKGio6Slpqeoqaqr","ct":"W5ZbKZbzPXRyGzrmplIDy9MTfrt44jVt7uUgNKf1Z9KfvHrjK+5suGZHYgeOY8Di46mfWuuP0hpgLRGSXbyibWkxjtnqRorjYO6kYRJ9PcJyl79gwp2KJjEgLcPJjxcmh4O4vM1IOMC43zOfz9LW"}
  • v is the format version, 1. A copy in any other version is ignored.

  • cachedAt is when the copy was saved, in milliseconds since the Unix epoch, by the machine's clock. It can be read without the key, and it's bound to the copy, so changing it makes the copy fail to open.

  • nonce is the 12-byte nonce, and ct the ciphertext with its 16-byte tag after it, both in standard base64.

  • Decrypted, a copy is the secret's JSON as your Burrow returned it, such as {"name":"DATABASE_URL","value":"...","type":"string","version":3}. .environment decrypts to the names it lists, such as {"names":["API_KEY","DATABASE_URL"]}.

Test vector#

The CLI and every SDK are tested against these values, so a copy one of them writes opens in all the others. The file above holds the first of them. From these inputs:

text
seed        000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
machine ID  0b4f6c1e-8a3d-4d8e-9f21-7c5e2a9b1d34
secret      payments/prod-main/DATABASE_URL
saved at    1760000000000
nonce       oKGio6Slpqeoqaqr
plaintext   {"name":"DATABASE_URL","value":"postgres://app:s3cret@db:5432/app","type":"string","version":3}

the key and the ciphertext with its tag are:

text
key         6bcdea7a5f9ef728225a1a23fdc904c93da57de58c08adad6a3e8983b71d126a
ct          W5ZbKZbzPXRyGzrmplIDy9MTfrt44jVt7uUgNKf1Z9KfvHrjK+5suGZHYgeOY8Di46mfWuuP0hpgLRGSXbyibWkxjtnqRorjYO6kYRJ9PcJyl79gwp2KJjEgLcPJjxcmh4O4vM1IOMC43zOfz9LW

With the same key, nonce and time, the list {"names":["API_KEY","DATABASE_URL"]} sealed for payments/prod-main/ is:

text
ct          W5ZbKZbzbGxqBFnzt1kd09MVCcUWhFgb2cYNEoeICKKj8VTqLtfYimYMALUm6vrNiHyQig==

Limits#

  • The copies are as safe as the machine's private key. Anyone who can read the identity's identity.pem can derive the cache key and read every copy, so keep the identity's folder readable only by the account your apps run as.

  • Without that key, a copy can't be read: a backup or copy of the cache folder alone reveals no secret values.

  • Names and times aren't encrypted. Project, environment and secret names are the folder and file names, and each copy's saved time is in the clear.

  • A machine you remove or disable on your Burrow loses its copies the next time it reaches the Burrow. Until then, including on a machine that never reaches it again, its copies keep answering, and --offline reads don't contact the Burrow at all.

  • A dynamic secret's copy holds a login your Burrow made for this machine, which the Burrow removes when its lease ends. A rotated secret's copy holds the account's password at the time, which stops working when the password is rotated. The cache makes no change on your Burrow for either, so these copies answer as they were saved, and may no longer work.