Authentication & Authorization#
EOS supports optional authentication and authorization backed by Zitadel, an open-source identity provider. Authentication is disabled by default. In that mode, expose the REST API, web UI, and MCP endpoint only on trusted networks.
How It Works#
Identity#
Accounts live in one central Zitadel instance that every EOS instance at an institution can share, so a person uses a single account everywhere. EOS stores no accounts or passwords. Tokens are validated locally against Zitadel’s JWKS, with no per-request call to Zitadel:
Web UI: OpenID Connect sign-in (authorization code + PKCE).
REST API and MCP endpoint:
Authorization: Bearer <token>on every request.
Roles#
Role |
Scope |
Permissions |
|---|---|---|
|
One instance |
Read-only |
|
One instance |
Viewer, plus submit and cancel tasks, protocols, and campaigns, reset resources, and tune optimizers |
|
One instance |
Submitter, plus author task, protocol, and device definitions |
|
Specific labs in one instance |
Editor, plus load protocols, reload devices and task plugins, run the simulator, and device RPC for their labs |
|
One instance |
Everything, including loading labs and packages and managing users |
Roles are cumulative and live in each instance’s own database, so one account can be superuser
in one instance and viewer in another. The token identifies the user. Authorization is local to each instance.
Accounts#
There is no self-registration. Superusers create accounts from the Management page’s Users tab
or eos auth create-user, and the user gets a temporary password to change at first sign-in.
Submitted protocol runs and campaigns are owned by the authenticated user.
Machine Access#
Scripts and MCP clients use Zitadel service accounts with either JWT access tokens (client credentials flow, validated locally) or personal access tokens (long-lived opaque tokens, handy for MCP configs, validated by introspection with the EOS API app credentials).
Setup#
Deployment is automated. Each path below creates the org, project, both apps, and the
eos-user-admin service user, and is safe to re-run.
Single box. eos setup deploys Zitadel and writes .env and web_ui/.env for you.
Dedicated identity host, shared by several EOS instances. The standalone deployer needs only Docker:
cd docker/zitadel
python3 deploy.py --url https://eos-auth.lab.internal --tls-mode internal
python3 deploy.py --url http://localhost:8080 # local http
It generates secrets, brings up the stack, provisions everything, and writes eos-instance.env
with the values to paste into each instance. Use --ui-url to register an instance’s web UI
redirect (default http://localhost:3000), re-running with another to add more.
Existing Zitadel you already run. Create an instance-admin PAT in its console, then point the same deployer at it to provision without deploying:
python3 deploy.py --issuer https://auth.example.org --admin-pat <PAT>
Afterward the only manual step is granting yourself superuser:
eos auth create-user <username> <email> --superuser.
TLS#
The bundled Caddy proxy terminates TLS, so no external reverse proxy is needed. Set
ZITADEL_TLS_MODE (or deploy.py --tls-mode). Published ports follow from
EOS_AUTH_ISSUER:
internal(recommended for isolated lab networks): a self-signed certificate for the issuer’s host. Export its root CA and distribute it to every browser and EOS service that reaches Zitadel, or JWKS and token fetches fail:docker compose --profile auth exec zitadel-proxy cat /data/caddy/pki/authorities/local/root.crt.custom: your own certificate atdocker/zitadel/tls/cert.pemandtls/key.pem. Preferred when an institutional CA your machines already trust issued it, since it skips the step above.acme: automatic Let’s Encrypt for a public domain reachable on ports 80 and 443. SetZITADEL_ACME_EMAIL.none: plain HTTP, for local development or behind your own TLS proxy.
Configuration reference#
The deployers write these values. Set them by hand only when pointing EOS at a Zitadel you
provisioned yourself. In config.yml, or the matching EOS_AUTH_* variables in .env:
auth:
enabled: true
issuer: https://auth.example.org
org_id: "<org-id>"
project_id: "<project-id>"
service_user_pat: "<eos-user-admin PAT>"
introspection_client_id: "<EOS API app client id>"
introspection_client_secret: "<EOS API app client secret>"
web_api:
cors_origins: ["https://<ui-host>"]
Zitadel audiences tokens to the project, so project_id is also the accepted audience.
In web_ui/.env:
AUTH_ENABLED=true
AUTH_SECRET=<random string, e.g. openssl rand -base64 32>
AUTH_ISSUER=https://auth.example.org
AUTH_CLIENT_ID=<EOS Web UI app client id>
AUTH_ORG_ID=<org-id>
AUTH_PROJECT_ID=<project-id>
AUTH_PAT=<eos-user-admin PAT>
AUTH_INTROSPECTION_CLIENT_ID=<EOS API app client id>
AUTH_INTROSPECTION_CLIENT_SECRET=<EOS API app client secret>
Managing Users from the CLI#
The eos auth CLI mirrors the Users tab. Users can be referenced by username, email, or Zitadel
user ID.
eos auth create-user alice alice@example.org [--superuser]
eos auth list-users
eos auth deactivate-user alice
eos auth assign-role alice editor
eos auth assign-role alice lab_admin --lab color_lab
eos auth revoke-role alice editor
eos auth list-roles
Calling the API with a Token#
# PAT or JWT access token
curl -H "Authorization: Bearer $TOKEN" https://<eos-host>:8070/api/tasks/types