TILLSECRETS · LEAKS

Your secrets, found where they shouldn’t be

Beta

TillSecrets knows every value it holds, so it can recognise one that has escaped. It checks TillForge pushes, commits made in the dashboard or by an agent, and TillPulse events, and you can check any file yourself. A find is a leak anomaly for your admins, and a secret the vault generated can rotate itself.

01Where

Where it looks

WhereWhat happens to a vault value
A push to TillForgeThe push is refused unless the line or file is allowlisted. Either way the admins are told.
A commit made in the dashboard or through the APIRefused the same way, before anything is written.
A commit made by an agentRefused. Allow markers don’t apply to agents.
A TillPulse eventReplaced with [REDACTED:vault-secret] before the event is stored.
tilldev secrets scan, or the check APIReported to you. One outside your access is reported to the admins instead.

Every current and previous value of every static secret in the workspace counts, whichever project or environment it lives in. A push is refused for an old value too, because an old value may still work where it was issued.

02Matching

How a value is recognised

Nothing sends whole files. The CLI, TillForge and TillPulse pick out the strings that could be a secret (tokens, the right side of KEY=value, the password in a URL, the body of a PEM block), and send only those. The vault compares each one with its values by a hash keyed with your workspace's own key, so a string that isn't a vault value tells it nothing and is not kept.

A match inside your access names the secret. One outside it is shown as a vault secret outside your access, and is still reported to the admins, so checking strings can't be used to find out which values exist. Checks are rate-limited and audited as leak.check.

03Pushes

A push carrying a secret

terminal
$ git push
remote: push rejected: 1 secret(s) detected — they must not enter history.
remote:   config/billing.env:3  TillSecrets value: the current value of checkout/prod/STRIPE_SECRET_KEY (tillsecrets-vault)  sk_l**************************E5
remote: A TillSecrets value was found: your workspace admins have been told. Load it at runtime instead
remote: (tilldev secrets exec), and rotate it if the commit left your machine.

Take the value out of the commit and load it at runtime instead, with tilldev secrets exec or an SDK. If the commit ever left your machine, rotate the secret. When a value belongs in the repository (a sandbox key in a test, a sample in documentation), allowlist it with a reason. The push goes through, the finding is shown to you, and the admins are still told.

allow markers
# One line, with the reason
STRIPE_TEST_KEY=sk_test_51Hx… # tillforge:allow-secret: shared sandbox key, no live access

# A whole file: put this anywhere in it
# tillforge:allow-file-secrets: fixtures for the billing tests

The same markers work for the pattern scanner described under Branch protection.

04TillPulse

Secrets in error reports

An exception message, a breadcrumb or a request header can carry a live key. Before a TillPulse event is stored, its strings are checked and any vault value is replaced with [REDACTED:vault-secret]. If the value appears in an escaped or re-indented form, the whole field is replaced. The leak is raised against the TillPulse project, with the event id and the field it was in, and a repeat in later events counts as further sightings of the same leak.

This needs TillSecrets active in the same workspace. It adds a short round trip to event processing, and strings already checked are remembered for a few minutes.

05Check

Check files yourself

terminal
# Files holding a vault value. Exit 1 unless every one is allowlisted.
tilldev secrets scan .
tilldev secrets scan config/ .env.production --json

# What the next commit will carry, and a hook that runs that before every commit.
tilldev secrets scan --staged
tilldev secrets scan --install-hook

In a git checkout a directory is listed the way git sees it, so ignored files are skipped; name a file to check it anyway. The hook refuses a commit carrying a vault value that isn't allowlisted, and won't replace a pre-commit hook it didn't write. If the vault can't be reached, the hook lets the commit through with a warning. A manual scan fails instead.

06Alerts

Who hears about it

Each find is an anomaly of kind Leak under Secrets → Anomalies: high for the current value, low for an old one. There is one open anomaly per secret version and place (a repository, a TillPulse project, or the person or agent who ran a check); further finds count as sightings. For a current value every owner and admin is emailed. Route leaks anywhere else with a TillPulse alert rule limited to "kinds": ["leak"].

The secret's row in the dashboard shows Leaked while a leak is open. Resolve a leak as Rotated, Not a leak or Investigated.

07Rotate

Rotating a leaked secret

A secret whose current value the vault generated (random, HMAC, password, Ed25519, RSA, a post-quantum key or a TillForge token) can be made again from the same recipe. Rotate… on its row, the Rotate button on an open leak, or tilldev secrets rotate stores a new value as the next version and revokes the TillDev credentials the old values carried. A key pair gets a new public half, which you then update wherever it is registered.

terminal
# Rotate it automatically when its current value turns up in a push or a TillPulse event
tilldev secrets update STRIPE_WEBHOOK_SECRET --env prod --on-leak rotate

# Or rotate it now, from the same recipe
tilldev secrets rotate STRIPE_WEBHOOK_SECRET --env prod

# Page someone on a leak
tilldev secrets anomalies route --project checkout --kinds leak --min-severity high --pagerduty <integration-key>

With Rotate automatically (--on-leak rotate), a current value found in a push or a TillPulse event rotates the secret straight away, acting as the admin who turned it on; if that person is no longer an admin, the leak is raised without rotating. A find from a check never rotates anything. Anything still using the old value stops working, so turn this on for secrets whose users read them at runtime.

A value set by hand can’t rotate itself
TillSecrets didn't make it, so it can't make a new one. Rotate it where it was issued, then set the new value.
08Limits

What it can and can’t catch

  • Only the exact value is recognised. A value split across lines, encoded, hashed or partly shown is not, and strings under 8 bytes aren't checked.
  • If the vault can't be reached, a push goes through with a warning and the rest of the secret scan still runs. A TillPulse event is stored as it arrived.
  • Dynamic credentials aren't matched, since each lease is new. Canaries aren't matched either: their own alarm covers them.
  • History already in a repository isn't checked. Run tilldev secrets scan over a checkout to cover what's there now.
  • A push checks at most 50,000 strings, and a scan 100,000; the rest go unchecked and the scan says so.
  • A find in the minute after a leak is resolved may be counted on the resolved one rather than raising a new one.
09API

API

endpoints
POST  /api/secrets/leaks/check              {"values": ["…"], "label": "release script"}
      → {"checked": 1, "matches": [{"index": 0, "current": true, "label": "…",
                                     "secret": {"id": "…", "ref": "secretref://…"} | null}]}

PATCH /api/secrets/secrets/{id}             {"on_leak": "notify" | "rotate"}
POST  /api/secrets/secrets/{id}/rotate      Idempotency-Key: <key>

GET   /api/secrets/anomalies                leaks are kind "leak"

Checking is open to anyone who can use TillSecrets, agents included, at 30 calls a minute. Rotating and changing on_leak are for owners and admins. See the API reference.

Audit
leak.detect for each new leak, leak.check for each check, secret.generate for each rotation, and secret.update when on_leak changes.