# Secret management

A secret belongs in one place that owns it, is read at start-up or on demand, and is never written into source, a screen, or a log. Copies are the whole problem: a copy of a value that is being rotated elsewhere keeps working until the next restart, and then fails in a way that looks like a broken deploy rather than a stale credential.

- **Status:** Reference
- **Audience:** both, developer
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/docs/technology/secret-management/

## What counts as one

Anything whose disclosure lets somebody act as you: database passwords, provider API keys, OAuth client secrets and refresh tokens, signing keys, webhook shared secrets, session-signing material, and the tokens that authenticate one of your own services to another.

Two things are commonly and wrongly excluded. **Internal service tokens** are secrets even when the service is on the same host — an endpoint that trusts anything on loopback is an endpoint that trusts anything that gets onto loopback. And **identifiers that name a secret** are not secrets, which is the useful half of the distinction: a configuration file can safely say *which* secret to fetch as long as it does not carry the value.

## Where one should live

1. **In a store built for it**, with access control and an access record, rather than in a file that happens to be less public than the others.
2. **Fetched by identity where possible.** A machine that proves who it is and receives a short-lived credential leaves nothing durable to steal from the host.
3. **Read at boot or on use, not copied at deploy time.** The moment a value is duplicated into a second location, rotation has two jobs and will only be told about one.
4. **Never echoed back.** A settings field that is write-only closes the most common accidental disclosure — a screenshot, a support session, a shared screen.
5. **Never in an error.** A message that includes the credential it failed with is a credential in a log, a ticket and a monitoring system.

> **Note** Fail closed. A service whose token is unset should refuse service, not run without authentication. The interesting failure is not the one that errors — it is the one that quietly opens.

## Does Connect use a secret store?

**Yes, and the arrangement is worth stating precisely.** The database password is deliberately **not** in the application's configuration file. The managed database service owns the master credential and rotates it into a secret store; configuration names the secret, and boot reads the password from there. Naming a secret is safe; holding a copy of it is not.

Provider credentials are sealed by the settings store on every save and never returned to a screen. The realtime voice worker's own endpoint is authenticated by a shared token and answers 503 when that token is unset — it is never open by omission. The browser line's password is generated rather than chosen, stored encrypted, served only to its owner, and revocable one line at a time.

Sign-in has no password to manage at all. Google OAuth is the only route in; there is no password login to phish, reuse or leak. The application's own machine-readable API description and its interactive explorers are closed and stay closed, so the surface a stolen credential could explore is smaller than it would otherwise be.

## The copy that becomes a fuse

This is the failure worth internalising, because it is silent, delayed, and looks like something else entirely.

1. A password is copied into a configuration file, for convenience.
   - Result: Everything works. The running process is already connected and the copy is never exercised.
2. The managing service rotates the credential on its schedule.
   - Result: Still nothing. The live connection is unaffected, and the copy is now wrong without anybody being told.
3. Something restarts — a deploy, a host replacement, an ordinary restart.
   - Result: The process reads the stale copy and authenticates as nobody. This exact sequence happened here, and it read convincingly as a bad deploy: the change that shipped got the blame, and the change had nothing to do with it.

The general shape: a duplicated secret does not fail when it is duplicated, or when it is rotated, but at the next restart — which is usually adjacent to an unrelated change. The fix is not a better copy; it is no copy.

## When one leaks

- **Revoke first.** Rotate the credential before investigating. A secret that has been exposed is exposed; the only action that changes that is making the old value useless.
- **Removing the commit is not remediation.** History is cloned, cached and mirrored. Treat the value as compromised from the moment it was written.
- **Check what the credential could reach.** Blast radius is decided long before the leak, by whether one credential opens one thing or everything.
- **Look for the copies.** A leaked value usually exists in more than one place, and rotating the one you found leaves the others working.
- **Record it.** What was exposed, for how long, what it could reach, and what was rotated. That record is the difference between an incident and a rumour.

Scanning for secret-shaped material before it is committed is worth the small friction — a false positive costs a line of explanation, a missed key costs a rotation and everything that follows it. The same rule governs this documentation corpus: the build refuses a page containing anything that looks like a credential, on the assumption that a broad check with occasional false alarms beats a narrow one that is quiet.

## Questions

### Is it safe for a configuration file to mention a secret?

Mentioning is fine; holding is not. A configuration that names which stored secret to fetch carries no value worth stealing, and rotation has exactly one place to update. That is the arrangement in use for the database credential here.

### Why does a service refuse to start when its token is missing?

Because the alternative is running without authentication. An endpoint that falls back to open when configuration is incomplete is a hole that appears precisely when somebody is mid-way through setting something up.

### Are there passwords to manage for signing in?

No. Google OAuth is the only sign-in route and there is no password login at all, so there is no password store, no reset flow and no reused credential to worry about on that surface.

## Related

- [Credential rotation](https://connectbyjbrh.com/docs/technology/credential-rotation/)
- [How provider credentials are stored](https://connectbyjbrh.com/docs/security/credential-sealing/)
- [Encryption at rest](https://connectbyjbrh.com/docs/technology/encryption-at-rest/)
- [OAuth 2.0](https://connectbyjbrh.com/docs/technology/oauth/)
- [Public and authenticated surfaces](https://connectbyjbrh.com/docs/security/public-vs-private/)
- [PostgreSQL](https://connectbyjbrh.com/docs/technology/postgresql/)
- [A copy of a password somebody else rotates](https://connectbyjbrh.com/research/stale-credentials/)

## What this page is based on

- `docs-source/sources/GENERAL.md` §10 — security, credentials and the rotated database password
- `docs-source/sources/PHONE.md` §3 and §11 — the worker token and the softphone password
- Connect capability registry (docs-source/facts.py)
