TILLSECRETS · DELIVERY

Encrypted delivery and host keys

Beta

When a service token pulls config, reads it with exec --env, or leases a credential, the answer comes back encrypted to a post-quantum key as well as over TLS. Bind the token to a host key and it only answers to that one host: a copy of the token anywhere else gets nothing, and your admins are told.

01Default

What happens on every call

The CLI and the SDKs send a public key with each request. The answer is encrypted to it with X-Wing (ML-KEM-768 with X25519) and AES-256-GCM, and the client refuses an answer that isn't. With no host key, the client makes a fresh key pair for each call and forgets it afterwards.

  • A recording of the traffic can't be opened later, even by someone who breaks the TLS key exchange with a future quantum computer.
  • A proxy or load balancer that ends TLS, and anything that logs bodies there, sees ciphertext.
  • Each answer is bound to its request: it opens only for that request's key, nonce and endpoint.

This needs no setup. Values at rest were already AES-256-GCM under your workspace's key; this covers the trip to your host.

02Host keys

Binding a token to a host

A host key is an X-Wing key pair made on the host. The private half stays in a file only the service's user can read. You give its public half to a new service token, and from then on that token answers only encrypted to it.

terminal
# 1. On the host, as the user the service runs as
tilldev secrets host-key init
# ✓ Made a host key at /home/app/.tilldev/host-key (only this user can read it; it never leaves this host)
# fingerprint  SHA256:4mJ0p1u9oQyq2xG0l7m1bqz9wVd0Zb0g8c6pYwU1H3s
# public key   /home/app/.tilldev/host-key.pub

# 2. Where you manage tokens, with host-key.pub copied over (it is public)
tilldev secrets tokens create --project web --env prod --host-key @host-key.pub --quiet > web.token

# 3. On the host
TILLSECRETS_TOKEN="$(cat web.token)" tilldev secrets exec --env prod -- node server.js

In the dashboard, paste the contents of host-key.pub into Host key when you create a token. The token list shows each bound token's fingerprint. A token's host key can't be changed: to move a service to a new host, make the new host's key and a new token, then revoke the old one.

Stolen tokens
A bound token on its own is useless: it gets no values, from any endpoint that returns them. Someone would need the token and the key file from the same host.
03Key file

Where clients look for the key

OrderWhereUse it for
1--host-key <file>The CLI, one command
2TILLSECRETS_HOST_KEY_FILEA key kept somewhere other than the home directory
3TILLSECRETS_HOST_KEYThe file’s contents, on a platform with no disk (a Worker secret, a PaaS variable)
4~/.tilldev/host-keyThe default from host-key init

The CLI and the Node SDK refuse a key file that other users can read, the way ssh does. tilldev secrets host-key show prints the public key and fingerprint again.

04SDKs

In code

Node reads the same places as the CLI:

server.ts
import { load } from '@tillstack/secrets-node'

// Finds the key in TILLSECRETS_HOST_KEY_FILE, TILLSECRETS_HOST_KEY or ~/.tilldev/host-key.
await load()

On the edge, pass the key from a secret:

worker.ts
import { createClient } from '@tillstack/secrets-edge'

interface Env {
  TILLSECRETS_TOKEN: string
  TILLSECRETS_HOST_KEY: string
}

export default {
  async fetch(_req: Request, env: Env) {
    const { secrets } = await createClient({ token: env.TILLSECRETS_TOKEN, hostKey: env.TILLSECRETS_HOST_KEY }).pull()
    return new Response(secrets.STRIPE_KEY ? 'configured' : 'missing')
  },
}

@tillstack/secrets-core takes the same hostKey option. seal: false, or --no-seal in the CLI, turns encryption off; a bound token then gets nothing.

05Alerts

When a bound token is used without its key

terminal
$ TILLSECRETS_TOKEN=ts_… tilldev secrets pull
✗ 403: this token answers only encrypted to its host key (SHA256:4mJ0p1u9…); give the host its key with TILLSECRETS_HOST_KEY

The answer is 403 with reason host_key_required (no key offered) or host_key_mismatch (a different key), before anything is decrypted. Then:

  • A high-severity anomaly of kind host_key opens for the token, with the IP and endpoint. Repeats count on the same one until it is resolved.
  • Every owner and admin is emailed once per anomaly, and it goes to any TillPulse rule that matches it.
  • The audit log records token.host_key_refused.

If the host just hasn't been given its key yet, give it the key and resolve the anomaly as Setup. Otherwise the token has been copied: revoke it.

06Format

For your own client

The same scheme covers POST /api/secrets/pull, the lease service's POST /v1/lease and GET /v1/config:

protocol
POST /api/secrets/pull
Authorization: Bearer ts_…
x-tillsecrets-seal-key: <X-Wing public key, 1216 bytes, base64url>
x-tillsecrets-seal-nonce: <16–64 random bytes, base64url, new for each request>

→ 200 {"sealed": "xwing-aes256gcm.v1|<KEM ciphertext>.<nonce>.<ciphertext + tag>",
       "seal_key": "SHA256:<fingerprint of that public key>"}

context  = "tillsecrets-delivery/v1\n" + purpose + "\n" + seal_key + "\n" + request nonce
           (purpose: pull, lease or config)
key      = HKDF-SHA256(X-Wing shared secret, salt = empty, info = context), 32 bytes
opened   = AES-256-GCM(key, 12-byte nonce, additional data = context)
         = the JSON the endpoint returns without sealing

A host key file holds the base64 of the 32-byte X-Wing seed; the key pair is derived from it. @noble/post-quantum implements X-Wing if you are writing a client in JavaScript.

07Limits

What this does and doesn’t cover

  • TillSecrets decrypts a value to encrypt it to your host. This protects the trip to the host, not a compromised TillSecrets.
  • A host that is broken into has both the token and the key. Scope tokens to the keys a host needs, and revoke on the first alert.
  • Revoking a lease early needs only the token; it returns no values.
  • Dashboard reveals, the broker and CI delivery don't use host keys; they run under a person's approval or a protected branch instead.
  • X-Wing is an IETF draft. Its construction is fixed, and each answer names the scheme it uses so a later one can be added.
  • Everything else in TillDev, and what is still classical, is in the cryptography inventory.
08API

API

request
POST /api/secrets/projects/{id}/tokens
{"name": "web", "scope": "read", "environment_id": "…", "host_key": "<host-key.pub contents>"}

→ 201 {"token": {"id": "…", "token_prefix": "ts_4f1c", "host_key_fp": "SHA256:…", "secret": "ts_…"}}

Owners and admins mint tokens. See the API reference.