Running Krywerk Server¶
Guide for operators (the IT administration of an organization or a service provider): installation, identity provider, backup, updates, monitoring, data protection. As of F8 (2026-10-14).
Krywerk Server is the key server for team folders: it distributes the encrypted vault keys to authorized people and devices. It is compatible with the Cryptomator apps (Hub protocol) and with the Krywerk app. The files themselves are not stored on the server but in the organization's cloud storage (OneDrive, SharePoint, Nextcloud …).
Krywerk app / Cryptomator app ──HTTPS──▶ Reverse proxy (Caddy) ──▶ krywerk-server ──▶ SQLite (/data)
│ │
└──── Sign-in in the browser (OIDC) ──▶ Keycloak ──(optional)──▶ Entra ID / other provider
└──── encrypted files ───────────────▶ Cloud storage (OneDrive, …)
1. What the server stores – and what it doesn't¶
| Stored (SQLite file) | Never on the server |
|---|---|
| Users: ID from the identity provider, display name, email, first/last name | Passwords (sign-in is handled by the identity provider) |
| Groups from the token (name, memberships) | Vault masterkeys in plaintext |
| Public keys, encrypted private keys of users | Unlock codes (account keys) |
| Devices: name, type, public key, last access, last IP address | Files, file names, contents |
| Team folders: name, description, members/roles, archive status | |
| Access grants: vault key, encrypted for each authorized person |
Owning the database alone is not enough to decrypt any team folder: that additionally requires the unlock code or an unlocked device key of an authorized person. Even so, the database is confidential (member lists, email addresses, IP addresses) – protect backups accordingly.
The access log (standard output) contains time, method, path (with vault or device ID) and status – never tokens,
keys, headers or contents. Turn it off with KRYWERK_SERVER_QUIET=1.
2. Requirements¶
- A Linux server (or VM) with Docker Engine ≥ 24 and the Compose plugin; 1 vCPU and 512 MB RAM are enough for the server, with your own Keycloak rather 2 GB RAM.
- A DNS name for the key server, e.g.
schluessel.example.org, and with your own Keycloak a second one, e.g.login.example.org. Ports 80 and 443 reachable from outside (Let's Encrypt). - An identity provider (OIDC). Recommended: Keycloak – existing, or shipped via
compose.keycloak.yaml. Accounts from Microsoft Entra ID (Microsoft 365) or another provider are connected there (section 4).
Why not Entra ID directly? The official Cryptomator desktop app signs in via a loopback address with a random path (
http://127.0.0.1:<port>/<random>). As far as we know, Entra ID requires the path to be registered and rejects such sign-ins; in addition, the apps don't send ascope. Keycloak allows arbitrary loopback ports and paths (RFC 8252) and serves as a broker in front of Entra ID – which is also how Cryptomator Hub does it. See section 5 for the experimental direct connection.
3. Installation with Docker (recommended)¶
The files are in crates/kw-server/deploy/:
| File | Purpose |
|---|---|
compose.yaml |
Krywerk Server + Caddy (automatic TLS via Let's Encrypt) |
compose.keycloak.yaml |
Add-on: your own Keycloak with Postgres |
Caddyfile, Caddyfile.keycloak |
Reverse proxy (HSTS, security headers; Keycloak administration only from internal networks) |
keycloak/realm-krywerk.json |
Realm template: client cryptomator, roles, groups claim |
krywerk.env.example |
Server settings |
backup.sh, restore.sh |
Backup and restore |
krywerk-server.service, krywerk-server.toml |
Running without Docker (section 6) |
3.1 Image¶
Without a registry, the image is built from source on the server (Rust toolchain in the build container, ~1 min):
git clone <repo> /opt/krywerk && cd /opt/krywerk/crates/kw-server/deploy
docker compose build
The image (ghcr.io/mzilinski/krywerk-server:<version>, krywerk-server when built locally) contains only the
program and CA certificates, runs as an unprivileged user (UID 10001) with a read-only file system; data lives in the
volume data (/data). Custom CAs (e.g. an internal Keycloak with its own CA): mount the certificate file and set
SSL_CERT_FILE=/path/ca.pem.
3.2 With your own Keycloak¶
cp krywerk.env.example krywerk.env # variant A is preset: adjust the domains
cat > .env <<EOF
KRYWERK_DOMAIN=schluessel.example.org
KEYCLOAK_DOMAIN=login.example.org
KEYCLOAK_ADMIN_PASSWORD=$(openssl rand -base64 24)
KEYCLOAK_DB_PASSWORD=$(openssl rand -base64 24)
EOF
chmod 600 .env
alias kc='docker compose -f compose.yaml -f compose.keycloak.yaml'
kc up -d # first start ~1 min (realm import)
kc run --rm server check # check configuration + identity provider
krywerk.env (variant A): KRYWERK_SERVER_OIDC_ISSUER=https://login.example.org/realms/krywerk and
KRYWERK_SERVER_OIDC_INTERNAL_URL=http://keycloak:8080/realms/krywerk (discovery/JWKS internally; the apps still
receive the public addresses).
Then in Keycloak (https://login.example.org/admin/, user admin, password from .env; the console is only
reachable from private networks – adjust the rule in Caddyfile.keycloak):
- Select the realm krywerk. Create accounts or connect an identity provider (section 4).
- Roles: every user has
user(default role). Anyone allowed to create team folders gets the rolecreate-vaults(ideally via a group, e.g. "Create team folders" → Role mapping).adminmay view and archive all team folders. - Groups: groups in Keycloak appear in the apps for sharing (claim
groups, full path,/Area/Teambecomes nested groups). The server knows a group as soon as one of its members has signed in once.
3.3 With an existing Keycloak¶
Use only compose.yaml. Create a client in the existing realm (values as in keycloak/realm-krywerk.json): client
ID cryptomator, public (client authentication off), standard flow on, PKCE S256, redirect URIs
http://127.0.0.1/*, org.cryptomator.ios:/hub/auth, org.cryptomator.android:/hub/auth, web origins +; mapper
"Group Membership" (claim groups, full group path on, add to access token on); roles user, create-vaults,
admin as realm roles. If a Cryptomator Hub runs in the same realm, don't name the groups claim groups (Hub
reads it as a role list → 403); use e.g. krywerk_groups instead and set KRYWERK_SERVER_GROUPS_CLAIM=krywerk_groups.
3.4 Web administration¶
In the browser at https://schluessel.example.org/verwaltung/: overview, team folders, people (lock), custom groups,
emergency requests, audit log. Turn it on with KRYWERK_SERVER_WEB=1 in krywerk.env (template: on).
- Client
krywerk-verwaltung: the realm template (3.2) already creates it. With an existing Keycloak (3.3), create a public client: standard flow, PKCE S256, redirect URIhttps://schluessel.example.org/verwaltung/*, post-logout redirect+, the same groups mapper as forcryptomator. - The role
adminmakes someone part of the IT administration (all team folders, people, groups, audit log). Managers of team folders only see their own folders there. - Internal network only: the Caddy templates block
/verwaltungfor public addresses (block@verwaltung_extern; adjust the ranges or remove it). TZ=Europe/Berlininkrywerk.envmakes times appear in local time.
Details, security and limits (e.g. "Grant access" stays in the app): docs/web-verwaltung.md.
3.5 Checking¶
curl https://schluessel.example.org/health # → ok
curl https://schluessel.example.org/api/config # apiLevel 4, public sign-in endpoints
Then enter "Organization account → Key server address" in the Krywerk app and sign in, or use
krywerk-hub --server https://schluessel.example.org konto-einrichten.
4. Accounts from Entra ID (Microsoft 365) via Keycloak¶
In Entra ID (Microsoft Entra admin center → Applications → App registrations → New registration):
- Name "Krywerk sign-in", account type "Accounts in this organizational directory only".
- Redirect URI (web):
https://login.example.org/realms/krywerk/broker/entra/endpoint. - Certificates & secrets → new client secret (note the expiry date!).
- Optional groups: Token configuration → Add groups claim → "Security groups" or "Groups assigned to the application" (recommended, keeps the token small). Entra ID delivers object IDs (GUIDs).
- Optional: Enterprise applications → "Krywerk sign-in" → Properties → "Assignment required" = Yes, then allow only assigned people/groups.
In Keycloak (realm krywerk → Identity providers → "Microsoft" or "OpenID Connect v1.0"):
- Alias
entra, client ID = application ID, client secret = the secret from step 3; for "OpenID Connect" the discovery URLhttps://login.microsoftonline.com/<tenant-id>/v2.0/.well-known/openid-configuration. - "Sync mode" = Force (update name/email at every sign-in), "Trust email" on.
- Groups: one mapper "Advanced Claim to Group" per Entra group (claim
groupscontains the GUID → Keycloak group "Finance department" or similar). Alternatively, maintain groups in Keycloak only. - Role
create-vaults: a mapper "Advanced Claim to Role" for an Entra group, or assign it directly in Keycloak. - Redirect straight to Microsoft (without the Keycloak sign-in page): in
krywerk.envKRYWERK_SERVER_OIDC_AUTH_ENDPOINT=https://login.example.org/realms/krywerk/protocol/openid-connect/auth?kc_idp_hint=entra– the apps keep existing query parameters.
Other providers (Google Workspace, statewide education/administration logins, SAML IdPs) are connected in Keycloak the same way, as identity providers.
5. Entra ID directly (experimental, untested)¶
Only for environments where exclusively the Krywerk app is used and Keycloak is not an option. Known obstacles:
(1) loopback redirect with a random path (see section 2) – the official desktop app will fail; (2) Entra ID requires
scope – solvable via the query of the authorization endpoint
(KRYWERK_SERVER_OIDC_AUTH_ENDPOINT=…/oauth2/v2.0/authorize?scope=api%3A%2F%2F<client-id>%2FTresore.Zugriff%20openid%20profile);
(3) tokens without their own API scope are issued for Microsoft Graph and cannot be verified – therefore, in the app
registration, "Expose an API" (scope Tresore.Zugriff), manifest accessTokenAcceptedVersion: 2, app roles
Tresore.Anlegen and Krywerk.Admin; platform "Mobile and desktop applications" with redirect http://127.0.0.1.
Settings: block "Variante B" in krywerk.env.example. Groups appear as GUIDs; from about 200 groups on, Entra ID
sends an overage reference instead (memberships then remain unchanged).
6. Without Docker (systemd)¶
cargo build --release -p kw-server # in the repository root; pure Rust, no OpenSSL
Then install as described in the header of deploy/krywerk-server.service (dedicated system user, data in
/var/lib/krywerk, configuration /etc/krywerk/krywerk-server.toml, hardened unit). TLS via a reverse proxy in
front of 127.0.0.1:8380 (Caddy: schluessel.example.org { reverse_proxy 127.0.0.1:8380 }) with
trust_proxy = true. Backup: sudo -u krywerk krywerk-server --config … backup <target> via timer/cron.
7. Day-to-day operation¶
7.1 Commands¶
| Command | Purpose |
|---|---|
krywerk-server check |
Check configuration, database and identity provider (before start/update) |
krywerk-server backup <target> |
Consistent backup while running, verified afterwards (file mode 0600) |
krywerk-server verify <file> |
Verify a backup: integrity, schema version, counts (no configuration needed) |
krywerk-server restore <file\|-> [--force] |
Restore a backup (the server must be stopped) |
krywerk-server healthcheck |
GET /health against the local server (Docker HEALTHCHECK) |
With Docker, use docker compose run --rm server <command> or docker compose exec server krywerk-server <command>.
7.2 Backup¶
What to back up: the server's database (it contains everything) – with your own Keycloak also its database
(docker compose … exec keycloak-db pg_dump -U keycloak keycloak > keycloak.sql) – as well as krywerk.env/.env.
The team folders themselves are backed up by the cloud storage.
# daily at 02:15, keep for 30 days (crontab of the operating user)
15 2 * * * /opt/krywerk/crates/kw-server/deploy/backup.sh /var/backups/krywerk 30 >>/var/log/krywerk-backup.log 2>&1
backup.sh uses docker compose in the deploy/ folder; with the Keycloak add-on, set
COMPOSE_FILE=compose.yaml:compose.keycloak.yaml first. Copy backups off-site regularly and practice a restore at
least every quarter (krywerk-server verify on the copy is not enough as practice – restore once into a test
instance and open a team folder).
7.3 Restore¶
./restore.sh /var/backups/krywerk/krywerk-server-<time>.sqlite3
stops the server, verifies the backup, restores it (the previous database remains in the volume as
krywerk-server.sqlite3.vor-restore-<time>) and restarts. Consequences: anything created after the backup is
missing: new team folders are no longer known (opening → "No access"), newly unlocked devices must be unlocked again,
new access grants must be granted again. Team folders whose keys existed only on the server are lost without a
backup – unless there is a recovery key (F9). Hence: back up regularly.
7.4 Updates¶
cd /opt/krywerk && git pull
cd crates/kw-server/deploy
./backup.sh /var/backups/krywerk 30 # back up first
docker compose build && docker compose run --rm server check
docker compose up -d # database migrations run automatically at startup
Going back to an older version after a schema change is only possible with the backup taken before (older servers
refuse newer databases with a clear message). Update the Caddy/Keycloak/Postgres images regularly
(docker compose pull && docker compose up -d; Keycloak major versions according to its upgrade guide).
7.5 Monitoring¶
GET https://schluessel.example.org/health→200 ok(database reachable), otherwise 503. Successful requests are not logged.- Docker reports the state (
docker compose ps→healthy). - Logs:
docker compose logs -f server. Frequent 401 → check identity provider/clock; 403 → missing role or access grant (normal when people without access open folders). - Put the expiry of the Entra client secret (Keycloak broker) in the calendar.
7.6 Troubleshooting¶
| Symptom | Cause / solution |
|---|---|
check: "Discovery meldet issuer … statt …" (discovery reports issuer … instead of …) |
KRYWERK_SERVER_OIDC_ISSUER must match the iss of the tokens exactly (no trailing /) |
check: provider not reachable |
Check OIDC_INTERNAL_URL; the server needs outbound access to the provider; custom CA → SSL_CERT_FILE |
| Apps: "Server not trustworthy" | public_url/KRYWERK_DOMAIN must be HTTPS; apps accept HTTP only for localhost |
| All requests 401 | Server clock (NTP), issuer, possibly OIDC_AUDIENCE set incorrectly; tokens of another client (azp) – then check OIDC_CLIENT_ID or OIDC_ALLOWED_CLIENTS |
| All requests 403 | Role user missing (default role in the realm), or empty ROLE_USER if the provider doesn't deliver roles |
| "Not allowed to create team folders" | Assign the role create-vaults |
| Group can't be found | A member must have signed in once; groups claim in the token? (GROUPS_CLAIM) |
restore: "still responding" |
Stop the server first (restore.sh does that) |
| Web administration: "Invalid parameter: redirect_uri" at the identity provider | Add the redirect https://<KRYWERK_DOMAIN>/verwaltung/anmeldung to the client krywerk-verwaltung |
| Web administration: "Session expired" on every form | The proxy overwrites Referrer-Policy with no-referrer (browsers then send Origin: null) – use ?Referrer-Policy in Caddy |
| Web administration: 403 "internal network only" | Adjust the Caddy block @verwaltung_extern |
| Web administration: no "People" menu | Role admin missing in the token (roles claim) |
8. Security and data protection¶
- Hardening: only Caddy is reachable from outside; the server runs as an unprivileged user with a read-only file system and no capabilities. Keycloak administration internal only. Install updates promptly.
- Separate roles: server operator ≠ member of all team folders. The operator sees metadata but no contents. The
role
adminallows viewing/archiving all team folders, but not decrypting them. - Revoking access applies to future retrievals; anyone who has had the key before could still decrypt older copies (vault format 8 has no key rotation). For departures that carry a risk, use "Renew key" (F10).
- GDPR: add the processing to the record of processing activities (purpose: access control for encrypted team
folders; data: see section 1; deletion: accounts don't disappear automatically when they are deleted in the
identity provider – currently manual). The last IP address per device is stored (shown as "last used"); limit access
logs with Docker log rotation (
/etc/docker/daemon.json:log-optsmax-size). Audit log (krywerk_event, web administration → Audit log): who changed members, devices or folders and retrieved keys, and when; no contents, keys or tokens. RetentionKRYWERK_SERVER_AUDIT_RETENTION_DAYS(default 365 days), IP addresses only withKRYWERK_SERVER_AUDIT_RECORD_IP=true. List it in the record of processing activities; only the IT administration has access (managers see their own folders). - Emergency: without the database and without a recovery key, team folders can no longer be opened. For every important team folder, set up at least two managers and (from F9 on) a sealed, deposited recovery key or emergency access.
9. Go-live checklist¶
- [ ] DNS records, ports 80/443, server with automatic security updates
- [ ] Identity provider set up, a test account can sign in, role
create-vaultsassigned - [ ]
docker compose run --rm server checkwithout errors,/health→ ok - [ ] Krywerk app: connect organization account, set up account, create a team folder and share it with a test person
- [ ] Web administration:
/verwaltung/reachable from the internal network, 403 from outside; sign-in with anadminaccount - [ ]
backup.shvia cron, first backup verified (verify), restore practiced in a test instance - [ ] Docker log rotation, monitoring of
/health, expiry date of the Entra secret noted - [ ] Entry in the record of processing activities, users informed