Security
Security that feels like hospitality.
An identity provider is only as good as the walls between its tenants and the care it takes with secrets. Here is how Usher is built. We describe what the software does; we make no claims of certification.
Architecture
Separate domains, separate networks.
Usher’s own pages and every customer’s issuer live on different registrable domains. The databases sit on a network that cannot reach the internet, and the one way out is a proxy that checks where it is going.
- Caddy terminates TLS for every host and strips headers that only Usher’s own services may set.
- The auth tier answers for every environment, told apart by host; nothing is cached for one host and served to another.
- Each service talks to Valkey as its own user, limited to its own keys. There is no default user.
- Statements carrying tokens and codes are never written to the database log with their values.
The internet
People, your apps, your servers
The edge
Caddy: TLS, HSTS, strips internal headers
usher.hallelo.net
Site and Console
usher.hallelo.net, console.usher.hallelo.net
API
api.usher.hallelo.net
*.id.hallelo.net
Auth tier
Every environment’s issuer and Lobby
Data network · no route out
Postgres
A schema per environment
Valkey
Each service limited to its own keys
The only way out
Egress proxy
For every customer-supplied URL
Discovery, JWKS, logout
Public addresses only
Isolation
Nothing shared that matters.
Each project environment gets its own Postgres schema, its own signing keys and its own secrets sealed with its own data key. Its issuer is a host on the tenant domain, and its cookies are host-only, so a browser never sends one environment’s cookie to another.
usher.hallelo.net
The site, Usher Console, Usher accounts
*.id.hallelo.net
One issuer per environment, on a separate domain
Users, sessions and tokens live in a schema made for this environment. Queries run against that schema, never a shared table with a tenant column.
Copy Acme production’s sealed Google client secret into Northwind production.
Encryption at rest
Envelope encryption, bound to its place.
Secrets you give Usher are sealed with AES-256-GCM under a data key that belongs to one environment. Data keys are themselves wrapped by a versioned master key held outside the database.
- Per-environment data keys
- Compromising one environment’s data key reveals nothing about another’s.
- AAD binding
- The environment and the purpose are bound into every ciphertext, so a value can’t be moved between rows or tenants.
- Versioned keys
- Each sealed value records the key version it was made with, so keys can be rotated and old values rewrapped.
- Authenticated
- GCM’s tag means a tampered value fails to open, rather than decrypting to something else.
Master key
Held outside the database, versioned
Data key, one per environment
Wrapped by the master key
The secret
Sealed with AES-256-GCM under the data key
u1.<key version>.<iv>.<ciphertext + tag>
aad = u1 | version | environment | purpose
Practices
The details, done carefully.
Passwords, if you use them
Hashed with Argon2id at OWASP’s recommended cost (19 MiB, two passes), with a cap on how many run at once so a flood of sign-ins can’t take the service down. New passwords are checked against known breaches through Have I Been Pwned’s range API, which sees only a short prefix of the password’s hash. The minimum length is 12 by default.
Nothing stored that could be replayed
Client secrets, tokens, magic-link tokens and emailed codes are stored hashed. Backup codes are encrypted. An authorisation code lasts five minutes; an emailed code allows three tries.
Rate limits on every door
Sign-in, sign-up, emailed codes, magic links, password resets and second-factor checks each have their own limit: ten password sign-ins a minute, for example, and five emailed codes every ten minutes.
A strict Content-Security-Policy
Every Lobby page gets a fresh nonce and a policy built around it: only this origin’s scripts carrying the nonce run, forms post only to the same origin, and other sites can’t frame the page.
An audit log you can trust
Changes to workspaces, projects and environments are written to an append-only log, each entry hash-chained to the one before, so an edited or missing entry shows.
An egress proxy for customer URLs
Every request Usher makes to an address a customer gave it (OIDC discovery, JWKS, back-channel logout) leaves through one proxy. It resolves the name once, refuses private, loopback, link-local and cloud-metadata addresses, and connects to the address it checked, so DNS rebinding can’t turn a customer URL into an internal one.
Handles that can’t pass for someone else
A production environment’s handle is the first label of its issuer. Handles that read as Usher, a sign-in service or a well-known brand (look-alike digits included) are refused, and a handle once used is never given out again, because old apps may still trust its tokens.
Sessions that end when you say
Usher checks its own sessions on every request rather than trusting a cached copy, so a session you end is over at once. Access tokens are short-lived (ten minutes by default), and your API can introspect them.
Responsible disclosure
Found something? Tell us first.
If you think you’ve found a security problem in Usher, we’d like to hear about it, and we’ll work with you to fix it.
- What you found, where, and how to reproduce it.
- What someone could do with it, as you understand it.
- How we can reach you, and whether you’d like to be credited.
Please test only against environments you own, don’t access other people’s data, and give us a reasonable time to fix the problem before you share it. To report a page pretending to be an Usher sign-in, see Report abuse.