Skip to content

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 a scope. 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.

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):

  1. Select the realm krywerk. Create accounts or connect an identity provider (section 4).
  2. Roles: every user has user (default role). Anyone allowed to create team folders gets the role create-vaults (ideally via a group, e.g. "Create team folders" → Role mapping). admin may view and archive all team folders.
  3. Groups: groups in Keycloak appear in the apps for sharing (claim groups, full path, /Area/Team becomes 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 URI https://schluessel.example.org/verwaltung/*, post-logout redirect +, the same groups mapper as for cryptomator.
  • The role admin makes 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 /verwaltung for public addresses (block @verwaltung_extern; adjust the ranges or remove it).
  • TZ=Europe/Berlin in krywerk.env makes 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):

  1. Name "Krywerk sign-in", account type "Accounts in this organizational directory only".
  2. Redirect URI (web): https://login.example.org/realms/krywerk/broker/entra/endpoint.
  3. Certificates & secrets → new client secret (note the expiry date!).
  4. 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).
  5. 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"):

  1. Alias entra, client ID = application ID, client secret = the secret from step 3; for "OpenID Connect" the discovery URL https://login.microsoftonline.com/<tenant-id>/v2.0/.well-known/openid-configuration.
  2. "Sync mode" = Force (update name/email at every sign-in), "Trust email" on.
  3. Groups: one mapper "Advanced Claim to Group" per Entra group (claim groups contains the GUID → Keycloak group "Finance department" or similar). Alternatively, maintain groups in Keycloak only.
  4. Role create-vaults: a mapper "Advanced Claim to Role" for an Entra group, or assign it directly in Keycloak.
  5. Redirect straight to Microsoft (without the Keycloak sign-in page): in krywerk.env KRYWERK_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 admin allows 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-opts max-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. Retention KRYWERK_SERVER_AUDIT_RETENTION_DAYS (default 365 days), IP addresses only with KRYWERK_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-vaults assigned
  • [ ] docker compose run --rm server check without 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 an admin account
  • [ ] backup.sh via 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