Janus
A self-hosted credential boundary. Applications authenticate to Janus; only Janus reads the upstream secret from OpenBao and adds it on the way out. A Java 25 and Spring Boot 4 modular monolith, with a React console for identities, connections and grants. It runs in production, and my other application depends on it.
- Status
- Maintained, in production
- Role
- Sole developer, from security model to production.
The initial problem
An API key gets copied into a project, committed by accident, and rotated never. Every application holding a key can leak it. And nothing tells you which service called what, or how often.
The solution
An authenticated proxy. The caller proves its identity, Janus checks the grant, applies the quota, reads the secret from OpenBao, and forwards the request. The secret never crosses back: not in a response, not in a log, not in an error message. Every call is audited under a correlation ID.
Key decisions
Java 25 and Spring Boot 4, the current LTS. Proven with nothing at stake, before production forces the upgrade.
A modular monolith, not microservices. One artifact to deploy. The need did not justify the complexity.
A grant admits everything under a slug by default. An allowlist would only be a second, staler copy of what the upstream already enforces. Scoping exists for the keys that cannot say no themselves.
Architecture
Your application
holds a caller key, never the API's
the request
The API's own key is readable only here
Janus
checks identity, then the grant, then the quota
reads the key
OpenBao
holds the API's key
the same request, with the key added
The third-party API
TMDB, and whatever else is registered
the response comes back scrubbed, under a correlation ID
A modular monolith, layered controller to service to repository. The HTTP class decides nothing, the service owns the transaction and the audit record, and entities enforce their own invariants. Two independent security chains: one for the console, one for the gateway. PostgreSQL holds identities, connections, grants and audits, never a plaintext secret. Proxied calls run on virtual threads, so concurrency is bounded by the database pool rather than by the servlet container.
I moved to Java 25 and Spring Boot 4 while nothing was at stake, so the upgrade would not arrive later as an emergency. It runs on a server I administer, and when it breaks I am the one reading the logs.
What is left
The project is maintained, in production.
Publish real screenshots of the console
Document the threat model as a page of its own
Widen coverage on the token exchange paths
Technologies
Java 25 · Spring Boot 4 · Virtual threads · React 19 · TypeScript · Vite · Tailwind CSS · PostgreSQL · OpenBao · Maven · Docker Compose · Traefik · nginx · GitLab CI
Jonathan Blanchard