Seal and publish a secrets file
Everything that writes a sealed file happens offline, in kallisto-ctl. The
server only ever reads. This guide goes from nothing to a file in a bucket that
a resolver can serve, and then through a rotation.
kallisto-ctl ships in the container image at /usr/local/bin/kallisto-ctl, or
you can build it from source.
1. Generate a seal key
Section titled “1. Generate a seal key”kallisto-ctl gen-keyIt prints 32 random bytes, hex-encoded, on stdout and stores them nowhere. Put
the value straight into your secret store: the orchestrator’s secrets, a CI
secret, a password manager. Every machine that serves the file needs it as
KALLISTO_SEAL_KEY.
For this session:
export KALLISTO_SEAL_KEY=$(kallisto-ctl gen-key)The key is only ever read from the environment. --seal-key exists solely to be
refused, because command-line arguments are readable by every process on the
host through ps.
2. Write the plaintext
Section titled “2. Write the plaintext”{ "version": 1, "secrets": { "app/db": { "username": "app", "password": "change-me" }, "app/stripe": { "webhook_secret": "change-me" } }, "policies": {}, "tokens": {}}versionis the content version. It must go up every time you publish.- Each key in
secretsis a path without the mount prefix.app/dbis served at/v1/secret/data/app/db. Each value is the KV-v2dataobject. - Empty
policiesandtokensmean every read is permitted. That fits one app with its own file. To give several apps different access, see add tokens and policies.
The full schema is in the sealed file reference. Keep the plaintext out of git, or keep it encrypted there.
3. Seal it
Section titled “3. Seal it”kallisto-ctl seal --in plain.json --out secrets.kalsealed version 1 into secrets.kal: 2 secret(s), 0 polic(ies), 0 token(s)Check the result without printing anything sensitive:
kallisto-ctl verify --in secrets.kalsecrets.kal: authentic version 1 secrets 2 policies 0 tokens 0A last line reports authorization: none when the file has no token table and
every read is permitted, enforced when it has one. verify checks the
authentication tag and reports counts. It never prints a secret or a path.
4. Upload it
Section titled “4. Upload it”Any S3-compatible tool works. For example:
aws s3 cp secrets.kal s3://kallisto/prod/payment.kal# ormc cp secrets.kal myminio/kallisto/prod/payment.kalGive the credential each resolver uses read-only access to that one object. Write access to the bucket is the attacker position the design defends against, so no resolver should hold it.
Point the resolver at the object in its configuration. See configure the source.
5. Roll out a change
Section titled “5. Roll out a change”Rotating a secret, revoking a token and changing a policy are all the same motion: edit the plaintext, raise the version, seal, upload.
# after editing plain.json and raising "version" to 2kallisto-ctl seal --in plain.json --out secrets.kalaws s3 cp secrets.kal s3://kallisto/prod/payment.kalEvery resolver picks it up on its next poll, 30 seconds by default.
kallisto_file_version on /v1/sys/health and /v1/sys/metrics shows which
version each machine is serving.
If the file already on disk holds the same or a higher version, seal stops,
because a resolver would refuse the result. You can also raise the version in
place on an existing sealed file:
kallisto-ctl bump-version --in secrets.kal # +1kallisto-ctl bump-version --in secrets.kal --to 10 # a specific versionseal --force overwrites an existing output regardless of its version. Use it
only when rebuilding from scratch, with resolvers that have never seen the
older, higher version.
Rotating the seal key
Section titled “Rotating the seal key”The seal key and the token key are independent, so rotating the seal key leaves every token working.
- Generate a new key.
- Seal the plaintext under it, with a higher version.
- Upload the new file.
- Update
KALLISTO_SEAL_KEYon each machine and restart its resolver.
Keep that order. Between steps 3 and 4, a resolver still holding the old key
cannot authenticate the new file, so it keeps serving the table it already has
and counts each failed poll in kallisto_refresh_failures_total. After the
restart, its fallback copy is still under the old key: it logs
sealed file failed authentication for the cache, then loads the new file from
the bucket.
Reversing steps 3 and 4 means a restarted resolver can authenticate neither the bucket’s file nor its fallback copy, and it stays sealed until the upload.
Reading a file back
Section titled “Reading a file back”kallisto-ctl open --in secrets.kal --yes-print-secrets-to-stdoutPrints the whole plaintext. The flag is spelled out so it cannot be typed by accident or left unnoticed in a script.