← Liana Grigory
Insights

What is actually a secret

A value the browser must transmit is not a secret, however much it looks like one. Getting that boundary wrong wastes effort in one direction and leaks credentials in the other.

By Liana Grigory · 10 September 2026 · 9 min read

Every project I run has a different idea of where a secret lives. Firebase functions read from Google Secret Manager. Cloudflare Workers read from bindings populated by wrangler secret put. Local development reads from a gitignored .env. Continuous integration reads from repository secrets. Four stores, four access patterns, one mental model needed — and the mental model is the thing that actually prevents leaks, not any individual store.

The model I use has two rules. A secret is a value whose disclosure grants someone capability. And a value the browser must transmit is not a secret, no matter how much it looks like one. Almost every mistake I have made or reviewed in this area comes from getting the second one wrong in one direction or the other.

The values that are public by design

This is the part that is genuinely counter-intuitive, and treating it as a matter of caution rather than of fact produces bad engineering in both directions.

A Firebase web API key looks exactly like a credential. It is a long opaque string beginning AIzaSy, it is called a key, and it sits in the client bundle where anyone can read it. The instinct to hide it is strong and completely unfounded: the browser has to send it on every request, so it is unavoidably public, and moving it into an environment variable that gets inlined into the same bundle at build time achieves precisely nothing. It is not a secret. It is a project identifier, and it authorises nothing on its own.

The same is true of a reCAPTCHA or Turnstile site key, a Stripe publishable key, and an analytics beacon token. Each has a matching value that is secret — the reCAPTCHA secret key, the Stripe sk_ key — and the entire security model rests on keeping those two things straight.

What actually protects a public key is not concealment, because concealment is impossible. It is:

  • Referrer and origin restrictions on the key, so it only functions from your domains.
  • Server-side verification of anything the client asserts. A Turnstile token means nothing until your server has exchanged it with the verification endpoint. A Stripe publishable key cannot move money; only the secret key can.
  • Security rules, which are the real access control. A Firebase API key with no rules behind it is a disaster, but the key was never the problem.

I put this in writing on projects because the failure mode is social rather than technical: somebody sees a long key in a public bundle, raises it as a security incident, and the fix that gets applied is a false one that consumes a day and leaves the real gap — the missing referrer restriction, the unverified token — untouched.

The values that really are secret

The working list, and it is short: service account JSON, anything beginning sk_live_ or sk_test_ or whsec_, SMTP passwords, OAuth client secrets, session signing keys, reCAPTCHA and Turnstile secret keys, and any private key of any kind. Signing material for mobile builds — keystores, .p8 and .p12 files — belongs in the same category and, additionally, should not live inside a git working tree at all, because the most common way it gets committed is a wildcard git add from a directory nobody thought about.

The test for anything ambiguous: if a stranger had this string, what could they do? If the answer is "identify the project", it is public. If the answer is "act as the project", it is secret.

Four stores, one discipline

The stores differ mechanically and it is worth knowing how, because the differences determine what "rotate this" actually costs.

Google Secret Manager, which Firebase functions bind to, is versioned. You add a version, and the function picks it up on next deploy. Old versions remain accessible until you disable or destroy them, which is a feature during a rotation and a liability afterwards if you forget to destroy the one that leaked. Access is IAM-controlled, which means it is auditable and can be scoped per service account rather than per project.

Cloudflare Workers secrets are write-only from the outside. Once wrangler secret put has stored a value there is no command that reads it back; the Worker sees it as a binding at runtime and nothing else does. That is a genuinely better default than an environment variable, and it has one practical consequence: the value must exist somewhere else too, because you will eventually need to know what it was, and "read it back from production" is not available.

Repository secrets in CI are the highest-risk store, because CI runs arbitrary code from pull requests and its whole job is to have credentials. Scope them narrowly — a deploy token that can deploy and do nothing else — and treat any workflow that runs untrusted code as a workflow that must not see them.

The local .env is the one that leaks. Not because it is badly designed but because it lives in the working directory, and the working directory is what gets committed. Three things make it safe, and all three have to be true: .gitignore covering .env, .env.* and *.env; a committed .env.example carrying key names with empty values so a new checkout knows what to fill in without anything sensitive being present; and a pre-commit secret scan, because .gitignore only protects files you remembered to name.

Rotation is the only control that survives a mistake

Everything above is prevention, and prevention has a failure rate above zero. The question that determines how bad a leak is turns out to be a boring operational one: how long does it take to replace this value, and does anything break while you do?

A credential you can rotate in ten minutes turns a serious incident into an annoying afternoon. A credential that is hardcoded in three deployed services, referenced by a name nobody remembers, and shared between two products turns the same leak into a week of archaeology conducted under pressure. The difference is not how well the secret was hidden. It is entirely a matter of how it was wired.

The wiring that makes rotation cheap:

  • One purpose per credential. A token that deploys should not also read DNS. When it leaks, you revoke it without wondering what else stops working.
  • Read at runtime, never at build time. A secret inlined into a bundle has been copied into every build artefact and every cache that holds one. Rotating it means rebuilding and redeploying everything, and the old value stays in the old artefacts.
  • Never in a URL. Query strings end up in server logs, proxy logs, browser history and referrer headers. This is the single most common way a secret escapes a system that was otherwise handling it correctly.
  • Never in a scratch script. The temporary file written to debug something at eleven at night is the file that gets committed. Read from the environment even in throwaway code, particularly in throwaway code.

Git history does not forget

A secret committed and then removed in the next commit is still in the repository, still in every clone, and still in the fork somebody made. Removing it from the working tree changes nothing about its availability. Once a value has been pushed, it is compromised, and the only real remediation is rotation — history rewriting is cleanup, not a fix, and it does not reach the clones.

Which is why the controls that matter are the ones that operate before the push: a pre-commit scan that blocks the commit, and provider-side push protection that rejects the push. Both are free, both take minutes to enable, and both are worth more than any amount of care, because care is exactly what fails at the end of a long session.

What I would do differently

The mistake I keep seeing — including in my own earlier work — is treating this as a question of storage. It is not. Where a value sits is the least interesting property it has. What matters is what it authorises, whether that authority is scoped to one job, how fast it can be replaced, and whether anything in the pipeline copies it somewhere you were not thinking about.

A secret in the right store with unlimited scope and no rotation path is worse than an over-cautiously-handled public key, because the first one will eventually hurt and the second one was never going to.

Written by Liana Grigory, Entrepreneur and Software Engineer, from work on The Care Royal, Tegula Stone and Unified Savers. Everything above describes decisions actually made on those systems, including the ones that turned out to be wrong.

Home · Terms of Use · Privacy Notice