Get Started
This walkthrough takes you from an empty machine to a working Zen IdP with one user who can sign in and one application that accepts that sign-in. It uses Docker (see other installation methods), which is the quickest way to run a reliable deployment, and a local issuer URL so you can test without a domain or TLS.
Every step below produces something you can see, so you always know where you are. Expect the whole thing to take about fifteen minutes.
Is Zen IdP right for you?
Zen IdP is small on purpose, and it is honest about what it is. Before you spend time on this guide, check the fit.
It is a good fit when:
- You run internal tools for a team and want one shared sign-in for all of them.
- You maintain a home lab and want single sign-on without operating a database server, a complex identity platform or pay an external provider.
- You deploy in an offline or air-gapped network where a cloud identity provider cannot reach.
- You need to give contractors, auditors, or collaborators an OIDC identity for your stack without onboarding them into anything heavier.
- You prefer identity as code: users, clients, and policy as reviewed YAML in your repository instead of rows in an admin panel.
- You are comfortable managing your users in YAML files or automated scripts that write them for you.
It is the wrong tool when you need:
- Sign-in for the general public. There is no self-service sign-up, no password reset, no social login, and no email delivery. Every identity is declared by an operator in YAML.
- The highest degree of security. Authentication is TOTP, which real-time phishing can defeat (please always educate your users about this, regardless of which IdP you use). There are no passkeys, WebAuthn, adaptive policies, or hardware keys.
- A full identity platform. There is no LDAP or Active Directory sync, no HR provisioning, no multi-tenancy, and no dynamic client registration. Established platforms such as Keycloak, or a commercial provider, are the better answer there.
If your users can use an authenticator app, your applications speak OIDC, and you want an identity layer you can fully understand, version control and own, continue.
Pull the image
Zen IdP publishes images for amd64 and arm64 on Docker Hub and GitHub Container Registry:
docker pull varavel/zen-idp
Pin the exact version you deploy. The latest tag only follows stable releases, so pinning keeps upgrades deliberate. See Installation for the full details, including the GHCR mirror and other installation methods.
Generate your bootstrap credentials
One command produces everything a fresh deployment needs:
docker run --rm varavel/zen-idp generate-secrets
The output looks like this, with real values instead of the placeholders:
WARNING: This output contains plaintext credentials. Store it securely.
Root secret
ZEN_IDP_SECRET=...
Administrator
plain: ...
hash: "..."
OIDC client
plain: ...
hash: "..."
Important:
- Store plaintext values securely.
- Put only hashes in YAML.
- Never reuse one OIDC client secret or its hash across different clients.
- Each execution creates a completely independent credential bundle.
- When adding another client, use only the new OIDC client section.
- Do not replace the root secret or administrator credentials unless intentionally rotating them.
Three values matter right now:
- Root secret goes into the environment of the service, never into YAML.
- Administrator plain and hash give you access to the admin interface. You sign in with the plain value and put the hash in YAML.
- OIDC client plain and hash are the credentials of your first application. The application gets the plain value, the YAML gets the hash.
Save the output now. You will paste parts of it into the configuration and the environment in the next steps.
You can run the command multiple times to get new credentials so you can add multiple clients to your config, never reuse the same client secret across apps.
Write your configuration
Create a directory for your deployment and a configuration file inside it:
zen-idp/
config/
zen-idp.yaml
Open config/zen-idp.yaml and start with this (or explore all options):
config:
# The public URL of your Zen IdP. HTTP is accepted here because this guide
# runs locally; production issuers must be HTTPS.
issuer: "http://localhost:8080"
# The administrator signs in with the generated plain password.
# Paste the generated administrator hash here, never the plain value.
security:
admin_password_hash: "$argon2id$..."
clients:
# Your first application. It authenticates with the generated client
# plain secret, so it is a confidential client.
- id: "my-app"
name: "My App"
secret_hash: "$argon2id$..."
redirect_uris:
- "http://localhost:3000/callback"
users:
# A small valid user. This person signs in with "alice" and a TOTP
# code from their authenticator app (only sub is required).
- sub: "alice"
name: "Alice"
email: "[email protected]"
This is a complete, working configuration. A few things worth noticing:
issueris the base URL for every endpoint. Localhost with HTTP is fine for this walkthrough. Real deployments use an HTTPS URL behind a reverse proxy.- The two hashes are the ones
generate-secretsprinted. The application's redirect URI must match exactly what the application will send later, character by character. - Every field beyond
subon a user is optional, includingnameandemail. See Users for the full model, including custom claims.
Validate before you run
Make a habit of validating configuration before every deploy. It runs the exact same discovery and validation as startup, so if it passes, serve will start:
docker run --rm \
-v ./config:/data/config \
varavel/zen-idp \
validate-config
The image expects configuration in /data/config by default, which is why mounting your config directory is enough. Validation deliberately does not need the root secret or the database, so you can also run it in CI on every commit.
If validation fails, the error tells you the exact file and the exact problem. Fix it and repeat until you get a clean pass.
Run the service
Run it with your configuration mounted, the state directory writable, and the root secret in the environment:
docker run -d \
--name zen-idp \
-p 8080:8080 \
-v ./config:/data/config \
-v ./state:/data/db \
-e ZEN_IDP_SECRET="paste the generated root secret here" \
varavel/zen-idp
Then check that it is healthy:
curl http://localhost:8080/health
The image also ships a built-in health check that runs the same probe every 30 seconds, so your container engine reports the real state of the service. The first start takes a moment because the signing identity is derived from the root secret, but you should see ok within seconds.
Enroll your first user
Users cannot sign in until they have enrolled an authenticator app, and enrollment happens through a one-time link.
- Open
http://localhost:8080/adminand sign in with the administrator plain password from step 2. - Find your user
alice, create an enrollment token, and give it a lifetime, for example one hour. - The admin interface gives you a one-time enrollment link. Open it in the browser where you will set up the authenticator, or send it to Alice over a channel you trust.
- The enrollment page shows a QR code. Scan it with any authenticator app, such as Google Authenticator, Authy, 1Password, Aegis, Bitwarden, etc.
- The link works exactly once. When the QR has been shown, the token is consumed.
The credentials never left your machine: the TOTP secret is derived from your root secret and Alice's subject, encoded into a standard enrollment QR, and shown once to exactly the right person. See Authentication for the complete flow, including what to do when someone loses their device.
Sign in through your application
There is no standalone login page to visit by design. Signing in always happens as part of an application's OIDC flow, which is what makes it single sign-on.
Point your application at Zen IdP with the values it asks for:
| The application asks for | You give it |
|---|---|
| Issuer, discovery URL, or authority | http://localhost:8080 |
| Client ID | my-app |
| Client secret | the generated client plain value |
Open your application, ask it to sign you in, and it will redirect to Zen IdP. Enter alice and the six digit code from your authenticator app. You come back signed in, and the application has received an ID token with Alice's claims.
The next application you register gets the same treatment, and Alice signs in once for all of them until her session expires.
What just happened
You now have a complete identity provider:
- Configuration lives in a file you can review and version.
- The root secret derives the signing key and Alice's TOTP credential from thin air, nothing sensitive is stored.
- The state directory holds a SQLite file with Alice's session and nothing else of value.
- The admin interface can issue enrollment tokens for new users, and the audit log records what you did in it.
When you are ready for the real thing, with a domain and TLS, continue with Installation for deployment details and Operations for running behind a reverse proxy, upgrades, etc.