Kanidm is a modern identity management platform written in Rust. It gives you a single place to store people, groups, and service accounts, then exposes them over OAuth2/OIDC, a read-only LDAPS interface, and a Unix daemon for SSH and PAM logins. It is passkey-first, has no Java or Kerberos stack under it, and runs comfortably in under 1 GB of RAM, which makes it the best fit of the major identity servers for a small RamNode instance.
This guide covers a single-server production deployment on a RamNode KVM VPS: sizing, DNS, TLS with Let's Encrypt, container deployment, the first admin accounts, OAuth2 application integration, Unix client enrollment, backups, and upgrades.
Verified against Kanidm 1.11.x, released August 2026. Kanidm ships quarterly and only supports upgrades one minor version at a time, so pin your version and read the release notes before every jump.
1. Pick the right RamNode instance
Kanidm is a single static binary with an embedded SQLite-backed database. It is small.
| Deployment size | vCPU | RAM | Disk | Notes |
|---|---|---|---|---|
| Lab or personal (under 50 accounts) | 1 | 1 GB | 20 GB | Comfortable |
| Small business (50 to 500 accounts) | 2 | 2 GB | 40 GB | Recommended baseline |
| Larger, with OAuth2 traffic | 2 to 4 | 4 GB | 60 GB+ | Adds headroom for backups and logs |
Order a Standard or Premium KVM plan. Choose a location close to your users, since every OAuth2 redirect and every PAM login pays the round trip. Both IPv4 and IPv6 work; Kanidm binds dual-stack by default.
Pick a distribution you already patch on schedule. This guide uses Debian 13 as the container host, but any distribution with Docker or Podman works identically.
2. Prepare the server
SSH in as root and update.
apt update && apt full-upgrade -y
apt install -y curl ca-certificates ufwSet a real FQDN. Kanidm ties its domain and origin values to this name and changing them later is a domain rename operation, not a config edit.
hostnamectl set-hostname idm.example.comAdd the host to /etc/hosts so local lookups never depend on external DNS:
echo "203.0.113.10 idm.example.com idm" >> /etc/hostsCreate the DNS records at your provider:
idm.example.com. A 203.0.113.10
idm.example.com. AAAA 2001:db8::10Set the PTR record for the VPS in the RamNode control panel under the instance's networking section. Kanidm does not require reverse DNS, but every other service you will connect to it does, and it makes log correlation sane.
Confirm the clock is disciplined. Token and passkey validation are time-sensitive.
timedatectl set-ntp true
timedatectl statusFirewall
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw allow 80/tcp # Let's Encrypt HTTP-01 only
ufw allow 443/tcp # Web UI, OAuth2, REST API
ufw allow 636/tcp # LDAPS, drop this rule if you do not need LDAP
ufw enableRestrict 636 to the IPs of the applications that will actually read from LDAP rather than leaving it open to the internet:
ufw delete allow 636/tcp
ufw allow from 198.51.100.25 to any port 636 proto tcp3. Install the container runtime
curl -fsSL https://get.docker.com | sh
systemctl enable --now dockerPodman works equally well. Substitute podman for docker throughout and use a Quadlet unit instead of Compose if you prefer rootless operation.
4. Issue a TLS certificate
Kanidm refuses to run without TLS. There is no plaintext mode and no self-signed shortcut that clients will accept, so get a real certificate first.
apt install -y certbot
certbot certonly --standalone -d idm.example.com --agree-tos -m admin@example.com --no-eff-emailCreate the data directory and copy the certificate in. The container process runs as UID 1000, so ownership matters.
mkdir -p /srv/kanidm/data
cp /etc/letsencrypt/live/idm.example.com/fullchain.pem /srv/kanidm/data/chain.pem
cp /etc/letsencrypt/live/idm.example.com/privkey.pem /srv/kanidm/data/key.pem
chown -R 1000:1000 /srv/kanidm/data
chmod 640 /srv/kanidm/data/key.pemAutomate renewal with a deploy hook so you are not hand-copying files every 60 days:
cat > /etc/letsencrypt/renewal-hooks/deploy/kanidm.sh <<'EOF'
#!/bin/bash
set -e
DOMAIN=idm.example.com
DEST=/srv/kanidm/data
cp /etc/letsencrypt/live/$DOMAIN/fullchain.pem $DEST/chain.pem
cp /etc/letsencrypt/live/$DOMAIN/privkey.pem $DEST/key.pem
chown 1000:1000 $DEST/chain.pem $DEST/key.pem
chmod 640 $DEST/key.pem
docker restart kanidmd
EOF
chmod +x /etc/letsencrypt/renewal-hooks/deploy/kanidm.shCertbot's standalone plugin needs port 80 free at renewal time. Nothing else in this guide binds it, so leave 80 open in the firewall and unused.
5. Write the server configuration
Create /srv/kanidm/data/server.toml:
version = "2"
bindaddress = "[::]:8443"
ldapbindaddress = "[::]:3636"
db_path = "/data/kanidm.db"
tls_chain = "/data/chain.pem"
tls_key = "/data/key.pem"
domain = "idm.example.com"
origin = "https://idm.example.com"
log_level = "info"
[online_backup]
path = "/data/backups/"
schedule = "00 03 * * *"
versions = 14Two fields decide the fate of the deployment. domain is the SPN suffix and the WebAuthn relying party ID. origin is the URL users type in their browser, including the port if it is not 443. If you front Kanidm with a proxy or change the hostname later, every passkey registered against the old domain becomes invalid. Decide the name now.
mkdir -p /srv/kanidm/data/backups
chown -R 1000:1000 /srv/kanidm/data6. Start the server
Create /srv/kanidm/docker-compose.yml:
services:
kanidmd:
image: docker.io/kanidm/server:1.11.0
container_name: kanidmd
restart: unless-stopped
network_mode: host
volumes:
- /srv/kanidm/data:/dataHost networking keeps the source IP intact in the logs and avoids NAT hairpinning for the LDAPS listener. Bring it up:
cd /srv/kanidm
docker compose up -d
docker logs -f kanidmdThe container listens on 8443 and 3636 internally. Publish them on the standard ports with nftables redirects, or simply change bindaddress to [::]:443 and ldapbindaddress to [::]:636 and add cap_add: [NET_BIND_SERVICE] to the Compose service. The redirect approach:
apt install -y nftables
nft add table ip nat
nft 'add chain ip nat prerouting { type nat hook prerouting priority -100 ; }'
nft add rule ip nat prerouting tcp dport 443 redirect to :8443
nft add rule ip nat prerouting tcp dport 636 redirect to :3636Persist those rules in /etc/nftables.conf before you reboot.
Confirm the service answers:
curl -sI https://idm.example.com/status7. Recover the built-in admin accounts
Kanidm ships two privileged accounts and neither has a password until you set one. admin manages the server itself (domain settings, replication, schema). idm_admin manages people and groups. Use idm_admin for daily work.
docker exec -it kanidmd kanidmd recover-account admin
docker exec -it kanidmd kanidmd recover-account idm_adminEach command prints a generated password once. Store both in your password manager immediately.
Install the client tools on your workstation, then log in:
kanidm login --name idm_admin
kanidm self whoami --name idm_adminThe client reads ~/.config/kanidm or /etc/kanidm/config:
uri = "https://idm.example.com"8. Create your first people and groups
kanidm person create alice "Alice Nguyen" --name idm_admin
kanidm person update alice --legalname "Alice Nguyen" --mail alice@example.com --name idm_admin
kanidm group create sysadmins --name idm_admin
kanidm group add-members sysadmins alice --name idm_adminDo not set passwords for people from the admin CLI. Issue a self-service reset token and send it to them instead:
kanidm person credential create-reset-token alice --name idm_adminThe output includes a URL and a short code. The user opens it, enrolls a passkey or sets a password plus TOTP, and you never handle their credential. Tokens are single use and expire.
Delegate group administration so you are not the bottleneck:
kanidm group create helpdesk --name idm_admin
kanidm group add-members idm_people_admins helpdesk --name idm_admin9. Wire up an OAuth2 application
This is the main reason to run Kanidm. Any app that speaks OIDC can hand off login. Grafana as the example:
kanidm system oauth2 create grafana "Grafana" https://grafana.example.com --name idm_admin
kanidm system oauth2 add-redirect-url grafana https://grafana.example.com/login/generic_oauth --name idm_admin
kanidm group create grafana_users --name idm_admin
kanidm group add-members grafana_users alice --name idm_admin
kanidm system oauth2 update-scope-map grafana grafana_users openid profile email --name idm_admin
kanidm system oauth2 show-basic-secret grafana --name idm_adminPoint the application at the discovery endpoint:
https://idm.example.com/oauth2/openid/grafana/.well-known/openid-configurationMap application roles with claim maps rather than hardcoding usernames in the app:
kanidm group create grafana_admins --name idm_admin
kanidm system oauth2 update-scope-map grafana grafana_admins openid profile email --name idm_admin
kanidm system oauth2 update-claim-map grafana grafana_role grafana_admins Admin --name idm_admin
kanidm system oauth2 update-claim-map-join grafana grafana_role array --name idm_adminFor applications that only support public clients (native or SPA), disable the basic secret and enable PKCE-only flow with kanidm system oauth2 enable-public grafana.
10. Enroll a Linux client for SSH and PAM
On any other RamNode instance you want to authenticate against Kanidm, install the Unix tools (kanidm-unixd), then configure /etc/kanidm/config:
uri = "https://idm.example.com"And /etc/kanidm/unixd:
pam_allowed_login_groups = ["sysadmins"]
default_shell = "/bin/bash"
home_prefix = "/home/"
home_attr = "name"
uid_attr_map = "name"
gid_attr_map = "name"Create a POSIX identity for the account and the group:
kanidm person posix set alice --shell /bin/bash --name idm_admin
kanidm group posix set sysadmins --name idm_admin
kanidm person posix set-password alice --name idm_adminStart the daemons and test resolution before touching PAM:
systemctl enable --now kanidm-unixd kanidm-unixd-tasks
kanidm-unix status
getent passwd alice
kanidm-unix auth-test aliceOnly after auth-test succeeds, add the Kanidm modules to your PAM stack and kanidm to the passwd and group lines in /etc/nsswitch.conf. Keep a second root SSH session open while you do this. Locking yourself out of a VPS is recoverable through the RamNode VNC console, but the console is a slow place to debug PAM.
For SSH keys, publish them from Kanidm:
kanidm person ssh add-publickey alice laptop "ecdsa-sha2-nistp521 AAAA..." --name idm_adminThen on the client, set AuthorizedKeysCommand /usr/sbin/kanidm_ssh_authorizedkeys and AuthorizedKeysCommandUser nobody in sshd_config.
11. LDAPS for legacy applications
Kanidm exposes a read-only LDAPS interface for software that cannot speak OIDC. There is no plaintext 389 listener and no StartTLS, by design.
ldapsearch -H ldaps://idm.example.com:636 \
-x -D 'dn=token' -w "$(kanidm service-account api-token generate ...)" \
-b 'dc=idm,dc=example,dc=com' '(name=alice)'Create a service account with a read-only API token for each application rather than binding as a person. Remember that writes are not supported over LDAP; anything that wants to change a password through LDAP will fail. That is the intended behavior.
12. Backups
The [online_backup] block already writes a nightly consistent snapshot into /srv/kanidm/data/backups. Ship those off the VPS:
cat > /etc/cron.daily/kanidm-offsite <<'EOF'
#!/bin/bash
rsync -az --delete /srv/kanidm/data/backups/ backup@backup.example.com:/srv/backups/kanidm/
EOF
chmod +x /etc/cron.daily/kanidm-offsiteAlso snapshot /srv/kanidm/data/server.toml, since a database without its matching domain and origin is not a working restore.
To restore:
docker compose down
docker run --rm -v /srv/kanidm/data:/data docker.io/kanidm/server:1.11.0 \
kanidmd database restore /data/backups/kanidm.backup.json
docker compose up -dTake a RamNode snapshot of the whole instance before every upgrade. It is the fastest rollback available.
13. Upgrades
Kanidm supports upgrading exactly one minor version at a time. Going from 1.9 to 1.11 directly will refuse to start and you will be restoring from backup.
docker compose down
# edit docker-compose.yml, bump 1.11.0 to 1.12.0
docker compose pull
docker compose up -d
docker logs -f kanidmdThe server performs schema migrations on first boot of the new version. Watch the log until it reports ready. Client tools and sync connectors must match the server minor version, so upgrade kanidm on your workstation at the same time.
14. Hardening checklist
- Move SSH to key-only auth and disable root password login before Kanidm goes live.
- Set a domain display name and enforce your credential policy:
kanidm group account-policy webauthn-attestation-ca-list,credential-type-minimum, andauth-session-expiryare all set per group, so create astaffpolicy group and a stricteradminsone. - Require passkeys for privileged groups:
kanidm group account-policy credential-type-minimum admins passkey --name idm_admin. - Keep
adminfor break-glass only. Day-to-day work runs asidm_adminor a delegated group. - Log shipping:
docker logsis fine for one host, but pipe to journald or a remote collector so authentication failures are visible. - Review
kanidm system oauth2 listquarterly and delete integrations you retired.
15. Troubleshooting
Container exits immediately with a TLS error. The certificate files are unreadable by UID 1000, or chain.pem is missing intermediates. Use fullchain.pem, not cert.pem.
Passkeys stop working after a hostname change. Expected. WebAuthn binds credentials to the origin. Users must re-enroll.
Browser reports an origin mismatch. origin in server.toml must match the URL in the address bar exactly, scheme and port included.
LDAPS bind fails with invalid credentials. People cannot bind with their normal password unless you have configured an LDAP-specific credential. Use a service account API token.
Login works but getent passwd returns nothing on the client. The account has no POSIX attributes. Run kanidm person posix set.
Recovery when you lose both admin passwords. Stop the container, run kanidmd recover-account admin against the data volume, and you are back in. There is no remote path to this, which is why the RamNode console access matters.
Where to go next
Add a second instance and configure replication once you depend on this for production logins. Kanidm's replication is designed for read-write pairs across sites, which pairs well with running a second VPS in a different RamNode location. Until then, keep the nightly backup shipping off-box and take a snapshot before every change to the domain configuration.
