Introduction to Kanidm
Kanidm is an identity management server, acting as an authority on account information, authentication and authorisation within a technical environment.
The intent of the Kanidm project is to:
- Provide a single source of truth for authorisation and authentication.
- Make system, network, application and web authentication easy and accessible.
- Secure and reliable by default, aiming for the highest levels of quality and stability.
Why do I want Kanidm?
Whether you work in a business, a volunteer organisation, or are an enthusiast who manages their personal services, you need methods of authenticating and identifying to your systems. These systems also need to determine what authorisation and privileges you have while accessing them.
We've probably all been in workplaces where you end up with multiple accounts on various systems -
one for a workstation, different SSH keys for different tasks, maybe some shared account passwords.
Not only is it difficult for people to manage all these different credentials and what they have
access to, but it also means that sometimes these credentials have more access or privilege than
they require. In the worst case this can lead to weak credentials (corpname123
is a common
example) or credentials that are disclosed via git repos.
Kanidm solves this problem by acting as a central authority of accounts in your organisation. This allows each account to associate many devices and strong credentials with different privileges. An example of how this looks:
graph LR; device@{ shape: circle, label: "Your Device" }; ssh@{ shape: processes, label: "SSH Servers" }; website@{ shape: processes, label: "Websites" }; wifi@{ shape: processes, label: "Wifi / VPN" }; radius@{ shape: div-rect, label: "RADIUS Server" }; kanidm@{ shape: lin-cyl, label: "Kanidm" }; device-- Trusts OAuth -->website; website-- Reads User Data -->kanidm; device-- OAuth -->kanidm; device-- SSH -->ssh; ssh-- Retrieve SSH Keys -->kanidm; device-- Login to -->wifi; wifi-- RADIUS Credentials -->radius; radius-- Verifies Credentials -->kanidm;
A key design goal is that you authenticate with your device in some manner, and then your device will continue to authenticate you in the future. Each of these different types of credentials, from SSH keys, application passwords, RADIUS passwords and others, are "things your device knows" or "things your device has". Each credential has limited capability and scope, and can only access that exact service or resource.
This helps improve security; a compromise of the service or the network transmission does not grant you unlimited access to your account and all its privileges. As the credentials are specific to a device, if a device is compromised you can revoke its associated credentials. If a specific service is compromised, only the credentials for that service need to be revoked.
Due to this model, and the design of Kanidm to centre the device and to have more per-service credentials, workflows and automation are added or designed to reduce human handling.
For Developers
Looking for the rustdoc
documentation for the libraries themselves?
Click here!
Evaluation Quickstart
This section will guide you through a quick setup of Kanidm for evaluation. It's recommended that for a production deployment you follow the steps in the installation chapter instead as there are a number of security considerations you should be aware of for production deployments.
Requirements
The only thing you'll need for this is Docker, Podman, or a compatible containerd environment installed and running.
Get the software
docker pull docker.io/kanidm/server:latest
Create your configuration
Create server.toml
. The important parts are the domain
and origin
. For this example, if you
use localhost
and https://localhost:8443
this will match later commands.
# The webserver bind address. Requires TLS certificates.
# If the port is set to 443 you may require the
# NET_BIND_SERVICE capability.
# Defaults to "127.0.0.1:8443"
bindaddress = "[::]:8443"
#
# The read-only ldap server bind address. Requires
# TLS certificates. If set to 636 you may require
# the NET_BIND_SERVICE capability.
# Defaults to "" (disabled)
# ldapbindaddress = "[::]:3636"
#
# HTTPS requests can be reverse proxied by a loadbalancer.
# To preserve the original IP of the caller, these systems
# will often add a header such as "Forwarded" or
# "X-Forwarded-For". If set to true, then this header is
# respected as the "authoritative" source of the IP of the
# connected client. If you are not using a load balancer
# then you should leave this value as default.
# Defaults to false
# trust_x_forward_for = false
#
# The path to the kanidm database.
db_path = "/data/kanidm.db"
#
# If you have a known filesystem, kanidm can tune the
# database page size to match. Valid choices are:
# [zfs, other]
# If you are unsure about this leave it as the default
# (other). After changing this
# value you must run a vacuum task.
# - zfs:
# * sets database pagesize to 64k. You must set
# recordsize=64k on the zfs filesystem.
# - other:
# * sets database pagesize to 4k, matching most
# filesystems block sizes.
# db_fs_type = "zfs"
#
# The number of entries to store in the in-memory cache.
# Minimum value is 256. If unset
# an automatic heuristic is used to scale this.
# You should only adjust this value if you experience
# memory pressure on your system.
# db_arc_size = 2048
#
# TLS chain and key in pem format. Both must be present
tls_chain = "/data/chain.pem"
tls_key = "/data/key.pem"
#
# The log level of the server. May be one of info, debug, trace
#
# NOTE: this can be overridden by the environment variable
# `KANIDM_LOG_LEVEL` at runtime
# Defaults to "info"
# log_level = "info"
#
# The DNS domain name of the server. This is used in a
# number of security-critical contexts
# such as webauthn, so it *must* match your DNS
# hostname. It is used to create
# security principal names such as `william@idm.example.com`
# so that in a (future) trust configuration it is possible
# to have unique Security Principal Names (spns) throughout
# the topology.
#
# ⚠️ WARNING ⚠️
#
# Changing this value WILL break many types of registered
# credentials for accounts including but not limited to
# webauthn, oauth tokens, and more.
# If you change this value you *must* run
# `kanidmd domain rename` immediately after.
domain = "idm.example.com"
#
# The origin for webauthn. This is the url to the server,
# with the port included if it is non-standard (any port
# except 443). This must match or be a descendent of the
# domain name you configure above. If these two items are
# not consistent, the server WILL refuse to start!
# origin = "https://idm.example.com"
origin = "https://idm.example.com:8443"
#
[online_backup]
# The path to the output folder for online backups
path = "/data/kanidm/backups/"
# The schedule to run online backups (see https://crontab.guru/)
# every day at 22:00 UTC (default)
schedule = "00 22 * * *"
# four times a day at 3 minutes past the hour, every 6th hours
# schedule = "03 */6 * * *"
# We also support non standard cron syntax, with the following format:
# sec min hour day of month month day of week year
# (it's very similar to the standard cron syntax, it just allows to specify the seconds
# at the beginning and the year at the end)
# Number of backups to keep (default 7)
# versions = 7
Start the container
First we create a docker volume to store the data, then we start the container.
docker volume create kanidmd
docker create --name kanidmd \
-p '443:8443' \
-p '636:3636' \
-v kanidmd:/data \
docker.io/kanidm/server:latest
Copy the configuration to the container
docker cp server.toml kanidmd:/data/server.toml
Generate evaluation certificates
docker run --rm -i -t -v kanidmd:/data \
docker.io/kanidm/server:latest \
kanidmd cert-generate
Start Kanidmd Container
docker start kanidmd
Recover the Admin Role Passwords
The admin
account is used to configure Kanidm itself.
docker exec -i -t kanidmd \
kanidmd recover-account admin
The idm_admin
account is used to manage persons and groups.
docker exec -i -t kanidmd \
kanidmd recover-account idm_admin
Setup the client configuration
This happens on your computer, not in the container.
# ~/.config/kanidm
uri = "https://localhost:8443"
verify_ca = false
Check you can login
kanidm login --name idm_admin
Create an account for yourself
kanidm person create <your username> <Your Displayname>
Set up your account credentials
kanidm person credential create-reset-token <your username>
Then follow the presented steps.
What next?
You'll probably want to set it up properly, so that other computers can access it, so choose a domain name and complete the full server installation.
Alternatively you might like to try configurig one of these:
- OAuth2 for web services
- PAM and nsswitch for authentication to Linux systems
- Replication, if one Kanidm instance isn't enough
Supported Features
This is a list of supported features and standards within Kanidm.
Authorisation
Cryptography
- Password Storage
- RFC6238 Time Based One Time Password
- RFC7519 JSON Web Token
- RFC7516 JSON Web Encryption
Data Import
- RFC4533 LDAP Content Synchronisation
- RFC4519 LDAP Schema
- FreeIPA User Schema
- RFC7644 SCIM Bulk Data Import
Database
LDAP
- RFC4511 LDAP (read-only)
- bind (simple)
- search
- filter
- whoami
- compare
- LDAPS (LDAP over TLS)
OAuth2 / OpenID Connect
- RFC6749 OAuth 2.0 Authorisation Framework
- Authorisation Code Grant
- Client Credentials Grant
- RBAC scope mapping
- RFC6819 OAauth 2.0 Threat Model and Security Considerations
- RFC7009 Token Revocation
- RFC7662 OAuth 2.0 Token Introspection
- RFC7636 Proof Key for Code Exchange (SHA256 Only)
- RFC8414 OAuth 2.0 Authorisation Server Metadata
- RFC9068 OAuth 2.0 JWT Access Tokens
- OpenID Connect Core 1.0
- RBAC claim and scope mapping
- PII scope claim requests
- ES256
id_token
signatures
- OpenID Connect Discovery 1.0
RADIUS
Replication
Unix Client
- PAM/nsswitch client authentication
Webauthn
Kanidm - Support and Release Processes
Introduction
This document describes the Kanidm project teams support and release version processes.
While we have made every effort to ensure this document is complete, in the event that something is missing or if you have a question, please open a discussion.
The version of this document found on the project page is considered authoritative and applies to all versions.
Release Schedule and Versioning
Kanidm is released on a 3 month (quarterly) schedule.
- February 1
- May 1
- August 1
- November 1
Versions follow semver using major.minor.patch
with an optional -tag
.
- development branch:
X.Y.Z-dev
- pre-release branch:
X.Y.Z-pre
- stable branch:
X.Y.Z
Release branches will be created and tagged 7 days prior to a release. The version will be set to the pre-release version.
%%{init: { "gitGraph": { "mainBranchName": "master", "parallelCommits": true, "showCommitLabel": false }}}%% gitGraph commit tag: "1.2.0-dev" commit branch 1.2.0 commit tag: "1.2.0-pre" commit commit tag: "1.2.0" commit commit tag: "1.2.1" commit commit tag: "1.2.2" commit commit tag: "1.2.3" commit commit type: HIGHLIGHT tag: "Final 1.2.x Release" checkout master commit tag: "1.3.0-dev" commit commit commit commit branch 1.3.0 commit tag: "1.3.0-pre" commit commit tag: "1.3.0" commit commit tag: "1.3.1" commit commit tag: "1.3.2" checkout master commit tag: "1.4.0-dev" commit commit commit commit commit commit
The first stable release is version 1.2.0. All releases prior are release candidate, beta or alpha builds and will no longer be supported as of the release of 1.2.0.
Support
Stable releases will be supported for 4 months after their release date. This allows a 1 month support overlap between N and N+1 versions.
Stable releases will have the following support provided during their support window.
- Security fixes
- Fixes for flaws leading to dataloss or corruption
- Fixes that impact the ability to upgrade
- Other quality fixes at the discretion of the project team
Distributions should track the stable branch.
Replication
All nodes must be the same version. A topology may contain up to a maximum of 2 nodes.
Upgrade Policy
Upgrades are supported from 1 release (minor version) before the current stable release.
Example: A server running 1.2.0 may upgrade to version 1.3.0. A server running 1.2.0 may not upgrade to 1.4.0.
Tool Versions
Command line tools (kanidm
) and sync connectors (kanidm-ipa-sync
, kanidm-ldap-sync
) must be
matched versions with the server that they communicate with at all times.
Unix Clients
The server supports communication from unix clients with a version from 1 release (minor version) before the servers release.
Example: A server running 1.4.0 supports unix clients running 1.4.0 and 1.3.0 of the unix client tools. Unix client tools of 1.2.0 and lower would not be supported.
API stability
Kanidm has a number of APIs with different stability guarantees. APIs that are stable will only receive breaking changes in the case of an ethics, security or potential data corruption issue.
Stable APIs are:
- LDAP protocol operations
- JSON HTTP end points which use elements from
proto/src/v1
All other APIs and interactions are not considered stable. Changes will be minimised if possible. This includes but is not limited to:
- unix domain socket API of
kanidm_unixd
resolver - unix admin domain socket for
kanidmd
server - web user interface design and operations
- entry schema
- CLI interface of any command provided by kanidm unless otherwise noted above
- JSON HTTP end points which use elements from
proto/src/internal.rs
- SCIM operations from
proto/src/scim_v1
Deprecation Policy
Features or APIs may be removed with 1 release versions notice. Deprecations will be announced in the Release Notes
Python module
The python module will typically trail changes in functionality of the core Rust code, and will be developed as we it for our own needs - please feel free to add functionality or improvements, or ask for them in a Github issue!
All code changes will include full type-casting wherever possible.
Project Discretion
In the event of an unforeseen or extraordinary situation, the project team may make decisions contradictory to this document at their discretion. In these situation, the project team will make every effort to communicate the reason for the decision and will attempt to minimise disruption to users.
Installing the Server
This chapter will describe how to plan, configure, deploy and update your Kanidm instances.
Choosing a Domain Name
This book makes many references to a "domain name". This is the DNS domain name that you intend to use for Kanidm.
This isn't always simple, and this chapter covers the key issues to consider when choosing a domain.
warning
Bad choices of domain name may have security impacts on your Kanidm instance, not limited to credential phishing, theft, session leaks and more.
Changing domain name is hard to do – it not only
means reconfiguring all LDAP and OAuth clients, but will also break all
registered WebAuthn credentials for all users (which are bound to an
Origin
).
It's critical that you consider and follow the advice in this chapter, and aim to get it right the first time.
You'll save yourself (and your users) a lot of work later!
tip
We believe these practices are applicable regardless of your organisation's size (even if your Kanidm instance is just for you!), or if you think your organisation is not "important enough" to be the target of attacks.
While some suggestions may seem "extreme" or "paranoid", they generally come from Kanidm's authors' collective decades of experience managing, maintaining, and securing networks and systems at both very large and very small organisations both inside and outside the technology industry.
Considerations
Use a domain under your exclusive control
You should always use a domain name that you've registered and directly control its DNS.
While example.com
and top-level domains ending in .example
appear throughout
this book, these are examples only. You should not use this
outside of testing.
You'll need a registered domain for a CA (certificate authority) to issue you a TLS certificate which is widely accepted by browsers. This will also prevent those same CAs from issuing a certificate for that domain to someone else.
If you use a domain controlled by someone else (eg: a Dynamic DNS provider, or your cloud provider), they could take over that domain whenever they like. They could also use control of DNS or email to convince a CA to issue a certificate for that domain.
Any party who holds a valid certificate for the domain can steal or issue credentials.
Avoid non-public and reserved domains
Avoid using "made-up" (eg: .lan
) or reserved domains (eg:
.local
), because your clients may leak credentials if they move
to another network, aren't connected to a VPN, or if it
collides with new TLDs.
Properly-configured TLS can prevent most (but not all) leakage, but defence in depth is best.
This will also ensure your infrastructure is accessible regardless of your users' local network conditions.
Domain authorities
Domain authorities can set their own eligibility policies for registering a top-level domain. They may also allow a third-party to challenge your claim to a top-level domain, subject to a dispute resolution policy. These policies may change over time for commercial or political reasons.
If your domain is on a ccTLD (country TLD), it may be de-registered should that
country cease to exist (eg: as for .io
).
Top-level domains containing "kanidm"
We ask that you do not use the word kanidm
as part of your instance's
top-level (or public-suffix-level) domain, eg: contoso-kanidm.example
.
Use something like auth
, idm
, login
or sso
instead – they're shorter,
too!
We're OK with you using kanidm
in a subdomain to point to your Kanidm
instance, eg: kanidm.example.com
.
We've worked hard to build this project, and using its name in conjunction with an organisation not associated with the project dilutes the name's brand value.
Subdomains and Cross-Origin policy
Browsers allow a server on a subdomain to use intra-domain resources, and access and set credentials and cookies from all of its parents until a public suffix. This can allow a malicious or compromised service to attack other services which share a parent domain.
Public suffix rules are mostly predictable, but has some exceptional cases. For example:
-
host.a.example.com
can access and set cookies for:host.a.example.com
(itself)a.example.com
example.com
But not the public suffix
.com
. -
host.a.example.qld.gov.au
can access and set cookies for:host.a.example.qld.gov.au
(itself)a.example.qld.gov.au
example.qld.gov.au
But not any public suffix:
qld.gov.au
(Queensland state government)gov.au
(Australian federal government).au
(Australia)
-
host.a.example.nsw.gov.au
can access and set cookies for:host.a.example.nsw.gov.au
(itself)a.example.nsw.gov.au
example.nsw.gov.au
nsw.gov.au
(NSW state government has opted out)
But not any public suffix:
gov.au
(Australian federal government).au
(Australia)
This can be an issue if Kanidm shares a domain with:
- applications which serve raw, user-supplied data in APIs (eg: blob/file storage and Matrix homeservers)
- third-party servers outside of your organisation's control (eg: SaaS apps)
- anything which can be deployed to with minimal oversight (eg: a web host that allows uploading content via unencrypted FTP)
- DNS entries that resolve to arbitrary IP addresses (eg:
192-0-2-1.ipv4.example.com
to192.0.2.1
, and192.0.2.1
is not under the authority ofexample.com
)
In most cases, hosting Kanidm on a subdomain of a separate top-level (or
existing public-suffix level) domain (eg: idm.contoso-auth.example
) is
sufficient to isolate your Kanidm deployment's Origin
from other applications
and services.
warning
There is generally no need to request additions to the public suffix list to deploy Kanidm securely, even for multi-environment deployments.
The only exception is to remove an existing opt-out that affects your
domain where it must operate under a particular suffix (eg: a NSW government
agency using example.nsw.gov.au
).
Such requests are a major burden for the volunteer-operated list, can take months to roll out to clients, and changes may have unintended side-effects.
By comparison, registering a separate domain is easy, and takes minutes.
tip
Web apps (and APIs) that authenticate with OAuth 2.0/OpenID Connect never need to share cookies or Origin with Kanidm, so they do not need to be on the same top-level (or public-suffix-level) domain.
Large public auth providers (eg: Google, Meta, Microsoft) work the same way with both first and third-party web apps.
Kanidm requires its own hostname
Kanidm must be the only thing running on a hostname, served from /
, with all
its paths served as-is.
It cannot:
- be run from a subdirectory (eg:
https://example.com/kanidm/
) - have other services accessible on the hostname in subdirectories (eg:
https://idm.example.com/wiki/
) - have other services accessible over HTTP or HTTPS at the same hostname on a
different port (eg:
https://idm.example.com:8080/
)
These introduce similar security risks to the subdomain issues described above.
One reasonable exception is to serve ACME HTTP-01 challenges (for
Let's Encrypt) at http://${hostname}/.well-known/acme-challenge/
. You'll need
a separate HTTP server to respond to these challenges, and ensure that only
authorised processes can request a certificate for Kanidm's hostname.
tip
The /.well-known/
path (RFC 8615) can be assigned security-sensitive
meaning in other protocols, similar to ACME HTTP-01.
Kanidm currently uses this path for OpenID Connect Discovery, and may use it for other integrations in the future.
Avoid wildcard and widely-scoped certificates
CAs can issue wildcard TLS certificates, which apply to all subdomains in the
same domain (eg: *.example.com
).
This is used by some organisations to avoid leaking information about what services exist on a domain in certificate transparency logs. However, this information will exposed anyway whenever a client makes a DNS query.
If a service is issued a wildcard TLS certificate which also covers a Kanidm installation on the same domain, any DNS hijacking could let that service impersonate Kanidm to those clients, and steal credentials.
While DNS-over-HTTPS generally prevents local hijacking, it's possible for a network to disable it when automatically enabled, or just block it entirely.
Sharing a single certificate between many services increases the risk that the private key may be exposed, and broadens the impact scope.
Separate production and testing environments
If running more than one instance of Kanidm, ensure that no two deployments share the same subdomain. This prevents credential and cookie transfers between the two environments. For example:
- Production:
idm.example.com
- Testing:
idm-test.example.com
If you instead had an instance of Kanidm at idm.example.com
for production and
another at test.idm.example.com
for testing, then the test instance could
access the credentials and cookies of the production environment.
This also prevents credentials intended for the test environment from being used in production (where there may be stricter controls).
Regional deployments
You could have multiple instances of Kanidm configured with replication, with a
single domain name and origin (eg: idm.example.com
).
You could then make regional instances accessible from different host names (eg:
au.idm.example.com
and nz.idm.example.com
).
This allows credentials and cookies to be freely transferred between hosts that are part of a single environment.
Recommendations
For maximum security, your Kanidm domain name should be a subdomain of a top-level domain (or domain under a public suffix) that has no other services assigned it, eg:
- Origin:
https://idm.example-auth.example
- Domain name:
idm.example-auth.example
If you have strict security controls for all apps on your top-level domain, you could run Kanidm on a subdomain of your main domain, eg:
- Origin:
https://idm.example.com
- Domain name:
idm.example.com
But running Kanidm on a separate top-level domain makes it much easier to restrict changes that could affect your IDM infrastructure.
note
Using a subdomain is the inverse of the common Active Directory practice
of using the organisation's primary top-level domain directly, eg:
example.com
.
Multi-environment and regional deployments
If we were to run regional instances, and have a separate testing environment, the following domain and hostnames could be used:
Production environment
- Origin:
https://idm.example.com
- Domain name:
idm.example.com
- Host names:
australia.idm.example.com
,newzealand.idm.example.com
This allows us to have named regional instances such as
https://australia.idm.example.com
which still works with WebAuthn and cookies
which are transferable between instances.
It is critical no other hosts are registered under idm.example.com
.
Testing environment
- Origin:
https://idm-test.example.com
- Domain name:
idm-test.example.com
- Host names:
australia.idm-test.example.com
,newzealand.idm-test.example.com
This puts the testing instance under a separate subdomain of the top-level
domain to production (idm.example.com
), so cookies and WebAuthn tokens can
not be transferred between them.
This provides proper isolation between the instances.
Bad domain names
Domains you should avoid:
-
idm.local
-
The
.local
top-level domain is reserved for multicast DNS.If a client visits another network, it may try to contact
idm.local
believing it is on its usual network. If TLS certificate verification were disabled (or not configured correctly), this would leak credentials. -
example.com
-
Using the top-level domain directly allows any subdomain of that domain to access credentials and cookies intended for Kanidm.
-
idm.example.nsw.gov.au
-
nsw.gov.au
has opted out of being a public suffix, so all domains under that suffix (exceptschools.nsw.gov.au
) share origin and cookies. -
idm.examplekanidm.example
-
Kanidm is the brand for this project.
Multi-instance with overlap
-
Production:
- Origin:
https://idm.example.com
- Domain name:
idm.example.com
- Origin:
-
Testing:
- Origin:
https://test.idm.example.com
- Domain name:
test.idm.example.com
- Origin:
While the production instance has a valid and well defined subdomain that doesn't conflict, because the testing instance is a subdomain of production, it allows production cookies to leak to the testing environment.
Testing environments may have weaker security controls in some cases which can then allow compromise of services using the production instance.
Preparing for your Deployment
Software Installation Method
We provide docker images for the server components. They can be found at:
- https://hub.docker.com/r/kanidm/server
- https://hub.docker.com/r/kanidm/radius
- https://hub.docker.com/r/kanidm/tools
You can fetch these by running the commands:
docker pull kanidm/server:latest
docker pull kanidm/radius:latest
docker pull kanidm/tools:latest
note
Our preferred deployment method is in containers, and this documentation assumes you're running in docker. Kanidm will alternately run as a daemon/service, and server builds are available for multiple platforms if you prefer this option. You may need to adjust the example commands throughout this document to suit your desired server type if you choose not to use containers.
Development Version
If you are interested in running the latest code from development, you can do this by changing the
docker tag to kanidm/server:devel
instead. Many people run the development version, and it is
extremely reliable, but occasional rough patches may occur. If you report issues, we will make every
effort to help resolve them.
System Requirements
CPU
Kanidm relies on modern CPU optimisations for many operations. As a result your cpu must be either:
x86_64
supportingx86_64_v2
operations.aarch64
supportingneon_v8
operations.
Older or unsupported CPUs may raise a SIGILL
(Illegal Instruction) on hardware that is not
supported by the project.
tip
You can check your CPU flags on Linux with the command lscpu
Memory
Kanidm extensively uses memory caching, trading memory consumption to improve parallel throughput. You should expect to see 64KB of ram per entry in your database, depending on cache tuning and settings.
Disk
You should expect to use up to 8KB of disk per entry you plan to store. At an estimate 10,000 entry databases will consume 40MB, 100,000 entry will consume 400MB.
For best performance, you should use non-volatile memory express (NVME), or other Flash storage media.
TLS
You'll need a volume where you can place configuration, certificates and the database:
docker volume create kanidmd
You should have a chain.pem and key.pem in your kanidmd volume. The reason for requiring Transport Layer Security (TLS, which replaces the deprecated Secure Sockets Layer, SSL) is explained in why tls. In summary, TLS is our root of trust between the server and clients, and a critical element of ensuring a secure system.
The key.pem should be a single PEM private key, with no encryption. The file content should be similar to:
-----BEGIN PRIVATE KEY-----
MII...<base64>
-----END PRIVATE KEY-----
The chain.pem is a series of PEM formatted certificates. The leaf certificate, or the certificate that matches the private key should be the first certificate in the file. This should be followed by the series of intermediates, and the final certificate should be the CA root. For example:
-----BEGIN CERTIFICATE-----
<leaf certificate>
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
<intermediate certificate>
-----END CERTIFICATE-----
[ more intermediates if needed ]
-----BEGIN CERTIFICATE-----
<ca/croot certificate>
-----END CERTIFICATE-----
note
If you are using Let's Encrypt the provided files "fullchain.pem" and "privkey.pem" are already correctly formatted as required for Kanidm.
You can validate that the leaf certificate matches the key with the command:
# ECDSA
openssl ec -in key.pem -pubout | openssl sha1
1c7e7bf6ef8f83841daeedf16093bda585fc5bb0
openssl x509 -in chain.pem -noout -pubkey | openssl sha1
1c7e7bf6ef8f83841daeedf16093bda585fc5bb0
# RSA
# openssl rsa -noout -modulus -in key.pem | openssl sha1
d2188932f520e45f2e76153fbbaf13f81ea6c1ef
# openssl x509 -noout -modulus -in chain.pem | openssl sha1
d2188932f520e45f2e76153fbbaf13f81ea6c1ef
If your chain.pem contains the CA certificate, you can validate this file with the command:
openssl verify -CAfile chain.pem chain.pem
If your chain.pem does not contain the CA certificate (Let's Encrypt chains do not contain the CA, for example) then you can validate with this command.
openssl verify -untrusted fullchain.pem fullchain.pem
note
Here "-untrusted" flag means a list of further certificates in the chain to build up to the root is provided, but that the system CA root should be consulted. Verification is NOT bypassed or allowed to be invalid.
If these verifications pass you can now use these certificates with Kanidm. To put the certificates in place you can use a shell container that mounts the volume such as:
docker run --rm -i -t -v kanidmd:/data -v /my/host/path/work:/work opensuse/leap:latest \
/bin/sh -c "cp /work/* /data/"
OR for a shell into the volume:
docker run --rm -i -t -v kanidmd:/data opensuse/leap:latest /bin/sh
Server Configuration
In this section we will configure your server and create its container instance.
Configuring server.toml
There are two methods for configuration:
- Providing a configuration file in the volume named
server.toml
. (Within the container it should be/data/server.toml
) - Using environment variables to specify configuration options (uppercased, prefixed with
KANIDM_
).
You can use one or both methods, but environment variables take precedence over options specified in files.The full options and explanations are in the kanidmd_core::config::ServerConfig docs page for your particular build.
warning
You MUST set the domain
, origin
, tls_chain
and tls_path
options via one method or the
other, or the server cannot start!
The following is a commented example configuration.
# The webserver bind address. Requires TLS certificates.
# If the port is set to 443 you may require the
# NET_BIND_SERVICE capability.
# Defaults to "127.0.0.1:8443"
bindaddress = "[::]:8443"
#
# The read-only ldap server bind address. Requires
# TLS certificates. If set to 636 you may require
# the NET_BIND_SERVICE capability.
# Defaults to "" (disabled)
# ldapbindaddress = "[::]:3636"
#
# HTTPS requests can be reverse proxied by a loadbalancer.
# To preserve the original IP of the caller, these systems
# will often add a header such as "Forwarded" or
# "X-Forwarded-For". If set to true, then this header is
# respected as the "authoritative" source of the IP of the
# connected client. If you are not using a load balancer
# then you should leave this value as default.
# Defaults to false
# trust_x_forward_for = false
#
# The path to the kanidm database.
db_path = "/data/kanidm.db"
#
# If you have a known filesystem, kanidm can tune the
# database page size to match. Valid choices are:
# [zfs, other]
# If you are unsure about this leave it as the default
# (other). After changing this
# value you must run a vacuum task.
# - zfs:
# * sets database pagesize to 64k. You must set
# recordsize=64k on the zfs filesystem.
# - other:
# * sets database pagesize to 4k, matching most
# filesystems block sizes.
# db_fs_type = "zfs"
#
# The number of entries to store in the in-memory cache.
# Minimum value is 256. If unset
# an automatic heuristic is used to scale this.
# You should only adjust this value if you experience
# memory pressure on your system.
# db_arc_size = 2048
#
# TLS chain and key in pem format. Both must be present
tls_chain = "/data/chain.pem"
tls_key = "/data/key.pem"
#
# The log level of the server. May be one of info, debug, trace
#
# NOTE: this can be overridden by the environment variable
# `KANIDM_LOG_LEVEL` at runtime
# Defaults to "info"
# log_level = "info"
#
# The DNS domain name of the server. This is used in a
# number of security-critical contexts
# such as webauthn, so it *must* match your DNS
# hostname. It is used to create
# security principal names such as `william@idm.example.com`
# so that in a (future) trust configuration it is possible
# to have unique Security Principal Names (spns) throughout
# the topology.
#
# ⚠️ WARNING ⚠️
#
# Changing this value WILL break many types of registered
# credentials for accounts including but not limited to
# webauthn, oauth tokens, and more.
# If you change this value you *must* run
# `kanidmd domain rename` immediately after.
domain = "idm.example.com"
#
# The origin for webauthn. This is the url to the server,
# with the port included if it is non-standard (any port
# except 443). This must match or be a descendent of the
# domain name you configure above. If these two items are
# not consistent, the server WILL refuse to start!
# origin = "https://idm.example.com"
origin = "https://idm.example.com:8443"
#
[online_backup]
# The path to the output folder for online backups
path = "/data/kanidm/backups/"
# The schedule to run online backups (see https://crontab.guru/)
# every day at 22:00 UTC (default)
schedule = "00 22 * * *"
# four times a day at 3 minutes past the hour, every 6th hours
# schedule = "03 */6 * * *"
# We also support non standard cron syntax, with the following format:
# sec min hour day of month month day of week year
# (it's very similar to the standard cron syntax, it just allows to specify the seconds
# at the beginning and the year at the end)
# Number of backups to keep (default 7)
# versions = 7
This example is located in examples/server_container.toml.
warning
You MUST set the "domain" name correctly, aligned with your "origin", else the server may refuse to start or some features (e.g. WebAuthn, OAuth2) may not work correctly!
Check the configuration is valid
You should test your configuration is valid before you proceed. This defaults to using
-c /data/server.toml
. The kanidmd
volume was created in the
evaluation quickstart
docker run --rm -i -t -v kanidmd:/data \
kanidm/server:latest /sbin/kanidmd configtest
Run the Server
Now we can run the server so that it can accept connections. The container defaults to using a
configuration file in /data/server.toml
.
docker run -p 443:8443 -v kanidmd:/data kanidm/server:latest
Using the NET_BIND_SERVICE
capability
If you plan to run without using docker port mapping or some other reverse proxy, and your
bindaddress
or ldapbindaddress
port is less than 1024
you will need the NET_BIND_SERVICE
in
docker to allow these port binds. You can add this with --cap-add
in your docker run command.
docker run --cap-add NET_BIND_SERVICE \
--network [host OR macvlan OR ipvlan] \
-v kanidmd:/data \
kanidm/server:latest
tip
However you choose to run your server, you should document and keep note of the docker run / create command you chose to start the instance. This will be used in the upgrade procedure.
Default Admin Accounts
Now that the server is running, you can initialise the default admin accounts. There are two
parallel admin accounts that have separate functions. admin
which manages Kanidm's configuration,
and idm_admin
which manages accounts and groups in Kanidm.
You should consider these as "break-glass" accounts. They exist to allow the server to be bootstrapped and accessed in emergencies. They are not intended for day-to-day use.
These commands will generate a new random password for the admin accounts. You must run the commands
as the same user as the kanidmd process or as root. This defaults to using -c /data/server.toml
.
docker exec -i -t <container name> \
kanidmd recover-account admin
# new_password: "xjgG4..."
docker exec -i -t <container name> \
kanidmd recover-account idm_admin
# new_password: "9Eux1..."
Security Hardening
Kanidm ships with a secure-by-default configuration, however that is only as strong as the environment that Kanidm operates in. This means the security of your container environment and server is extremely important when running Kanidm.
This chapter will detail a number of warnings and security practices you should follow to ensure that Kanidm operates in a secure environment.
The main server is a high-value target for a potential attack, as Kanidm serves as the authority on identity and authorisation in a network. Compromise of the Kanidm server is equivalent to a full-network take over, also known as "game over".
The unixd resolver is also a high value target as it can be accessed to allow unauthorised access to a server, to intercept communications to the server, or more. This also must be protected carefully.
For this reason, Kanidm's components must be secured and audited. Kanidm avoids many classic attacks by being developed in a memory safe language, but risks still exist in the operating environment.
Startup Warnings
At startup Kanidm will warn you if the environment it is running in is suspicious or has risks. For example:
kanidmd server -c /tmp/server.toml
WARNING: permissions on /tmp/server.toml may not be secure. Should be readonly to running uid. This could be a security risk ...
WARNING: /tmp/server.toml has 'everyone' permission bits in the mode. This could be a security risk ...
WARNING: /tmp/server.toml owned by the current uid, which may allow file permission changes. This could be a security risk ...
WARNING: permissions on ../insecure/ca.pem may not be secure. Should be readonly to running uid. This could be a security risk ...
WARNING: permissions on ../insecure/cert.pem may not be secure. Should be readonly to running uid. This could be a security risk ...
WARNING: permissions on ../insecure/key.pem may not be secure. Should be readonly to running uid. This could be a security risk ...
WARNING: ../insecure/key.pem has 'everyone' permission bits in the mode. This could be a security risk ...
WARNING: DB folder /tmp has 'everyone' permission bits in the mode. This could be a security risk ...
Each warning highlights an issue that may exist in your environment. It is not possible for us to prescribe an exact configuration that may secure your system. This is why we only present possible risks and you must make informed decisions on how to resolve them.
Should be Read-only to Running UID
Files, such as configuration files, should be read-only to the UID of the Kanidm daemon. If an attacker is able to gain code execution, they are then unable to modify the configuration to write, or to over-write files in other locations, or to tamper with the systems configuration.
This can be prevented by changing the files ownership to another user, or removing "write" bits from the group.
'everyone' Permission Bits in the Mode
This means that given a permission mask, "everyone" or all users of the system can read, write or execute the content of this file. This may mean that if an account on the system is compromised the attacker can read Kanidm content and may be able to further attack the system as a result.
This can be prevented by removing "everyone: execute bits from parent directories containing the configuration, and removing "everyone" bits from the files in question.
Owned by the Current UID, Which May Allow File Permission Changes
File permissions in UNIX systems are a discretionary access control system, which means the named UID owner is able to further modify the access of a file regardless of the current settings. For example:
[william@amethyst 12:25] /tmp > touch test
[william@amethyst 12:25] /tmp > ls -al test
-rw-r--r-- 1 william wheel 0 29 Jul 12:25 test
[william@amethyst 12:25] /tmp > chmod 400 test
[william@amethyst 12:25] /tmp > ls -al test
-r-------- 1 william wheel 0 29 Jul 12:25 test
[william@amethyst 12:25] /tmp > chmod 644 test
[william@amethyst 12:26] /tmp > ls -al test
-rw-r--r-- 1 william wheel 0 29 Jul 12:25 test
Notice that even though the file was set to "read only" to william, and no permission to any other users, user "william" can change the bits to add write permissions back or permissions for other users.
This can be prevent by making the file owner a different UID than the running process for kanidm.
A Secure Example
Between these three issues it can be hard to see a possible strategy to secure files, however one way exists - group read permissions. The most effective method to secure resources for Kanidm is to set configurations to:
[william@amethyst 12:26] /etc/kanidm > ls -al server.toml
-r--r----- 1 root kanidm 212 28 Jul 16:53 server.toml
The Kanidm server should be run as "kanidm:kanidm" with the appropriate user and user private group created on your system. This applies to unixd configuration as well.
For the database your data folder should be:
[root@amethyst 12:38] /data/kanidm > ls -al .
total 1064
drwxrwx--- 3 root kanidm 96 29 Jul 12:38 .
-rw-r----- 1 kanidm kanidm 544768 29 Jul 12:38 kanidm.db
This means 770 root:kanidm. This allows Kanidm to create new files in the folder, but prevents Kanidm from being able to change the permissions of the folder. Because the folder does not have "everyone" mode bits, the content of the database is secure because users can now cd/read from the directory.
Configurations for clients, such as /etc/kanidm/config, should be secured with read-only permissions and owned by root:
[william@amethyst 12:26] /etc/kanidm > ls -al config
-r--r--r-- 1 root root 38 10 Jul 10:10 config
This file should be "everyone"-readable, which is why the bits are defined as such.
Running as Non-root in docker
The commands provided in this book will run kanidmd as "root" in the container to make the onboarding smoother. However, this is not recommended in production for security reasons.
You should allocate unique UID and GID numbers for the service to run as on your host system. In
this example we use 1000:1000
You will need to adjust the permissions on the /data
volume to ensure that the process can manage
the files. Kanidm requires the ability to write to the /data
directory to create the database
files. This UID/GID number should match the above. You could consider the following changes to help
isolate these changes:
docker run --rm -i -t -v kanidmd:/data opensuse/leap:latest /bin/sh
mkdir /data/db/
chown 1000:1000 /data/db/
chmod 750 /data/db/
sed -i -e "s/db_path.*/db_path = \"\/data\/db\/kanidm.db\"/g" /data/server.toml
chown root:root /data/server.toml
chmod 644 /data/server.toml
Note that the example commands all run inside the docker container.
You can then use this to run the Kanidm server in docker with a user:
docker run --rm -i -t -u 1000:1000 -v kanidmd:/data kanidm/server:latest /sbin/kanidmd ...
hint
You need to use the UID or GID number with the -u
argument, as the container can't resolve
usernames from the host system.
Minimum TLS key lengths
We enforce a minimum RSA and ECDSA key sizes. If your key is insufficiently large, the server will refuse to start and inform you of this.
Currently accepted key sizes are minimum 2048 bit RSA and 224 bit ECDSA.
Server Updates
General Update Notes
During some upgrades the Kanidm project must apply new constraints or limits on your data. If we are unable to migrate these without disruption, we rely on administrators to make informed choices before the upgrade can proceed.
When these are required, we will give you one release cycle ahead of time to make changes. To check for changes that will affect your instance you should run.
kanidmd domain upgrade-check
# Running domain upgrade check ...
# domain_name : localhost
# domain_uuid : 7dcc7a71-b488-4e2c-ad4d-d89fc49678cb
# ------------------------
# upgrade_item : gidnumber range validity
# status : PASS
If any task yields a FAIL
then a future upgrade will also fail. A FAIL
status will provide you
a list of actions and affected entries that must be resolved before the next upgrade can complete
successfully. If all tasks yield a PASS
status then you can begin the upgrade process.
Docker Update Procedure
Docker doesn't follow a "traditional" method of updates. Rather you remove the old version of the container and recreate it with a newer version. This document will help walk you through that process.
tip
You should have documented and preserved your Kanidm container create / run command from the
server preparation guide. If not, you'll need to use docker inspect
to work out how to recreate
these parameters.
Upgrade Check
Perform the pre-upgrade check.
docker exec -i -t <container name> \
kanidmd domain upgrade-check
# Running domain upgrade check ...
# domain_name : localhost
# domain_uuid : 7dcc7a71-b488-4e2c-ad4d-d89fc49678cb
# ------------------------
# upgrade_item : gidnumber range validity
# status : PASS
Preserving the Previous Image
You may wish to preserve the previous image before updating. This is useful if an issue is encountered in upgrades.
docker tag kanidm/server:latest kanidm/server:<DATE>
docker tag kanidm/server:latest kanidm/server:2022-10-24
Update your Image
Pull the latest version of Kanidm.
docker pull kanidm/server:latest
docker pull kanidm/radius:latest
docker pull kanidm/tools:latest
Perform a backup
Update your Instance
warning
Downgrades are not possible. It is critical you know how to backup and restore before you proceed with this step.
Docker updates operate by deleting and recreating the container. All state that needs to be preserved is within your storage volume.
docker stop <previous instance name>
You can test that your configuration is correct with the new version, and the server should correctly start.
docker run --rm -i -t -v kanidmd:/data \
kanidm/server:latest /sbin/kanidmd configtest
You can then follow through with the upgrade by running the create / run command with your existing volume.
docker run [Your Arguments Here] -v kanidmd:/data \
OTHER_CUSTOM_OPTIONS \
kanidm/server:latest
Once you confirm the upgrade is successful you can delete the previous instance
docker rm <previous instance name>
If you encounter an issue you can revert to the previous version. Upgrades are performed in a single transaction and no changes to your data are made unless the upgrade was successful.
docker stop <new instance name>
docker start <previous instance name>
If you deleted the previous instance, you can recreate it from your preserved tag instead.
docker run [Your Arguments Here] -v kanidmd:/data \
OTHER_CUSTOM_OPTIONS \
kanidm/server:<DATE>
In rare and exceptional cases, if the server from your previous version fails to start, you will need to restore from backup.
Client Tools
To interact with Kanidm as an administrator, you'll need to use our command line tools. If you haven't installed them yet, install them now.
Kanidm configuration
You can configure kanidm
to help make commands simpler by modifying ~/.config/kanidm
or
/etc/kanidm/config
.
uri = "https://idm.example.com"
ca_path = "/path/to/ca.pem"
The full configuration reference is in the
definition of KanidmClientConfig
.
Once configured, you can test this with:
kanidm self whoami --name anonymous
Session Management
To authenticate as a user (for use with the command line), you need to use the login
command to
establish a session token.
kanidm login --name USERNAME
kanidm login --name admin
kanidm login -D USERNAME
kanidm login -D admin
Once complete, you can use kanidm
without re-authenticating for a period of time for
administration.
You can list active sessions with:
kanidm session list
Sessions will expire after a period of time. To remove these expired sessions locally you can use:
kanidm session cleanup
To log out of a session:
kanidm logout --name USERNAME
kanidm logout --name admin
Multiple Instances
In some cases you may have multiple Kanidm instances. For example you may have a production instance and a development instance. This can introduce friction for admins when they need to change between those instances.
The Kanidm cli tool allows you to configure multiple instances and swap between them with an
environment variable, or the --instance
flag. Instances maintain separate session stores.
uri = "https://idm.example.com"
ca_path = "/path/to/ca.pem"
["development"]
uri = "https://idm.dev.example.com"
ca_path = "/path/to/dev-ca.pem"
The instance can then be selected with:
export KANIDM_INSTANCE=development
kanidm login -D username@idm.dev.example.com
To return to the default instance you unset
the KANIDM_INSTANCE
variable.
Installing Client Tools
note
Running different release versions will likely present incompatibilities. Ensure you're running matching release versions of client and server binaries. If you have any issues, check that you are running the latest version of Kanidm.
From packages
Kanidm currently is packaged for the following systems:
- OpenSUSE Tumbleweed
- OpenSUSE Leap 15.4/15.5/15.6
- macOS
- Arch Linux
- CentOS Stream 9
- Debian
- Fedora 38
- NixOS
- Ubuntu
- Alpine Linux
The kanidm
client has been built and tested from Windows, but is not (yet) packaged routinely.
OpenSUSE Tumbleweed / Leap 15.6
Kanidm is available in Tumbleweed and Leap 15.6. You can install the clients with:
zypper ref
zypper in kanidm-clients
OpenSUSE Leap 15.4/15.5
Using zypper you can add the Kanidm leap repository with:
zypper ar -f obs://network:idm network_idm
Then you need to refresh your metadata and install the clients.
zypper ref
zypper in kanidm-clients
macOS - Homebrew
Kanidm provides a Homebrew cask, which lets Homebrew build and install the CLI client tools from source:
brew tap kanidm/kanidm
brew install kanidm
tip
Rust developers: this formula will install a Rust toolchain with Homebrew,
and add it to your PATH
. This may interfere with any Rust toolchain
you've installed with rustup
.
You can unlink Homebrew's Rust toolchain (removing it from your PATH
) with:
brew unlink rust
Homebrew will always use its version of Rust when building Rust packages, even when it is unlinked.
Alternatively, you may wish to install the Kanidm CLI with cargo
instead – this will use whatever Rust toochain you've already installed.
Arch Linux
Fedora / Centos Stream
note
Kanidm frequently uses new Rust versions and features, however Fedora and CentOS frequently are behind in Rust releases. As a result, they may not always have the latest Kanidm versions available.
Fedora has limited support through the development repository. You need to add the repository metadata into the correct directory:
# Fedora
wget https://download.opensuse.org/repositories/network:/idm/Fedora_38/network:idm.repo
# Centos Stream 9
wget https://download.opensuse.org/repositories/network:/idm/CentOS_9_Stream/network:idm.repo
You can then install with:
dnf install kanidm-clients
NixOS
Ubuntu and Debian
See https://kanidm.github.io/kanidm_ppa/ for nightly-built packages of the current development builds, and how to install them.
Alpine Linux
Kanidm is available in the Alpine Linux testing repository.
To install the Kanidm client use:
apk add kanidm-clients
Tools Container
In some cases if your distribution does not have native kanidm-client support, and you can't access cargo for the install for some reason, you can use the cli tools from a docker container instead.
This is a "last resort" and we don't really recommend this for day to day usage.
echo '{}' > ~/.cache/kanidm_tokens
chmod 666 ~/.cache/kanidm_tokens
docker pull kanidm/tools:latest
docker run --rm -i -t \
--network host \
--mount "type=bind,src=/etc/kanidm/config,target=/etc/kanidm/config" \
--mount "type=bind,src=$HOME/.config/kanidm,target=/home/kanidm/.config/kanidm" \
--mount "type=bind,src=$HOME/.cache/kanidm_tokens,target=/home/kanidm/.cache/kanidm_tokens" \
kanidm/tools:latest \
/sbin/kanidm --help
If you have a ca.pem you may need to bind mount this in as required as well.
tip
You can alias the docker run command to make the tools easier to access such as:
alias kanidm="docker run ..."
Cargo
The tools are available as a cargo download if you have a rust tool chain available. To install rust you should follow the documentation for rustup. These will be installed into your home directory. To update these, re-run the install command. You will likely need to install additional development libraries, specified in the Developer Guide.
cargo install kanidm_tools
Administration
This chapter describes some of the routine administration tasks for running a Kanidm server, such as making backups and restoring from backups, testing server configuration, reindexing, verifying data consistency, and renaming your domain.
Backup and Restore
With any Identity Management (IDM) software, it's important you have the capability to restore in case of a disaster - be that physical damage or a mistake. Kanidm supports backup and restore of the database with three methods.
It is important that you only attempt to restore data with the same version of the server that the backup originated from.
Method 1 - Automatic Backup
Automatic backups can be generated online by a kanidmd server
instance by including the
[online_backup]
section in the server.toml
. This allows you to run regular backups, defined by a
cron schedule, and maintain the number of backup versions to keep. An example is located in
examples/server.toml.
Method 2 - Manual Backup
This method uses the same process as the automatic process, but is manually invoked. This can be useful for pre-upgrade backups
To take the backup (assuming our docker environment) you first need to stop the instance:
docker stop <container name>
docker run --rm -i -t -v kanidmd:/data -v kanidmd_backups:/backup \
kanidm/server:latest /sbin/kanidmd database backup -c /data/server.toml \
/backup/kanidm.backup.json
docker start <container name>
You can then restart your instance. DO NOT modify the backup.json as it may introduce data errors into your instance.
To restore from the backup:
docker stop <container name>
docker run --rm -i -t -v kanidmd:/data -v kanidmd_backups:/backup \
kanidm/server:latest /sbin/kanidmd database restore -c /data/server.toml \
/backup/kanidm.backup.json
docker start <container name>
Method 3 - Manual Database Copy
This is a simple backup of the data volume containing the database files. Ensure you copy the whole folder, rather than individual files in the volume!
docker stop <container name>
# Backup your docker's volume folder
# cp -a /path/to/my/volume /path/to/my/backup-volume
docker start <container name>
Restoration is the reverse process where you copy the entire folder back into place.
Database Maintenance
Reindexing
In some (rare) cases you may need to reindex. Please note the server will sometimes reindex on startup as a result of the project changing its internal schema definitions. This is normal and expected - you may never need to start a reindex yourself as a result!
You only need to reindex if you add custom schema elements and you see a message in your logs such as:
Index EQUALITY name not found
Index {type} {attribute} not found
This indicates that an index of type equality has been added for name
, but the indexing process
has not been run. The server will continue to operate and the query execution code will correctly
process the query - however it will not be the optimal method of delivering the results as we need
to disregard this part of the query and act as though it's un-indexed.
Reindexing will resolve this by forcing all indexes to be recreated based on their schema definitions.
docker stop <container name>
docker run --rm -i -t -v kanidmd:/data \
kanidm/server:latest /sbin/kanidmd database reindex -c /data/server.toml
docker start <container name>
Vacuum
Vacuuming is the process of reclaiming un-used pages from the database freelists, as well as performing some data reordering tasks that may make some queries more efficient. It is recommended that you vacuum after a reindex is performed or when you wish to reclaim space in the database file.
Vacuum is also able to change the pagesize of the database. After changing db_fs_type
(which
affects pagesize) in server.toml, you must run a vacuum for this to take effect:
docker stop <container name>
docker run --rm -i -t -v kanidmd:/data \
kanidm/server:latest /sbin/kanidmd database vacuum -c /data/server.toml
docker start <container name>
Verification
The server ships with a number of verification utilities to ensure that data is consistent such as referential integrity or memberof.
Note that verification really is a last resort - the server does a lot to prevent and self-heal from errors at run time, so you should rarely if ever require this utility. This utility was developed to guarantee consistency during development!
You can run a verification with:
docker stop <container name>
docker run --rm -i -t -v kanidmd:/data \
kanidm/server:latest /sbin/kanidmd database verify -c /data/server.toml
docker start <container name>
If you have errors, please contact the project to help support you to resolve these.
Domain Rename
There are some cases where you may need to rename the domain. You should have configured this initially in the setup, however you may have a situation where a business is changing name, merging or other needs which may prompt this needing to be changed.
warning
This WILL break ALL PassKeys (WebAuthn tokens) that have been enrolled, which MAY cause accounts to be locked out and unrecoverable until further action is taken. DO NOT CHANGE the domain name unless REQUIRED and have a plan on how to manage these issues.
note
This operation MAY take an extensive amount of time as ALL accounts and groups in the domain MUST have their Security Principal Names (SPNs) regenerated. This WILL also cause a large delay in replication once the system is restarted.
You should make a backup before proceeding with this operation.
When you have a created a migration plan and strategy on handling the invalidation of webauthn, you can then rename the domain.
First, stop the instance.
docker stop <container name>
Second, change domain
and origin
in server.toml
.
Third, trigger the database domain rename process.
docker run --rm -i -t -v kanidmd:/data \
kanidm/server:latest /sbin/kanidmd domain rename -c /data/server.toml
Finally, you can now start your instance again.
docker start <container name>
Monitoring the platform
The monitoring design of Kanidm is still very much in its infancy - take part in the discussion at github.com/kanidm/kanidm/issues/216.
kanidmd status endpoint
kanidmd currently responds to HTTP GET requests at the /status
endpoint with a JSON object of
either "true" or "false". true
indicates that the platform is responding to requests.
URL | <hostname>/status |
---|---|
Example URL | https://example.com/status |
Expected response | One of either true or false (without quotes) |
Additional Headers | x-kanidm-opid |
Content Type | application/json |
Cookies | kanidm-session |
OpenTelemetry Tracing
Configure OTLP trace exports by setting a otel_grpc_url
in the server configuration. This'll
enable OpenTelemetry traces to be sent for observability use cases.
Example:
otel_grpc_url = "http://my-otel-host:4317"
Troubleshooting
Max Span Size Exceeded
On startup, we run some big processes that might hit a "max trace size" in certain configurations. Grafana Tempo defaults to 5MB, which is sensible for most things, but ... 😁
Grafana Tempo config to allow larger spans:
distributor:
receivers:
otlp:
protocols:
grpc:
max_recv_msg_size_mib: 20
Recycle Bin
The recycle bin is a storage of deleted entries from the server. This allows recovery from mistakes for a period of time.
warning
The recycle bin is a best effort - when recovering in some cases not everything can be "put back" the way it was. Be sure to check your entries are valid once they have been revived.
Where is the Recycle Bin?
The recycle bin is stored as part of your main database - it is included in all backups and restores, just like any other data. It is also replicated between all servers.
How do Things Get Into the Recycle Bin?
Any delete operation of an entry will cause it to be sent to the recycle bin. No configuration or specification is required.
How Long Do Items Stay in the Recycle Bin?
Currently they stay up to 1 week before they are removed. This may change in the future though.
Managing the Recycle Bin
You can display all items in the Recycle Bin with:
kanidm recycle-bin list --name admin
You can show a single item with:
kanidm recycle-bin get --name admin <uuid>
An entry can be revived with:
kanidm recycle-bin revive --name admin <uuid>
Edge Cases
The recycle bin is a best effort to restore your data - there are some cases where the revived entries may not be the same as their were when they were deleted. This generally revolves around reference types such as group membership, or when the reference type includes supplemental map data such as the OAuth2 scope map type.
An example of this data loss is the following steps:
add user1
add group1
add user1 as member of group1
delete user1
delete group1
revive user1
revive group1
In this series of steps, due to the way that referential integrity is implemented, the membership of user1 in group1 would be lost in this process. To explain why:
add user1
add group1
add user1 as member of group1 // refint between the two established, and memberof added
delete user1 // group1 removes member user1 from refint
delete group1 // user1 now removes memberof group1 from refint
revive user1 // re-add groups based on directmemberof (empty set)
revive group1 // no members
These issues could be looked at again in the future, but for now we think that deletes of groups is rare - we expect recycle bin to save you in "oops" moments, and in a majority of cases you may delete a group or a user and then restore them. To handle this series of steps requires extra code complexity in how we flag operations. For more, see This issue on github.
Accounts and groups
Accounts and Groups are the primary reasons for Kanidm to exist. Kanidm is optimised as a repository for these data. As a result, there are many concepts and important details to understand.
Service Accounts vs Person Accounts
Kanidm separates accounts into two types. Person accounts (or persons) are intended for use by humans that will access the system in an interactive way. Service accounts are intended for use by computers or services that need to identify themself to Kanidm. Generally a person or group of persons will be responsible for and will manage service accounts. Because of this distinction these classes of accounts have different properties and methods of authentication and management.
Groups
Groups represent a collection of entities. This generally is a collection of persons or service accounts. Groups are commonly used to assign privileges to the accounts that are members of a group. This allows easier administration over larger systems where privileges can be assigned to groups in a logical manner, and then only membership of the groups need administration, rather than needing to assign privileges to each entity directly and uniquely.
Groups may also be nested, where a group can contain another group as a member. This allows hierarchies to be created again for easier administration.
Default Accounts and Groups
Kanidm ships with a number of default service accounts and groups. This is to give you the best out-of-box experience possible, as well as supplying best practice examples related to modern Identity Management (IDM) systems.
There are two "break-glass" system administration accounts.
admin
is the default service account which has privileges to configure and administer Kanidm as a
whole. This account can manage access controls, schema, integrations and more. However the admin
can not manage persons by default.
idm_admin
is the default service account which has privileges to create persons and to manage
these accounts and groups. They can perform credential resets and more.
Both the admin
and the idm_admin
user should NOT be used for daily activities - they exist for
initial system configuration, and for disaster recovery scenarios. You should delegate permissions
as required to named user accounts instead.
The majority of the builtin groups are privilege groups that provide rights over Kanidm administrative actions. These include groups for account management, person management (personal and sensitive data), group management, and more.
admin
and idm_admin
both inherit their privileges from these default groups. This allows you to
assign persons to these roles instead.
Reauthentication and Session Privilege
Kanidm sessions have a concept of session privilege. Conceptually you can consider this like sudo
on unix systems or uac
on windows. This allows a session to briefly access its write permissions
by reauthentication with the identical credential they logged in with.
This allows safe assignment of high privilege roles to persons since their sessions do not have access to their write privileges by default. They must reauthenticate and use their privileges within a short time window.
However, these sessions always retain their read privileges - meaning that they can still access and view high levels of data at any time without reauthentication.
In high risk environments you should still consider assigning separate administration accounts to users if this is considered a risk.
Recovering the Initial Admin Accounts
By default the admin
and idm_admin
accounts have no password, and can not be accessed. They need
to be "recovered" from the server that is running the kanidmd server.
You should have already recovered the admin account during your setup process. If not, refer to the server configuration chapter on how to recover these accounts.
These accounts will be used through the remainder of this document for managing the server.
Viewing Default Groups
You should take some time to inspect the default groups which are related to default roles and permissions. Each group has a description to explain its purpose. These can be viewed with:
kanidm group list --name idm_admin
kanidm group get <name>
People Accounts
A person represents a human's account in Kanidm. The majority of your users will be a person who will use this account in their daily activities. These entries may contain personally identifying information that is considered by Kanidm to be sensitive. Because of this, there are default limits to who may access these data.
Creating Person Accounts
Members of the idm_people_admins
group have the privileges to create new persons in the system. By
default idm_admin
has this permission.
kanidm login --name idm_admin
kanidm person create demo_user "Demonstration User" --name idm_admin
kanidm person get demo_user --name idm_admin
Kanidm allows person accounts to include personally identifying attributes, such as their legal name and email address.
Initially, a person does not have these attributes. If desired, a person may be modified to have these attributes.
# Note, both the --legalname and --mail flags may be omitted
kanidm person update demo_user --legalname "initial name" --mail "initial@email.address"
You can also use anonymous to view accounts - note that you won't see certain fields due to the limits of the anonymous access control profile.
kanidm login --name anonymous
kanidm person get demo_user --name anonymous
note
Only members of idm_people_pii_read
and idm_people_admins
may read personal information by
default.
Also
warning
Persons may change their own displayname, name and legal name at any time. You MUST NOT use these
values as primary keys in external systems. You MUST use the uuid
attribute present on all
entries as an external primary key.
Account Validity
Kanidm supports accounts that are only able to authenticate between a pair of dates and times; the
"valid from" and "expires" timestamps define these points in time. By default members of
idm_people_admins
may change these values.
The account validity can be displayed with:
kanidm person validity show demo_user --name idm_admin
user: demo_user
valid after: any time
expire: never
kanidm person validity show demo_user --name idm_admin
valid after: 2020-09-25T21:22:04+10:00
expire: 2020-09-25T01:22:04+10:00
These datetimes are stored in the server as UTC, but presented according to your local system time to aid correct understanding of when the events will occur.
You may set these time and date values in any timezone you wish (such as your local timezone), and the server will transform these to UTC. These time values are in ISO8601 format, and you should specify this as:
YYYY-MM-DDThh:mm:ssZ+-hh:mm
Year-Month-Day T hour:minutes:seconds Z +- timezone offset
Set the earliest time the account can start authenticating:
kanidm person validity begin-from demo_user '2020-09-25T11:22:04+00:00' --name idm_admin
Set the expiry or end date of the account:
kanidm person validity expire-at demo_user '2020-09-25T11:22:04+00:00' --name idm_admin
To unset or remove these values the following can be used, where any|clear
means you may use
either any
or clear
.
kanidm person validity begin-from demo_user any|clear --name idm_admin
kanidm person validity expire-at demo_user clear|epoch|now --name idm_admin
To "lock" an account, you can set the expire_at
value to now
or epoch
. Even in the situation
where the "valid from" is after the expire_at
, the expire_at
will be respected.
These validity settings impact all authentication functions of the account (kanidm, ldap, radius).
Allowing people accounts to change their mail attribute
By default, Kanidm allows an account to change some attributes, but not their mail address.
Adding the user to the idm_people_self_write_mail
group, as shown below, allows the user to edit
their own mail.
kanidm group add-members idm_people_self_write_mail demo_user --name idm_admin
Authentication and Credentials
A primary job of a system like Kanidm is to manage credentials for persons. This can involve a range of operations from new user onboarding, credential resets, and self service.
Types of Credentials
Passkeys
This is the preferred method of authentication in Kanidm. Passkeys represent "all possible cryptographic" authenticators that support Webauthn. Examples of this include Yubikeys, TouchID, Windows Hello, TPM's and more.
These devices are unphishable, self contained multifactor authenticators and are considered the most secure method of authentication in Kanidm.
warning
Kanidm's definition of Passkeys may differ from that of other systems. This is because we adopted the term very early, before it has changed and evolved.
Attested Passkeys
These are the same as Passkeys, except that the device must present a cryptographic certificate or origin during registration. This allows account policy to be defined to only allow the use of certain models of authenticator. In general only FIDO2 keys or TPM's are capable of meeting attestation requirements.
Password + TOTP
This is a classic Time-based One Time Password combined with a password. Different to other systems Kanidm will prompt for the TOTP first before the password. This is to prevent drive by bruteforce against the password of the account and testing if the password is vulnerable.
While this authentication method is mostly secure, we do not advise it for high security environments due to the fact it is still possible to perform realtime phishing attacks.
Resetting Person Account Credentials
Members of the groups idm_people_admins
, idm_people_on_boarding
and idm_service_desk
have the
rights to initiate a credential reset for a person.
NOTE: If the person is a member of
idm_high_privilege
then these resets are not allowed. This is to preventidm_service_desk
and similar roles from privilege escalation by resetting the credentials of a higher privileged account. If a person who is a member ofidm_high_privilege
requires a credential reset, this must be initiated by a member ofidm_people_admins
.
Onboarding a New Person / Resetting Credentials
These processes are very similar. You can send a credential reset link to a user so that they can directly enroll their own credentials. To generate this link or qrcode:
kanidm person credential create-reset-token <account_id> [<time to live in seconds>]
kanidm person credential create-reset-token demo_user --name idm_admin
kanidm person credential create-reset-token demo_user 86400 --name idm_admin
# The person can use one of the following to allow the credential reset
#
# Scan this QR Code:
#
# █████████████████████████████████████████████
# █████████████████████████████████████████████
# ████ ▄▄▄▄▄ █▄██ ▀▀▀▄▀▀█ ▄▀▀▀▀▄▀▀▄█ ▄▄▄▄▄ ████
# ████ █ █ █▀ ▄▄▄▀█ █▀ ██ ▀ ▀▄█ █ █ ████
# ████ █▄▄▄█ █ █▄█ ▀ ▄███▄ ▀▄▀▄ █ █▄▄▄█ ████
# ████▄▄▄▄▄▄▄█ █▄▀▄█▄█ █▄▀▄▀▄█▄█ █▄█▄▄▄▄▄▄▄████
# ████ ▀█▀ ▀▄▄▄ ▄▄▄▄▄▄▄█▀ ▄█▀█▀ ▄▀ ▄ █▀▄████
# ████▄ █ ▀ ▄█▀█ ▀█ ▀█▄ ▀█▀ ▄█▄ █▀▄▀██▄▀█████
# ████ ▀▀▀█▀▄██▄▀█ ▄▀█▄▄█▀▄▀▀▀▀▀▄▀▀▄▄▄▀ ▄▄ ████
# ████ █▄▀ ▄▄ ▄▀▀ ▀ █▄█ ▀▀ █▀▄▄█▄ ▀ ▄ ▀▀████
# ████ █▀▄ █▄▄ █ █▀▀█▀█▄ ▀█▄█▄█▀▄▄ ▀▀ ▄▄ ▄████
# █████ ▀█▄▀▄▄▀▀ ██▀▀█▄█▄█▄█ █▀▄█ ▄█ ▄▄▀▀█████
# ████▄▄▀ ▄▄ ▀▀▄▀▀ ▄▄█ ▄ █▄ ▄▄ ▀▀▀▄▄ ▀▄▄██████
# ████▄▄▀ ▀▀▄▀▄ ▀▀▀▀█▀█▄▀▀ ▄▄▄ ▄ ▄█▀ ▄ ▄ ████
# ████▀▄ ▀▄▄█▀█▀▄ ▄██▄█▀ ▄█▀█ ▀▄ ███▄█ ▄█▄████
# ██████ ▀▄█▄██▀ ▀█▄▀ ▀▀▄ ▀▀█ ██▀█▄▄▀██ ▀▀████
# ████▄▄██▄▄▄▄ ▀▄██▀█ ███▀ ██▄▀▀█ ▄▄▄ ███ ████
# ████ ▄▄▄▄▄ █▄ ▄▄ ▀█▀ ▀▀ █▀▄▄▄▄█ █▄█ ▀▀ ▀████
# ████ █ █ █▄█▄▀ ██▀█▄ ▀█▄▀▄ ▀▀▄ ▄▄▄▀ ████
# ████ █▄▄▄█ ██▀█ ▀▄▀█▄█▄█▄▀▀▄▄ ▀ ▄▄▄█▀█ █████
# ████▄▄▄▄▄▄▄█▄█▄▄▄▄▄▄█▄█▄██▄█▄▄▄█▄██▄███▄▄████
# █████████████████████████████████████████████
# ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
#
# This link: https://localhost:8443/ui/reset?token=8qDRG-AE1qC-zjjAT-0Fkd6
# Or run this command: kanidm person credential use-reset-token 8qDRG-AE1qC-zjjAT-0Fkd6
If the user wishes you can direct them to https://idm.mydomain.name/ui/reset
where they can
manually enter their token value.
Once the credential reset has been committed the token is immediately invalidated and can never be used again. By default the token is valid for 1 hour. You can request a longer token validity time when creating the token. Tokens are only allowed to be valid for a maximum of 24 hours.
Resetting Credentials Directly
You can perform a password reset on the demo_user
, for example, as the idm_admin
user, who is a
default member of this group. The lines below prefixed with #
are the interactive credential
update interface. This allows the user to directly manage the credentials of another account.
warning
Don't use the direct credential reset to lock or invalidate an account. You should expire the account instead.
kanidm person credential update demo_user --name idm_admin
# spn: demo_user@idm.example.com
# Name: Demonstration User
# Primary Credential:
# uuid: 0e19cd08-f943-489e-8ff2-69f9eacb1f31
# generated password: set
# Can Commit: true
#
# cred update (? for help) # : pass
# New password:
# New password: [hidden]
# Confirm password:
# Confirm password: [hidden]
# success
#
# cred update (? for help) # : commit
# Do you want to commit your changes? yes
# success
kanidm login --name demo_user
kanidm self whoami --name demo_user
Credential Deletion
When a person deletes a credential, all sessions that were created by that credential are immediately logged out and invalidated.
Reauthentication / Privilege Access Mode
To allow for longer lived sessions in Kanidm, by default sessions are issued in a "privilege
capable" but read-only mode. In order to access privileges for a short time, you must
re-authenticate. This re-issues your session with a small time limited read-write session
internally. You can consider this to be like sudo
on a unix system or UAC
on windows where you
reauthenticate for short periods to access higher levels of privilege.
When using a user command that requires these privileges you will be warned:
kanidm person credential update william
# Privileges have expired for william@idm.example.com - you need to re-authenticate again.
To reauthenticate
kanidm reauth -D william
note
During reauthentication an account must use the same credential that was used to initially authenticate to the session. The reauth flow will not allow any other credentials to be used!
Groups
Groups are a collection of other entities that exist within Kanidm.
Creating Groups
Members of idm_group_admins
can create new groups. idm_admin
by default has these privileges.
kanidm group create demo_group --name idm_admin
kanidm group add-members demo_group demo_user --name idm_admin
kanidm group list-members demo_group --name idm_admin
After addition, you will see a reverse link from our demo_user
showing that it is now a member
of the group demo_group
. Kanidm makes all group membership determinations by inspecting an
entry's "memberof" attribute.
kanidm person get demo_user --name idm_admin
Nested Groups
Kanidm supports groups being members of groups, allowing nested groups. These nesting relationships are shown through the "memberof" attribute on groups and accounts. This allows nested groups to reflect on accounts.
An example can be easily shown with:
kanidm group create group_1 --name idm_admin
kanidm group create group_2 --name idm_admin
kanidm person create nest_example "Nesting Account Example" --name idm_admin
kanidm group add-members group_1 group_2 --name idm_admin
kanidm group add-members group_2 nest_example --name idm_admin
kanidm person get nest_example --name anonymous
This should result in output similar to:
memberof: idm_all_persons@localhost
memberof: idm_all_accounts@localhost
memberof: group_2@localhost
memberof: group_1@localhost
name: nest_example
Delegated Administration
Kanidm supports delegated administration though the "entry managed by" field. This allows specifying a group or user account that is the "entry manager" of a group. This allows the entry manager to modify the group without the need to define new access controls.
The entry_managed_by
attribute of a group may only be modified by members of
idm_access_control_admins
. During entry creation idm_group_admins
may set entry_managed_by
,
but may not change it post creation.
kanidm group create <NAME> [ENTRY_MANAGED_BY]
kanidm group create delegated_access_group demo_group --name idm_admin
kanidm group get delegated_access_group --name idm_admin
Now, as our demo_user
is a member of demo_group
they have delegated administration of
delegated_access_group
.
kanidm login --name demo_user
note the use of demo_user --\
v
kanidm group add-members delegated_access_group admin --name demo_user
kanidm group get delegated_access_group --name demo_user
Service Accounts
Creating Service Accounts
Members of idm_service_account_admins
have the privileges to create new service accounts. By
default idm_admin
has this access.
When creating a service account you must delegate entry management to another group or account. This allows other users or groups to update the service account.
The entry_managed_by
attribute of a service account may be created and modified by members of
idm_service_account_admins
.
NOTE: If a service account is a member of
idm_high_privilege
itsentry_managed_by
may only be modified by members ofidm_access_control_admins
kanidm service-account create <ACCOUNT_ID> <display-name> <entry-managed-by>
kanidm service-account create demo_service "Demonstration Service" demo_group --name idm_admin
kanidm service-account get demo_service --name idm_admin
By delegating the administration of this service account to demo_group
this allows our demo_user
to administer the service account.
Generating API Tokens For Service Accounts
Service accounts can have API tokens generated and associated with them. These tokens can be used for identification of the service account, and for granting extended access rights where the service account may previously have not had the access. Additionally service accounts can have expiry times and other auditing information attached.
To show API tokens for a service account:
kanidm service-account api-token status --name ENTRY_MANAGER ACCOUNT_ID
kanidm service-account api-token status --name demo_user demo_service
By default API tokens are issued to be "read only", so they are unable to make changes on behalf of the service account they represent. To generate a new read only API token with optional expiry time:
kanidm service-account api-token generate --name ENTRY_MANAGER ACCOUNT_ID LABEL [EXPIRY]
kanidm service-account api-token generate --name demo_user demo_service "Test Token"
kanidm service-account api-token generate --name demo_user demo_service "Test Token" 2020-09-25T11:22:02+10:00
If you wish to issue a token that is able to make changes on behalf of the service account, you must
add the --rw
flag during the generate command. It is recommended you only add --rw
when the API
token is performing writes to Kanidm.
kanidm service-account api-token generate --name ENTRY_MANAGER ACCOUNT_ID LABEL [EXPIRY] --rw
kanidm service-account api-token generate --name demo_user demo_service "Test Token" --rw
kanidm service-account api-token generate --name demo_user demo_service "Test Token" 2020-09-25T11:22:02+10:00 --rw
To destroy (revoke) an API token you will need its token id. This can be shown with the "status" command.
kanidm service-account api-token status --name ENTRY_MANAGER ACCOUNT_ID
kanidm service-account api-token status --name demo_user demo_service
kanidm service-account api-token destroy --name ENTRY_MANAGER ACCOUNT_ID TOKEN_ID
kanidm service-account api-token destroy --name demo_user demo_service 4de2a4e9-e06a-4c5e-8a1b-33f4e7dd5dc7
API Tokens with Kanidm HTTPS/REST API
The API token issued for a service account can be used by putting the token into the HTTP request
Authorization
header with the format Bearer <token>
.
For more see the MDN documentation for Authorisation
API Tokens with LDAP
API tokens can also be used to gain extended search permissions with LDAP. To do this you can bind
with a dn of dn=token
and provide the API token as the password.
ldapwhoami -H ldaps://URL -x -D "dn=token" -w "TOKEN"
ldapwhoami -H ldaps://idm.example.com -x -D "dn=token" -w "..."
# u: demo_service@idm.example.com
Anonymous Account
Within Kanidm there is a single "special" account. This is the anonymous service account. This allows clients without any credentials to perform limited read actions against Kanidm.
The anonymous account is primarily used by stateless unix clients to read account and group information.
Authentication
Even though anonymous does not have credentials it still must authenticate to establish a session to
access Kanidm. To achieve this there is a special anonymous
credential method. Anonymous is the
only account that may use this credential method.
OAuth2 / OIDC
Anonymous is a service account which prevents it from using OAuth2/OIDC to access other applications.
Access
By default anonymous has limited access to information in Kanidm. Anonymous may read the following data.
NOTE: The
Name
attribute is the user's public username. This is different to their private and sensitiveLegalName
attribute.
People
- Name
- DisplayName
- MemberOf
- Uuid
- GidNumber
- LoginShell
- SshPublicKey
Groups
- Name
- Member
- DynMember
- GidNumber
Disabling the Anonymous Account
The anonymous is like any other and can be expired to prevent its use. See the account validity section
When disabled, this will prevent stateless unix clients from authenticating to Kanidm.
Account Policy
Account Policy defines the security requirements that accounts must meet and influences users sessions.
Policy is defined on groups so that membership of a group influences the security of its members. This allows you to express that if you can access a system or resource, then the account must also meet the policy requirements.
All account policy settings may be managed by members of idm_account_policy_admins
. This is
assigned to idm_admin
by default.
Default Account Policy
A default Account Policy is applied to idm_all_persons
. This provides the defaults that influence
all people in Kanidm. This policy can be modified the same as any other group's policy.
Enforced Attributes
Auth Expiry
The maximum length in seconds that an authentication session may exist for.
Credential Type Minimum
The minimum security strength of credentials that may be assigned to this account. In order from weakest to strongest:
any
mfa
passkey
attested_passkey
Password Minimum Length
The minimum length for passwords (if they are allowed).
Privilege Expiry
The maximum length in seconds (<= 3600) that privileges will exist after reauthentication for to a read/write session.
Webauthn Attestation
The list of certificate authorities and device aaguids that must be used by members of this policy. This allows limiting devices to specific models.
To generate this list you should use fido-mds-tool
.
Policy Resolution
When an account is affected by multiple policies, the strictest component from each policy is applied. This can mean that two policies interact and make their combination stricter than their parts.
value | ordering |
---|---|
auth-expiry | smallest value |
credential-type-minimum | largest value |
password-minimum-length | largest value |
privilege-expiry | smallest value |
webauthn-attestation-ca-list | intersection of equal values |
Example Resolution
If we had two policies where the first defined:
auth-session: 86400
password-minimum-length: 10
privilege-expiry: 600
webauthn-attestation-ca-list: [ "yubikey 5ci", "yubikey 5fips" ]
And the second
auth-session: 3600
password-minimum-length: 15
privilege-expiry: 3600
webauthn-attestation-ca-list: [ "yubikey 5fips", "feitian epass" ]
As the value of auth-session from the second is smaller we would take that. We would take the smallest value of privilege-expiry from the first. We would take the largest value of password-minimum-length. From the intersection of the webauthn attestation CA lists we would take only the elements that are in both. This leaves:
auth-session: 3600
password-minimum-length: 15
privilege-expiry: 600
webauthn-attestation-ca-list: [ "yubikey 5fips" ]
Enabling Account Policy
Account Policy is enabled on a group with the command:
kanidm group account-policy enable <group name>
kanidm group account-policy enable my_admin_group
Note that the Account Policy is already enabled for idm_all_persons
.
Setting Maximum Session Time
The auth-session value influences the maximum time in seconds that an authenticated session can exist. After this time, the user must reauthenticate.
This value provides a difficult balance - forcing frequent re-authentications can frustrate and annoy users. However extremely long sessions allow a stolen or disclosed session token/device to read data for an extended period. Due to Kanidm's read/write separation this mitigates the risk of disclosed sessions as they can only read data, not write it.
To set the maximum authentication session time
kanidm group account-policy auth-expiry <group name> <seconds>
kanidm group account-policy auth-expiry my_admin_group 86400
Setting Minimum Password Length
The password-minimum-length value defines the character length of passwords that are acceptable. There are no other tunables for passwords in account policy. Other settings such as complexity, symbols, numbers and so on, have been proven to not matter in any real world attacks.
To set this value:
kanidm group account-policy password-minimum-length <group name> <length>
kanidm group account-policy password-minimum-length my_admin_group 12
Setting Maximum Privilege Time
The privilege-expiry time defines how long a session retains its write privileges after a reauthentication. After this time (maximum 1 hour), the session returns to read-only mode.
To set the maximum privilege time
kanidm group account-policy privilege-expiry <group name> <seconds>
kanidm group account-policy privilege-expiry my_admin_group 900
kanidm group account-policy privilege-expiry my_admin_group 86400 # NB: will be limited to 3600
Setting Webauthn Attestation CA Lists
The list should be generated with fido-mds-tool
. This will emit JSON that can be directly used
with Kanidm.
kanidm group account-policy webauthn-attestation-ca-list <group name> <attestation ca list json>
kanidm group account-policy webauthn-attestation-ca-list idm_all_persons '{"cas":{"D6E4b4Drh .... }'
NOTE:
fido-mds-tool
is available in thekanidm:tools
container.
Global Settings
There are a small number of account policy settings that are set globally rather than on a per group basis.
Denied Names
Users of Kanidm can change their name at any time. However, there are some cases where you may wish to deny some name values from being usable. This can be due to conflicting system account names or to exclude insulting or other abusive terms.
To achieve this you can set names to be in the denied-name list:
kanidm system denied-names append <name> [<name> ...]
You can display the currently denied names with:
kanidm system denied-names show
To allow a name to be used again it can be removed from the list:
kanidm system denied-names remove <name> [<name> ...]
Password Quality
Kanidm enforces that all passwords are checked by the library "zxcvbn". This has a large number of checks for password quality. It also provides constructive feedback to users on how to improve their passwords if they are rejected.
Some things that zxcvbn looks for is use of the account name or email in the password, common passwords, low entropy passwords, dates, reverse words and more.
This library can not be disabled - all passwords in Kanidm must pass this check.
Password Badlisting
This is the process of configuring a list of passwords to exclude from being able to be used. This is especially useful if a specific business has been notified of compromised accounts, allowing you to maintain a list of customised excluded passwords.
The other value to this feature is being able to badlist common passwords that zxcvbn does not detect, or from other large scale password compromises.
By default we ship with a preconfigured badlist that is updated over time as new password breach lists are made available.
The password badlist by default is append only, meaning it can only grow, but will never remove passwords previously considered breached.
You can display the current badlist with:
kanidm system pw-badlist show
You can update your own badlist with:
kanidm system pw-badlist upload "path/to/badlist" [...]
Multiple bad lists can be listed and uploaded at once. These are preprocessed to identify and remove passwords that zxcvbn and our password rules would already have eliminated. That helps to make the bad list more efficient to operate over at run time.
Password Rotation
Kanidm will never support this "anti-feature". Password rotation encourages poor password hygiene and is not shown to prevent any attacks - rather it significantly weakens password security.
POSIX Accounts and Groups
Kanidm has features that enable its accounts and groups to be consumed on POSIX-like machines, such as Linux, FreeBSD or others. Both service accounts and person accounts can be used on POSIX systems.
Notes on POSIX Features
Many design decisions have been made in the POSIX features of Kanidm that are intended to make distributed systems easier to manage and client systems more secure.
UID and GID Numbers
In Kanidm there is no difference between a UID and a GID number. On most UNIX systems a user will
create all files with a primary user and group. The primary group is effectively equivalent to the
permissions of the user. It is very easy to see scenarios where someone may change the account to
have a shared primary group (ie allusers
), but without changing the umask on all client systems.
This can cause users' data to be compromised by any member of the same shared group.
To prevent this, many systems create a "user private group", or UPG. This group has the GID number matching the UID of the user, and the user sets their primary group ID to the GID number of the UPG.
As there is now an equivalence between the UID and GID number of the user and the UPG, there is no
benefit in separating these values. As a result Kanidm accounts only have a GID number, which is
also considered to be its UID number as well. This has the benefit of preventing the accidental
creation of a separate group that has an overlapping GID number (the uniqueness
attribute of the
schema will block the creation).
UPG Generation
Due to the requirement that a user have a UPG for security, many systems create these as two independent items. For example, in /etc/passwd and /etc/group:
# passwd
william:x:654401105:654401105::/home/william:/bin/zsh
# group
william:x:654401105:
Other systems like FreeIPA use a plugin that generates a UPG as a separate group entry on creation of the account. This means there are two entries for an account, and they must be kept in lock-step. This poses a risk of desynchronisation that can and will happen on these systems leading to possible issues.
Kanidm does neither of these. As the GID number of the user must be unique, and a user implies the UPG must exist, we can generate UPG's on-demand from the account. This has an important side effect - that you are unable to add any members to a UPG - given the nature of a user private group, this is the point.
GID Number Generation
Kanidm has asynchronous replication as a feature between database servers. In this case, we need to be able to allocate stable and reliable GID numbers to accounts on replicas that may not be in continual communication.
To do this, we use the last 24 bits of the account or group's UUID to generate the GID number. We
can only use the UID range 1879048192
(0x70000000
) to 2147483647
(0x7fffffff
) due to
limitations of the Linux kernel and
systemd reserving other uids in the range for its exclusive use.
A valid concern is the possibility of duplication in the lower 24 bits. Given the birthday problem, if you have ~7700 groups and accounts, you have a 50% chance of duplication. With ~5000 you have a 25% chance, ~930 you have a 1% chance and with 290 you have a 0.1% chance.
We advise that if you have a site with greater than approximately 2,000 users you should use an external system to allocate GID numbers serially or consistently to avoid potential duplication events.
We recommend the use of the range 65536
through 524287
for manual allocation. This leaves the
range 1000
through 65535
for OS/Distro purposes, and allows Kanidm to continue dynamic
allocation in the range 1879048192
to 2147483647
if you choose a hybrid allocation approach.
This design decision is made as most small sites will benefit greatly from the auto-allocation policy and the simplicity of its design, while larger enterprises will already have IDM or business process applications for HR/People that are capable of supplying this kind of data in batch jobs.
Enabling POSIX Attributes
Enabling POSIX Attributes on Accounts
To enable POSIX account features and IDs on an account, you require the permission
idm_unix_admins
. This is provided to idm_admins
by default.
You can then use the following command to enable POSIX extensions on a person or service account.
kanidm [person OR service-account] posix set --name idm_admin <account_id> [--shell SHELL --gidnumber GID]
kanidm person posix set --name idm_admin demo_user
kanidm person posix set --name idm_admin demo_user --shell /bin/zsh
kanidm person posix set --name idm_admin demo_user --gidnumber 2001
kanidm service-account posix set --name idm_admin demo_account
kanidm service-account posix set --name idm_admin demo_account --shell /bin/zsh
kanidm service-account posix set --name idm_admin demo_account --gidnumber 2001
You can view the accounts POSIX token details with:
kanidm person posix show --name anonymous demo_user
kanidm service-account posix show --name anonymous demo_account
Enabling POSIX Attributes on Groups
To enable POSIX group features and IDs on an account, you require the permission idm_unix_admins
.
This is provided to idm_admins
by default.
You can then use the following command to enable POSIX extensions:
kanidm group posix set --name idm_admin <group_id> [--gidnumber GID]
kanidm group posix set --name idm_admin demo_group
kanidm group posix set --name idm_admin demo_group --gidnumber 2001
You can view the accounts POSIX token details with:
kanidm group posix show --name anonymous demo_group
POSIX-enabled groups will supply their members as POSIX members to clients. There is no special or separate type of membership for POSIX members required.
Troubleshooting Common Issues
subuid conflicts with Podman
Due to the way that Podman operates, in some cases using the Kanidm client inside non-root containers with Kanidm accounts may fail with an error such as:
ERROR[0000] cannot find UID/GID for user NAME: No subuid ranges found for user "NAME" in /etc/subuid
This is a fault in Podman and how it attempts to provide non-root containers, when UID/GIDs are greater than 65535. In this case, you may manually allocate your users GID number to be between 1000 - 65535, which may not trigger the fault.
Access Control
While Kanidm exists to make authorisation decisions on behalf of other services, internally it must make decisions about writes operations to the entries within its database. To make these choices, Kanidm has an internal set of access controls which are the rules describing who may perform what actions.
Default Permissions
The project ships default access controls which are designed to limit and isolate the privileges of accounts whenever possible.
This separation is the reason why admins
and idm_admins
exist as separate groups. There are two
distinct access silos within Kanidm. Access to manage Kanidm as a service (such as application
integrations and domain naming) and access to manage people and groups. This is to limit the
possible harm that an attacker may make if they gain access to these roles.
Assigning Permissions to Persons
Kanidm supports privilege access mode so that high-level permissions can be assigned to users who must reauthenticate before using those privileges. The privileges then are only accessible for a short period of time. This can allow you to assign high level permissions to regular person accounts rather than requiring separate privilege access accounts (PAA) or privileged access workstations (PAW).
Assigning Permissions to Service Accounts
Service account tokens can be issued with read-only or read-write flags, where read-only tokens are unable to modify content of Kanidm. This can allow the service account to have higher level permissions assigned but only usable with a short lived or isolated read-write token.
Permission Delegation
A number of types in Kanidm allow permission delegation such as groups and service accounts. This allows entries to be assigned an entry manager who has write access to that entity but not all entities of the same class.
High Privilege Groups
Kanidm has a special group called idm_high_privilege
. This acts as a "taint" on its members to
indicate that they have an elevated level of access within Kanidm or other systems.
This taint flag exists to prevent lateral movement from other roles that have higher levels of privilege.
An example is idm_service_desk
which has the ability to trigger credential resets for users. This
is an important aspect of the service desk role. However, a member of the service desk should not be
able to modify the credentials of their peers, nor should they be able to escalate by accessing the
credentials of users in a role such as idm_admins
. Since idm_service_desk
and idm_admins
are
both tainted with idm_high_privilege
then this lateral movement is not possible. Only high
privileged roles are able to then reset the accounts of high privilege users.
You may add other groups to idm_high_privilege
to achieve the same taint effect for other
services.
Default Permission Groups
Kanidm ships with default permission groups. You can use these to enable (or disable) accounts to performing certain actions or tasks within Kanidm as required by modifying the memberships of these groups.
group name | description |
---|---|
domain_admins | modify the name of this domain |
idm_access_control_admins | write access controls |
idm_account_policy_admins | modify account policy requirements for user authentication |
idm_group_admins | create and modify groups |
idm_mail_servers | read mail attributes needed to be a mail server |
idm_mail_service_admins | grant permissions to service accounts to act as mail servers |
idm_oauth2_admins | create and modify OAuth2 integrations |
idm_people_admins | create and modify persons |
idm_people_on_boarding | create (but not modify) persons. Intended for use with service accounts |
idm_people_pii_read | allow read to personally identifying information |
idm_people_self_mail_write | allow self-modification of the mail attribute |
idm_people_self_name_write | allow self-modification of the name related attributes |
idm_radius_servers | read user radius secrets. Intended for use with service accounts |
idm_radius_service_admins | create and reset user radius secrets, and allow users to access radius |
idm_recycle_bin_admins | modify and restore entries from the recycle bin |
idm_schema_admins | add and modify elements of schema |
idm_service_account_admins | create and modify service accounts |
idm_unix_admins | enable posix attributes on accounts and groups |
idm_application_admins | create and modify ldap applications which use per-user application passwords |
Default Roles
Kanidm ships with 3 high level permission groups. These roles have no inherent permissions, they are created by being members of the default permission groups.
group name | description |
---|---|
idm_admins | manage persons and their groups |
idm_service_desk | assist persons with credential resets or other queries |
system_admins | manage the operation of Kanidm as a database and service |
Service Integrations
This chapter describes interfaces that Kanidm provides that allows external services and applications to integrate with and trust Kanidm as an authentication and identity provider.
LDAP
While many applications can support external authentication and identity services through OAuth2, not all services can. Lightweight Directory Access Protocol (LDAP) has been the "universal language" of authentication for many years, with almost every application in the world being able to search and bind to LDAP. As many organisations still rely on LDAP, Kanidm can host a read-only LDAP interface for these legacy applications and services.
warning
The LDAP server in Kanidm is not a complete LDAP implementation. This is intentional, as Kanidm wants to cover the common use cases - simple bind and search. The parts we do support are RFC compliant however.
What is LDAP
LDAP is a protocol to read data from a directory of information. It is not a server, but a way to communicate to a server. There are many famous LDAP implementations such as Active Directory, 389 Directory Server, DSEE, FreeIPA and many others. Because it is a standard, applications can use an LDAP client library to authenticate users to LDAP, given "one account" for many applications - an IDM just like Kanidm!
Data Mapping
Kanidm entries cannot be mapped 100% to LDAP's objects. This is because LDAP types are simple key-values on objects which are all UTF8 strings (or subsets thereof) based on validation (matching) rules. Kanidm internally implements complex structured data types such as tagging on SSH keys, or multi-value credentials. These can not be represented in LDAP.
Many of the structures in Kanidm do not correlate closely to LDAP. For example, Kanidm only has a GID number, where LDAP's schemas define both a UID number and a GID number.
Entries in the database also have a specific name in LDAP, related to their path in the directory tree. Kanidm is a flat model, so we have to emulate some tree-like elements, and ignore others.
For this reason, when you search the LDAP interface, Kanidm will make some mapping decisions.
- The Kanidm domain name is used to generate the DN of the suffix by default.
- The
domain_info
object becomes the suffix root. - All other entries are direct subordinates of the
domain_info
for DN purposes. - Distinguished Names (DNs) are generated from the spn, name, or uuid attribute.
- Bind DNs can be remapped and rewritten, and may not even be a DN during bind.
These decisions were made to make the path as simple and effective as possible, relying more on the Kanidm query and filter system rather than attempting to generate a tree-like representation of data. As almost all clients can use filters for entry selection we don't believe this is a limitation for the consuming applications.
Security
LDAPS vs StartTLS
StartTLS is not supported due to security risks such as credential leakage and MITM attacks that are fundamental in how StartTLS works. StartTLS can not be repaired to prevent this. LDAPS is the only secure method of communicating to any LDAP server. Kanidm will use its certificates for both HTTPS and LDAPS.
Writes
The structure of LDAP is too simplistic for writing to the complex entries that Kanidm internally contains. As a result, writes are rejected for all users via the LDAP interface.
Access Controls
LDAP only supports password authentication. As LDAP is used heavily in POSIX environments the LDAP bind for any DN will use its configured posix password.
As the POSIX password is not equivalent in strength to the primary credentials of Kanidm (which in most cases is multi-factor authentication), the LDAP bind does not grant rights to elevated read permissions. All binds have the permissions of "anonymous" even if the anonymous account is locked.
The exception is service accounts which can use api-tokens during an LDAP bind for elevated read permissions.
The ability to bind with the POSIX password can be disabled to prevent password bruteforce attempts. This does not prevent api-token binds.
Filtering Objects
It is recommended that client applications filter accounts that can authenticate with
(class=account)
and groups with (class=group)
.
Server Configuration
To configure Kanidm to provide LDAP, add the argument to the server.toml
configuration:
ldapbindaddress = "127.0.0.1:3636"
You should configure TLS certificates and keys as usual - LDAP will reuse the Web server TLS material.
Showing LDAP Entries and Attribute Maps
By default Kanidm is limited in what attributes are generated or remapped into LDAP entries. However, the server internally contains a map of extended attribute mappings for application specific requests that must be satisfied.
An example is that some applications expect and require a 'CN' value, even though Kanidm does not provide it. If the application is unable to be configured to accept "name" it may be necessary to use Kanidm's mapping feature. Currently these are compiled into the server, so you may need to open an issue with your requirements for attribute maps.
To show what attribute maps exists for an entry you can use the attribute search term '+'.
# To show Kanidm attributes
ldapsearch ... -x '(name=admin)' '*'
# To show all attribute maps
ldapsearch ... -x '(name=admin)' '+'
Attributes that are in the map can be requested explicitly, and this can be combined with requesting Kanidm native attributes.
ldapsearch ... -x '(name=admin)' cn objectClass displayname memberof
Group Memberships
Group membership is defined in RFC2307bis or Active Directory style. This means groups are determined from the "memberof" attribute which contains a DN to a group.
People Accounts
Persons can bind (authenticate) to the LDAP server if they are configured as a posix account and have a valid posix password set.
When a person is bound to the directory, they inherit the permissions of anonymous - not their account. This is because a posix password as single factor authentication is not as secure and should not grant the same privileges as the accounts standard credentials.
Service Accounts
If you have issued api tokens for a service account they can be used to gain extended read permissions for those service accounts.
Api tokens can also be used to gain extended search permissions with LDAP. To do this you can bind
with a dn of dn=token
and provide the api token in the password.
note
The dn=token
keyword is guaranteed to not be used by any other entry, which is why it was chosen
as the keyword to initiate api token binds.
ldapwhoami -H ldaps://URL -x -D "dn=token" -w "TOKEN"
ldapwhoami -H ldaps://idm.example.com -x -D "dn=token" -w "..."
# u: demo_service@idm.example.com
Changing the Basedn
By default the basedn of the LDAP server is derived from the domain name. For example, a domain name
of idm.example.com
will become dc=idm,dc=example,dc=com
.
However, you may wish to change this to something shorter or at a higher level within your domain name.
warning
Changing the LDAP Basedn will require you to reconfigure your client applications so they search the correct basedn. Be careful when changing this value!
As an admin you can change the domain ldap basedn with:
kanidm system domain set-ldap-basedn <new basedn>
kanidm system domain set-ldap-basedn o=kanidm -D admin
Basedns are validated to ensure they are either dc=
, ou=
or o=
. They must have one or more of
these components and must only contain alphanumeric characters.
After the basedn is changed, the new value will take effect after a server restart. If you have a replicated topology, you must restart all servers.
Disable POSIX Password Binds
If you do not have applications that require LDAP password binds, then you should disable this function to limit access.
kanidm system domain set-ldap-allow-unix-password-bind [true|false]
kanidm system domain set-ldap-allow-unix-password-bind -D admin false
Examples
Given a default install with domain "idm.example.com" the configured LDAP DN will be "dc=idm,dc=example,dc=com".
# from server.toml
ldapbindaddress = "[::]:3636"
This can be queried with:
LDAPTLS_CACERT=ca.pem ldapsearch \
-H ldaps://127.0.0.1:3636 \
-b 'dc=idm,dc=example,dc=com' \
-x '(name=test1)'
# test1@example.com, idm.example.com
dn: spn=test1@idm.example.com,dc=idm,dc=example,dc=com
objectclass: account
objectclass: memberof
objectclass: object
objectclass: person
objectclass: posixaccount
displayname: Test User
gidnumber: 12345
memberof: spn=group240@idm.example.com,dc=idm,dc=example,dc=com
name: test1
spn: test1@idm.example.com
entryuuid: 22a65b6c-80c8-4e1a-9b76-3f3afdff8400
LDAP binds can use any unique identifier of the account. The following are all valid bind DNs for the object listed above.
ldapwhoami ... -x -D 'name=test1'
ldapwhoami ... -x -D 'spn=test1@idm.example.com'
ldapwhoami ... -x -D 'test1@idm.example.com'
ldapwhoami ... -x -D 'test1'
ldapwhoami ... -x -D '22a65b6c-80c8-4e1a-9b76-3f3afdff8400'
ldapwhoami ... -x -D 'spn=test1@idm.example.com,dc=idm,dc=example,dc=com'
ldapwhoami ... -x -D 'name=test1,dc=idm,dc=example,dc=com'
Troubleshooting
Can't contact LDAP Server (-1)
Most LDAP clients are very picky about TLS, and can be very hard to debug or display errors. For example these commands:
ldapsearch -H ldaps://127.0.0.1:3636 -b 'dc=idm,dc=example,dc=com' -x '(name=test1)'
ldapsearch -H ldap://127.0.0.1:3636 -b 'dc=idm,dc=example,dc=com' -x '(name=test1)'
ldapsearch -H ldap://127.0.0.1:3389 -b 'dc=idm,dc=example,dc=com' -x '(name=test1)'
All give the same error:
ldap_sasl_bind(SIMPLE): Can't contact LDAP server (-1)
This is despite the fact:
- The first command is a certificate validation error.
- The second is a missing LDAPS on a TLS port.
- The third is an incorrect port.
To diagnose errors like this, you may need to add "-d 1" to your LDAP commands or client.
OAuth2
OAuth is a web authorisation protocol that allows "single sign on". It's key to note OAuth only provides authorisation, as the protocol in its default forms do not provide identity or authentication information. All that OAuth can provide is information that an entity is authorised for the requested resources.
OAuth can tie into extensions allowing an identity provider to reveal information about authorised sessions. This extends OAuth from an authorisation only system to a system capable of identity and authorisation. Two primary methods of this exist today: RFC7662 token introspection, and OpenID connect.
Resource Server and Clients
This is the resource that a user wants to access. Common examples could be Nextcloud, a Wiki, or a chat service. In these cases the service is both the client and the resource server within the OAuth2 workflow. We will refer to the combination of both client and resource server as a service.
It's important for you to know how your service will interact with OAuth2. For example, does it rely on OpenID connect for identity information, or does it support RFC7662 token introspection?
In general, Kanidm requires that your service supports three things:
-
HTTP basic authentication to the authorisation server (Kanidm)
-
PKCE
S256
code verification (code_challenge_methods_supported
) -
If it uses OIDC,
ES256
for token signatures (id_token_signing_alg_values_supported
)
If your service doesn't support PKCE or only supports RS256
token signatures,
see extended options for legacy clients.
Kanidm issues tokens which are RFC 9068 JWTs, allowing service introspection.
note
Previous versions of this document incorrectly described "clients" as "resource servers" due to clarity issues in the OAuth2 RFC.
Kanidm's OAuth2 URLs
Kanidm will expose its OAuth2 APIs at the following URLs, substituting
:client_id:
with an OAuth2 client ID.
-
OpenID Connect Discovery 1.0 URL (recommended)
-
https://idm.example.com/oauth2/openid/:client_id:/.well-known/openid-configuration
This document includes all the URLs and attributes an app needs to be able to authenticate using OIDC with Kanidm, except for the
client_id
andclient_secret
.Use this document wherever possible, as it will allow you to easily build and/or configure an interoperable OIDC client without needing to code or configure anything special for Kanidm (or another provider).
Note: some apps automatically append
/.well-known/openid-configuration
to the end of an OIDC Discovery URL, so you may need to omit that. -
https://idm.example.com/oauth2/openid/:client_id:/.well-known/oauth-authorization-server
-
User auth
-
https://idm.example.com/ui/oauth2
-
API auth
-
https://idm.example.com/oauth2/authorise
Note: "authorise" is spelled the British English (non-OED) way.
-
Token endpoint
-
https://idm.example.com/oauth2/token
-
https://idm.example.com/oauth2/token/introspect
-
https://idm.example.com/oauth2/token/revoke
-
OpenID Connect issuer URI
-
https://idm.example.com/oauth2/openid/:client_id:
-
OpenID Connect user info
-
https://idm.example.com/oauth2/openid/:client_id:/userinfo
-
Token signing public key
-
https://idm.example.com/oauth2/openid/:client_id:/public_key.jwk
Configuration
Create the Kanidm Configuration
By default, members of the system_admins
or idm_hp_oauth2_manage_priv
groups are able to create
or manage OAuth2 client integrations.
You can create a new client by specifying its client name, application display name and the landing page (home page) of the client. The landing page is where users will be redirected to from the Kanidm application portal.
kanidm system oauth2 create <name> <displayname> <landing page url>
kanidm system oauth2 create nextcloud "Nextcloud Production" https://nextcloud.example.com
You must now configure the redirect URL where the application expects OAuth2 requests to be sent.
kanidm system oauth2 add-redirect-url <name> <redirect url>
kanidm system oauth2 add-redirect-url nextcloud https://nextcloud.example.com/oauth2/handler
You can create a scope map with:
kanidm system oauth2 update-scope-map <name> <kanidm_group_name> [scopes]...
kanidm system oauth2 update-scope-map nextcloud nextcloud_users email profile openid
tip
OpenID connect allows a number of scopes that affect the content of the resulting authorisation token. If one of the following scopes are requested by the OpenID client, then the associated claims may be added to the authorisation token. It is not guaranteed that all of the associated claims will be added.
- profile - name, family_name, given_name, middle_name, nickname, preferred_username, profile, picture, website, gender, birthdate, zoneinfo, locale, and updated_at
- email - email, email_verified
- address - address
- phone - phone_number, phone_number_verified
- groups - groups
warning
If you are creating an OpenID Connect (OIDC) client you MUST provide a scope map containing
openid
. Without this, OpenID Connect clients WILL NOT WORK!
kanidm system oauth2 update-scope-map nextcloud nextcloud_users openid
You can create a supplemental scope map with:
kanidm system oauth2 update-sup-scope-map <name> <kanidm_group_name> [scopes]...
kanidm system oauth2 update-sup-scope-map nextcloud nextcloud_admins admin
Once created you can view the details of the client.
kanidm system oauth2 get nextcloud
---
name: nextcloud
class: oauth2_resource_server
class: oauth2_resource_server_basic
class: object
displayname: Nextcloud Production
oauth2_rs_basic_secret: hidden
oauth2_rs_origin_landing: https://nextcloud.example.com
oauth2_rs_token_key: hidden
You can see the value of oauth2_rs_basic_secret
with:
kanidm system oauth2 show-basic-secret nextcloud
---
<secret>
Configure the Client/Resource Server
On your client, you should configure the client ID as the name
from Kanidm, and the password to be
the value shown in oauth2_rs_basic_secret
. Ensure that the code challenge/verification method is
set to S256.
You should now be able to test authorisation to the client.
Scope Relationships
For an authorisation to proceed, the client will request a list of scopes. For example, when a user wishes to login to the admin panel of the resource server, it may request the "admin" scope from Kanidm for authorisation. But when a user wants to login, it may only request "access" as a scope from Kanidm.
As each service may have its own scopes and understanding of these, Kanidm isolates scopes to each service connected to Kanidm. Kanidm has two methods of granting scopes to accounts (users).
The first is scope mappings. These provide a set of scopes if a user is a member of a specific group within Kanidm. This allows you to create a relationship between the scopes of a service, and the groups/roles in Kanidm which can be specific to that service.
For an authorisation to proceed, all scopes requested by the client must be available in the final scope set that is granted to the account.
The second part is supplemental scope mappings. These function the same as scope maps where membership of a group provides a set of scopes to the account. However these scopes are NOT consulted during authorisation decisions made by Kanidm. These scopes exist to allow optional properties to be provided (such as personal information about a subset of accounts to be revealed) or so that the service may make its own authorisation decisions based on the provided scopes.
This use of scopes is the primary means to control who can access what resources. These access decisions can take place either on Kanidm or the service.
For example, if you have a client that always requests a scope of "read", then users with scope maps that supply the read scope will be allowed by Kanidm to proceed to the service. Kanidm can then provide the supplementary scopes into provided tokens, so that the service can use these to choose if it wishes to display UI elements. If a user has a supplemental "admin" scope, then that user may be able to access an administration panel of the service. In this way Kanidm is still providing the authorisation information, but the control is then exercised by the service.
Public Client Configuration
Some applications are unable to provide client authentication. A common example is single page web applications that act as the OAuth2 client and its corresponding webserver is the resource server. In this case, the SPA is unable to act as a confidential client since the basic secret would need to be embedded in every client.
Another common example is native applications that use a redirect to localhost. These can't have a client secret embedded, so must act as public clients.
Public clients for this reason require PKCE to bind a specific browser session to its OAuth2 exchange. PKCE can not be disabled for public clients for this reason.
To create an OAuth2 public client:
kanidm system oauth2 create-public <name> <displayname> <origin>
kanidm system oauth2 create-public mywebapp "My Web App" https://webapp.example.com
To allow localhost redirection
kanidm system oauth2 enable-localhost-redirects <name>
kanidm system oauth2 disable-localhost-redirects <name>
kanidm system oauth2 enable-localhost-redirects mywebapp
Alternate Redirect URLs
warning
Security Risk!
You MUST NOT share a single OAuth2 client definition between multiple applications.
The ability to configure multiple redirect URLs is NOT intended to allow you to share a single Kanidm client definition between multiple OAuth2 clients.
Sharing OAuth2 client configurations between applications FUNDAMENTALLY BREAKS the OAuth2 security model and is NOT SUPPORTED as a configuration. The Kanidm Project WILL NOT support you if you attempt this.
Multiple origins are ONLY to allow supplemental redirects within the same client application.
Some services may have a website URL as well as native applications with opaque origins. These native applications require alternate redirection URLs to be configured so that after an OAuth2 exchange, the system can redirect to the native application.
To support this Kanidm allows supplemental opaque origins to be configured on clients.
kanidm system oauth2 add-redirect-url <name> <url>
kanidm system oauth2 remove-redirect-url <name> <url>
kanidm system oauth2 add-redirect-url nextcloud app://ios-nextcloud
Supplemental URLs are shown in the OAuth2 client configuration in the oauth2_rs_origin
attribute.
Strict Redirect URLs
Kanidm previously enforced that redirection targets only matched by origin, not the full URL. In 1.4.0 these URLs will enforce a full URL match instead.
To indicate your readiness for this transition, all OAuth2 clients must have the field
strict-redirect-url
enabled. Once enabled, the client will begin to enforce the 1.4.0 strict
validation behaviour.
If you have not enabled strict-redirect-url
on all OAuth2 clients the upgrade to 1.4.0 will refuse
to proceed.
To enable or disable strict validation:
kanidm system oauth2 enable-strict-redirect-url <name>
kanidm system oauth2 disable-strict-redirect-url <name>
Extended Options for Legacy Clients
Not all clients support modern standards like PKCE or ECDSA. In these situations it may be necessary to disable these on a per-client basis. Disabling these on one client will not affect others. These settings are explained in detail in our FAQ
warning
Changing these settings MAY have serious consequences on the security of your services. You should avoid changing these if at all possible!
To disable PKCE for a confidential client:
kanidm system oauth2 warning-insecure-client-disable-pkce <client name>
To use the legacy RSA PKCS1-5 SHA256 cryptographic algorithm
(id_token_signing_alg_values_supported
= RS256
):
kanidm system oauth2 warning-enable-legacy-crypto <client name>
In this mode, Kanidm will not offer ES256
support for the client at all.
Resetting Client Security Material
In the case of disclosure of the basic secret or some other security event where you may wish to invalidate a services active sessions/tokens. You can reset the secret material of the server with:
kanidm system oauth2 reset-secrets
Each client has unique signing keys and access secrets, so this is limited to each service.
Custom Claim Maps
Some OAuth2 services may consume custom claims from an id token for access control or other policy decisions. Each custom claim is a key:values set, where there can be many values associated to a claim name. Different applications may expect these values to be formatted (joined) in different ways.
Claim values are mapped based on membership to groups. When an account is a member of multiple groups that would receive the same claim, the values of these maps are merged.
To create or update a claim map on a client:
kanidm system oauth2 update-claim-map <name> <claim_name> <kanidm_group_name> [values]...
kanidm system oauth2 update-claim-map nextcloud account_role nextcloud_admins admin login ...
To change the join strategy for a claim name. Valid strategies are csv (comma separated value), ssv (space separated value) and array (a native json array). The default strategy is array.
kanidm system oauth2 update-claim-map-join <name> <claim_name> [csv|ssv|array]
kanidm system oauth2 update-claim-map-join nextcloud account_role csv
Example claim formats:
# csv
claim: "value_a,value_b"
# ssv
claim: "value_a value_b"
# array
claim: ["value_a", "value_b"]
To delete a group from a claim map
kanidm system oauth2 delete-claim-map <name> <claim_name> <kanidm_group_name>
kanidm system oauth2 delete-claim-map nextcloud account_role nextcloud_admins
Example OAuth2 Configurations
Apache mod_auth_openidc
Add the following to a mod_auth_openidc.conf
. It should be included in a mods_enabled
folder or
with an appropriate include.
# NB: may be just path, reduces copy-paste
OIDCRedirectURI /oauth2/callback
OIDCCryptoPassphrase <random password here>
OIDCProviderMetadataURL https://kanidm.example.com/oauth2/openid/<client name>/.well-known/openid-configuration
OIDCScope "openid"
OIDCUserInfoTokenMethod authz_header
OIDCClientID <client name>
OIDCClientSecret <client password>
OIDCPKCEMethod S256
OIDCCookieSameSite On
# Set the `REMOTE_USER` field to the `preferred_username` instead of the UUID.
# Remember that the username can change, but this can help with systems like Nagios which use this as a display name.
# OIDCRemoteUserClaim preferred_username
Other scopes can be added as required to the OIDCScope
line, eg:
OIDCScope "openid scope2 scope3"
In the virtual host, to handle OIDC redirect, a special location must be defined:
# NB: you must allocate this virtual location matching OIDCRedirectURI and allow it for _any valid user_
<Location /oauth2/callback>
AuthType openid-connect
Require valid-user
</Location>
In the virtual host, to protect a location/directory see wiki:
<Directory /foo>
AuthType openid-connect
# you can authorize by the groups if you requested OIDCScope "openid groups"
# Require claim groups:<spn | uuid>
Require claim groups:apache_access_allowed@example.com
# or authorize by exact preferred_username
# Require user john.doe
</Directory>
GitLab
GitLab is a Git-based software development platform, which supports OpenID Connect on self-managed installations only (ie: not GitLab.com).
To set up a self-managed GitLab instance to authenticate with Kanidm:
-
Add an email address to your regular Kanidm account, if it doesn't have one already:
kanidm person update your_username -m your_username@example.com
-
Create a new Kanidm group for your GitLab users (
gitlab_users
), and add your regular account to it:kanidm group create gitlab_users kanidm group add-members gitlab_users your_username
-
Create a new OAuth2 application configuration in Kanidm (
gitlab
), configure the redirect URL, and scope access to thegitlab_users
group:kanidm system oauth2 create gitlab GitLab https://gitlab.example.com/users/sign_in kanidm system oauth2 add-redirect-url gitlab https://gitlab.example.com/users/auth/openid_connect/callback kanidm system oauth2 update-scope-map gitlab gitlab_users email openid profile groups
-
Get the
gitlab
OAuth2 client secret from Kanidm:kanidm system oauth2 show-basic-secret gitlab
-
Configure GitLab to authenticate to Kanidm with OpenID Connect in
/etc/gitlab/gitlab.rb
:# Allow OpenID Connect for single sign on gitlab_rails['omniauth_allow_single_sign_on'] = ['openid_connect'] # Automatically approve any account from an OmniAuth provider. # # This is insecure if you *don't* control *all* the providers in use. # For example, if you allowed sign in Kanidm *and* with some public identity # provider, it will let anyone with an account sign in to your GitLab # instance. gitlab_rails['omniauth_block_auto_created_users'] = false # Automatically link existing users to Kanidm by email address. # # This is insecure if users are allowed to change their own email address # in Kanidm (disabled by default), or any provider doesn't validate # ownership of email addresses. gitlab_rails['omniauth_auto_link_user'] = ['openid_connect'] # Update the user's profile with info from Kanidm whenever they log in. # GitLab locks these fields when sync is enabled. gitlab_rails['omniauth_sync_profile_from_provider'] = ['openid_connect'] gitlab_rails['omniauth_sync_profile_attributes'] = ['name', 'email'] # Connect to Kanidm gitlab_rails['omniauth_providers'] = [ { name: "openid_connect", label: "Kanidm", icon: "https://kanidm.example.com/pkg/img/logo-192.png", args: { name: "openid_connect", scope: ["openid","profile","email"], response_type: "code", # Point this at your Kanidm host. "gitlab" is the OAuth2 client ID. # Don't include a trailing slash! issuer: "https://kanidm.example.com/oauth2/openid/gitlab", discovery: true, client_auth_method: "query", # Key the GitLab identity by UUID. uid_field: "sub", pkce: true, client_options: { # OAuth2 client ID identifier: "gitlab", secret: "YOUR KANIDM BASIC SECRET HERE", redirect_uri: "https://gitlab.example.com/users/auth/openid_connect/callback" } }, }, ]
[!TIP]
If you're running GitLab in Docker (or other container platform), you can add this configuration to the
GITLAB_OMNIBUS_CONFIG
environment variable. -
Restart GitLab (
gitlab-ctl reconfigure
), and wait for it to come back up again (this may take several minutes).
Once GitLab is up and running, you should now see a "Kanidm" option on your GitLab sign-in page below the normal login form.
Once you've got everything working, you may wish configure GitLab to:
More information about these features is available in GitLab's documentation.
JetBrains Hub and YouTrack
These instructions were tested with the on-prem version of JetBrains YouTrack 2024.3.44799 and its built-in Hub.
JetBrains Hub is an authentication and authorisation system for TeamCity and YouTrack, which also provides a "single pane of glass" view of those applications.
TeamCity is a CI/CD tool, and YouTrack is a project and issue management tool.
The on-prem version of YouTrack comes with a built-in version of Hub, which it uses for all authentication.
JetBrains Hub supports OAuth2, but has some limitations:
-
JetBrains Hub's OAuth2 Auth Module does not support PKCE (as a client), which is a security issue.
-
JetBrains Hub does not automatically update profile attributes after account creation.
However, users can update their own profile manually.
-
JetBrains Hub does not support using an auto-configuration URL, which means you have to set a lot of options manually (which this guide will describe).
To set up YouTrack (with its built-in JetBrains Hub) to authenticate with Kanidm using OAuth2:
-
Add an email address to your regular Kanidm account, if it doesn't have one already:
kanidm person update your_username -m your_username@example.com
-
Create a new Kanidm group for your YouTrack users (
youtrack_users
), and add your regular account to it:kanidm group create youtrack_users kanidm group add-members youtrack_users your_username
-
Create a new OAuth2 application configuration in Kanidm (
youtrack
), disable the PKCE requirement (this is insecure, but YouTrack doesn't support it), and scope access to theyoutrack_users
group:kanidm system oauth2 create youtrack YouTrack https://youtrack.example.com kanidm system oauth2 warning-insecure-client-disable-pkce youtrack kanidm system oauth2 update-scope-map gitlab gitlab_users email openid profile groups
-
(optional) By default, Kanidm presents the account's full SPN (eg:
your_username@kanidm.example.com
) as its "preferred username".You can set
youtrack
to use a short username (eg:your_username
) with:kanidm system oauth2 prefer-short-username youtrack
-
Log in to YouTrack with an account that has full system administrator rights.
-
Open the Auth Modules configuration in YouTrack (⚙️ Administration → Access Management → Auth Modules)
-
Click New module → OAuth2, and enter the following details:
- Name:
Kanidm
- Authorization URL:
https://kanidm.example.com/ui/oauth2
Click Create, and you'll be taken to the Auth Module's settings page.
- Name:
-
Copy the Redirect URI from YouTrack and set it in Kanidm:
kanidm system oauth2 add-redirect-url youtrack https://youtrack.example.com/hub/...
-
Configure the Kanidm Auth Module as follows:
- Button image
-
Upload a Kanidm or other organisational logo.
This will appear on the login form (with no text) to prompt users to sign in.
By default, this is the OAuth2 logo.
- Client ID
-
youtrack
- Client secret
-
Copy the secret from the output of this command:
kanidm system oauth2 show-basic-secret youtrack
- Extension grant
-
Leave blank
- Authorization Service Endpoints
- Authorization URL
-
https://kanidm.example.com/ui/oauth2
- Token endpoint URL
-
https://kanidm.example.com/oauth2/token
- User data endpoint URL
-
https://kanidm.example.com/oauth2/openid/youtrack/userinfo
- Email endpoint URL
-
Leave blank
- Avatar endpoint URL
-
Leave blank
- Field mapping
- User ID
-
sub
- Username
-
preferred_username
- Full name
-
name
-
email
- Additional settings
- Scope
-
openid,profile,email
- User creation
- Enabled
-
Click Save at the bottom of the page.
-
Click Enable module at the top of the page.
-
Click Test login... at the top of the page to try logging in with Kanidm.
You may need to allow pop-ups for YouTrack in your browser for this to work.
YouTrack's log in page should now have show the button image you set for Kanidm below the normal log in form – which you can use to log in with Kanidm.
Miniflux
Miniflux is a feedreader that supports OAuth 2.0 and OpenID connect. It automatically appends the
.well-known
parts to the discovery endpoint. The application name in the redirect URL needs to
match the OAUTH2_PROVIDER
name.
OAUTH2_PROVIDER = "oidc";
OAUTH2_CLIENT_ID = "miniflux";
OAUTH2_CLIENT_SECRET = "<oauth2_rs_basic_secret>";
OAUTH2_REDIRECT_URL = "https://feeds.example.com/oauth2/oidc/callback";
OAUTH2_OIDC_DISCOVERY_ENDPOINT = "https://idm.example.com/oauth2/openid/<name>";
Nextcloud
Install the module from the nextcloud market place - it can also be found in the Apps section of your deployment as "OpenID Connect user backend".
In Nextcloud's config.php you need to allow connection to remote servers and enable PKCE:
'allow_local_remote_servers' => true,
'user_oidc' => [
'use_pkce' => true,
],
You may optionally choose to add:
'allow_user_to_change_display_name' => false,
'lost_password_link' => 'disabled',
If you forget this, you may see the following error in logs:
Host 172.24.11.129 was not connected to because it violates local access rules
In the settings menu, configure the discovery URL and client ID and secret.
You can choose to disable other login methods with:
php occ config:app:set --value=0 user_oidc allow_multiple_user_backends
You can login directly by appending ?direct=1
to your login page. You can re-enable other backends
by setting the value to 1
Outline
These instructions were tested with self-hosted Outline 0.80.2.
Outline is a wiki / knowledge base which can be self-hosted.
Self-hosted Outline supports authentication with OpenID Connect, with some limitations:
-
Outline does not support group or ACL delegation.
On a new Outline installation, the first user who authenticates to Outline will be granted administrative rights.
-
Outline only automatically updates the user's email address on log in.
It will set the user's preferred name on first log in only.
To set up a new self-hosted Outline instance to authenticate with Kanidm:
-
Add an email address to your regular Kanidm account, if it doesn't have one already:
kanidm person update your_username -m your_username@example.com
-
Create a new Kanidm group for your Outline users (
outline_users
), and only add your regular account to it:kanidm group create outline kanidm group add-members outline_users your_username
Warning: don't add any other users when first setting up Outline. The first user who logs in will gain administrative rights.
-
Create a new OAuth2 application configuration in Kanidm (
outline
), disable the PKCE requirement (this is insecure, but Outline doesn't support it), configure the redirect URL, and scope access to theoutline_users
group:kanidm system oauth2 create outline Outline https://outline.example.com kanidm system oauth2 warning-insecure-client-disable-pkce outline kanidm system oauth2 add-redirect-url outline https://outline.example.com/auth/oidc.callback kanidm system oauth2 update-scope-map outline outline_users email openid profile groups
-
Get the
outline
OAuth2 client secret from Kanidm:kanidm system oauth2 show-basic-secret outline
-
Configure Outline to authenticate to Kanidm with OpenID Connect in Outline's environment file (
docker.env
/.env
):OIDC_CLIENT_ID=outline OIDC_CLIENT_SECRET=YOUR KANIDM BASIC SECRET HERE OIDC_AUTH_URI=https://kanidm.example.com/ui/oauth2 OIDC_TOKEN_URI=https://kanidm.example.com/oauth2/token OIDC_USERINFO_URI=https://kanidm.example.com/oauth2/openid/outline/userinfo OIDC_LOGOUT_URI= # Prevent redirect loop on logout OIDC_DISABLE_REDIRECT=true # Outline doesn't seem to actually use this. OIDC_USERNAME_CLAIM=preferred_username OIDC_DISPLAY_NAME=Kanidm OIDC_SCOPES=openid profile email
-
Restart Outline and wait for it to come back up again.
Outline's login form should now show a Continue with Kanidm button, which can be used to sign in.
Migrating between Outline authentication providers
warning
While Outline supports multiple authentication providers, we'd recommend running Outline with a single authentication provider (once you've tested it works correctly).
When migrating from one authentication provider to another, Outline will attempt to match based on email address. This can be vulnerable to account take-over if email addresses are not validated in all providers and Outline is configured with multiple authentication providers.
Each Outline user only has a single credential associated with it (provider +
sub
), even if Outline is configured to use multiple identity providers. This
is set to the last-used credential on login (detailed below).
When using Kanidm, sub
is the user's UUID, and is stable even if their Kanidm
account is renamed or changes email address – but Outline will only update the
email address automatically.
When a user authenticates to Outline, it will attempt to match the credential with an Outline user:
-
Find a matching user by credential (provider +
sub
). -
If there is a matching user, the user is logged in.
-
Find a matching user by email address.
-
If there's no matching Outline user with that email address, Outline will create a new user account (if allowed by Security → Access → Allowed domains), and the user is logged in.
If a user account is not allowed to be created, the login will be rejected.
-
If the matching user's credential is associated with this provider, Outline will (currently) reject the login attempt.
-
At this point, the matching user's credential must be associated with a different provider, and it is treated as a migration.
Outline replaces the matching user's credential with the one currently used, and logs them in.
As long as all email addresses are verified and unique to a single account in each provider, this should allow you to easily and securely migrate from one identity provider to another.
However, if emails are not verified in even a single provider, this could make Outline vulnerable to account take-over.
Outline has no UI for managing or displaying external credentials, so it's difficult to troubleshoot.
ownCloud
These instructions were tested with ownCloud 10.15.10.
To set up an ownCloud instance to authenticate with Kanidm:
-
Install the ownCloud OpenID Connect app (for web auth) and ownCloud OAuth2 app (for desktop and mobile app auth) from the ownCloud Market.
-
Add an email address to your regular Kanidm account, if it doesn't have one already:
kanidm person update your_username -m your_username@example.com
-
Create a new Kanidm group for your ownCloud users (
owncloud_users
), and add your regular account to it:kanidm group create owncloud_users kanidm group add-members owncloud_users your_username
-
Create a new OAuth2 application configuration in Kanidm (
owncloud
), allow use of legacy crypto (ownCloud does not supportES256
), configure the redirect URLs, and scope access to theowncloud_users
group:kanidm system oauth2 create owncloud ownCloud https://owncloud.example.com kanidm system oauth2 warning-enable-legacy-crypto owncloud kanidm system oauth2 add-redirect-url owncloud https://owncloud.example.com/apps/openidconnect/redirect kanidm system oauth2 update-scope-map owncloud owncloud_users email openid profile groups
-
(optional) By default, Kanidm presents the account's full SPN (eg:
your_username@kanidm.example.com
) as its "preferred username". You can setowncloud
to use a short username (eg:your_username
) with:kanidm system oauth2 prefer-short-username owncloud
-
Get the
owncloud
OAuth2 client secret from Kanidm:kanidm system oauth2 show-basic-secret owncloud
-
Create a JSON configuration file (
oidc-config.json
) for ownCloud's OIDC App.To key users by UID (most secure configuration, but not suitable if you have existing ownCloud accounts) – so their UID is their ownCloud username, use this configuration:
{ "provider-url": "https://kanidm.example.com/oauth2/openid/owncloud", "client-id": "owncloud", "client-secret": "YOUR CLIENT SECRET HERE", "loginButtonName": "Kanidm", "mode": "userid", "search-attribute": "sub", "auto-provision": { "enabled": true, "email-claim": "email", "display-name-claim": "name", "update": {"enabled": true} }, "scopes": ["openid", "profile", "email"] }
To key users by email address (vulnerable to account take-over, but allows for migrating existing ownCloud accounts), modify the
mode
andsearch-attribute
settings to use theemail
attribute:{ "mode": "email", "search-attribute": "email" }
-
Deploy the config file you created with
occ
.The exact command varies depending on how you've deployed ownCloud.
occ config:app:set openidconnect openid-connect --value="$(<oidc-config.json)"
ownCloud's login page should now show "Alternative logins" below the normal login form, which you can use to sign in.
warning
Do not configure OIDC Service Discovery rewrite rules
(/.well-known/openid-configuration
) in ownCloud – this breaks the ownCloud
desktop and mobile clients.
The ownCloud desktop and mobile clients use hard coded secrets which cannot be entered into Kanidm, because this is a security risk.
With the ownCloud OAuth2 app installed, the ownCloud clients will instead authenticate to ownCloud Server as an OAuth provider (which has the hard coded secrets installed by default), which then in turn can authenticate to ownCloud locally or to Kanidm with your own client ID/secret.
To use OIDC Service Discovery with the ownCloud clients, you'd need to create OAuth2 client configurations in Kanidm for the ownCloud Android, desktop and iOS apps, and get those secrets added to the clients either by:
- modifying and recompiling the apps yourself from source, or,
- using an iOS MDM configuration (iOS only), or,
- requesting branded apps as part of an ownCloud Enterprise subscription
Setting that up is beyond the scope of this document.
Velociraptor
Velociraptor supports OIDC. To configure it select "Authenticate with SSO" then "OIDC" during the interactive configuration generator. Alternately, you can set the following keys in server.config.yaml:
GUI:
authenticator:
type: OIDC
oidc_issuer: https://idm.example.com/oauth2/openid/:client_id:/
oauth_client_id: <client name/>
oauth_client_secret: <client secret>
Velociraptor does not support PKCE. You will need to run the following:
kanidm system oauth2 warning-insecure-client-disable-pkce <client name>
Initial users are mapped via their email in the Velociraptor server.config.yaml config:
GUI:
initial_users:
- name: <email address>
Accounts require the openid
and email
scopes to be authenticated. It is recommended you limit
these to a group with a scope map due to Velociraptors high impact.
# kanidm group create velociraptor_users
# kanidm group add_members velociraptor_users ...
kanidm system oauth2 create_scope_map <client name> velociraptor_users openid email
Grafana
Grafana is a open source analytics and interactive visualization web application. It provides charts, graphs and alerts when connected to supported data source.
Prepare the environment:
kanidm system oauth2 create grafana "grafana.domain.name" https://grafana.domain.name
kanidm system oauth2 update-scope-map grafana grafana_users email openid profile groups
kanidm system oauth2 enable-pkce grafana
kanidm system oauth2 get grafana
kanidm system oauth2 show-basic-secret grafana
<SECRET>
Create Grafana user groups:
kanidm group create 'grafana_superadmins'
kanidm group create 'grafana_admins'
kanidm group create 'grafana_editors'
kanidm group create 'grafana_users'
Setup the claim-map that will set what role each group will map to in Grafana:
kanidm system oauth2 update-claim-map-join 'grafana' 'grafana_role' array
kanidm system oauth2 update-claim-map 'grafana' 'grafana_role' 'grafana_superadmins' 'GrafanaAdmin'
kanidm system oauth2 update-claim-map 'grafana' 'grafana_role' 'grafana_admins' 'Admin'
kanidm system oauth2 update-claim-map 'grafana' 'grafana_role' 'grafana_editors' 'Editor'
Don't forget that every Grafana user needs be member of one of above group and have name and e-mail:
kanidm person update <user> --legalname "Personal Name" --mail "user@example.com"
kanidm group add-members 'grafana_users' 'my_user_group_or_user_name'
And add the following to your Grafana config:
[auth.generic_oauth]
enabled = true
name = Kanidm
client_id = grafana
client_secret = <SECRET>
scopes = openid,profile,email,groups
auth_url = https://idm.example.com/ui/oauth2
token_url = https://idm.example.com/oauth2/token
api_url = https://idm.example.com/oauth2/openid/grafana/userinfo
use_pkce = true
use_refresh_token = true
allow_sign_up = true
login_attribute_path = preferred_username
groups_attribute_path = groups
role_attribute_path = contains(grafana_role[*], 'GrafanaAdmin') && 'GrafanaAdmin' || contains(grafana_role[*], 'Admin') && 'Admin' || contains(grafana_role[*], 'Editor') && 'Editor' || 'Viewer'
allow_assign_grafana_admin = true
Vouch Proxy
warning
Vouch proxy requires a unique identifier but does not use the proper scope, "sub". It uses the fields "username" or "email" as primary identifiers instead. As a result, this can cause user or deployment issues, at worst security bypasses. You should avoid Vouch Proxy if possible due to these issues.
note
You need to run at least version 0.37.0
Vouch Proxy supports multiple OAuth and OIDC login providers. To configure it you need to pass:
oauth:
auth_url: https://idm.wherekanidmruns.com/ui/oauth2
callback_url: https://login.wherevouchproxyruns.com/auth
client_id: <name> # Found in kanidm system oauth2 get XXXX (should be the same as XXXX)
client_secret: <oauth2_rs_basic_secret> # Found in kanidm system oauth2 get XXXX
code_challenge_method: S256
provider: oidc
scopes:
- email # Required due to vouch proxy reliance on mail as a primary identifier
token_url: https://idm.wherekanidmruns.com/oauth2/token
user_info_url: https://idm.wherekanidmruns.com/oauth2/openid/<name>/userinfo
The email
scope needs to be passed and thus the mail attribute needs to exist on the account:
kanidm person update <ID> --mail "YYYY@somedomain.com" --name idm_admin
How Does OAuth2 Work?
OAuth2 uses a number of terms in ways that can make it unclear and difficult to understand.
A user wishes to access a service (resource, resource server) through an OAuth2 client. The client does not have an active session for the userm so it redirects to the authorisation server (Kanidm) to determine if the user has the appropriate permissions (scopes) for the requested resources, and should be allowed to proceed.
The authorisation server checks the current session of the user and may present a login flow if required. Based on the identity of the user and the requested scopes, the authorisation server makes a decision if it allows the authorisation to proceed. The user is then prompted to consent to the authorisation from the authorisation server to the client as some identity information may be revealed by granting this consent.
If successful and consent is given, the user is redirected back to the client with an authorisation code. The client then contacts the authorisation server directly with this code and exchanges it for a valid OAuth token.
The client may then optionally contact the token introspection endpoint of the authorisation server about the provided OAuth token, which yields extra metadata about the identity that holds the token from the authorisation. This metadata may include identity information, but also may include extended metadata, sometimes referred to as "claims". Claims are information bound to a token based on properties of the session that may allow the client to make extended authorisation decisions without the need to contact the authorisation server to arbitrate.
In many cases the client and the resource server are the same entity. When the client and resource server are separate services, the client can then forward the access token to the resource server for authorisation of the user's request.
It's important to note that OAuth2 at its core is an authorisation system which has layered identity-providing elements on top.
Why Does Sharing a Client Weaken OAuth2?
By sharing a client ID between multiple applications this implies that all of these applications are a singular client in Kanidm's view. This means tokens issued to one application can be reused on any other application.
This limits your ability to enforce scopes, presents a large compromise blast radius if a token is stolen, and also increases the damage radius if the client credentials are stolen.
Generally this also provides a bad user experience since the Kanidm application portal only lists a single landing URL of the client, so subsequent applications that "piggy back" can not be redirected to from the application portal meaning that users will not easily be able to access the application.
PAM and nsswitch
PAM and nsswitch are the core mechanisms used by Linux and BSD clients to resolve identities from an IDM service like Kanidm into accounts that can be used on the machine for various interactive tasks.
The UNIX Daemon
Kanidm provides a UNIX daemon that runs on any client that wants to support PAM and nsswitch. This service has many features which are useful even without Kanidm as a network authentication service.
The Kanidm UNIX Daemon:
- Caches Kanidm users and groups for users with unreliable networks, or for roaming users.
- Securely caches user credentials with optional TPM backed cryptographic operations.
- Automatically creates home directories for users.
- Caches and resolves the content of
/etc/passwd
and/etc/group
improving system performance. - Has a small set of hardened libraries to reduce attack surface.
We recommend you install the client daemon from your system package manager:
# OpenSUSE
zypper in kanidm-unixd-clients
# Fedora
dnf install kanidm-unixd-clients
You can check the daemon is running on your Linux system with:
systemctl status kanidm-unixd
You can check the privileged tasks daemon is running with:
systemctl status kanidm-unixd-tasks
note
The kanidm_unixd_tasks
daemon is not required for PAM and nsswitch functionality. If disabled,
your system will function as usual. It is however strongly recommended due to the features it
provides.
You can also configure unixd with the file /etc/kanidm/unixd:
note
All users in Kanidm can change their name (and their spn) at any time. If you change home_attr
from uuid
you must have a plan on how to manage these directory renames in your system. We
recommend that you have a stable ID (like the UUID), and symlinks from the name to the UUID
folder. Automatic support is provided for this via the unixd tasks daemon, as documented here.
Ubuntu users please see: Why aren't snaps launching with home_alias set?
## Kanidm Unixd Service Configuration - /etc/kanidm/unixd
# The configuration file version.
version = '2'
# Kanidm unix will bind all cached credentials to a local Hardware Security
# Module (HSM) to prevent exfiltration and attacks against these. In addition,
# any internal private keys will also be stored in this HSM.
#
# * soft: A software hsm that encrypts all local key material
# * tpm: Use a tpm for all key storage and binding
# * tpm_if_possible: If a hardware tpm exists it is used, otherwise fall back to the software tpm.
# If the hardware tpm has previously been used, software tpm will not be used.
#
# Default: tpm_if_possible
# hsm_type = "tpm"
# If using `hsm_type = "tpm"`, this allows configuration of the TCTI name of
# the tpm to use. For more, see: https://tpm2-tools.readthedocs.io/en/latest/man/common/tcti/
#
# You should leave this value as the default kernel resource manager.
#
# Default: device:/dev/tpmrm0
# tpm_tcti_name = "device:/dev/tpmrm0"
# Default shell for users if no value is set.
#
# Default: /bin/sh
# default_shell = "/bin/sh"
# The prefix prepended to where home directories are stored. Must end with a trailing `/`.
#
# Default: /home/
# home_prefix = "/home/"
# The attribute to use for the stable home directory path. Valid choices are
# `uuid`, `name`, `spn`.
# > **NOTICE:** All users in Kanidm can change their name (and their spn) at any time. If you change
# > `home_attr` from `uuid` you _must_ have a plan on how to manage these directory renames in your
# > system. We recommend that you have a stable ID (like the UUID), and symlinks from the name to the
# > UUID folder. Automatic support is provided for this via the unixd tasks daemon, as documented
# > here.
# Default: uuid
# home_attr = "uuid"
# Controls the prefix that will be prepended to the home name when using mounted home directories from
# a location that is outside of them `home_prefix`. Must end with a trailing `/`. If unset then home
# directories will be created in `home_prefix`.
#
# This is option is useful when implementing a networked home directory layout. A common implementation
# is to configure a networked filesystem that contains user directories mounted at `/u/` (eg /u/$user_uuid)
# and then symlink the mounted directory into /home/$user_name when the user logs in. This can be accomplished
# with a configuration that includes the following:
#
# > home_attr = "uuid"
# > home_alias = "name"
# > home_prefix = "/home/"
# > home_mount_prefix = "/u/"
#
# ⚠️ If you expect directories to be created by kanidm unixd tasks in an alternate mount prefix, then
# you need to run `systemctl edit kanidm-unixd-tasks` to allow the daemon to write to these locations.
#
# > [Service]
# > ReadWritePaths=/home /var/run/kanidm-unixd /u
#
#
# Default: unset
#
# home_mount_prefix = "/u/"
#
# The default token attribute used for generating symlinks pointing to the user's home
# directory. If set, this will become the value of the home path to nss calls. It is recommended you
# choose a "human friendly" attribute here. Valid choices are `none`, `uuid`, `name`, `spn`. Defaults
# to `spn`.
#
# Default: spn
# home_alias = "spn"
# Controls if home directories should be prepopulated with the contents of `/etc/skel`
# when first created.
#
# Default: false
# use_etc_skel = false
# Chooses which attribute is used for domain local users in presentation of the uid value.
#
# Default: spn
# NOTE: Users from a trust will always use spn.
# uid_attr_map = "spn"
# Chooses which attribute is used for domain local groups in presentation of the gid value.
# Default: spn
# NOTE: Groups from a trust will always use spn.
# gid_attr_map = "spn"
# `selinux` controls whether the `kanidm_unixd_tasks` daemon should detect and enable SELinux runtime
# compatibility features to ensure that newly created home directories are labeled correctly. This
# setting as no bearing on systems without SELinux, as these features will automatically be disabled
# if SELinux is not detected when the daemon starts. Note that `kanidm_unixd_tasks` must also be built
# with the SELinux feature flag for this functionality.
#
# Default: true
# selinux = true
# allows kanidm to "override" the content of a user or group that is defined locally when a name
# collision occurs. By default kanidm will detect when a user/group conflict with their entries from
# `/etc/passwd` or `/etc/group` and will ignore the kanidm entry. However if you want kanidm to
# override users or groups from the local system, you must list them in this field. Note that this can
# have many unexpected consequences, so it is not recommended to enable this.
#
# Default: Empty set (no overrides)
# allow_local_account_override = ["admin"]
# This section enables the Kanidm provider
[kanidm]
# Defines a set of POSIX groups where membership of any of these groups
# will be allowed to login via PAM. All POSIX users and groups can be
# resolved by NSS regardless of PAM login status. You may specify a
# group's name, SPN or UUID
#
# Default: empty set (no access allowed)
pam_allowed_login_groups = ["posix_group"]
# Allow extension (mapping) of a local system groups members with members from a
# kanidm provided group. An example of this is that the local group
# `libvirt` can has it's membership extended with the members from
# `virt-admins`. This section can be repeated many times.
#
# Default: empty set (no group maps)
# [[kanidm.map_group]]
# local = "libvirt"
# with = "virt-admins"
# [[kanidm.map_group]]
# local = "admins"
# with = "system-admins"
If you are using the Kanidm provider features, you also need to configure
/etc/kanidm/config
. This is the covered in client_tools.
You can start, and then check the status of the daemon with the following commands:
systemctl enable --now kanidm-unixd
kanidm-unix status
If the daemon is working, you should see:
working!
If it is not working, you will see an error message:
[2020-02-14T05:58:10Z ERROR kanidm-unix] Error ->
Os { code: 111, kind: ConnectionRefused, message: "Connection refused" }
For more information, see the Troubleshooting section.
nsswitch
When the daemon is running you can add the nsswitch libraries to /etc/nsswitch.conf
passwd: kanidm compat
group: kanidm compat
NOTE: Unlike other nsswitch modules, Kanidm should be before compat or files. This is because Kanidm caches and provides the content from
/etc/passwd
and/etc/group
.
Then create a user and enable POSIX feature on the user.
Test that the POSIX extended user is able to be resolved with:
getent passwd <account name>
getent passwd testunix
testunix:x:3524161420:3524161420:testunix:/home/testunix:/bin/sh
You can also do the same for groups.
getent group <group name>
getent group testgroup
testgroup:x:2439676479:testunix
hint
Remember to also create a UNIX password with something like
kanidm account posix set_password --name idm_admin demo_user
. Otherwise there will be no
credential for the account to authenticate with.
PAM
warning
Modifications to PAM configuration may leave your system in a state where you are unable to login or authenticate. You should always have a recovery shell open while making changes (for example, root), or have access to single-user mode at the machine's console.
Pluggable Authentication Modules (PAM) is the mechanism a UNIX-like system that authenticates users, and to control access to some resources. This is configured through a stack of modules that are executed in order to evaluate the request, and then each module may request or reuse authentication token information.
Before You Start
You should backup your /etc/pam.d directory from its original state as you may change the PAM configuration in a way that will not allow you to authenticate to your machine.
cp -a /etc/pam.d /root/pam.d.backup
Configuration Examples
Documentation examples for the following Linux distributions are available:
- SUSE / OpenSUSE
- Fedora
- Debian / Ubuntu - Installed with the packages from kanidm/kanidm_ppa.
SUSE / OpenSUSE
To configure PAM on SUSE you must modify four files, which control the various stages of authentication:
/etc/pam.d/common-account
/etc/pam.d/common-auth
/etc/pam.d/common-password
/etc/pam.d/common-session
important
By default these files are symlinks to their corresponding -pc
file, for example,
common-account -> common-account-pc
. If you directly edit these you are updating the inner
content of the -pc
file and it WILL be reset on a future upgrade. To prevent this you must first
copy the -pc
files. You can then edit the files safely.
# These steps must be taken as root
rm /etc/pam.d/common-account
rm /etc/pam.d/common-auth
rm /etc/pam.d/common-session
rm /etc/pam.d/common-password
cp /etc/pam.d/common-account-pc /etc/pam.d/common-account
cp /etc/pam.d/common-auth-pc /etc/pam.d/common-auth
cp /etc/pam.d/common-session-pc /etc/pam.d/common-session
cp /etc/pam.d/common-password-pc /etc/pam.d/common-password
NOTE: Unlike other PAM modules, Kanidm replaces the functionality of
pam_unix
and can authenticate local users securely.
The content should look like:
# /etc/pam.d/common-account
# Controls authorisation to this system (who may login)
account sufficient pam_kanidm.so ignore_unknown_user
account required pam_deny.so
# /etc/pam.d/common-auth
# Controls authentication to this system (verification of credentials)
auth required pam_env.so
auth sufficient pam_kanidm.so ignore_unknown_user
auth required pam_deny.so
# /etc/pam.d/common-password
# Controls flow of what happens when a user invokes the passwd command. Currently does NOT
# push password changes back to kanidm
password required pam_unix.so nullok shadow try_first_pass
# /etc/pam.d/common-session
# Controls setup of the user session once a successful authentication and authorisation has
# occurred.
session optional pam_systemd.so
session required pam_limits.so
session optional pam_umask.so
session optional pam_kanidm.so
session optional pam_env.so
warning
Ensure that pam_mkhomedir
or pam_oddjobd
are not present in any stage of your PAM
configuration, as they interfere with the correct operation of the Kanidm tasks daemon.
Fedora / CentOS
warning
Kanidm currently has no support for SELinux policy - this may mean you need to run the daemon with
permissive mode for the unconfined_service_t
daemon type. To do this run:
semanage permissive -a unconfined_service_t
. To undo this run
semanage permissive -d unconfined_service_t
.
You may also need to run audit2allow
for sshd and other types to be able to access the UNIX
daemon sockets.
These files are managed by authselect as symlinks. You can either work with authselect, or remove the symlinks first.
Without authselect
If you just remove the symlinks:
Edit the content.
# /etc/pam.d/password-auth
auth required pam_env.so
auth required pam_faildelay.so delay=2000000
auth sufficient pam_kanidm.so ignore_unknown_user
auth required pam_deny.so
account sufficient pam_usertype.so issystem
account sufficient pam_kanidm.so ignore_unknown_user
account required pam_deny.so
password requisite pam_pwquality.so try_first_pass local_users_only
password sufficient pam_unix.so sha512 shadow nullok try_first_pass use_authtok
password required pam_deny.so
session optional pam_keyinit.so revoke
session required pam_limits.so
-session optional pam_systemd.so
session [success=1 default=ignore] pam_succeed_if.so service in crond quiet use_uid
session required pam_unix.so
session optional pam_kanidm.so
-
# /etc/pam.d/system-auth
auth required pam_env.so
auth required pam_faildelay.so delay=2000000
auth sufficient pam_fprintd.so
auth sufficient pam_kanidm.so ignore_unknown_user
auth required pam_deny.so
account sufficient pam_usertype.so issystem
account sufficient pam_kanidm.so ignore_unknown_user
account required pam_deny.so
password requisite pam_pwquality.so try_first_pass local_users_only
password sufficient pam_unix.so sha512 shadow nullok try_first_pass use_authtok
password required pam_deny.so
session optional pam_keyinit.so revoke
session required pam_limits.so
-session optional pam_systemd.so
session [success=1 default=ignore] pam_succeed_if.so service in crond quiet use_uid
session required pam_unix.so
session optional pam_kanidm.so
With authselect
To work with authselect:
You will need to create a new profile.
First run the following command:
authselect create-profile kanidm -b sssd
A new folder, /etc/authselect/custom/kanidm, should be created. Inside that folder, create or overwrite the following three files: nsswitch.conf, password-auth, system-auth. password-auth and system-auth should be the same as above. nsswitch should be modified for your use case. A working example looks like this:
passwd: kanidm compat systemd
group: kanidm compat systemd
shadow: files
hosts: files dns myhostname
services: files
netgroup: files
automount: files
aliases: files
ethers: files
gshadow: files
networks: files dns
protocols: files
publickey: files
rpc: files
Then run:
authselect select custom/kanidm
to update your profile.
Troubleshooting PAM/nsswitch
Check POSIX-status of Group and Configuration
If authentication is failing via PAM, make sure that a list of groups is configured in
/etc/kanidm/unixd
:
pam_allowed_login_groups = ["example_group"]
Check the status of the group with kanidm group posix show example_group
. If you get something
similar to the following example:
> kanidm group posix show example_group
Using cached token for name idm_admin
Error -> Http(500, Some(InvalidAccountState("Missing class: account && posixaccount OR group && posixgroup")),
"b71f137e-39f3-4368-9e58-21d26671ae24")
POSIX-enable the group with kanidm group posix set example_group
. You should get a result similar
to this when you search for your group name:
> kanidm group posix show example_group
[ spn: example_group@kanidm.example.com, gidnumber: 3443347205 name: example_group, uuid: b71f137e-39f3-4368-9e58-21d26671ae24 ]
Also, ensure the target user is in the group by running:
> kanidm group list_members example_group
Increase Logging
For the unixd daemon, you can increase the logging with:
systemctl edit kanidm-unixd.service
And add the lines:
[Service]
Environment="RUST_LOG=kanidm=debug"
Then restart the kanidm-unixd.service.
The same pattern is true for the kanidm-unixd-tasks.service daemon.
To debug the pam module interactions add debug
to the module arguments such as:
auth sufficient pam_kanidm.so debug
Check the Socket Permissions
Check that the /var/run/kanidm-unixd/sock
has permissions mode 777, and that non-root readers can
see it with ls or other tools.
Ensure that /var/run/kanidm-unixd/task_sock
has permissions mode 700, and that it is owned by the
kanidm unixd process user.
Verify that You Can Access the Kanidm Server
You can check this with the client tools:
kanidm self whoami --name anonymous
Ensure the Libraries are Correct
You should have:
/usr/lib64/libnss_kanidm.so.2
/usr/lib64/security/pam_kanidm.so
The exact path may change depending on your distribution, pam_unixd.so
should be co-located with
pam_kanidm.so. Look for it with the find command:
find /usr/ -name 'pam_unix.so'
For example, on a Debian machine, it's located in /usr/lib/x86_64-linux-gnu/security/
.
Increase Connection Timeout
In some high-latency environments, you may need to increase the connection timeout. We set this low to improve response on LANs, but over the internet this may need to be increased. By increasing the conn_timeout, you will be able to operate on higher latency links, but some operations may take longer to complete causing a degree of latency.
By increasing the cache_timeout, you will need to refresh less often, but it may result in an account lockout or group change until cache_timeout takes effect. Note that this has security implications:
# /etc/kanidm/unixd
# Seconds
conn_timeout = 8
# Cache timeout
cache_timeout = 60
Invalidate or Clear the Cache
You can invalidate the kanidm_unixd cache with:
kanidm-unix cache-invalidate
You can clear (wipe) the cache with:
kanidm-unix cache-clear
There is an important distinction between these two - invalidated cache items may still be yielded to a client request if the communication to the main Kanidm server is not possible. For example, you may have your laptop in a park without wifi.
Clearing the cache, however, completely wipes all local data about all accounts and groups. If you are relying on this cached (but invalid) data, you may lose access to your accounts until other communication issues have been resolved.
Home directories are not created via SSH
Ensure that UsePAM yes
is set in sshd_config
. Without this the pam session module won't be
triggered which prevents the background task being completed.
RADIUS
Remote Authentication Dial In User Service (RADIUS) is a network protocol that is commonly used to authenticate Wi-Fi devices or Virtual Private Networks (VPNs). While it should not be a sole point of trust/authentication to an identity, it's still an important control for protecting network resources.
Kanidm has a philosophy that each account can have multiple credentials which are related to their devices, and limited to specific resources. RADIUS is no exception and has a separate credential for each account to use for RADIUS access.
Disclaimer
It's worth noting some disclaimers about Kanidm's RADIUS integration.
One Credential - One Account
Kanidm normally attempts to have credentials for each device and application rather than the legacy model of one to one.
The RADIUS protocol is only able to attest a single password based credential in an authentication attempt, which limits us to storing a single RADIUS password credential per account. However, despite this limitation, it still greatly improves the situation by isolating the RADIUS credential from the primary or application credentials of the account. This solves many common security concerns around credential loss or disclosure, and prevents rogue devices from locking out accounts as they attempt to authenticate to Wi-Fi with expired credentials.
Alternatelly, Kanidm supports mapping users with special configuration of certificates allowing some systems to use EAP-TLS for RADIUS authentication. This returns to the "per device" credential model.
Cleartext Credential Storage
RADIUS offers many different types of tunnels and authentication mechanisms. However, most client devices "out of the box" only attempt a single type when a WPA2-Enterprise network is selected: MSCHAPv2 with PEAP. This is a challenge-response protocol that requires clear text or Windows NT LAN Manager (NTLM) credentials.
As MSCHAPv2 with PEAP is the only practical, universal RADIUS-type supported on all devices with minimal configuration, we consider it imperative that it MUST be supported as the default. Esoteric RADIUS types can be used as well, but this is up to administrators to test and configure.
Due to this requirement, we must store the RADIUS material as clear text or NTLM hashes. It would be silly to think that NTLM is secure as it relies on the obsolete and deprecated MD4 cryptographic hash, providing only an illusion of security.
This means Kanidm stores RADIUS credentials in the database as clear text.
We believe this is a reasonable decision and is a low risk to security because:
- The access controls around RADIUS secrets by default are strong, limited to only self-account read and RADIUS-server read.
- As RADIUS credentials are separate from the primary account credentials and have no other rights, their disclosure is not going to lead to a full account compromise.
- Having the credentials in clear text allows a better user experience as clients can view the credentials at any time to enroll further devices.
Service Accounts Do Not Have Radius Access
Due to the design of service accounts, they do not have access to radius for credential assignment. If you require RADIUS usage with a service account you may need to use EAP-TLS or some other authentication method.
Account Credential Configuration
For an account to use RADIUS they must first generate a RADIUS secret unique to that account. By default, all accounts can self-create this secret.
kanidm person radius generate-secret --name william william
kanidm person radius show-secret --name william william
Account Group Configuration
In Kanidm, accounts which can authenticate to RADIUS must be a member of an allowed group. This allows you to define which users or groups may use a Wi-Fi or VPN infrastructure, and provides a path for revoking access to the resources through group management. The key point of this is that service accounts should not be part of this group:
kanidm group create --name idm_admin radius_access_allowed
kanidm group add-members --name idm_admin radius_access_allowed william
RADIUS Server Service Account
To read these secrets, the RADIUS server requires an account with the correct privileges. This can be created and assigned through the group "idm_radius_servers", which is provided by default.
First, create the service account and add it to the group:
kanidm service-account create --name admin radius_service_account "Radius Service Account"
kanidm group add-members --name admin idm_radius_servers radius_service_account
Now reset the account password, using the admin
account:
kanidm service-account credential generate --name admin radius_service_account
Deploying a RADIUS Container
We provide a RADIUS container that has all the needed integrations. This container requires some
cryptographic material, with the following files being in /etc/raddb/certs
. (Modifiable in the
configuration)
filename | description |
---|---|
ca.pem | The signing CA of the RADIUS certificate |
dh.pem | The output of openssl dhparam -in ca.pem -out ./dh.pem 2048 |
cert.pem | The certificate for the RADIUS server |
key.pem | The signing key for the RADIUS certificate |
The configuration file (/data/kanidm
) has the following template:
uri = "https://example.com" # URL to the Kanidm server
verify_hostnames = true # verify the hostname of the Kanidm server
verify_ca = false # Strict CA verification
ca = /data/ca.pem # Path to the kanidm ca
auth_token = "ABC..." # Auth token for the service account
# See: kanidm service-account api-token generate
# Default vlans for groups that don't specify one.
radius_default_vlan = 1
# A list of Kanidm groups which must be a member
# before they can authenticate via RADIUS.
radius_required_groups = [
"radius_access_allowed@idm.example.com",
]
# A mapping between Kanidm groups and VLANS
radius_groups = [
{ spn = "radius_access_allowed@idm.example.com", vlan = 10 },
]
# A mapping of clients and their authentication tokens
radius_clients = [
{ name = "test", ipaddr = "127.0.0.1", secret = "testing123" },
{ name = "docker" , ipaddr = "172.17.0.0/16", secret = "testing123" },
]
# radius_cert_path = "/etc/raddb/certs/cert.pem"
# the signing key for radius TLS
# radius_key_path = "/etc/raddb/certs/key.pem"
# the diffie-hellman output
# radius_dh_path = "/etc/raddb/certs/dh.pem"
# the CA certificate
# radius_ca_path = "/etc/raddb/certs/ca.pem"
A fully configured example
url = "https://example.com"
# The auth token for the service account
auth_token = "ABC..."
# default vlan for groups that don't specify one.
radius_default_vlan = 99
# if the user is in one of these Kanidm groups,
# then they're allowed to authenticate
radius_required_groups = [
"radius_access_allowed@idm.example.com",
]
radius_groups = [
{ spn = "radius_access_allowed@idm.example.com", vlan = 10 }
]
radius_clients = [
{ name = "localhost", ipaddr = "127.0.0.1", secret = "testing123" },
{ name = "docker" , ipaddr = "172.17.0.0/16", secret = "testing123" },
]
Moving to Production
To expose this to a Wi-Fi infrastructure, add your NAS in the configuration:
radius_clients = [
{ name = "access_point", ipaddr = "10.2.3.4", secret = "<a_random_value>" }
]
Then re-create/run your docker instance and expose the ports by adding
-p 1812:1812 -p 1812:1812/udp
to the command.
If you have any issues, check the logs from the RADIUS output, as they tend to indicate the cause of the problem. To increase the logging level you can re-run your environment with debug enabled:
docker rm radiusd
docker run --name radiusd \
-e DEBUG=True \
-p 1812:1812 \
-p 1812:1812/udp
--interactive --tty \
--volume /tmp/kanidm:/etc/raddb/certs \
kanidm/radius:latest
Note: the RADIUS container is configured to provide Tunnel-Private-Group-ID, so if you wish to use Wi-Fi-assigned VLANs on your infrastructure, you can assign these by groups in the configuration file as shown in the above examples.
SSSD
SSSD is an alternative PAM and nsswitch provider that is commonly available on Linux.
warning
SSSD should be considered a "last resort". If possible, always use the native Kanidm pam and nsswitch tools instead.
Limitations
SSSD has many significant limitations compared to Kanidm's native PAM and nsswitch provider.
Performance
Kanidm's native provider outperforms SSSD significantly for both online and offline user resolving and operations. Because of this, SSSD can cause higher load on the Kanidm server due to its design limitations.
Features
SSSD is not able to access all of the features of Kanidm, limiting the integration options available to you.
Security
By default Kanidm uses state of the art cryptographic methods with configurable TPM binding of cached local credentials. SSSD uses significantly weaker methods to cache passwords. This means that you should not be caching credentials with SSSD, limiting deployment flexibility.
In addition, Kanidm's providers are written in Rust rather than C, meaning they have less surface area for attack and compromise. These providers have been through multiple security audits performed by the SUSE product security teams.
Support
If you choose to use the SSSD provider the Kanidm project will only provide "best effort" for compatibility and issue resolution.
Configuration
An example configuration for SSSD is provided.
# Example configuration for SSSD to resolve accounts via Kanidm
#
# This should always be a "last resort". If possible you should always use the
# kanidm pam and nsswitch resolver as these will give you a better and more
# reliable setup.
#
# Changing the values of this config is not recommended.
#
# Support for environments using SSSD is "best effort".
# Setup for ssh keys
# Inside /etc/ssh/sshd_config add the lines:
# AuthorizedKeysCommand /usr/bin/sss_ssh_authorizedkeys %u
# AuthorizedKeysCommandUser nobody
# You can test with the command: sss_ssh_authorizedkeys <username>
[sssd]
services = nss, pam, ssh
config_file_version = 2
domains = ldap
[nss]
homedir_substring = /home
[domain/ldap]
# Uncomment this for more verbose logging.
# debug_level=3
id_provider = ldap
auth_provider = ldap
access_provider = ldap
chpass_provider = ldap
ldap_schema = rfc2307bis
ldap_search_base = o=idm
# Your URI must be LDAPS. Kanidm does not support StartTLS.
ldap_uri = ldaps://idm.example.com
# These allow SSSD to resolve user primary groups, which in Kanidm are implied by
# the existence of the user. Ensure you change the search base to your ldap_search_base.
ldap_group_object_class = object
ldap_group_search_base = o=idm?subtree?(|(objectClass=posixAccount)(objectClass=posixGroup))
# To use cacert dir, place *.crt files in this path then run:
# /usr/bin/openssl rehash /etc/openldap/certs
# or (for older versions of openssl)
# /usr/bin/c_rehash /etc/openldap/certs
# ldap_tls_cacertdir = /etc/openldap/certs
# Path to the cacert
# ldap_tls_cacert = /etc/openldap/certs/ca.crt
# Only users who match this filter can login and authorise to this machine. Note
# that users who do NOT match, will still have their uid/gid resolve, but they
# can't login.
#
# Note that because of how Kanidm presents group names, this value SHOULD be an SPN
ldap_access_filter = (memberof=idm_all_accounts@idm.example.com)
# Set the home dir override. Kanidm does not support configuration of homedirs as an
# attribute, and will use the uid number of the account. This is because users can
# change their uid at anytime, so you must have home directories configured in a stable
# way that does not change.
#
# Beware, than SSSD will incorrectly treat this value as a signed integer rather than unsigned
# so some users will have a -uidnumber instead.
override_homedir = /home/%U
# This prevents an issue where SSSD incorrectly attempts to recursively walk all
# entries in Kanidm.
#
# ⚠️ NEVER CHANGE THIS VALUE ⚠️
ignore_group_members = True
# Disable caching of credentials by SSSD. SSSD uses less secure local password storage
# mechanisms, and is a risk for credential disclosure.
#
# ⚠️ NEVER CHANGE THIS VALUE ⚠️
cache_credentials = False
SSH Key Distribution
To support SSH authentication securely to a large set of hosts running SSH, we support distribution of SSH public keys via the Kanidm server. Both persons and service accounts support SSH public keys on their accounts.
Configuring Accounts
To view the current SSH public keys on accounts, you can use:
kanidm person|service-account \
ssh list-publickeys --name <login user> <account to view>
kanidm person|service-account \
ssh list-publickeys --name idm_admin william
All users by default can self-manage their SSH public keys. To upload a key, a command like this is the best way to do so:
kanidm person|service-account \
ssh add-publickey --name william william 'test-key' "`cat ~/.ssh/id_ecdsa.pub`"
To remove (revoke) an SSH public key, delete them by the tag name:
kanidm person|service-account ssh delete-publickey --name william william 'test-key'
Security Notes
As a security feature, Kanidm validates all public keys to ensure they are valid SSH public keys. Uploading a private key or other data will be rejected. For example:
kanidm person|service-account ssh add-publickey --name william william 'test-key' "invalid"
Enter password:
... Some(SchemaViolation(InvalidAttributeSyntax)))' ...
Server Configuration
Public Key Caching Configuration
If you have kanidm_unixd
running, you can use it to locally cache SSH public keys. This means you
can still SSH into your machines, even if your network is down, you move away from Kanidm, or some
other interruption occurs.
The kanidm_ssh_authorizedkeys
command is part of the kanidm-unix-clients
package, so should be
installed on the servers. It communicates to kanidm_unixd
, so you should have a configured
PAM/nsswitch setup as well.
You can test this is configured correctly by running:
kanidm_ssh_authorizedkeys <account name>
If the account has SSH public keys you should see them listed, one per line.
To configure servers to accept these keys, you must change their /etc/ssh/sshd_config to contain the lines:
PubkeyAuthentication yes
UsePAM yes
AuthorizedKeysCommand /usr/sbin/kanidm_ssh_authorizedkeys %u
AuthorizedKeysCommandUser nobody
Restart sshd, and then attempt to authenticate with the keys.
It's highly recommended you keep your client configuration and sshd_configuration in a configuration management tool such as salt or ansible.
note
With a working SSH key setup, you should also consider adding the following sshd_config
options
as hardening.
PermitRootLogin no
PasswordAuthentication no
PermitEmptyPasswords no
GSSAPIAuthentication no
KerberosAuthentication no
Direct Communication Configuration
In this mode, the authorised keys commands will contact Kanidm directly.
note
As Kanidm is being contacted directly there is no SSH public key cache. Any network outage or communication loss may prevent you accessing your systems. You should only use this version if you have a requirement for it.
The kanidm_ssh_authorizedkeys_direct
command is part of the kanidm-clients package, so should be
installed on the servers.
To configure the tool, you should edit /etc/kanidm/config, as documented in clients
You can test this is configured correctly by running:
kanidm_ssh_authorizedkeys_direct -D anonymous <account name>
If the account has SSH public keys you should see them listed, one per line.
To configure servers to accept these keys, you must change their /etc/ssh/sshd_config to contain the lines:
PubkeyAuthentication yes
UsePAM yes
AuthorizedKeysCommand /usr/bin/kanidm_ssh_authorizedkeys_direct -D anonymous %u
AuthorizedKeysCommandUser nobody
Restart sshd, and then attempt to authenticate with the keys.
It's highly recommended you keep your client configuration and sshd_configuration in a configuration management tool such as salt or ansible.
Service Integration Examples
This chapter demonstrates examples of services and their configuration to integrate with Kanidm.
If you wish to contribute more examples, please open a PR in the Kanidm Project Book.
Kubernetes Ingress
Guard your Kubernetes ingress with Kanidm authentication and authorization.
Prerequisites
We recommend you have the following before continuing:
- Kanidm
- Kubernetes v1.23 or above
- Nginx Ingress
- A fully qualified domain name with an A record pointing to your k8s ingress.
- CertManager with a Cluster Issuer installed.
Instructions
-
Create a Kanidm account and group:
- Create a Kanidm account. Please see the section Creating Accounts.
- Give the account a password. Please see the section Resetting Account Credentials.
- Make the account a person. Please see the section People Accounts.
- Create a Kanidm group. Please see the section Creating Accounts.
- Add the account you created to the group you create.
-
Create a Kanidm OAuth2 resource:
- Create the OAuth2 resource for your domain. Please see the section Create the Kanidm Configuration.
- Add a scope mapping from the resource you created to the group you create with the openid, profile, and email scopes. Please see the section Create the Kanidm Configuration.
-
Create a
Cookie Secret
to for the placeholder<COOKIE_SECRET>
in step 4:docker run -ti --rm python:3-alpine python -c 'import secrets,base64; print(base64.b64encode(base64.b64encode(secrets.token_bytes(16))).decode("utf-8"));'
-
Create a file called
k8s.kanidm-nginx-auth-example.yaml
with the block below. Replace every<string>
(drop the<>
) with appropriate values:<FQDN>
: The fully qualified domain name with an A record pointing to your k8s ingress.<KANIDM_FQDN>
: The fully qualified domain name of your Kanidm deployment.<COOKIE_SECRET>
: The output from step 3.<OAUTH2_RS_NAME>
: Please see the output from step 2.1 or get the OAuth2 resource you create from that step.<OAUTH2_RS_BASIC_SECRET>
: Please see the output from step 2.1 or get the OAuth2 resource you create from that step.
This will deploy the following to your cluster:
- modem7/docker-starwars - An example web site.
- OAuth2 Proxy - A OAuth2 proxy is used as an OAuth2 client with NGINX Authentication Based on Subrequest Result.
--- apiVersion: v1 kind: Namespace metadata: name: kanidm-example labels: pod-security.kubernetes.io/enforce: restricted --- apiVersion: apps/v1 kind: Deployment metadata: namespace: kanidm-example name: website labels: app: website spec: revisionHistoryLimit: 1 replicas: 1 selector: matchLabels: app: website template: metadata: labels: app: website spec: containers: - name: website image: modem7/docker-starwars imagePullPolicy: Always ports: - containerPort: 8080 securityContext: allowPrivilegeEscalation: false capabilities: drop: ["ALL"] securityContext: runAsNonRoot: true seccompProfile: type: RuntimeDefault --- apiVersion: v1 kind: Service metadata: namespace: kanidm-example name: website spec: selector: app: website ports: - protocol: TCP port: 8080 targetPort: 8080 --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: annotations: cert-manager.io/cluster-issuer: lets-encrypt-cluster-issuer nginx.ingress.kubernetes.io/auth-url: "https://$host/oauth2/auth" nginx.ingress.kubernetes.io/auth-signin: "https://$host/oauth2/start?rd=$escaped_request_uri" name: website namespace: kanidm-example spec: ingressClassName: nginx tls: - hosts: - <FQDN> secretName: <FQDN>-ingress-tls # replace . with - in the hostname rules: - host: <FQDN> http: paths: - path: / pathType: Prefix backend: service: name: website port: number: 8080 --- apiVersion: apps/v1 kind: Deployment metadata: labels: k8s-app: oauth2-proxy name: oauth2-proxy namespace: kanidm-example spec: replicas: 1 selector: matchLabels: k8s-app: oauth2-proxy template: metadata: labels: k8s-app: oauth2-proxy spec: containers: - args: - --provider=oidc - --email-domain=* - --upstream=file:///dev/null - --http-address=0.0.0.0:4182 - --oidc-issuer-url=https://<KANIDM_FQDN>/oauth2/openid/<OAUTH2_RS_NAME> - --code-challenge-method=S256 env: - name: OAUTH2_PROXY_CLIENT_ID value: <OAUTH2_RS_NAME> - name: OAUTH2_PROXY_CLIENT_SECRET value: <OAUTH2_RS_BASIC_SECRET> - name: OAUTH2_PROXY_COOKIE_SECRET value: <COOKIE_SECRET> image: quay.io/oauth2-proxy/oauth2-proxy:latest imagePullPolicy: Always name: oauth2-proxy ports: - containerPort: 4182 protocol: TCP securityContext: allowPrivilegeEscalation: false capabilities: drop: ["ALL"] securityContext: runAsNonRoot: true seccompProfile: type: RuntimeDefault --- apiVersion: v1 kind: Service metadata: labels: k8s-app: oauth2-proxy name: oauth2-proxy namespace: kanidm-example spec: ports: - name: http port: 4182 protocol: TCP targetPort: 4182 selector: k8s-app: oauth2-proxy --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: oauth2-proxy namespace: kanidm-example spec: ingressClassName: nginx rules: - host: <FQDN> http: paths: - path: /oauth2 pathType: Prefix backend: service: name: oauth2-proxy port: number: 4182 tls: - hosts: - <FQDN> secretName: <FQDN>-ingress-tls # replace . with - in the hostname
-
Apply the configuration by running the following command:
kubectl apply -f k8s.kanidm-nginx-auth-example.yaml
-
Check your deployment succeeded by running the following commands:
kubectl -n kanidm-example get all kubectl -n kanidm-example get ingress kubectl -n kanidm-example get Certificate
You may use kubectl's describe and log for troubleshooting. If there are ingress errors see the Ingress NGINX documentation's troubleshooting page. If there are certificate errors see the CertManger documentation's troubleshooting page.
Once it has finished deploying, you will be able to access it at
https://<FQDN>
which will prompt you for authentication.
Cleaning Up
-
Remove the resources create for this example from k8s:
kubectl delete namespace kanidm-example
-
Remove the objects created for this example from Kanidm:
- Delete the account created in section Instructions step 1.
- Delete the group created in section Instructions step 2.
- Delete the OAuth2 resource created in section Instructions step 3.
References
Example OAuth2 Configurations
Apache mod_auth_openidc
Add the following to a mod_auth_openidc.conf
. It should be included in a mods_enabled
folder or
with an appropriate include.
# NB: may be just path, reduces copy-paste
OIDCRedirectURI /oauth2/callback
OIDCCryptoPassphrase <random password here>
OIDCProviderMetadataURL https://kanidm.example.com/oauth2/openid/<client name>/.well-known/openid-configuration
OIDCScope "openid"
OIDCUserInfoTokenMethod authz_header
OIDCClientID <client name>
OIDCClientSecret <client password>
OIDCPKCEMethod S256
OIDCCookieSameSite On
# Set the `REMOTE_USER` field to the `preferred_username` instead of the UUID.
# Remember that the username can change, but this can help with systems like Nagios which use this as a display name.
# OIDCRemoteUserClaim preferred_username
Other scopes can be added as required to the OIDCScope
line, eg:
OIDCScope "openid scope2 scope3"
In the virtual host, to handle OIDC redirect, a special location must be defined:
# NB: you must allocate this virtual location matching OIDCRedirectURI and allow it for _any valid user_
<Location /oauth2/callback>
AuthType openid-connect
Require valid-user
</Location>
In the virtual host, to protect a location/directory see wiki:
<Directory /foo>
AuthType openid-connect
# you can authorize by the groups if you requested OIDCScope "openid groups"
# Require claim groups:<spn | uuid>
Require claim groups:apache_access_allowed@example.com
# or authorize by exact preferred_username
# Require user john.doe
</Directory>
GitLab
GitLab is a Git-based software development platform, which supports OpenID Connect on self-managed installations only (ie: not GitLab.com).
To set up a self-managed GitLab instance to authenticate with Kanidm:
-
Add an email address to your regular Kanidm account, if it doesn't have one already:
kanidm person update your_username -m your_username@example.com
-
Create a new Kanidm group for your GitLab users (
gitlab_users
), and add your regular account to it:kanidm group create gitlab_users kanidm group add-members gitlab_users your_username
-
Create a new OAuth2 application configuration in Kanidm (
gitlab
), configure the redirect URL, and scope access to thegitlab_users
group:kanidm system oauth2 create gitlab GitLab https://gitlab.example.com/users/sign_in kanidm system oauth2 add-redirect-url gitlab https://gitlab.example.com/users/auth/openid_connect/callback kanidm system oauth2 update-scope-map gitlab gitlab_users email openid profile groups
-
Get the
gitlab
OAuth2 client secret from Kanidm:kanidm system oauth2 show-basic-secret gitlab
-
Configure GitLab to authenticate to Kanidm with OpenID Connect in
/etc/gitlab/gitlab.rb
:# Allow OpenID Connect for single sign on gitlab_rails['omniauth_allow_single_sign_on'] = ['openid_connect'] # Automatically approve any account from an OmniAuth provider. # # This is insecure if you *don't* control *all* the providers in use. # For example, if you allowed sign in Kanidm *and* with some public identity # provider, it will let anyone with an account sign in to your GitLab # instance. gitlab_rails['omniauth_block_auto_created_users'] = false # Automatically link existing users to Kanidm by email address. # # This is insecure if users are allowed to change their own email address # in Kanidm (disabled by default), or any provider doesn't validate # ownership of email addresses. gitlab_rails['omniauth_auto_link_user'] = ['openid_connect'] # Update the user's profile with info from Kanidm whenever they log in. # GitLab locks these fields when sync is enabled. gitlab_rails['omniauth_sync_profile_from_provider'] = ['openid_connect'] gitlab_rails['omniauth_sync_profile_attributes'] = ['name', 'email'] # Connect to Kanidm gitlab_rails['omniauth_providers'] = [ { name: "openid_connect", label: "Kanidm", icon: "https://kanidm.example.com/pkg/img/logo-192.png", args: { name: "openid_connect", scope: ["openid","profile","email"], response_type: "code", # Point this at your Kanidm host. "gitlab" is the OAuth2 client ID. # Don't include a trailing slash! issuer: "https://kanidm.example.com/oauth2/openid/gitlab", discovery: true, client_auth_method: "query", # Key the GitLab identity by UUID. uid_field: "sub", pkce: true, client_options: { # OAuth2 client ID identifier: "gitlab", secret: "YOUR KANIDM BASIC SECRET HERE", redirect_uri: "https://gitlab.example.com/users/auth/openid_connect/callback" } }, }, ]
[!TIP]
If you're running GitLab in Docker (or other container platform), you can add this configuration to the
GITLAB_OMNIBUS_CONFIG
environment variable. -
Restart GitLab (
gitlab-ctl reconfigure
), and wait for it to come back up again (this may take several minutes).
Once GitLab is up and running, you should now see a "Kanidm" option on your GitLab sign-in page below the normal login form.
Once you've got everything working, you may wish configure GitLab to:
More information about these features is available in GitLab's documentation.
JetBrains Hub and YouTrack
These instructions were tested with the on-prem version of JetBrains YouTrack 2024.3.44799 and its built-in Hub.
JetBrains Hub is an authentication and authorisation system for TeamCity and YouTrack, which also provides a "single pane of glass" view of those applications.
TeamCity is a CI/CD tool, and YouTrack is a project and issue management tool.
The on-prem version of YouTrack comes with a built-in version of Hub, which it uses for all authentication.
JetBrains Hub supports OAuth2, but has some limitations:
-
JetBrains Hub's OAuth2 Auth Module does not support PKCE (as a client), which is a security issue.
-
JetBrains Hub does not automatically update profile attributes after account creation.
However, users can update their own profile manually.
-
JetBrains Hub does not support using an auto-configuration URL, which means you have to set a lot of options manually (which this guide will describe).
To set up YouTrack (with its built-in JetBrains Hub) to authenticate with Kanidm using OAuth2:
-
Add an email address to your regular Kanidm account, if it doesn't have one already:
kanidm person update your_username -m your_username@example.com
-
Create a new Kanidm group for your YouTrack users (
youtrack_users
), and add your regular account to it:kanidm group create youtrack_users kanidm group add-members youtrack_users your_username
-
Create a new OAuth2 application configuration in Kanidm (
youtrack
), disable the PKCE requirement (this is insecure, but YouTrack doesn't support it), and scope access to theyoutrack_users
group:kanidm system oauth2 create youtrack YouTrack https://youtrack.example.com kanidm system oauth2 warning-insecure-client-disable-pkce youtrack kanidm system oauth2 update-scope-map gitlab gitlab_users email openid profile groups
-
(optional) By default, Kanidm presents the account's full SPN (eg:
your_username@kanidm.example.com
) as its "preferred username".You can set
youtrack
to use a short username (eg:your_username
) with:kanidm system oauth2 prefer-short-username youtrack
-
Log in to YouTrack with an account that has full system administrator rights.
-
Open the Auth Modules configuration in YouTrack (⚙️ Administration → Access Management → Auth Modules)
-
Click New module → OAuth2, and enter the following details:
- Name:
Kanidm
- Authorization URL:
https://kanidm.example.com/ui/oauth2
Click Create, and you'll be taken to the Auth Module's settings page.
- Name:
-
Copy the Redirect URI from YouTrack and set it in Kanidm:
kanidm system oauth2 add-redirect-url youtrack https://youtrack.example.com/hub/...
-
Configure the Kanidm Auth Module as follows:
- Button image
-
Upload a Kanidm or other organisational logo.
This will appear on the login form (with no text) to prompt users to sign in.
By default, this is the OAuth2 logo.
- Client ID
-
youtrack
- Client secret
-
Copy the secret from the output of this command:
kanidm system oauth2 show-basic-secret youtrack
- Extension grant
-
Leave blank
- Authorization Service Endpoints
- Authorization URL
-
https://kanidm.example.com/ui/oauth2
- Token endpoint URL
-
https://kanidm.example.com/oauth2/token
- User data endpoint URL
-
https://kanidm.example.com/oauth2/openid/youtrack/userinfo
- Email endpoint URL
-
Leave blank
- Avatar endpoint URL
-
Leave blank
- Field mapping
- User ID
-
sub
- Username
-
preferred_username
- Full name
-
name
-
email
- Additional settings
- Scope
-
openid,profile,email
- User creation
- Enabled
-
Click Save at the bottom of the page.
-
Click Enable module at the top of the page.
-
Click Test login... at the top of the page to try logging in with Kanidm.
You may need to allow pop-ups for YouTrack in your browser for this to work.
YouTrack's log in page should now have show the button image you set for Kanidm below the normal log in form – which you can use to log in with Kanidm.
Miniflux
Miniflux is a feedreader that supports OAuth 2.0 and OpenID connect. It automatically appends the
.well-known
parts to the discovery endpoint. The application name in the redirect URL needs to
match the OAUTH2_PROVIDER
name.
OAUTH2_PROVIDER = "oidc";
OAUTH2_CLIENT_ID = "miniflux";
OAUTH2_CLIENT_SECRET = "<oauth2_rs_basic_secret>";
OAUTH2_REDIRECT_URL = "https://feeds.example.com/oauth2/oidc/callback";
OAUTH2_OIDC_DISCOVERY_ENDPOINT = "https://idm.example.com/oauth2/openid/<name>";
Nextcloud
Install the module from the nextcloud market place - it can also be found in the Apps section of your deployment as "OpenID Connect user backend".
In Nextcloud's config.php you need to allow connection to remote servers and enable PKCE:
'allow_local_remote_servers' => true,
'user_oidc' => [
'use_pkce' => true,
],
You may optionally choose to add:
'allow_user_to_change_display_name' => false,
'lost_password_link' => 'disabled',
If you forget this, you may see the following error in logs:
Host 172.24.11.129 was not connected to because it violates local access rules
In the settings menu, configure the discovery URL and client ID and secret.
You can choose to disable other login methods with:
php occ config:app:set --value=0 user_oidc allow_multiple_user_backends
You can login directly by appending ?direct=1
to your login page. You can re-enable other backends
by setting the value to 1
Outline
These instructions were tested with self-hosted Outline 0.80.2.
Outline is a wiki / knowledge base which can be self-hosted.
Self-hosted Outline supports authentication with OpenID Connect, with some limitations:
-
Outline does not support group or ACL delegation.
On a new Outline installation, the first user who authenticates to Outline will be granted administrative rights.
-
Outline only automatically updates the user's email address on log in.
It will set the user's preferred name on first log in only.
To set up a new self-hosted Outline instance to authenticate with Kanidm:
-
Add an email address to your regular Kanidm account, if it doesn't have one already:
kanidm person update your_username -m your_username@example.com
-
Create a new Kanidm group for your Outline users (
outline_users
), and only add your regular account to it:kanidm group create outline kanidm group add-members outline_users your_username
Warning: don't add any other users when first setting up Outline. The first user who logs in will gain administrative rights.
-
Create a new OAuth2 application configuration in Kanidm (
outline
), disable the PKCE requirement (this is insecure, but Outline doesn't support it), configure the redirect URL, and scope access to theoutline_users
group:kanidm system oauth2 create outline Outline https://outline.example.com kanidm system oauth2 warning-insecure-client-disable-pkce outline kanidm system oauth2 add-redirect-url outline https://outline.example.com/auth/oidc.callback kanidm system oauth2 update-scope-map outline outline_users email openid profile groups
-
Get the
outline
OAuth2 client secret from Kanidm:kanidm system oauth2 show-basic-secret outline
-
Configure Outline to authenticate to Kanidm with OpenID Connect in Outline's environment file (
docker.env
/.env
):OIDC_CLIENT_ID=outline OIDC_CLIENT_SECRET=YOUR KANIDM BASIC SECRET HERE OIDC_AUTH_URI=https://kanidm.example.com/ui/oauth2 OIDC_TOKEN_URI=https://kanidm.example.com/oauth2/token OIDC_USERINFO_URI=https://kanidm.example.com/oauth2/openid/outline/userinfo OIDC_LOGOUT_URI= # Prevent redirect loop on logout OIDC_DISABLE_REDIRECT=true # Outline doesn't seem to actually use this. OIDC_USERNAME_CLAIM=preferred_username OIDC_DISPLAY_NAME=Kanidm OIDC_SCOPES=openid profile email
-
Restart Outline and wait for it to come back up again.
Outline's login form should now show a Continue with Kanidm button, which can be used to sign in.
Migrating between Outline authentication providers
warning
While Outline supports multiple authentication providers, we'd recommend running Outline with a single authentication provider (once you've tested it works correctly).
When migrating from one authentication provider to another, Outline will attempt to match based on email address. This can be vulnerable to account take-over if email addresses are not validated in all providers and Outline is configured with multiple authentication providers.
Each Outline user only has a single credential associated with it (provider +
sub
), even if Outline is configured to use multiple identity providers. This
is set to the last-used credential on login (detailed below).
When using Kanidm, sub
is the user's UUID, and is stable even if their Kanidm
account is renamed or changes email address – but Outline will only update the
email address automatically.
When a user authenticates to Outline, it will attempt to match the credential with an Outline user:
-
Find a matching user by credential (provider +
sub
). -
If there is a matching user, the user is logged in.
-
Find a matching user by email address.
-
If there's no matching Outline user with that email address, Outline will create a new user account (if allowed by Security → Access → Allowed domains), and the user is logged in.
If a user account is not allowed to be created, the login will be rejected.
-
If the matching user's credential is associated with this provider, Outline will (currently) reject the login attempt.
-
At this point, the matching user's credential must be associated with a different provider, and it is treated as a migration.
Outline replaces the matching user's credential with the one currently used, and logs them in.
As long as all email addresses are verified and unique to a single account in each provider, this should allow you to easily and securely migrate from one identity provider to another.
However, if emails are not verified in even a single provider, this could make Outline vulnerable to account take-over.
Outline has no UI for managing or displaying external credentials, so it's difficult to troubleshoot.
ownCloud
These instructions were tested with ownCloud 10.15.10.
To set up an ownCloud instance to authenticate with Kanidm:
-
Install the ownCloud OpenID Connect app (for web auth) and ownCloud OAuth2 app (for desktop and mobile app auth) from the ownCloud Market.
-
Add an email address to your regular Kanidm account, if it doesn't have one already:
kanidm person update your_username -m your_username@example.com
-
Create a new Kanidm group for your ownCloud users (
owncloud_users
), and add your regular account to it:kanidm group create owncloud_users kanidm group add-members owncloud_users your_username
-
Create a new OAuth2 application configuration in Kanidm (
owncloud
), allow use of legacy crypto (ownCloud does not supportES256
), configure the redirect URLs, and scope access to theowncloud_users
group:kanidm system oauth2 create owncloud ownCloud https://owncloud.example.com kanidm system oauth2 warning-enable-legacy-crypto owncloud kanidm system oauth2 add-redirect-url owncloud https://owncloud.example.com/apps/openidconnect/redirect kanidm system oauth2 update-scope-map owncloud owncloud_users email openid profile groups
-
(optional) By default, Kanidm presents the account's full SPN (eg:
your_username@kanidm.example.com
) as its "preferred username". You can setowncloud
to use a short username (eg:your_username
) with:kanidm system oauth2 prefer-short-username owncloud
-
Get the
owncloud
OAuth2 client secret from Kanidm:kanidm system oauth2 show-basic-secret owncloud
-
Create a JSON configuration file (
oidc-config.json
) for ownCloud's OIDC App.To key users by UID (most secure configuration, but not suitable if you have existing ownCloud accounts) – so their UID is their ownCloud username, use this configuration:
{ "provider-url": "https://kanidm.example.com/oauth2/openid/owncloud", "client-id": "owncloud", "client-secret": "YOUR CLIENT SECRET HERE", "loginButtonName": "Kanidm", "mode": "userid", "search-attribute": "sub", "auto-provision": { "enabled": true, "email-claim": "email", "display-name-claim": "name", "update": {"enabled": true} }, "scopes": ["openid", "profile", "email"] }
To key users by email address (vulnerable to account take-over, but allows for migrating existing ownCloud accounts), modify the
mode
andsearch-attribute
settings to use theemail
attribute:{ "mode": "email", "search-attribute": "email" }
-
Deploy the config file you created with
occ
.The exact command varies depending on how you've deployed ownCloud.
occ config:app:set openidconnect openid-connect --value="$(<oidc-config.json)"
ownCloud's login page should now show "Alternative logins" below the normal login form, which you can use to sign in.
warning
Do not configure OIDC Service Discovery rewrite rules
(/.well-known/openid-configuration
) in ownCloud – this breaks the ownCloud
desktop and mobile clients.
The ownCloud desktop and mobile clients use hard coded secrets which cannot be entered into Kanidm, because this is a security risk.
With the ownCloud OAuth2 app installed, the ownCloud clients will instead authenticate to ownCloud Server as an OAuth provider (which has the hard coded secrets installed by default), which then in turn can authenticate to ownCloud locally or to Kanidm with your own client ID/secret.
To use OIDC Service Discovery with the ownCloud clients, you'd need to create OAuth2 client configurations in Kanidm for the ownCloud Android, desktop and iOS apps, and get those secrets added to the clients either by:
- modifying and recompiling the apps yourself from source, or,
- using an iOS MDM configuration (iOS only), or,
- requesting branded apps as part of an ownCloud Enterprise subscription
Setting that up is beyond the scope of this document.
Velociraptor
Velociraptor supports OIDC. To configure it select "Authenticate with SSO" then "OIDC" during the interactive configuration generator. Alternately, you can set the following keys in server.config.yaml:
GUI:
authenticator:
type: OIDC
oidc_issuer: https://idm.example.com/oauth2/openid/:client_id:/
oauth_client_id: <client name/>
oauth_client_secret: <client secret>
Velociraptor does not support PKCE. You will need to run the following:
kanidm system oauth2 warning-insecure-client-disable-pkce <client name>
Initial users are mapped via their email in the Velociraptor server.config.yaml config:
GUI:
initial_users:
- name: <email address>
Accounts require the openid
and email
scopes to be authenticated. It is recommended you limit
these to a group with a scope map due to Velociraptors high impact.
# kanidm group create velociraptor_users
# kanidm group add_members velociraptor_users ...
kanidm system oauth2 create_scope_map <client name> velociraptor_users openid email
Grafana
Grafana is a open source analytics and interactive visualization web application. It provides charts, graphs and alerts when connected to supported data source.
Prepare the environment:
kanidm system oauth2 create grafana "grafana.domain.name" https://grafana.domain.name
kanidm system oauth2 update-scope-map grafana grafana_users email openid profile groups
kanidm system oauth2 enable-pkce grafana
kanidm system oauth2 get grafana
kanidm system oauth2 show-basic-secret grafana
<SECRET>
Create Grafana user groups:
kanidm group create 'grafana_superadmins'
kanidm group create 'grafana_admins'
kanidm group create 'grafana_editors'
kanidm group create 'grafana_users'
Setup the claim-map that will set what role each group will map to in Grafana:
kanidm system oauth2 update-claim-map-join 'grafana' 'grafana_role' array
kanidm system oauth2 update-claim-map 'grafana' 'grafana_role' 'grafana_superadmins' 'GrafanaAdmin'
kanidm system oauth2 update-claim-map 'grafana' 'grafana_role' 'grafana_admins' 'Admin'
kanidm system oauth2 update-claim-map 'grafana' 'grafana_role' 'grafana_editors' 'Editor'
Don't forget that every Grafana user needs be member of one of above group and have name and e-mail:
kanidm person update <user> --legalname "Personal Name" --mail "user@example.com"
kanidm group add-members 'grafana_users' 'my_user_group_or_user_name'
And add the following to your Grafana config:
[auth.generic_oauth]
enabled = true
name = Kanidm
client_id = grafana
client_secret = <SECRET>
scopes = openid,profile,email,groups
auth_url = https://idm.example.com/ui/oauth2
token_url = https://idm.example.com/oauth2/token
api_url = https://idm.example.com/oauth2/openid/grafana/userinfo
use_pkce = true
use_refresh_token = true
allow_sign_up = true
login_attribute_path = preferred_username
groups_attribute_path = groups
role_attribute_path = contains(grafana_role[*], 'GrafanaAdmin') && 'GrafanaAdmin' || contains(grafana_role[*], 'Admin') && 'Admin' || contains(grafana_role[*], 'Editor') && 'Editor' || 'Viewer'
allow_assign_grafana_admin = true
Vouch Proxy
warning
Vouch proxy requires a unique identifier but does not use the proper scope, "sub". It uses the fields "username" or "email" as primary identifiers instead. As a result, this can cause user or deployment issues, at worst security bypasses. You should avoid Vouch Proxy if possible due to these issues.
note
You need to run at least version 0.37.0
Vouch Proxy supports multiple OAuth and OIDC login providers. To configure it you need to pass:
oauth:
auth_url: https://idm.wherekanidmruns.com/ui/oauth2
callback_url: https://login.wherevouchproxyruns.com/auth
client_id: <name> # Found in kanidm system oauth2 get XXXX (should be the same as XXXX)
client_secret: <oauth2_rs_basic_secret> # Found in kanidm system oauth2 get XXXX
code_challenge_method: S256
provider: oidc
scopes:
- email # Required due to vouch proxy reliance on mail as a primary identifier
token_url: https://idm.wherekanidmruns.com/oauth2/token
user_info_url: https://idm.wherekanidmruns.com/oauth2/openid/<name>/userinfo
The email
scope needs to be passed and thus the mail attribute needs to exist on the account:
kanidm person update <ID> --mail "YYYY@somedomain.com" --name idm_admin
Traefik
Traefik is a flexible HTTP reverse proxy webserver that can be integrated with Docker to allow dynamic configuration and to automatically use LetsEncrypt to provide valid TLS certificates. We can leverage this in the setup of Kanidm by specifying the configuration of Kanidm and Traefik in the same Docker Compose configuration.
Example setup
Create a new directory and copy the following YAML file into it as docker-compose.yml
. Edit the
YAML to update the LetsEncrypt account email for your domain and the FQDN where Kanidm will be made
available. Ensure you adjust this file or Kanidm's configuration to have a matching HTTPS port; the
line traefik.http.services.kanidm.loadbalancer.server.port=8443
sets this on the Traefik side.
note
You will need to generate self-signed certificates for Kanidm, and copy the configuration into the
kanidm_data
volume. Some instructions are available in the "Installing the Server" section of
this book.
docker-compose.yml
version: "3.4"
services:
traefik:
image: traefik:v2.6
container_name: traefik
command:
- "--certificatesresolvers.http.acme.email=admin@example.com"
- "--certificatesresolvers.http.acme.storage=/letsencrypt/acme.json"
- "--certificatesresolvers.http.acme.tlschallenge=true"
- "--entrypoints.websecure.address=:443"
- "--entrypoints.websecure.http.tls=true"
- "--entrypoints.websecure.http.tls.certResolver=http"
- "--log.level=INFO"
- "--providers.docker=true"
- "--providers.docker.exposedByDefault=false"
- "--serverstransport.insecureskipverify=true"
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
ports:
- "443:443"
kanidm:
container_name: kanidm
image: kanidm/server:devel
restart: unless-stopped
volumes:
- kanidm_data:/data
labels:
- traefik.enable=true
- traefik.http.routers.kanidm.entrypoints=websecure
- traefik.http.routers.kanidm.rule=Host(`idm.example.com`)
- traefik.http.routers.kanidm.service=kanidm
- traefik.http.serversTransports.kanidm.insecureSkipVerify=true
- traefik.http.services.kanidm.loadbalancer.server.port=8443
- traefik.http.services.kanidm.loadbalancer.server.scheme=https
volumes:
kanidm_data: {}
Finally you may run docker-compose up
to start up both Kanidm and Traefik.
Replication
Introduction
Replication allows two or more Kanidm servers to exchange their databases and keep their content synchronised. This is critical to allow multiple servers to act in failover groups for highly available infrastructure.
Kanidm replication is eventually consistent. This means that there are no elections or quorums required between nodes - all nodes can accept writes and distribute them to all other nodes. This is important for security and performance.
Because replication is eventually consistent, this means that there can be small delays between different servers receiving a change. This may result in some users noticing discrepancies that are quickly resolved.
To minimise this, it's recommended that when you operate replication in a highly available deployment that you have a load balancer that uses sticky sessions so that users are redirected to the same server unless a failover event occurs. This will help to minimise discrepancies. Alternately you can treat replication and "active-passive" and have your load balancer failover between the two nodes. Since replication is eventually consistent, there is no need for a failover or failback procedure.
In this chapter we will cover the details of planning, deploying and maintaining replication between Kanidm servers.
Vocabulary
Replication requires us to use introduce specific words so that we can describe the replication environment.
Change
An update made in the database.
Node
A server that is participating in replication.
Pull
The act of requesting data from a remote server.
Push
The act of supplying data to a remote server.
Node Configuration
A descriptor that allows a node to pull from another node.
Converge
To approach the same database state.
Topology
The collection of servers that are joined in replication and converge on the same database content. The topology is defined by the set of node configurations.
Replication
The act of exchanging data from one node to another.
Supplier
The node that is supplying data to another node.
Consumer
The node that is replicating content from a supplier.
Refresh
Deleting all of a consumer's database content, and replacing it with the content of a supplier.
Incremental Replication
When a supplier provides a "differential" between the state of the consumer and the supplier for the consumer to apply.
Conflict
If a consumer can not validate a change that a supplier provided, then the entry may move to a conflict state. All nodes will converge to the same conflict state over time.
Tombstone
A marker entry that displays an entry has been deleted. This allow all servers to converge and delete the data.
Planning
warning
Replication is a newly developed feature. This means it requires manual configuration and careful monitoring. You should keep backups if you choose to proceed.
It is important that you plan your replication deployment before you proceed. You may have a need for high availability within a datacentre, geographic redundancy, or improvement of read scaling.
Improvement of Read Throughput
Addition of replicas can improve the amount of read and authentication operations performed over the topology as a whole. This is because read operations throughput is additive between nodes.
For example, if you had two servers that can process 1000 authentications per second each, then when in replication the topology can process 2000 authentications per second.
However, while you may gain in read throughput, you must account for downtime - you should not always rely on every server to be online.
The optimal loading of any server is approximately 50%. This allows overhead to absorb load if nearby nodes experience outages. It also allows for absorption of load spikes or other unexpected events.
It is important to note however that as you add replicas the write throughput does not increase in the same way as read throughput. This is because for each write that occurs on a node, it must be replicated and written to every other node. Therefore your write throughput is always bounded by the slowest server in your topology. In reality there is a "slight" improvement in writes due to coalescing that occurs as part of replication, but you should assume that writes are not improved through the addition of more nodes.
Directing Clients to Live Servers
Operating replicas of Kanidm allows you to minimise outages if a single or multiple servers experience downtime. This can assist you with patching and other administrative tasks that you must perform.
However, there are some key limitations to this fault tolerance.
You require a method to fail over between servers. This generally involves a load balancer, which
itself must be fault tolerant. Load balancers can be made fault tolerant through the use of
protocols like CARP
or VRRP
, or by configuration of routers with anycast.
If you elect to use CARP
or VRRP
directly on your Kanidm servers, then be aware that you will be
configuring your systems as active-passive, rather than active-active, so you will not benefit from
improved read throughput. Contrast, anycast will always route to the closest Kanidm server and will
failover to nearby servers so this may be an attractive choice.
You should NOT use DNS based failover mechanisms as clients can cache DNS records and remain "stuck" to a node in a failed state.
Maximum Downtime of a Server
Kanidm's replication protocol enforces limits on how long a server can be offline. This is due to how tombstones are handled. By default the maximum is 7 days. If a server is offline for more than 7 days a refresh will be required for that server to continue participation in the topology.
It is important you avoid extended downtime of servers to avoid this condition.
Deployment
warning
Replication is a newly developed feature. This means it requires manual configuration and careful monitoring. You should take regular backups if you choose to proceed.
Node Setup
On the servers that you wish to participate in the replication topology, you must enable replication in their server.toml to allow identity certificates to be generated.
# server.toml
# To proceed with replication, replace the line "ACK_HERE" with
# "i acknowledge that replication is in development = true" where the spaces
# are replaced with '_'
ACK_HERE
[replication]
# The hostname and port of the server that other nodes will connect to.
origin = "repl://localhost:8444"
# The bind address of the replication port.
bindaddress = "127.0.0.1:8444"
Once configured, deploy this config to your servers and restart the nodes.
Manual Node Configurations
note
In the future we will develop a replication coordinator so that you don't have to manually configure this. But for now, if you want replication, you have to do it the hard way.
Each node has an identify certificate that is internally generated and used to communicate with other nodes in the topology. This certificate is also used by other nodes to validate this node.
Let's assume we have two servers - A and B. We want B to consume (pull) data from A initially as A is our "first server".
First display the identity certificate of A.
# Server A
docker exec -i -t <container name> \
kanidmd show-replication-certificate
# certificate: "MII....."
Now on node B, configure the replication node config.
[replication]
# ...
[replication."repl://origin_of_A:port"]
type = "mutual-pull"
partner_cert = "MII... <as output from A show-replication-cert>"
Now we must configure A to pull from B.
# Server B
docker exec -i -t <container name> \
kanidmd show-replication-certificate
# certificate: "MII....."
Now on node A, configure the replication node config.
[replication]
# ...
[replication."repl://origin_of_B:port"]
type = "mutual-pull"
partner_cert = "MII... <as output from B show-replication-cert>"
Then restart both servers. Initially the servers will refuse to synchronise as their databases do
not have matching domain_uuids
. To resolve this you can instruct B to manually refresh from A
with:
# Server B
docker exec -i -t <container name> \
kanidmd refresh-replication-consumer
Partially Automated Node Configurations
note
In the future we will develop a replication coordinator so that you don't have to manually configure this. But for now, if you want replication, you have to do it the hard way.
This is the same as the manual process, but a single server is defined as the "primary" and the partner server is the "secondary". This means that if database issues occur the content of the primary will take precedence over the secondary. For our example we will define A as the primary and B as the secondary.
First display the identity certificate
# Server A
docker exec -i -t <container name> \
kanidmd show-replication-certificate
# certificate: "MII....."
Now a secondary, configure the replication node config.
[replication]
# ...
[replication."repl://origin_of_A:port"]
type = "mutual-pull"
partner_cert = "MII... <as output from A show-replication-cert>"
automatic_refresh = true
Now we must configure A to pull from B.
# Server B
docker exec -i -t <container name> \
kanidmd show-replication-certificate
# certificate: "MII....."
Now on node A, configure the replication node config. It is critical here that you do NOT set
automatic_refresh
.
[replication]
# ...
[replication."repl://origin_of_B:port"]
type = "mutual-pull"
partner_cert = "MII... <as output from B show-replication-cert>"
# automatic_refresh = false
Then restart both servers. B (secondary) will automatically refresh from A (primary) and then replication will continue bi-directionally from that point.
Administration
Renew Replication Identity Certificate
The replication identity certificate defaults to an expiry of 180 days.
To renew this run the command:
docker exec -i -t <container name> \
kanidmd renew-replication-certificate
# certificate: "MII....."
You must then copy the new certificate to other nodes in the topology.
note
In the future we will develop a replication coordinator so that you don't have to manually renew this. But for now, if you want replication, you have to do it the hard way.
Refresh a Lagging Consumer
If a consumer has been offline for more than 7 days, its error log will display that it requires a refresh.
You can manually perform this on the affected node.
docker exec -i -t <container name> \
kanidmd refresh-replication-consumer
Synchronisation Concepts
Introduction
In some environments Kanidm may be the first Identity Management system introduced. However many existing environments have existing IDM systems that are well established and in use. To allow Kanidm to work with these, it is possible to synchronise data between these IDM systems.
Currently Kanidm can consume (import) data from another IDM system. There are two major use cases for this:
- Running Kanidm in parallel with another IDM system
- Migrating from an existing IDM to Kanidm
An incoming IDM data source is bound to Kanidm by a sync account. All synchronised entries will have
a reference to the sync account that they came from defined by their sync_parent_uuid
. While an
entry is owned by a sync account we refer to the sync account as having authority over the content
of that entry.
The sync process is driven by a sync tool. This tool extracts the current state of the sync from Kanidm, requests the set of changes (differences) from the IDM source, and then submits these changes to Kanidm. Kanidm will update and apply these changes and commit the new sync state on success.
In the event of a conflict or data import error, Kanidm will halt and rollback the synchronisation to the last good state. The sync tool should be reconfigured to exclude the conflicting entry or to remap it's properties to resolve the conflict. The operation can then be retried.
This process can continue long term to allow Kanidm to operate in parallel to another IDM system. If this is for a migration however, the sync account can be finalised. This terminates the sync account and removes the sync parent uuid from all synchronised entries, moving authority of the entry into Kanidm.
Alternatelly, the sync account can be terminated which removes all synchronised content that was submitted.
Creating a Sync Account
Creating a sync account requires administration permissions. By default this is available to members
of the system_admins
group which admin
is a memberof by default.
kanidm system sync create <sync account name>
kanidm system sync create ipasync
Once the sync account is created you can then generate the sync token which identifies the sync tool.
kanidm system sync generate-token <sync account name> <token label>
kanidm system sync generate-token ipasync mylabel
token: eyJhbGci...
warning
The sync account token has a high level of privilege, able to create new accounts and groups. It should be treated carefully as a result!
If you need to revoke the token, you can do so with:
kanidm system sync destroy-token <sync account name>
kanidm system sync destroy-token ipasync
Destroying the token does NOT affect the state of the sync account and it's synchronised entries. Creating a new token and providing that to the sync tool will continue the sync process.
Operating the Sync Tool
The sync tool can now be run to replicate entries from the external IDM system into Kanidm.
You should refer to the chapter for the specific external IDM system you are using for details on the sync tool configuration.
The sync tool runs in batches, meaning that changes from the source IDM service will be delayed to appear into Kanidm. This is affected by how frequently you choose to run the sync tool.
If the sync tool fails, you can investigate details in the Kanidmd server output.
The sync tool can run "indefinitely" if you wish for Kanidm to always import data from the external source.
Yielding Authority of Attributes to Kanidm
By default Kanidm assumes that authority over synchronised entries is retained by the sync tool. This means that synchronised entries can not be written to in any capacity outside of a small number of internal Kanidm internal attributes.
An administrator may wish to allow synchronised entries to have some attributes written by the instance locally. An example is allowing passkeys to be created on Kanidm when the external synchronisation provider does not supply them.
In this case, the synchronisation agreement can be configured to yield its authority over these attributes to Kanidm.
To configure the attributes that Kanidm can control:
kanidm system sync set-yield-attributes <sync account name> [attr, ...]
kanidm system sync set-yield-attributes ipasync passkeys
This commands takes the set of attributes that should be yielded. To remove an attribute you declare the yield set with that attribute missing.
kanidm system sync set-yield-attributes ipasync passkeys
# To remove passkeys from being Kanidm controlled.
kanidm system sync set-yield-attributes ipasync
Finalising the Sync Account
If you are performing a migration from an external IDM to Kanidm, when that migration is completed you can nominate that Kanidm now owns all of the imported data. This is achieved by finalising the sync account.
warning
You can not undo this operation. Once you have finalised an agreement, Kanidm owns all of the synchronised data, and you can not resume synchronisation.
kanidm system sync finalise <sync account name>
kanidm system sync finalise ipasync
# Do you want to continue? This operation can NOT be undone. [y/N]
Once finalised, imported accounts can now be fully managed by Kanidm.
Terminating the Sync Account
If you decide to cease importing accounts or need to remove all imported accounts from a sync account, you can choose to terminate the agreement removing all data that was imported.
warning
You can not undo this operation. Once you have terminated an agreement, Kanidm deletes all of the synchronised data, and you can not resume synchronisation.
kanidm system sync terminate <sync account name>
kanidm system sync terminate ipasync
# Do you want to continue? This operation can NOT be undone. [y/N]
Once terminated all imported data will be deleted by Kanidm.
FreeIPA
FreeIPA is a popular opensource LDAP and Kerberos provider, aiming to be "Active Directory" for Linux.
Kanidm is able to synchronise from FreeIPA for the purposes of coexistence or migration.
Installing the FreeIPA Sync Tool
See installing the client tools. The ipa sync tool is part of the tools container.
Configure the FreeIPA Sync Tool
The sync tool is a bridge between FreeIPA and Kanidm, meaning that the tool must be configured to communicate to both sides.
Like other components of Kanidm, the FreeIPA sync tool will read your /etc/kanidm/config if present to understand how to connect to Kanidm.
The sync tool specific components are configured in its own configuration file.
# The sync account token as generated by "system sync generate-token".
sync_token = "eyJhb..."
# A cron-like expression of when to run when in scheduled mode. The format is:
# sec min hour day of month month day of week year
#
# The default of this value is "0 */5 * * * * *" which means "run every 5 minutes".
# schedule = ""
# If you want to monitor the status of the scheduled sync tool (you should)
# then you can set a bind address here.
#
# If not set, defaults to no status listener.
# status_bind = ""
# The LDAP URI to FreeIPA. This MUST be LDAPS. You should connect to a unique single
# server in the IPA topology rather than via a load balancer or dns srv records. This
# is to prevent replication conflicts and issues due to how 389-ds content sync works.
ipa_uri = "ldaps://specific-server.ipa.dev.kanidm.com"
# Path to the IPA CA certificate in PEM format.
ipa_ca = "/path/to/kanidm-ipa-ca.pem"
# The DN of an account with content sync rights. By default cn=Directory Manager has
# this access.
ipa_sync_dn = "cn=Directory Manager"
ipa_sync_pw = "directory manager password"
# The basedn to examine.
ipa_sync_base_dn = "dc=ipa,dc=dev,dc=kanidm,dc=com"
# By default Kanidm separates the primary account password and credentials from
# the unix credential. This allows the unix password to be isolated from the
# account password so that compromise of one doesn't compromise the other. However
# this can be surprising for new users during a migration. This boolean allows the
# user password to be set as the unix password during the migration for consistency
# and then after the migration they are "unlinked".
#
# sync_password_as_unix_password = false
# The sync tool can alter or exclude entries. These are mapped by their syncuuid
# (not their ipa-object-uuid). The syncuuid is derived from nsUniqueId in 389-ds.
# This is chosen oven DN because DN's can change with modrdn where nsUniqueId is
# immutable and requires an entry to be deleted and recreated.
[ac60034b-3498-11ed-a50d-919b4b1a5ec0]
# my-problematic-entry
exclude = true
# Remap the uuid of this entry to a new uuid on Kanidm
#
# map_uuid = <uuid>
# Remap the name of this entry to a new name on Kanidm
#
# map_name = <name>
# Remap the gidnumber for groups, and uidnumber for users
#
# map_gidnumber = <number>
This example is located in examples/kanidm-ipa-sync.
In addition to this, you must make some configuration changes to FreeIPA to enable synchronisation.
You can find the name of your 389 Directory Server instance with:
# Run on the FreeIPA server
dsconf --list
Using this you can show the current status of the retro changelog plugin to see if you need to change it's configuration.
# Run on the FreeIPA server
dsconf <instance name> plugin retro-changelog show
dsconf slapd-DEV-KANIDM-COM plugin retro-changelog show
You must modify the retro changelog plugin to include the full scope of the database suffix so that the sync tool can view the changes to the database. Currently dsconf can not modify the include-suffix so you must do this manually.
You need to change the nsslapd-include-suffix
to match your FreeIPA baseDN here. You can access
the basedn with:
ldapsearch -H ldaps://<IPA SERVER HOSTNAME/IP> -x -b '' -s base namingContexts
# namingContexts: dc=ipa,dc=dev,dc=kanidm,dc=com
You should ignore cn=changelog
and o=ipaca
as these are system internal namingContexts. You can
then create an ldapmodify like the following.
#![allow(unused)] fn main() { dn: cn=Retro Changelog Plugin,cn=plugins,cn=config changetype: modify replace: nsslapd-include-suffix nsslapd-include-suffix: dc=ipa,dc=dev,dc=kanidm,dc=com }
And apply it with:
ldapmodify -f change.ldif -H ldaps://<IPA SERVER HOSTNAME/IP> -x -D 'cn=Directory Manager' -W
# Enter LDAP Password:
You must then reboot your FreeIPA server.
Running the Sync Tool Manually
You can perform a dry run with the sync tool manually to check your configurations are correct and that the tool can synchronise from FreeIPA.
kanidm-ipa-sync [-c /path/to/kanidm/config] -i /path/to/kanidm-ipa-sync -n
kanidm-ipa-sync -i /etc/kanidm/ipa-sync -n
Running the Sync Tool Automatically
The sync tool can be run on a schedule if you configure the schedule
parameter, and provide the
option "--schedule" on the cli
kanidm-ipa-sync [-c /path/to/kanidm/config] -i /path/to/kanidm-ipa-sync --schedule
kanidm-ipa-sync -i /etc/kanidm/ipa-sync --schedule
As the sync tool is part of the tools container, you can run this with:
docker create --name kanidm-ipa-sync \
--user uid:gid \
-p 12345:12345 \
-v /etc/kanidm/config:/etc/kanidm/config:ro \
-v /path/to/ipa-sync:/etc/kanidm/ipa-sync:ro \
kanidm-ipa-sync -i /etc/kanidm/ipa-sync --schedule
Monitoring the Sync Tool
When running in schedule mode, you may wish to monitor the sync tool for failures. Since failures block the sync process, this is important to ensuring a smooth and reliable synchronisation process.
You can configure a status listener that can be monitored via tcp with the parameter status_bind
.
An example of monitoring this with netcat is:
# status_bind = "[::1]:12345"
# nc ::1 12345
Ok
It's important to note no details are revealed via the status socket, and is purely for Ok or Err status of the last sync. This status socket is suitable for monitoring from tools such as Nagios.
LDAP
If you have an LDAP server that supports sync repl (RFC4533 content synchronisation) then you are able to synchronise from it to Kanidm for the purposes of coexistence or migration.
If there is a specific Kanidm sync tool for your LDAP server, you should use that instead of the generic LDAP server sync.
Installing the LDAP Sync Tool
See installing the client tools.
Configure the LDAP Sync Tool
The sync tool is a bridge between LDAP and Kanidm, meaning that the tool must be configured to communicate to both sides.
Like other components of Kanidm, the LDAP sync tool will read your /etc/kanidm/config if present to understand how to connect to Kanidm.
The sync tool specific components are configured in its own configuration file.
# The sync account token as generated by "system sync generate-token".
sync_token = "eyJhb..."
# A cron-like expression of when to run when in scheduled mode. The format is:
# sec min hour day of month month day of week year
#
# The default of this value is "0 */5 * * * * *" which means "run every 5 minutes".
# schedule = ""
# If you want to monitor the status of the scheduled sync tool (you should)
# then you can set a bind address here.
#
# If not set, defaults to no status listener.
# status_bind = ""
# The LDAP URI to the server. This MUST be LDAPS. You should connect to a unique single
# server in the LDAP topology rather than via a load balancer or dns srv records. This
# is to prevent replication conflicts and issues due to how 389-ds and openldap sync works.
ldap_uri = "ldaps://specific-server.ldap.kanidm.com"
# Path to the LDAP CA certificate in PEM format.
ldap_ca = "/path/to/kanidm-ldap-ca.pem"
# The DN of an account with content sync rights. On 389-ds, by default cn=Directory Manager has
# this access. On OpenLDAP you must grant this access.
ldap_sync_dn = "cn=Directory Manager"
ldap_sync_pw = "directory manager password"
# The basedn to search
ldap_sync_base_dn = "dc=ldap,dc=dev,dc=kanidm,dc=com"
# Filter the entries that are synchronised with this filter
# NOTE: attribute-value-assertions with spaces require quoting!
ldap_filter = "(|(objectclass=person)(objectclass=posixgroup))"
# ldap_filter = "(cn=\"my value\")"
# By default Kanidm separates the primary account password and credentials from
# the unix credential. This allows the unix password to be isolated from the
# account password so that compromise of one doesn't compromise the other. However
# this can be surprising for new users during a migration. This boolean allows the
# user password to be set as the unix password during the migration for consistency
# and then after the migration they are "unlinked".
#
# sync_password_as_unix_password = false
# The objectclass used to identify persons to import to Kanidm.
#
# If not set, defaults to "person"
# person_objectclass = ""
# Attribute mappings. These allow you to bind values from your directory server
# to the values that Kanidm will import.
#
# person_attr_user_name = "uid"
# person_attr_display_name = "cn"
# person_attr_gidnumber = = "uidnumber"
# person_attr_login_shell = "loginshell"
# person_attr_password = "userpassword"
# If the password value requires a prefix for Kanidm to import it, this can be optionally
# provided here.
#
# person_password_prefix = ""
# The objectclass used to identify groups to import to Kanidm.
#
# If not set, defaults to "groupofnames"
# group_objectclass = ""
# Attribute mappings. These allow you to bind values from your directory server
# to the values that Kanidm will import.
#
# group_attr_name = "cn"
# group_attr_description = "description"
# group_attr_member = "member"
# group_attr_gidnumber = "gidnumber"
# The sync tool can alter or exclude entries. These are mapped by their syncuuid
# The syncuuid is derived from nsUniqueId in 389-ds. It is the entryUUID for OpenLDAP
# This is chosen oven DN because DN's can change with modrdn where nsUniqueId/entryUUID is
# immutable and requires an entry to be deleted and recreated.
[ac60034b-3498-11ed-a50d-919b4b1a5ec0]
# my-problematic-entry
exclude = true
# Remap the uuid of this entry to a new uuid on Kanidm
#
# map_uuid = <uuid>
# Remap the name of this entry to a new name on Kanidm
#
# map_name = <name>
# Remap the gidnumber for groups, and uidnumber for users
#
# map_gidnumber = <number>
This example is located in examples/kanidm-ldap-sync.
In addition to this, you may be required to make some configuration changes to your LDAP server to enable synchronisation.
OpenLDAP
You must enable the syncprov overlay in slapd.conf
moduleload syncprov.la
overlay syncprov
In addition you must grant an account full read access and raise its search limits.
access to *
by dn.base="cn=sync,dc=example,dc=com" read
by * break
limits dn.exact="cn=sync,dc=example,dc=com" time.soft=unlimited time.hard=unlimited size.soft=unlimited size.hard=unlimited
For more details see the openldap administration guide.
389 Directory Server
You can find the name of your 389 Directory Server instance with:
dsconf --list
Using this you can show the current status of the retro changelog plugin to see if you need to change its configuration.
dsconf <instance name> plugin retro-changelog show
dsconf slapd-DEV-KANIDM-COM plugin retro-changelog show
You must modify the retro changelog plugin to include the full scope of the database suffix so that the sync tool can view the changes to the database. Currently dsconf can not modify the include-suffix so you must do this manually.
You need to change the nsslapd-include-suffix
to match your LDAP baseDN here. You can access the
basedn with:
ldapsearch -H ldaps://<SERVER HOSTNAME/IP> -x -b '' -s base namingContexts
# namingContexts: dc=ldap,dc=dev,dc=kanidm,dc=com
You should ignore cn=changelog
as this is a system internal namingContext. You can then create an
ldapmodify like the following.
#![allow(unused)] fn main() { dn: cn=Retro Changelog Plugin,cn=plugins,cn=config changetype: modify replace: nsslapd-include-suffix nsslapd-include-suffix: dc=ipa,dc=dev,dc=kanidm,dc=com }
And apply it with:
ldapmodify -f change.ldif -H ldaps://<SERVER HOSTNAME/IP> -x -D 'cn=Directory Manager' -W
# Enter LDAP Password:
You must then reboot your 389 Directory Server.
Running the Sync Tool Manually
You can perform a dry run with the sync tool manually to check your configurations are correct and that the tool can synchronise from LDAP.
kanidm-ldap-sync [-c /path/to/kanidm/config] -i /path/to/kanidm-ldap-sync -n
kanidm-ldap-sync -i /etc/kanidm/ldap-sync -n
Running the Sync Tool Automatically
The sync tool can be run on a schedule if you configure the schedule
parameter, and provide the
option "--schedule" on the cli
kanidm-ldap-sync [-c /path/to/kanidm/config] -i /path/to/kanidm-ldap-sync --schedule
kanidm-ldap-sync -i /etc/kanidm/ldap-sync --schedule
As the sync tool is part of the tools container, you can run this with:
docker create --name kanidm-ldap-sync \
--user uid:gid \
-p 12345:12345 \
-v /etc/kanidm/config:/etc/kanidm/config:ro \
-v /path/to/ldap-sync:/etc/kanidm/ldap-sync:ro \
kanidm-ldap-sync -i /etc/kanidm/ldap-sync --schedule
Monitoring the Sync Tool
When running in schedule mode, you may wish to monitor the sync tool for failures. Since failures block the sync process, this is important for a smooth and reliable synchronisation process.
You can configure a status listener that can be monitored via tcp with the parameter status_bind
.
An example of monitoring this with netcat is:
# status_bind = "[::1]:12345"
# nc ::1 12345
Ok
It's important to note no details are revealed via the status socket, and is purely for Ok or Err status of the last sync. This status socket is suitable for monitoring from tools such as Nagios.
Troubleshooting
Some things to try.
Is the server started?
If you don't see "ready to rock! 🪨" in your logs, it's not started. Scroll back and look for errors!
Can you connect?
If the server's running on idm.example.com:8443
then a simple connectivity test is done using
curl.
Run the following command:
curl https://idm.example.com:8443/status
This is similar to what you should see:
➜ curl -vk https://idm.example.com:8443/status
* Trying 10.0.0.14:8443...
* Connected to idm.example.com (10.0.0.14) port 8443 (#0)
* successfully set certificate verify locations:
* CAfile: /etc/ssl/cert.pem
* CApath: none
* (304) (OUT), TLS handshake, Client hello (1):
* (304) (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-ECDSA-AES256-GCM-SHA384
* Server certificate:
* subject: C=AU; ST=Queensland; L=Brisbane; O=INSECURE EXAMPLE; OU=kanidm; CN=idm.example.com
* start date: Sep 20 09:28:18 2022 GMT
* expire date: Oct 21 09:28:18 2022 GMT
* SSL certificate verify result: self signed certificate in certificate chain (19), continuing anyway.
> GET /status HTTP/1.1
> Host: idm.example.com:8443
> User-Agent: curl/7.79.1
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< cache-control: no-store, max-age=0
< content-length: 4
< content-type: application/json
< date: Tue, 20 Sep 2022 11:52:23 GMT
< pragma: no-cache
< set-cookie: kanidm-session=+LQJKwL0UdAEMoTc0Zrgne2hU+N2nB+Lcf+J1OoI9n4%3DNE7xuL9yCq7B0Ai+IM3gq5T+YZ0ckDuDoWZKzhPMHmSk3oFSscp9vy9n2a5bBFjWKgeNwdLzRbYc4rvMqYi11A%3D%3D; HttpOnly; SameSite=Strict; Secure; Path=/; Expires=Wed, 21 Sep 2022 11:52:23 GMT
< x-content-type-options: nosniff
< x-kanidm-opid: 8b25f050-7f6e-4ce1-befe-90be3c4f8a98
<
* Connection #0 to host localhost left intact
true
This means:
- you've successfully connected to a host (10.0.0.14),
- TLS worked
- Received the status response "true"
If you see something like this:
➜ curl -v https://idm.example.com:8443
* Trying 10.0.0.1:8443...
* connect to 10.0.0.1 port 8443 failed: Connection refused
* Failed to connect to idm.example.com port 8443 after 5 ms: Connection refused
* Closing connection 0
curl: (7) Failed to connect to idm.example.com port 8443 after 5 ms: Connection refused
Then either your DNS is wrong (it's pointing at 10.0.0.1) or you can't connect to the server for some reason.
If you get errors about certificates, try adding -k
to skip certificate verification checking and
just test connectivity:
curl -vk https://idm.example.com:8443/status
Server things to check
- Has the config file got
bindaddress = "127.0.0.1:8443"
? Change it tobindaddress = "[::]:8443"
, so it listens on all interfaces. - Is there a firewall on the server?
- If you're running in docker, did you expose the port (
-p 8443:8443
) or configure the network to host/macvlan/ipvlan?
Client errors
When you receive a client error it will list an "Operation ID" sometimes also called the OpId or KOpId. This UUID matches to the UUID's in the logs allowing you to precisely locate the server logs related to the failing operation.
Try running commands with RUST_LOG=debug
to get more information:
RUST_LOG=debug kanidm login --name anonymous
Reverse Proxies not sending HTTP/1.1 requests
NGINX (and probably other proxies) send HTTP/1.0 requests to the upstream server by default. This'll lead to errors like this in your proxy logs:
*17 upstream prematurely closed connection while reading response header from upstream, client: 172.19.0.1, server: example.com, request: "GET / HTTP/1.1", upstream: "https://172.19.0.3:8443/", host: "example.com:8443"
The fix for NGINX is to set the
proxy_http_version
to 1.1
. This can go in the same block as the proxy_pass
option.
proxy_http_version 1.1
OpenTelemetry errors
If you see something like this:
OpenTelemetry trace error occurred. Exporter otlp encountered the following error(s): the grpc server returns error (The system is not in a state required for the operation's execution): , detailed error message: TRACE_TOO_LARGE: max size of trace (5000000) exceeded while adding 86725 bytes to trace a657b63f6ca0415eb70b6734f20f82cf for tenant single-tenant
Then you'l need to tweak the maximum trace size in your OTLP receiver. In Grafana Tempo you can add
the following keys to your tempo.yaml
, in this example we're setting it to 20MiB:
overrides:
defaults:
global:
max_bytes_per_trace: 20971520 # 20MiB
Frequently Asked Questions
... or ones we think people might ask.
Why TLS?
You may have noticed that Kanidm requires you to configure TLS in your container or server install.
One of the fundamental goals of the project is a secure-by-design rather than secure-by-configuration system, so TLS for all connections is mandatory. It is not an optional feature you add later.
note
Please respect the maintainers decision on TLS-by-default, no discussions on this topic will be entered into.
Why not allow HTTP (without TLS) between my load balancer and Kanidm?
Because Kanidm is one of the keys to a secure network, and insecure connections to them are not best practice. This can allow account hijacking, privilege escalation, credential disclosures, personal information leaks and more.
We believe that the entire path between a client and the server must be protected at all times. This includes the path between load balancers or proxies and Kanidm.
Can Kanidm authentication work without TLS?
No, it can not. TLS is required due to our use of the Secure
flag our cookies, which requires a
client to transmit them back to the origin site
if and only if the client
sees HTTPS as the protocol in the URL.
Kanidm's authentication system is a stepped challenge response design, where you initially request an "intent" to authenticate. Once you establish this intent, the server sets up a session-id into a secure cookie, and informs the client of what authentication methods can proceed.
If you do NOT have a HTTPS URL, the cookie with the session-id is not transmitted. The server detects this as an invalid-state request in the authentication design, and immediately breaks the connection, because it appears insecure. This prevents credential disclosure since the authentication session was not able to be established due to the lost session-id cookie.
Simply put, we are using settings like secure cookies to add constraints to the server so that you must perform and adhere to best practices - such as having TLS present on your communication channels.
This is another reason why we do not allow the server to start without a TLS certificate being configured.
WebAuthn
Similarly, WebAuthn and its various other names like Passkeys, FIDO2 or "scan the QR code to log in" will only work over TLS.
OAuth2
RFC6819 - OAuth2 Threat Model and Security Considerations is a comprehensive and valuable resource discussing the security of OAuth2 and influences OpenID Connect as well. In general Kanidm follows and implements many of the recommendations in this document, as well as choosing not to implement certain known insecure OAuth2 features.
Why is disabling PKCE considered insecure?
RFC7636 - Proof Key for Code Exchange by OAuth Public Clients
exists to prevent authorisation code interception attacks. This is where an attacker can retrieve
the authorisation code and then perform the code exchange without the user being aware. A successful
code exchange issues the attacker with an access_token
and optionally a refresh_token
. The RFC
has an excellent explanation of the attack. Additionally, this threat is discussed in
RFC6819 Section 4.4.1.
As Kanidm aims for "secure by default" design, even with confidential clients, we deem it
important to raise the bar for attackers. For example, an attacker may have access to the
client_id
and client_secret
of a confidential client as it was mishandled by a system
administrator. While they may not have direct access to the client/application systems, they could
still use this client_id+secret
to then carry out the authorisation code interception attack
listed.
For confidential clients (referred to as a basic
client in Kanidm due to the use of HTTP Basic for
client_id+secret
presentation) PKCE may optionally be disabled. This can allow authorisation code
attacks to be carried out - however if TLS is used and the client_secret
never leaks, then these
attacks will not be possible. Since there are many public references to system administrators
mishandling secrets such as these so we should not rely on this as our sole defence.
For public clients (which have no client_id
authentication) we strictly enforce PKCE since
disclosure of the authorisation code to an attacker will allow them to perform the code exchange.
OpenID connect internally has a nonce
parameter in its operations. Commonly it is argued that this
value removes the need for OpenID connect clients to implement PKCE. It does not. This parameter is
not equivalent or a replacement for PKCE. While the nonce
can assist with certain attack
mitigations, authorisation code interception is not prevented by the presence or validation of the
nonce
value.
We would strongly encourage OAuth2 client implementations to implement and support PKCE, as it provides defense in depth to known and exploited authorisation code interception attacks.
Why do you allow disabling PKCE but not TLS?
Because there are still many applications where PKCE is not available and it is not trivial to solve for all downstream applications. In the case that PKCE is absent on a single OAuth2 client, the scope of failure is reduced to that single client. This is not the case with TLS, which is trivial to configure, and in the case of compromise of an internal network between a load balancer and Kanidm, the attacker can access and steal all traffic and authentication data.
Why is RSA considered legacy?
While RSA is cryptographically sound, to achieve the same level as security as ECDSA it requires signatures and keys that are significantly larger. This has costs for network transmission and CPU time to verify these signatures. At this time (2024) to achieve the same level of security as a 256 bit ECDSA, RSA requires a 3072 bit key. Similarly a 384 bit ECDSA key requires a 8192 bit RSA for equivalent cryptographic strength, and a 521 bit ECDSA key would likely require a 16884 bit RSA key (or greater).
This means that going forward more applications will require ECDSA over RSA due to its increased strength for significantly faster and smaller key sizes.
Where this has more serious costs is our future desire to add support for Hardware Security Modules. Since RSA keys are much larger on these devices it may significantly impact performance of the HSM and may also limit the amount of keys we can store on the device. In the case of some HSM models, they do not even support RSA keys up to 8192 bits (but they do support ECDSA 384 and 521). An example of this is TPMs, which only support up to 4096 bit RSA keys at this time.
As a result, we want to guide people toward smaller, faster and more secure cryptographic standards like ECDSA. We want to encourage application developers to implement ECDSA in their OAuth2 applications as it is likely that limitations of RSA will be hit in the future.
Generally, it's also positive to encourage applications to review and update their cryptographic implementations over time too. Cryptography and security is not stangnant, it requires continual review, assessment and improvement.
Can I change the database backend from SQLite to - name of favourite database here -
No, it is not possible swap out the SQLite database for any other type of SQL server, nor will it be considered as an option.
ATTEMPTING THIS WILL BREAK YOUR KANIDM INSTANCE IRREPARABLY
This question is normally asked because people want to setup multiple Kanidm servers connected to a single database.
Kanidm does not use SQL as a database. Kanidm uses SQL as a durable key-value store and Kanidm implements its own database, caching, querying, optimisation and indexing on top of that key-value store.
As a result, because Kanidm specifically implements its own cache layer above the key-value store (sqlite in this example) then if you were to connect two Kanidm instances to the same key-value store, as each server has its own cache layer and they are not in contact, it is possible for writes on one server to never be observed by the second, and if the second were to then write over those entries it will cause loss of the changes from the first server.
Kanidm now implements its own eventually consistent distributed replication which also removes the need for external databases to be considered.
Why aren't snaps launching with home_alias
set?
Snaps rely on AppArmor and
AppArmor doesn't follow symlinks. When
home_alias
is any value other than none
a symlink will be created and pointing to home_attr
.
It is recommended to use alternative software packages to snaps.
All users in Kanidm can change their name (and their spn) at any time. If you change home_attr
from uuid
you must have a plan on how to manage these directory renames in your system.
Why so many crabs?
It's a rust thing.
Will you implement -insert protocol here-
Probably, on an infinite time-scale! As long as it's not STARTTLS. Please log an issue and start the discussion!
Why do the crabs have knives?
Why won't you take this FAQ thing seriously?
Look, people just haven't asked many questions yet. Sorry, there are no easter eggs in this document, but there may be elsewhere 🥚
Glossary
This is a glossary of terms used through out this book. While we make every effort to explains terms and acronyms when they are used, this may be a useful reference if something feels unknown to you.
Domain Names
- domain - This is the domain you "own". It is the highest level entity. An example would be
example.com
(since you do not own.com
). - subdomain - A subdomain is a domain name space under the domain. A subdomains of
example.com
area.example.com
andb.example.com
. Each subdomain can have further subdomains. - domain name - This is any named entity within your domain or its subdomains. This is the umbrella
term, referring to all entities in the domain.
example.com
,a.example.com
,host.example.com
are all valid domain names with the domainexample.com
. - origin - An origin defines a URL with a protocol scheme, optional port number and domain name
components. An example is
https://host.example.com
- effective domain - This is the extracted domain name from an origin excluding port and scheme.
Accounts
- trust - A trust is when two Kanidm domains have a relationship to each other where accounts can be used between the domains. The domains retain their administration boundaries, but allow cross authentication.
- replication - This is the process where two or more Kanidm servers in a domain can synchronise their database content.
- UAT - User Authentication Token. This is a token issue by Kanidm to an account after it has authenticated.
- SPN - Security Principal Name. This is a name of an account comprising its name and domain name. This allows distinction between accounts with identical names over a trust boundary
Internals
- entity, object, entry - Any item in the database. Generally these terms are interchangeable, but internally they are referred to as Entry.
- account - An entry that may authenticate to the server, generally allowing extended permissions and actions to be undertaken.
Access Control
- privilege - An expression of what actions an account may perform if granted
- target - The entries that will be affected by a privilege
- receiver - The entries that will be able to use a privilege
- acp - an Access Control Profile which defines a set of privileges that are granted to receivers to affect target entries.
- role - A term used to express a group that is the receiver of an access control profile allowing its members to affect the target entries.
Getting Started (for Developers)
Development Principles
As a piece of software that stores the identities of people, the project becomes bound to social and political matters. The decisions we make have consequences on many people - many who never have the chance to choose what software is used to store their identities (think employees in a business, or the users of a website).
This means we have a responsibility to not only be aware of our impact on our direct users (developers, system administrators, dev ops, security and more) but also the impact on indirect consumers - many of who are unlikely to be in a position to contact us to ask for changes and help.
Ethics / Rights
If you have not already, please see our documentation on rights and ethics
Humans First
We must at all times make decisions that put humans first. We must respect all cultures, languages and identities and how they are represented.
We will never put a burden on the user to correct for poor designs on our part.
This may mean we make technical choices that are difficult or more complex, or different to "how things have always been done". But we do this to ensure that all people can have their identities stored how they choose.
For example, any user may change their name, display name and legal name at any time. Many applications will break as they primary key from name when this occurs. But this is the fault of the application. Name changes must be allowed. Our job as technical experts is to allow that to happen.
Correct and Simple
As a piece of security sensitive software we must always put correctness first. All code must have tests. All developers must be able to run all tests on their machine and environment of choice.
This means that the following must always work:
git clone ...
cargo test
If a test or change would require extra requirements or preconfiguration (such as setting up an external database or service), then we can no longer provide the above. Testing must be easy and accessible - otherwise people will not run the tests, leading to poor quality.
The project must be simple. Any one should be able to understand how it works and why those decisions were made.
Hierarchy of Controls
When a possible risk arises we should always consider the hierarchy of controls. In descedending order of priority
- Elimination - eliminate the risk from existing
- Substitution - replace the risk with something less dangerous
- Engineering Controls - isolate the risk from causing harm
- Administrative Controls - educate about the risk, add warnings
- Personal Protection - document the risk
Languages
The core server will (for now) always be written in Rust. This is due to the strong type guarantees it gives, and how that can help raise the quality of our project.
Over-Configuration
Configuration will be allowed, but only if it does not impact the statements above. Having configuration is good, but allowing too much (i.e. a scripting engine for security rules) can give deployments the ability to violate human first principles, which reflects badly on us.
In addition every extra configuration item expands our testing matrix exponentially. We should optimally only offer one path that is correct for all users unless no other options are possible.
All configuration items, must be constrained to fit within our principles so that every Kanidm deployment, will aim to provide a positive experience to all people.
Setup the Server
It's important before you start trying to write code and contribute that you understand what Kanidm does and its goals.
An important first step is to install the server so if you have not done that yet, go and try that now! 😄
Setting up your Machine
Each operating system has different steps required to configure and build Kanidm.
MacOS
A prerequisite is Apple Xcode for access to git and compiler tools. You should install this first.
You will need rustup to install a Rust toolchain.
SUSE / OpenSUSE
You will need to install rustup and our build dependencies with:
zypper in rustup git libudev-devel sqlite3-devel libopenssl-3-devel libselinux-devel pam-devel tpm2-0-tss-devel
You can then use rustup to complete the setup of the toolchain.
In some cases you may need to build other vendored components, or use an alternate linker. In these cases we advise you to also install.
zypper in clang lld make sccache
You should also adjust your environment with:
export RUSTC_WRAPPER=sccache
export CC="sccache /usr/bin/clang"
export CXX="sccache /usr/bin/clang++"
And add the following to a cargo config of your choice (such as ~/.cargo/config), adjusting for cpu arch
[target.aarch64-unknown-linux-gnu]
linker = "clang"
rustflags = [
"-C", "link-arg=-fuse-ld=lld",
]
Fedora
You will need rustup to install a Rust toolchain.
You will also need some system libraries to build this:
systemd-devel sqlite-devel openssl-devel pam-devel
Building the Web UI requires additional packages:
perl-FindBin perl-File-Compare
Ubuntu
You need rustup to install a Rust toolchain.
You will also need some system libraries to build this, which can be installed by running:
sudo apt-get install libudev-dev libssl-dev pkg-config libpam0g-dev
Tested with Ubuntu 20.04 and 22.04.
Windows
caution
Our support for Windows is still in development, so you may encounter some compilation or build issues.
You need rustup to install a Rust toolchain.
An easy way to grab the dependencies is to install vcpkg.
This is how it works in the automated build:
-
Enable use of installed packages for the user system-wide:
vcpkg integrate install
-
Install the openssl dependency, which compiles it from source. This downloads all sorts of dependencies, including perl for the build.
vcpkg install openssl:x64-windows-static-md
There's a powershell script in the root directory of the repository which, in concert with openssl
will generate a config file and certs for testing.
Getting the Source Code
Get Involved
To get started, you'll need to fork or branch, and we'll merge based on pull requests.
Kanidm is (largely) a monorepo. This can be checked out with:
git clone https://github.com/kanidm/kanidm.git
cd kanidm
Other supporting projects can be found on the project github
If you are forking, then fork in GitHub and then add your remote.
git remote add myfork git@github.com:<YOUR USERNAME>/kanidm.git
Select an issue (always feel free to reach out to us for advice!), and create a branch to start working:
git branch <feature-branch-name>
git checkout <feature-branch-name>
cargo test
important
Kanidm is unable to accept code that is generated by an AI for legal reasons. copilot and other tools that generate code in this way can not be used in Kanidm.
When you are ready for review (even if the feature isn't complete and you just want some advice):
- Run the test suite:
cargo test
- Ensure rust formatting standards are followed:
cargo fmt --check
- Try following the suggestions from clippy, after running
cargo clippy
. This is not a blocker on us accepting your code! - Then commit your changes:
git commit -m 'Commit message' change_file.rs ...
git push <myfork> <feature-branch-name>
If you receive advice or make further changes, just keep committing to the branch, and pushing to your branch. When we are happy with the code, we'll merge in GitHub, meaning you can now clean up your branch.
git checkout master
git pull
git branch -D <feature-branch-name>
Rebasing
If you are asked to rebase your change, follow these steps:
git checkout master
git pull
git checkout <feature-branch-name>
git rebase master
Then be sure to fix any merge issues or other comments as they arise. If you have issues, you can always stop and reset with:
git rebase --abort
Building the Book
You'll need mdbook
and the extensions to build the book:
cargo install mdbook mdbook-mermaid mdbook-alerts
To build it:
make book
Or to run a local webserver:
cd book
mdbook serve
Designs
See the "Design Documents" section of this book.
Rust Documentation
A list of links to the library documentation is at kanidm.com/documentation.
Advanced
Minimum Supported Rust Version
The MSRV is specified in the package Cargo.toml
files.
We tend to be quite proactive in updating this to recent rust versions so we are open to increasing this value if required!
Build Profiles
Build profiles allow us to change the operation of Kanidm during it's compilation for development or release on various platforms. By default the "developer" profile is used that assumes the correct relative paths within the monorepo.
Setting different developer profiles while building is done by setting the environment variable
KANIDM_BUILD_PROFILE
to one of the bare filename of the TOML files in /profiles
.
For example, this will set the CPU flags to "none" and the location for the Web UI files to
/usr/share/kanidm/ui/pkg
:
KANIDM_BUILD_PROFILE=release_linux cargo build --release --bin kanidmd
Development Server for Interactive Testing
Especially if you wish to develop the WebUI then the ability to run the server from the source tree is critical.
Once you have the source code, you need encryption certificates to use with the server, because without certificates, authentication will fail.
We recommend using Let's Encrypt, but if this is not possible kanidmd
will create self-signed certificates in /tmp/kanidm
.
You can now build and run the server with the commands below. It will use a database in
/tmp/kanidm/kanidm.db
.
Start the server
cd server/daemon
./run_insecure_dev_server.sh
While the server is running, you can use the admin socket to generate an admin
password:
./run_insecure_dev_server.sh recover-account admin
Record the password above.
In a new terminal, you can now build and run the client tools with:
cargo run --bin kanidm -- --help
cargo run --bin kanidm -- login -H https://localhost:8443 -D anonymous -C /tmp/kanidm/ca.pem
cargo run --bin kanidm -- self whoami -H https://localhost:8443 -D anonymous -C /tmp/kanidm/ca.pem
cargo run --bin kanidm -- login -H https://localhost:8443 -D admin -C /tmp/kanidm/ca.pem
cargo run --bin kanidm -- self whoami -H https://localhost:8443 -D admin -C /tmp/kanidm/ca.pem
You may find it easier to modify ~/.config/kanidm
per the
book client tools section for extended administration locally.
Raw actions
notice
It's not recommended to use these tools outside of extremely complex or advanced development requirements. These are a last resort!
The server has a low-level stateful API you can use for more complex or advanced tasks on large numbers of entries at once. Some examples are below, but generally we advise you to use the APIs or CLI tools. These are very handy to "unbreak" something if you make a mistake however!
# Create from json (group or account)
kanidm raw create -H https://localhost:8443 -C ../insecure/ca.pem -D admin example.create.account.json
kanidm raw create -H https://localhost:8443 -C ../insecure/ca.pem -D idm_admin example.create.group.json
# Apply a json stateful modification to all entries matching a filter
kanidm raw modify -H https://localhost:8443 -C ../insecure/ca.pem -D admin '{"or": [ {"eq": ["name", "idm_person_account_create_priv"]}, {"eq": ["name", "idm_service_account_create_priv"]}, {"eq": ["name", "idm_account_write_priv"]}, {"eq": ["name", "idm_group_write_priv"]}, {"eq": ["name", "idm_people_write_priv"]}, {"eq": ["name", "idm_group_create_priv"]} ]}' example.modify.idm_admin.json
kanidm raw modify -H https://localhost:8443 -C ../insecure/ca.pem -D idm_admin '{"eq": ["name", "idm_admins"]}' example.modify.idm_admin.json
# Search and show the database representations
kanidm raw search -H https://localhost:8443 -C ../insecure/ca.pem -D admin '{"eq": ["name", "idm_admin"]}'
# Delete all entries matching a filter
kanidm raw delete -H https://localhost:8443 -C ../insecure/ca.pem -D idm_admin '{"eq": ["name", "test_account_delete_me"]}'
Build a Kanidm Container
Build a container with the current branch using:
make <TARGET>
Check make help
for a list of valid targets.
The following environment variables control the build:
ENV variable | Definition | Default |
---|---|---|
IMAGE_BASE | Base location of the container image. | kanidm |
IMAGE_VERSION | Determines the container's tag. | None |
CONTAINER_TOOL_ARGS | Specify extra options for the container build tool. | None |
IMAGE_ARCH | Passed to --platforms when the container is built. | linux/amd64,linux/arm64 |
CONTAINER_BUILD_ARGS | Override default ARG settings during the container build. | None |
CONTAINER_TOOL | Use an alternative container build tool. | docker |
BOOK_VERSION | Sets version used when building the documentation book. | master |
Container Build Examples
Build a kanidm
container using podman
:
CONTAINER_TOOL=podman make build/kanidmd
Build a kanidm
container and use a redis build cache:
CONTAINER_BUILD_ARGS='--build-arg "SCCACHE_REDIS=redis://redis.dev.blackhats.net.au:6379"' make build/kanidmd
Automatically Built Containers
To speed up testing across platforms, we're leveraging GitHub actions to build containers for test use.
Whenever code is merged with the master
branch of Kanidm, containers are automatically built for
kanidmd
and radius
. Sometimes they fail to build, but we'll try to keep them available.
To find information on the packages, visit the Kanidm packages page.
An example command for pulling and running the radius container is below. You'll need to authenticate with the GitHub container registry first.
docker pull ghcr.io/kanidm/radius:devel
docker run --rm -it \
-v $(pwd)/kanidm:/data/kanidm \
ghcr.io/kanidm/radius:devel
This assumes you have a kanidm
client configuration file in the current working directory.
Testing the OpenAPI generator things
There's a script in scripts/openapi_tests
which runs a few docker containers - you need to be
running a local instance on port 8443 to be able to pull the JSON file for testing.
Developer Ethics
Kanidm is a project that will store, process and present people's personal data. This means we have a responsibility to respect the data of all people who could be using our system - many who interact indirectly or do not have a choice in this platform.
Rights of people
All people using this software should expect to have the right to:
- Self control over their data, including the ability to alter or delete at any time.
- Free from harmful discrimination of any kind
- Informed consent over control and privacy of their data, including access and understand data held and shared on their behalf
- To be able to use and access this software regardless of ability, culture or language.
Examples of situations for consideration
Ability to be forgotten
Deletion is delete not flagging
When an account is deleted it must be truly deleted, not just flagged for future delete. Note that some functionality like the recycle bin, we must keep the account details, but a recycle bin purge does truly delete the account.
Self determination and autonomy
Self name change
People should be able to change their own name at anytime. Consider divorce, leaving abusive partners or other personal decisions around why a name change is relevant.
This is why names are self-service writeable at any time.
Free from harmful discrimination of any kind
Cultural and Social awareness of name formats
All name fields should be case sensitive utf8 with no max or min length limit. This is because names can take many forms such as.
- firstname middlename lastname
- firstname lastname
- firstname firstname lastname
- firstname lastname lastname
- firstname
- lastname firstname
And many many more that are not listed here. This is why our names are displayName as a freetext UTF8 field, with case sensitivity and no limits.
Informed consent and Privacy of their data
Access to legalName field
legalName should only be on a "need to know" basis, and only collected if required. This is to help people who may be stalked or harassed, or otherwise conscious of their privacy.
To use and access this software regardless of ability
Questions?
Please raise an issue with the project - we want to know if we have missed anything or can improve what we are doing.
Frequently Asked Questions
This is a list of common questions that are generally raised by developers or technical users.
Why don't you use library/project X?
A critical aspect of Kanidm is the ability to test it. Generally requests to add libraries or projects can come in different forms so I'll answer to a few of them:
Is the library in Rust?
If it's not in Rust, it's not eligible for inclusion. There is a single exception today (rlm python) but it's very likely this will also be removed in the future. Keeping a single language helps with testing, but also makes the project more accessible and consistent to developers. Additionally, features exist in Rust that help to improve the quality of the project from development to production.
Is the project going to create a microservice like architecture?
If the project (such as an external OAuth/OIDC gateway, or a different DB layer) would be used in a tight-knit manner to Kanidm then it is no longer a microservice, but a monolith with multiple moving parts. This creates production fragility and issues such as:
- Differences and difficulties in correlating log events
- Design choices of the project not being compatible with Kanidm's model
- Extra requirements for testing/production configuration
This last point is key. It is a critical part of Kanidm that the following must work on all machines, and run every single test in the suite.
git clone https://github.com/kanidm/kanidm.git
cd kanidm
cargo test
Not only this, but it's very important for quality that running cargo test
truly tests the entire
stack of the application - from the database, all the way to the client utilities and other daemons
communicating to a real server. Many developer choices have already been made to ensure that testing
is the most important aspect of the project to ensure that every feature is high quality and
reliable.
The addition of extra projects or dependencies would violate this principle and lead to a situation where it would not be possible to effectively test for all developers.
Why don't you use Raft/Etcd/MongoDB/Other to solve replication?
There are a number of reasons why these are generally not compatible. Generally these databases or technologies do solve problems, but they are not the problems in Kanidm.
CAP theorem
CAP theorem states that in a database you must choose only two of the three possible elements:
- Consistency - All servers in a topology see the same data at all times
- Availability - All servers in a topology can accept write operations at all times
- Partitioning - In the case of a network separation in the topology, all systems can continue to process read operations
Many protocols like Raft or Etcd are databases that provide PC guarantees. They guarantee that they are always consistent, and can always be read in the face of partitioning, but to accept a write, they must not be experiencing a partitioning event. Generally, this is achieved by the fact that these systems elect a single node to process all operations, and then re-elect a new node in the case of partitioning events. The elections will fail if a quorum is not met disallowing writes throughout the topology.
This doesn't work for Authentication systems and global scale databases. As you introduce non-negligible network latency, the processing of write operations will decrease in these systems. This is why Google's Spanner is a PA system.
PA systems are also considered to be "eventually consistent". All nodes can provide reads and writes at all times, but during a network partitioning or after a write there is a delay for all nodes to arrive at a consistent database state. A key element is that the nodes perform a consistency operation that uses application aware rules to allow all servers to arrive at the same state without communication between the nodes.
Update Resolution
Many databases do exist that are PA, such as CouchDB or MongoDB. However, they often do not have the properties required in update resolution that is required for Kanidm.
An example of this is that CouchDB uses object-level resolution. This means that if two servers update the same entry the "latest write wins". An example of where this won't work for Kanidm is if one server locks the account as an admin is revoking the access of an account, but another account updates the username. If the username update happened second, the lock event would be lost creating a security risk. There are certainly cases where this resolution method is valid, but Kanidm is not one.
Another example is MongoDB. While it does attribute level resolution, it does this without the application awareness of Kanidm. For example, in Kanidm if we have an account lock based on time, we can select the latest time value to over-write the following, or we could have a counter that can correctly increment/advance between the servers. However, Mongo is not aware of these rules, and it would not be able to give the experience we desire. Mongo is a very good database, it's just not the right choice for Kanidm.
Additionally, it's worth noting that most of these other databases would violate the previous desires to keep the language as Rust and may require external configuration or daemons which may not be possible to test.
How PAM/nsswitch Work
Linux and BSD clients can resolve identities from Kanidm into accounts via PAM and nsswitch.
Name Service Switch (NSS) is used for connecting the computers with different data sources to resolve name-service information. By adding the nsswitch libraries to /etc/nsswitch.conf, we are telling NSS to lookup password info and group identities in Kanidm:
passwd: compat kanidm
group: compat kanidm
When a service like sudo, sshd, su, etc. wants to authenticate someone, it opens the pam.d config of
that service, then performs authentication according to the modules defined in the pam.d config. For
example, if you run ls -al /etc/pam.d /usr/etc/pam.d
in SUSE, you can see the services and their
respective pam.d config.
Access Profiles Rework 2022
Access controls are critical for a project like Kanidm to determine who can access what on other entries. Our access controls have to be dynamic and flexible as administrators will want to define their own access controls. In almost every call in the server, they are consulted to determine if the action can be carried out. We also supply default access controls so that out of the box we are a complete and useful IDM.
The original design of the access control system was intended to satisfy our need for flexibility, but we have begun to discover a number of limitations. The design incorporating filter queries makes them hard to administer as we have not often publicly talked about the filter language and how it internally works. Because of their use of filters it is hard to see on an entry "what" access controls will apply to entries, making it hard to audit without actually calling the ACP subsystem. Currently the access control system has a large impact on performance, accounting for nearly 35% of the time taken in a search operation.
Additionally, the default access controls that we supply have started to run into limits and rough cases due to changes as we have improved features. Some of this was due to limited design with user cases in mind during development.
To resolve this a number of coordinating features need implementation to improve this situation. These features will be documented first, and the use cases second with each use case linking to the features that satisfy it.
Required Features to Satisfy
Refactor of default access controls
The current default privileges will need to be refactored to improve separation of privilege and improved delegation of finer access rights.
Access profiles target specifiers instead of filters
Access profiles should target a list of groups for who the access profile applies to, and who receives the access it is granting.
Alternately an access profile could target "self" so that self-update rules can still be expressed.
An access profile could target an OAuth2 definition for the purpose of allowing reads to members of a set of scopes that can access the service.
The access profile receiver would be group based only. This allows specifying that "X group of members can write self" meaning that any member of that group can write to themself and only themself.
In the future we could also create different target/receiver specifiers to allow other extended management and delegation scenarios. This improves the situation making things more flexible from the current filter system. It also may allow filters to be simplified to remove the SELF uuid resolve step in some cases.
Filter based groups
These are groups who's members are dynamically allocated based on a filter query. This allows a similar level of dynamic group management as we have currently with access profiles, but with the additional ability for them to be used outside of the access control context. This is the "bridge" allowing us to move from filter based access controls to "group" targeted.
A risk of filter based groups is "infinite churn" because of recursion. This can occur if you had a rule such a "and not memberof = self" on a dynamic group. Because of this, filters on dynamic groups may not use "memberof" unless they are internally provided by the kanidm project so that we can vet these rules as correct and without creating infinite recursion scenarios.
Access rules extracted to ACI entries on targets
The access control profiles are an excellent way to administer access where you can specific whom has access to what, but it makes it harder for the reverse query which is "who has access to this specific entity". Since this is needed for both search and auditing, by specifying our access profiles in the current manner, but using them to generate ACE rules on the target entry will allow the search and audit paths to answer the question of "who has access to this entity" much faster.
Sudo Mode
A flag should exist on a session defining "sudo" mode which requires a special account policy membership OR a re-authentication to enable. This sudo flag is a time window on a session token which can allow/disallow certain behaviours. It would be necessary for all write paths to have access to this value.
Account Policy
Account policy defines rules on accounts and what they can or can't do with regard to properties and authentication. This is required for sudo mode so that a group of accounts can be "always in sudo" mode and this enforces rules on session expiry.
Access Control Use Cases
Default Roles / Separation of Privilege
By default we attempt to separate privileges so that "no single account" has complete authority over the system.
Satisfied by:
- Refactor of default access controls
- Filter based groups
- Sudo Mode
System Admin
This role, also called "admins" is responsible to manage Kanidm as a service. It does NOT manage users or accounts.
The "admins" role is responsible to manage:
- The name of the domain
- Configuration of the servers and replication
- Management of external integrations (OAuth2)
Service Account Admin
The role would be called sa_admins
and would be responsible for top level management of service
accounts, and delegating authority for service account administration to managing users.
- Create service accounts
- Delegate service account management to owners groups
- Migrate service accounts to persons
The service account admin is capable of migrating service accounts to persons as it is "yielding" control of the entity, rather than an idm admin "taking" the entity which may have security impacts.
Service Desk
This role manages a subset of persons. The helpdesk roles are precluded from modification of "higher privilege" roles like service account, identity and system admins. This is due to potential privilege escalation attacks.
- Can create credential reset links
- Can lock and unlock accounts and their expiry.
Idm Admin
This role manages identities, or more specifically person accounts. In addition in is a "high privilege" service desk role and can manage high privilege users as well.
- Create persons
- Modify and manage persons
- All roles of service desk for all persons
Self Write / Write Privilege
Currently write privileges are always available to any account post-authentication. Writes should only be available after an extra "challenge" or "sudo" style extra auth, and only have a limited time window of usage. The write window can be extended during the session. This allows extremely long lived sessions contrast to the current short session life. It also makes it safer to provide higher levels of privilege to persons since these rights are behind a re-authentication event.
Some accounts should always be considered able to write, and these accounts should have limited authentication sessions as a result of this.
Satisfied by:
- Access profiles target specifiers instead of filters
- Sudo Mode
OAuth2 Service Read (Nice to Have)
For UX/UI integration, being able to list OAuth2 applications that are accessible to the user would be a good feature. To limit "who" can see the OAuth2 applications that an account can access a way to "allow read" but by proxy of the related users of the OAuth2 service. This will require access controls to be able to interpret the OAuth2 config and provide rights based on that.
Satisfied by:
- Access profiles target specifiers instead of filters
Administration
Access controls should be easier to manage and administer, and should be group based rather than filter based. This will make it easier for administrators to create and define their own access rules.
- Refactor of default access controls
- Access profiles target specifiers instead of filters
- Filter based groups
Service Account Access
Service accounts should be able to be "delegated" administration, where a group of users can manage a service account. This should not require administrators to create unique access controls for each service account, but a method to allow mapping of the service account to "who manages it".
- Sudo Mode
- Account Policy
- Access profiles target specifiers instead of filters
- Refactor of default access controls
Auditing of Access
It should be easier to audit whom has access to what by inspecting the entry to view what can access it.
- Access rules extracted to ACI entries on targets
- Access profiles target specifiers instead of filters
Access Profiles Original
Access Profiles (ACPs) are a way of expressing the set of actions which accounts are permitted to
perform on database records (object
) in the system.
As a result, there are specific requirements to what these can control and how they are expressed.
Access profiles define an action of allow
or deny
: deny
has priority over allow
and will
override even if applicable. They should only be created by system access profiles because certain
changes must be denied.
Access profiles are stored as entries and are dynamically loaded into a structure that is more
efficient for use at runtime. Schema
and its transactions are a similar implementation.
Search Requirements
A search access profile must be able to limit:
- the content of a search request and its scope.
- the set of data returned from the objects visible.
An example:
Alice should only be able to search for objects where the class is
person
and the object is a memberOf the group called "visible".Alice should only be able to see those the attribute
displayName
for those users (not theirlegalName
), and their public
Worded a bit differently. You need permission over the scope of entries, you need to be able to read the attribute to filter on it, and you need to be able to read the attribute to receive it in the result entry.
If Alice searches for (&(name=william)(secretdata=x))
, we should not allow this to proceed because
Alice doesn't have the rights to read secret data, so they should not be allowed to filter on it.
How does this work with two overlapping ACPs? For example: one that allows read of name and
description to class = group, and one that allows name to user. We don't want to say
(&(name=x)(description=foo))
and it to be allowed, because we don't know the target class of the
filter. Do we "unmatch" all users because they have no access to the filter components? (Could be
done by inverting and putting in an AndNot of the non-matchable overlaps). Or do we just filter our
description from the users returned (But that implies they DID match, which is a disclosure).
More concrete:
search {
action: allow
targetscope: Eq("class", "group")
targetattr: name
targetattr: description
}
search {
action: allow
targetscope: Eq("class", "user")
targetattr: name
}
SearchRequest {
...
filter: And: {
Pres("name"),
Pres("description"),
}
}
A potential defense is:
acp class group: Pres(name) and Pres(desc) both in target attr, allow
acp class user: Pres(name) allow, Pres(desc) deny. Invert and Append
So the filter now is:
And: {
AndNot: {
Eq("class", "user")
},
And: {
Pres("name"),
Pres("description"),
},
}
This would now only allow access to the name
and description
of the class group
.
If we extend this to a third, this would work. A more complex example:
search {
action: allow
targetscope: Eq("class", "group")
targetattr: name
targetattr: description
}
search {
action: allow
targetscope: Eq("class", "user")
targetattr: name
}
search {
action: allow
targetscope: And(Eq("class", "user"), Eq("name", "william"))
targetattr: description
}
Now we have a single user where we can read description
. So the compiled filter above as:
And: {
AndNot: {
Eq("class", "user")
},
And: {
Pres("name"),
Pres("description"),
},
}
This would now be invalid, first, because we would see that class=user
and william
has no name
so that would be excluded also. We also may not even have "class=user" in the second ACP, so we
can't use subset filter matching to merge the two.
As a result, I think the only possible valid solution is to perform the initial filter, then determine on the candidates if we could have have valid access to filter on all required attributes. IE this means even with an index look up, we still are required to perform some filter application on the candidates.
I think this will mean on a possible candidate, we have to apply all ACP, then create a union of the resulting targetattrs, and then compared that set into the set of attributes in the filter.
This will be slow on large candidate sets (potentially), but could be sped up with parallelism, caching or other methods. However, in the same step, we can also apply the step of extracting only the allowed read target attrs, so this is a valuable exercise.
Delete Requirements
A delete
profile must contain the content
and scope
of a delete.
An example:
Alice should only be able to delete objects where the
memberOf
ispurgeable
, and where they are not marked asprotected
.
Create Requirements
A create
profile defines the following limits to what objects can be created, through the
combination of filters and attributes.
An example:
Alice should only be able to create objects where the
class
isgroup
, and can only name the group, but they cannot add members to the group.
An example of a content requirement could be something like "the value of an attribute must pass a regular expression filter". This could limit a user to creating a group of any name, except where the group's name contains "admin". This a contrived example which is also possible with filtering, but more complex requirements are possible.
For example, we want to be able to limit the classes that someone could create on an object because classes often are used in security rules.
Modify Requirements
A modify
profile defines the following limits:
- a filter for which objects can be modified,
- a set of attributes which can be modified.
A modify
profile defines a limit on the modlist
actions.
For example: you may only be allowed to ensure presence
of a value. (Modify allowing purge,
not-present, and presence).
Content requirements (see Create Requirements) are out of scope at the moment.
An example:
Alice should only be able to modify a user's password if that user is a member of the students group.
note
modify
does not imply read
of the attribute. Care should be taken that we don't disclose the
current value in any error messages if the operation fails.
Targeting Requirements
The target
of an access profile should be a filter defining the objects that this applies to.
The filter limit for the profiles of what they are acting on requires a single special operation which is the concept of "targeting self".
For example: we could define a rule that says "members of group X are allowed self-write to the
mobilePhoneNumber
attribute".
An extension to the filter code could allow an extra filter enum of self
, that would allow this to
operate correctly, and would consume the entry in the event as the target of "Self". This would be
best implemented as a compilation of self -> eq(uuid, self.uuid)
.
Implementation Details
CHANGE: Receiver should be a group, and should be single value/multivalue? Can only be a group.
Example profiles:
search {
action: allow
receiver: Eq("memberof", "admins")
targetscope: Pres("class")
targetattr: legalName
targetattr: displayName
description: Allow admins to read all users names
}
search {
action: allow
receiver: Self
targetscope: Self
targetattr: homeAddress
description: Allow everyone to read only their own homeAddress
}
delete {
action: allow
receiver: Or(Eq("memberof", "admins), Eq("memberof", "servicedesk"))
targetscope: Eq("memberof", "tempaccount")
description: Allow admins or servicedesk to delete any member of "temp accounts".
}
// This difference in targetscope behaviour could be justification to change the keyword here
// to prevent confusion.
create {
action: allow
receiver: Eq("name", "alice")
targetscope: And(Eq("class", "person"), Eq("location", "AU"))
createattr: location
createattr: legalName
createattr: mail
createclass: person
createclass: object
description: Allow alice to make new persons, only with class person+object, and only set
the attributes mail, location and legalName. The created object must conform to targetscope
}
modify {
action: allow
receiver: Eq("name", "claire")
targetscope: And(Eq("class", "group"), Eq("name", "admins"))
presentattr: member
description: Allow claire to promote people as members of the admins group.
}
modify {
action: allow
receiver: Eq("name", "claire")
targetscope: And(Eq("class", "person"), Eq("memberof", "students"))
presentattr: sshkeys
presentattr: class
targetclass: unixuser
description: Allow claire to modify persons in the students group, and to grant them the
class of unixuser (only this class can be granted!). Subsequently, she may then give
the sshkeys values as a modification.
}
modify {
action: allow
receiver: Eq("name", "alice")
targetscope: Eq("memberof", "students")
removedattr: sshkeys
description: Allow allice to purge or remove sshkeys from members of the students group,
but not add new ones
}
modify {
action: allow
receiver: Eq("name", "alice")
targetscope: Eq("memberof", "students")
removedattr: sshkeys
presentattr: sshkeys
description: Allow alice full control over the ssh keys attribute on members of students.
}
// This may not be valid: Perhaps if <*>attr: is on modify/create, then targetclass, must
// must be set, else class is considered empty.
//
// This profile could in fact be an invalid example, because presentattr: class, but not
// targetclass, so nothing could be granted.
modify {
action: allow
receiver: Eq("name", "alice")
targetscope: Eq("memberof", "students")
presentattr: class
description: Allow alice to grant any class to members of students.
}
Formalised Schema
A complete schema would be:
Attributes
Name | Single/Multi | Type | Description |
---|---|---|---|
acp_allow | single value | bool | |
acp_enable | single value | bool | This ACP is enabled |
acp_receiver | single value | filter | ??? |
acp_targetscope | single value | filter | ??? |
acp_search_attr | multi value | utf8 case insense | A list of attributes that can be searched. |
acp_create_class | multi value | utf8 case insense | Object classes in which an object can be created. |
acp_create_attr | multi value | utf8 case insense | Attribute Entries that can be created. |
acp_modify_removedattr | multi value | utf8 case insense | Modify if removed? |
acp_modify_presentattr | multi value | utf8 case insense | ??? |
acp_modify_class | multi value | utf8 case insense | ??? |
Classes
Name | Must Have | May Have |
---|---|---|
access_control_profile | [acp_receiver, acp_targetscope] | [description, acp_allow] |
access_control_search | [acp_search_attr] | |
access_control_delete | ||
access_control_modify | [acp_modify_removedattr, acp_modify_presentattr, acp_modify_class] | |
access_control_create | [acp_create_class, acp_create_attr] |
note
empty sets really mean empty sets!
The ACP code will assert that both access_control_profile
and one of the
search/delete/modify/create
classes exists on an ACP. An important factor of this design is now
the ability to compose multiple ACP's into a single entry allowing a create/delete/modify
to
exist! However, each one must still list their respective actions to allow proper granularity.
"Search" Application
The set of access controls is checked, and the set where receiver matches the current identified user is collected. These then are added to the users requested search as:
And(<User Search Request>, Or(<Set of Search Profile Filters))
In this manner, the search security is easily applied, as if the targets to conform to one of the
required search profile filters, the outer And
condition is nullified and no results returned.
Once complete, in the translation of the entry -> proto_entry, each access control and its allowed set of attrs has to be checked to determine what of that entry can be displayed. Consider there are three entries, A, B, C. An ACI that allows read of "name" on A, B exists, and a read of "mail" on B, C. The correct behaviour is then:
A: name
B: name, mail
C: mail
So this means that the entry -> proto entry
part is likely the most expensive part of the access
control operation, but also one of the most important. It may be possible to compile to some kind of
faster method, but initially a simple version is needed.
"Delete" Application
Delete is similar to search, however there is the risk that the user may say something like:
Pres("class").
Were we to approach this like search, this would then have "every thing the identified user is
allowed to delete, is deleted". A consideration here is that Pres("class")
would delete "all"
objects in the directory, but with the access control present, it would limit the deletion to the
set of allowed deletes.
In a sense this is a correct behaviour - they were allowed to delete everything they asked to delete. However, in another it's not valid: the request was broad and they were not allowed access to delete everything they requested.
The possible abuse vector here is that an attacker could then use delete requests to enumerate the existence of entries in the database that they do not have access to. This requires someone to have the delete privilege which in itself is very high level of access, so this risk may be minimal.
So the choices are:
- Treat it like search and allow the user to delete what they are allowed to delete, but ignore other objects
- Deny the request because their delete was too broad, and they must specify a valid deletion request.
Option #2 seems more correct because the delete
request is an explicit request, not a request
where you want partial results. Imagine someone wants to delete users A and B at the same time, but
only has access to A. They want this request to fail so they KNOW B was not deleted, rather than it
succeed and have B still exist with a partial delete status.
However, a possible issue is that Option #2 means that a delete request of
And(Eq(attr, allowed_attribute), Eq(attr, denied))
, which is rejected may indicate presence of the
denied
attribute. So option #1 may help in preventing a security risk of information disclosure.
This is also a concern for modification, where the modification attempt may or may not fail depending on the entries and if you can/can't see them.
note
You can only delete
/modify
within the read scope you have. If you can't read it (based on the
read rules of search
), you can't delete
it. This is in addition to the filter rules of the
delete
applying as well. So performing a delete
of Pres(class)
, will only delete in your
read
scope and will never disclose if you are denied access.
"Create" Application
Create seems like the easiest to apply. Ensure that only the attributes in createattr
are in the
createevent
, ensure the classes only contain the set in createclass
, then finally apply
filter_no_index
to the entry to entry. If all of this passes, the create is allowed.
A key point is that there is no union of create
ACI's - the WHOLE ACI must pass, not parts of
multiple. This means if a control say "allows creating group with member" and "allows creating user
with name", creating a group with name
is not allowed - despite your ability to create an entry
with name
, its classes don't match. This way, the administrator of the service can define create
controls with specific intent for how they will be used without the risk of two controls causing
unintended effects (users
that are also groups
, or allowing invalid values.
An important consideration is how to handle overlapping ACI. If two ACI could match the create should we enforce both conditions are upheld? Or only a single upheld ACI allows the create?
In some cases it may not be possible to satisfy both, and that would block creates. The intent of the access profile is that "something like this CAN" be created, so I believe that provided only a single control passes, the create should be allowed.
"Modify" Application
Modify is similar to Create, however we specifically filter on the modlist
action of present
,
removed
or purged
with the action. The rules of create still apply; provided all requirements of
the modify are permitted, then it is allowed once at least one profile allows the change.
A key difference is that if the modify ACP lists multiple presentattr
types, the modify request is
valid if it is only modifying one attribute. IE we say presentattr: name, email
, but we only
attempt to modify email
.
Considerations
- When should access controls be applied? During an operation, we only validate schema after pre* Plugin application, so likely it has to be "at that point", to ensure schema-based validity of the entries that are allowed to be changed.
- Self filter keyword should compile to
eq("uuid", "....")
. When do we do this and how? memberof
could takename
oruuid
, we need to be able to resolve this correctly, but this is likely an issue inmemberof
which needs to be addressed, iememberof uuid
vsmemberof attr
.- Content controls in
create
andmodify
will be important to get right to avoid the security issues of LDAP access controls. Given thatclass
has special importance, it's only right to give it extra consideration in these controls. - In the future when
recyclebin
is added, are-animation
access profile should be created allowing revival of entries given certain conditions of the entry we are attempting to revive. A service-desk user should not be able to revive a deleted high-privilege user.
Access Control Defaults
- Do we need some kind of permission atoms to allow certain tasks?
Use Cases
-
User sign-up portal (need service account that can create users and do cred reset)
-
Role for service account generation.
-
Remote backup - this account should be able to trigger and retrieve a backup
-
Groups should be able to be changed by a managing group (managed by)
-
IP limits on accounts?
-
Users need to not be able to see other users.
- Means the user can't read member attr, but can see groups + group info.
-
Anonymous needs to be able to be blocked more easily.
-
Enable disable self-mail write
-
Enable disable self-name-change
To achieve
- IP access limits
- Managed By rules
- Better group specification syntax (not filters)
Domain Admin
graph LR DomainAdmin("Domain Admin") --> AccessControlAdmin("Access Control Admin") DomainAdmin("Domain Admin") --> AccountPolicyAdmin("Account Policy Admin") DomainAdmin("Domain Admin") --> DomainConfigAdmin("Domain Config Admin") DomainAdmin("Domain Admin") --> HPGroupAdmin("HP Group Admin") DomainAdmin("Domain Admin") --> SchemaAdmin("Schema Admin") DomainAdmin("Domain Admin") --> SyncAccountAdmin("Sync Account Admin")
IDM Admin
graph LR IdmAdmin("IDM Admin") --> GroupAdmin("Group Admin") IdmAdmin("IDM Admin") --> PersonAdmin("Person Admin") IdmAdmin("IDM Admin") --> PersonPIIModify("Person PII Modify") IdmAdmin("IDM Admin") --> PersonReadNoPII("Person Read No PII") IdmAdmin("IDM Admin") --> PosixAccountIncludesCredMod("POSIX Account - [Includes Cred Mod]") IdmAdmin("IDM Admin") --> RadiusAccountModify("Radius Account Modify")
Integration Admin
graph LR IntegrationAdmin("Integration Admin") --> OAuth2Admin("OAuth2 Admin") IntegrationAdmin("Integration Admin") --> PosixAccountConsumer("POSIX Account Consumer") IntegrationAdmin("Integration Admin") --> RadiusServiceAdmin("Radius Service Admin")
Help Desk
graph LR HelpDesk("Help Desk") --> PersonCredentialModify("Person Credential Modify") HelpDesk("Help Desk") --> PersonReadNoPII("Person Read No PII")
Account "Self"
graph LR SelfMailModify("Self Mail Modify") --> |"Modifies"| Self SelfRead("Self Read") --> |"Read"| Self SelfModify("Self Modify") --> |"Writes Secrets"| Self SelfNameModify("Self Name Modify") --> |"Modifies"| Self
Account-Related
Duplicated for Service Accounts, HP persons, HP service Accounts.
graph LR PersonOnBoard("Person On Board") --> |"Creates"| Persons("Persons") PersonAdmin("Person Admin") --> |"Creates Deletes"| Persons("Persons") PersonPIIModify --> |"Reads Modifies"| Persons PersonPIIModify("Person PII Modify") -.-> |"Member of"| PersonAdmin PersonCredentialModify("Person Credential Modify") -.-> |"Member of"| PersonAdmin PersonCredentialModify("Person Credential Modify") -.-> |"Member of"| PersonOnBoard PersonCredentialModify --> |"Reads Modifies"| Persons PersonCredentialModify --> |"Reads"| PersonReadNoPII("Person Read No PII") PersonAdmin --> PersonReadWithPII("Person Read - With PII") PersonReadWithPII --> PersonReadNoPII PersonReadNoPII --> |"Reads"| Persons PosixAccountIncludesCredMod --> |"Extends (Add Posix Account)"| Persons
Domain and Schema
graph LR DomainConfigAdmin("Domain Configuration Admin") --> |"Modifies Reads"| Domain DomainConfigAdmin("Domain Configuration Admin") --> |"Modifies Reads"| System SyncAccountAdmin("Sync Account Admin") --> |"Creates Modifies Deletes"| SyncAccounts("Sync Accounts") SchemaAdmin("Schema Admin") --> |"Creates Modifies"| Schema("Schema") AccessControlAdmin("Access Control Admin") --> |"Creates Modifies Deletes"| AccessControls("Access Controls")
High-Priv and Groups
graph LR GroupAdmin("Group Admin") --> |"Create Modify Delete"| Groups("Groups") AccountPolicyAdmin("Account Policy Admin") --> |"Modifies Extends"| Groups("Groups") GroupAdmin --> |"Modify Delete"| HPGroups("HP Groups") GroupAdmin --> |"Add Members"| HPGroup("HP Group") HPGroupAdmin("HP Group Admin") --> HPGroup GroupAdmin -.-> |"Inherits"| HPGroupAdmin
OAuth2 Specific
graph LR OAuth2Admin("OAuth2 Admin") --> |"Creates Modifies Delegates"| OAuth2RS("OAuth2 RS") ScopedMember("Scoped Member") --> |"Reads"| OAuth2RS
POSIX-Specific
graph LR PosixAccountConsumer("POSIX Account Consumer") --> |"Reads Auths"| PosixAccounts("Posix Accounts")
Radius
graph LR RadiusServiceAdmin("Radius Service Admin") --> |"Adds Members"| RadiusService("Radius Service") RadiusService --> |"Reads Secrets"| RadiusAccounts("Radius Accounts") RadiusAccountModify("Radius Account Modify") --> |"Writes Secrets"| RadiusAccounts
Recycle Bin Admin
graph LR RecycleBinAdmin("Recycle Bin Admin") --> |"Modifies Reads Revives"| RecycledEntries("Recycled Entries")
Architecture
Kanidm has a number of components and layers that make it up. As this project is continually evolving, if you have questions or notice discrepancies with this document please contact William (Firstyear) at any time.
Tools
Kanidm Tools are a set of command line clients that are intended to help administrators deploy, interact with, and support a Kanidm server installation. These tools may also be used for servers or machines to authenticate and identify users. This is the "human interaction" part of the server from a CLI perspective.
Clients
The kanidm
client is a reference implementation of the client library, that others may consume or
interact with to communicate with a Kanidm server instance. The tools above use this client library
for all of its actions. This library is intended to encapsulate some high level logic as an
abstraction over the REST API.
Proto
The kanidm
proto is a set of structures that are used by the REST and raw API's for HTTP
communication. These are intended to be a reference implementation of the on-the-wire protocol, but
importantly these are also how the server represents its communication. This makes this the
authoritative source of protocol layouts with regard to REST or raw communication.
Kanidmd (main server)
Kanidmd is intended to have minimal (thin) client tools, where the server itself contains most logic
for operations, transformations and routing of requests to their relevant datatypes. As a result,
the kanidmd
section is the largest component of the project as it implements nearly everything
required for IDM functionality to exist.
Search
Search is the "hard worker" of the server, intended to be a fast path with minimal overhead so that clients can acquire data as quickly as possible. The server follows the below pattern.
(1) All incoming requests are from a client on the left. These are either REST requests, or a structured protocol request via the raw interface. It's interesting to note the raw request is almost identical to the queryserver event types - whereas REST requests we have to generate request messages that can become events.
The frontend uses a webserver with a thread-pool to process and decode network I/O operations concurrently. This then sends asynchronous messages to a worker (actor) pool for handing.
(2) These search messages in the actors are transformed into "events" - a self-contained structure containing all relevant data related to the operation at hand. This may be the event origin (a user or internal), the requested filter (query), and perhaps even a list of attributes requested. These events are designed to ensure correctness. When a search message is transformed to a search event, it is checked by the schema to ensure that the request is valid and can be satisfied securely.
As these workers are in a thread pool, it's important that these are concurrent and do not lock or block - this concurrency is key to high performance and safety. It's also worth noting that this is the level where read transactions are created and committed - all operations are transactionally protected from an early stage to guarantee consistency of the operations.
-
When the event is known to be consistent, it is then handed to the queryserver - the query server begins a process of steps on the event to apply it and determine the results for the request. This process involves further validation of the query, association of metadata to the query for the backend, and then submission of the high-level query to the backend.
-
The backend takes the request and begins the low-level processing to actually determine a candidate set. The first step in query optimisation, to ensure we apply the query in the most efficient manner. Once optimised, we then use the query to query indexes and create a potential candidate set of identifiers for matching entries (5.). Once we have this candidate id set, we then retrieve the relevant entries as our result candidate set (6.) and return them (7.) to the backend.
-
The backend now deserialises the databases candidate entries into a higher level and structured (and strongly typed) format that the query server knows how to operate on. These are then sent back to the query server.
-
The query server now applies access controls over what you can / can't see. This happens in two phases. The first is to determine "which candidate entries you have the rights to query and view" and the second is to determine "which attributes of each entry you have the right to perceive". This separation exists so that other parts of the server can impersonate users and conduct searches on their behalf, but still internally operate on the full entry without access controls limiting their scope of attributes we can view.
-
From the entries reduced set (ie access controls applied), we can then transform each entry into it's protocol forms - where we transform each strong type into a string representation for simpler processing for clients. These protoentries are returned to the front end.
-
Finally, the protoentries are now sent to the client in response to their request.
Write
The write path is similar to the search path, but has some subtle differences that are worth paying attention to.
(1), (2) Like search, all client operations come from the REST or raw apis, and are transformed or generated into messages. These messages are sent to a single write worker. There is only a single write worker due to the use of copy-on-write structures in the server, limiting us to a single writer, but allowing search transaction to proceed without blocking in parallel.
(3) From the worker, the relevant event is created. This may be a "Create", "Modify" or "Delete" event. The query server handles these slightly differently. In the create path, we take the set of entries you wish to create as our candidate set. In modify or delete, we perform an impersonation search, and use the set of entries within your read bounds to generate the candidate set. This candidate set will now be used for the remainder of the writing operation.
It is at this point, we assert access controls over the candidate set and the changes you wish to make. If you are not within rights to perform these operations the event returns an error.
(4) The entries are now sent to the pre-operation plugins for the relevant operation type. This allows transformation of the candidate entries beyond the scope of your access controls, and to maintain some elements of data consistency. For example, one plugin prevents creation of system protected types where another ensures that uuid exists on every entry.
(5) These transformed entries are now returned to the query server.
(6) The backend is sent the list of entries for writing. Indexes are generated (7) as required based on the new or modified entries, and the entries themselves are written (8) into the core db tables. This operation returns a result (9) to the backend, which is then filtered up to the query server (10)
(11) Provided all operations to this point have been successful, we now apply post write plugins which may enforce or generate different properties in the transaction. This is similar to the pre plugins, but allows different operations. For example, a post plugin ensures uuid reference types are consistent and valid across the set of changes in the database. The most critical is memberof, which generates reverse reference links from entries to their group memberships, enabling fast rbac operations. These are done as post plugins because at this point internal searches can now yield and see the modified entries that we have just added to the indexes and datatables, which is important for consistency (and simplicity) especially when you consider batched operations.
(12) Finally the result is returned up (13) through (14) the layers (15) to the client to inform them of the success (or failure) of the operation.
IDM
TBD
Radius
The radius components are intended to be minimal to support a common set of radius operations in a container image that is simple to configure. If you require a custom configuration you should use the python tools here and configure your own radius instance as required.
User Authentication Flow
This authentication flow is for interactive users. If you're using a service account, use Bearer authentication with the token.
- Client sends an init request. This can be either:
AuthStep::Init
which just includes the username, orAuthStep::Init2
which can request a "privileged" session
- The server responds with a list of authentication methods.
(
AuthState::Choose(Vec<AuthAllowed>)
) - Client requests auth with a method (
AuthStep::Begin(AuthMech)
) - Server responds with an acknowledgement (
AuthState::Continue(Vec<AuthAllowed>)
). This is so the challenge can be included in the response, for Passkeys or other challenge-response methods.- If required, this challenge/response continues in a loop until the requirements are satisfied. For example, TOTP and then Password.
- The result is returned, either:
- Success, with the User Auth Token as a
String
. - Denied, with a reason as a
String
.
- Success, with the User Auth Token as a
sequenceDiagram; autonumber participant Client participant Kanidm Note over Client: "I'm Ferris and I want to start auth!" Client ->> Kanidm: AuthStep::Init(username) Note over Kanidm: "You can use the following methods" Kanidm ->> Client: AuthState::Choose(Vec<AuthAllowed>) loop Authentication Checks Note over Client: I want to use this mechanism Client->>Kanidm: AuthStep::Begin(AuthMech) Note over Kanidm: Ok, you can do that. Kanidm->>Client: AuthState::Continue(Vec<AuthAllowed>) Note over Client: Here is my credential Client->>Kanidm: AuthStep::Cred(AuthCredential) Note over Kanidm: Kanidm validates the Credential,<br /> and if more methods are required,<br /> return them. Kanidm->>Client: AuthState::Continue(Vec<AuthAllowed>) Note over Client, Kanidm: If there's no more credentials required, break the loop. end Note over Client,Kanidm: If Successful, return the auth token Kanidm->>Client: AuthState::Success(String Token) Note over Client,Kanidm: If Failed, return that and a message why. Kanidm-xClient: AuthState::Denied(String Token)
Cryptography Key Domains
Within Kanidm we have to manage a number of private keys with various cryptographic purposes. In the current design, we have evolved where for each purposes keys are managed in unique ways. However we need to improve this for a number reasons including shared keys for OAuth2 domains and a future integration with PKCS11.
Current System
In the current system we store keys in database records in a variety of bespoke ways for different uses. Often this means that keys are associated to the object they are integrated with, such as the OAuth2 client configuration. However this limits us since we may have users that wish to combine multiple OAuth2 clients into a single security domain, where access tokens may be exchanged between clients for forwarded authentication.
Another example is api-tokens for service accounts. In the past we associated a private key to each service-account, however this causes issues with key management. We want to combine all of these keys but preserve existing api-token signatures. Currently we have no mechanism to do this.
Use Cases
- Multiple OAuth2 clients wish to be able to access a shared resource server.
access_tokens
issued to clienta
orb
should be accepted by the resource server. - Support key-rotation within a key-domain, where former public keys are retained (can verify existing signatures but not create new ones), or can be revoked (verification can not succeed, and can not create new ones). Keys should be able to be retained for auditing purposes.
- Replication Coordinator role needs a way to issue keys that only work within the replication coordinator scope.
- Migration of key-domains and private keys into PKCS11 modules, or creation of new keys in PKCS11 to replace former keys.
Design
To accommodate future changes, keys will be associated to a Key Provider. Key Objects relate to a single Key Provider. Migration of a Key Object to another Key Provider in the future may be possible.
Entities that rely on a cryptographic key will relate to a Key Object.
┌─────────────────────┐
│ │
│ │
│ Key Provider │
│ │
│ │
│ │
└─────────────────────┘
▲
│
┌─┘
┌─────┼───────────────┐
┌┴─────┼──────────────┐│
┌─┴──────┴────────────┐ ││
┌─┴───────────────────┐ │ ││
│ │ │ ││
│ │ │ ││
│ Key Object │ │ ││
│ │ │ ├┘
│ │ ├─┘
│ ├─┘
└─────────────────────┘
▲
│
│
│
┌─────────────────────┐
│ │
│ Key Consumer │
│ │
│ * OAuth2 Client │
│ * Domain Keys │
│ │
└─────────────────────┘
Key Objects have a Key Type denoting the type of material they contain. The types will be named after the JWA algorithms from RFC7518. This allows easy mapping to OAuth2 concepts and PKCS11 in the future.
ES256
(ECDSA using P-256 and SHA-256,CKM_ECDSA_SHA256
)RS256
(RSASSA-PKCS1-v1_5
usingSHA-256
,CKM_SHA256_RSA_PKCS
)HS256
(HMAC using SHA-256,CKM_SHA256_HMAC
)
Possible future classes could be
A256GCM
(AES GCM with 256 bit keyCKM_AES_GCM
)
The type defines the possible operations of the Key Object but not how the operation is performed.
A key object MAY have multiple Key Types.
Key Objects also must define their structure related to their Key Provider. For example, a possible TPM Key Provider needs to store its Public and Private components in the Key Object, where our internal provider needs to store the DER portions of the keys.
Between the type and the provider, this provides a concrete way to determine how a key needs to be used.
For each private/public key pair, or each symmetric key, a record of its status (valid, retained, expired, revoked)
In the valid state, a key has a validity "from" a point in time. The latest valid_from
attribute
defines the currently active signing key for the object.
EXAMPLE
We have 3 keys defined with:
k1 { status: valid, valid_from: 10 }
k2 { status: valid, valid_from: 14 }
k3 { status: valid, valid_from: 19 }
Assume the current time is 15
. During a signing operation since k3
would not-yet be valid, then
we use the nearest key which is k2
.
If a signed object was presented with k3
and the time is 15
then we reject the signature as it
could not have validly been produced. (we may allow some small time window).
If a signed object was presented with k1
and the time is 15
, then we validate the signature as
k1
is still valid, and still is accepted for signatures.
Each key may have one Key Identifier. Key Identifiers must be unique.
Key rotation is triggered by adding a new key with a newer valid_from
attribute.
If a key object is missing a valid key, a new one MUST be generated.
On replication revoked, expired, retained and valid take precedence in that order. If two keys are marked as valid, the "latest write" wins.
On rotation the private key is discarded to prevent future use of a rotated key.
Keys must be merged, and not deleted.
class: KeyObject
uuid: ...
key_object_type: ECDSA_SHA256
key_object_type: RSA_SHA256
key_object_type: RSA_SHA256
key_internal_es256: { id: ..., status: valid, public_key, private_key }
key_internal_es256: { id: ..., status: retained, public_key }
key_internal_es256: { id: ..., status: retained, public_key, private_key }
hs256_private: { id: ..., status: valid, public_key, private_key }
rs256_public: { id: ..., status: valid, public_key }
┌─────────────────────┐ ┌─────────────────────┐
┌┴────────────────────┐│ ┌┴────────────────────┐│
┌─┴───────────────────┐ ││ ┌─┴───────────────────┐ ││
┌─┴───────────────────┐ │ ││ ┌─┴───────────────────┐ │ ││
│ │ │ ││ │ │ │ ││
│ │ │ ││ │ │ │ ││
│ Valid │ │ ││ │ Expired │ │ ││
│ │─┼─┼┴──────────▶│ │ │ ├┘
│ │ ├─┘ │ │ ├─┘
│ ├─┘ │ ├─┘
└─────────────────────┘ └─────────────────────┘
│ │
│ │
│ │
│ │
│ │
│ │
│ ┌─────────────────────┐ │
│ ┌┴────────────────────┐│ │
│ ┌─┴───────────────────┐ ││ │
│ ┌─┴───────────────────┐ │ ││ │
│ │ │ │ ││ │
│ │ │ │ ││ │
│ │ Revoked │ │ ││ │
└──▶│ │◀┼─┼┴───────┘
│ │ ├─┘
│ ├─┘
└─────────────────────┘
A central key-object store is maintained with keys in memory/other. This allows dynamic reference to these keys at run time. The object store must extract and have key-id lookup to a key-object.
Entries that use a keyObject have a reference to it.
class: oauth2_rs
key_object: Refer( ... )
This allows access to the keyObject from the primary store. Due to kanidm's transactions, it's guaranteed that any reference to a keyObject must be valid with a key in the keyObject store. Care must still be taken at run time in the extremely unlikely case this no longer holds true.
Key Objects likely will be referenced from other cached items like the domain, idmserver and oauth2 so Key Object changes will trigger reloads of these other services.
Calls to Key Objects must be async to allow for future cases with single threaded providers.
Future Considerations
Key objects map "closely" but probably not perfectly to pkcs11 objects. This will allow an easier migration in future to pkcs11 without needing to up-end all our existing key infra. We only need to move keyObjects into the pkcs11 model.
In the future we may need to migrate keyObjects to be part of their own "security" domain, which represents a pkcs11 or other key-trust store.
Key trust stores need to consider that some handlers are single threaded only, so we need to design some form of asynchronicity into our handlers so that they can use work queues to the HSM for keys.
We also need to consider key-wrapping for import of keys to HSM's on disjoint nodes. As well we probably need to consider keyObjects that are not always accessible to all nodes so that the replication coordinator keys may only be loaded on a subset of nodes. However I think that's a pkcs11 problem, not a problem for this change.
Internal to PKCS11 migration
In the future we need to consider how to perform a migration from internal keys to HSM's in a non disruptive manner.
The design presented above associates a Key Object with its Key Provider. There isn't a way to mix Key Objects with multiple possible providers.
However, we can create a migration helper object. This would be a Key Object / Key Provider that acts as a router toward different providers. This would allow us to route operations to the correct backend as required during the migration, until all objects reside in a single provider.
Alternately, an option is to allow a Key Provider (like a PKCS11 provider) to migrate Key Objects into itself with a marker that denotes that as internal to allow validation, but not to allow new signatures.
Domain Join - Machine Accounts
There are a number of features we have been considering that will require us to finally give in and support machine accounts also know as domain joining.
Feature Requirements
Limiting Unix Password Auth
Currently unix password authentication is targeted as the method for sudo. Initial access to the machine should come from ssh keys (and in future, ctap2).
In order to maintain compatibility with LDAP style authentication, we allow "anonymous hosts" to retrieve ssh public keys, and then perform sudo authentication.
This has the obvious caveat that anyone can stand up a machine that trusts a Kanidm instance. This presents a double edged sword:
- By configuring a machine to authenticate via Kanidm, there is full trust in the authentication decisions Kanidm makes.
- Users of Kanidm may be tricked into accessing a machine that is not managed by their IT or other central authority.
To prevent this, UNIX authentication should be configurable to prevent usage from unregistered machines. This will require the machine to present machine authentication credentials simultaneously with the user's credentials.
A potential change is removing the current unix password auth mechanism as a whole. Instead the user's auth token would contain a TPM bound credential that only the domain joined machine's TPM could access and use.
Requesting Cryptographic Credentials
When a user logs in to a machine, it may be required that they can use that authentication to identify themself to other systems. When a user authenticates with credentials such as ssh-keys, it's not possible to use these to request other forwardable credentials - and ssh agent forwarding only allows forwarding of ssh credentials, not other types of credentials that may be needed.
In this case, when a user authenticates with SSH, since they're using a trusted machine, Kanidm can request short-term and limited credentials on the users behalf.
An example is that we could dynamically request TLS certificates or Kerberos credentials.
Normally with ssh in this manner, everything has to use kerberos. This would force users to kinit on their machine to ssh and forward their credentials to the next machine. This causes friction since configuring kerberos on machines is an exercise in frustration, and with BYOD it gets even worse. In addition when using ssh with an ssh key, the only viable kinit mechanism is password or password + totp once the user has logged in. This is because pkcs11 can't be forwarded over ssh, nor can CTAP2, limiting kinit to weaker authentication mechanisms.
Security Considerations
- Anonymous joins should not be allowed or permitted.
- Join tokens need to be revoked (causing related machines to request re-enrollment) or expired (related machines can continue to function)
- Join tokens must be auditable.
- Private keys SHOULD be stored in a TPM, or at least a software HSM with a secured unlock key.
- The use of the private key must prevent replay attacks
Overview
Since the machine would now be an entity requiring authentication, we need to have a process to establish and maintain this trust relationship.
- A join token is created by a user who is authorised to perform domain joins.
- The machine is audited for a known trust state. This process may vary from site to site. A future improvement could be that the join token can only release on certain TPM PCR values.
- The join token is yielded to the Kanidm UNIX daemon which submits its signing key to the Kanidm server.
- The kanidm server verifies the submission and creates a machine account.
- The Kanidm UNIX daemon now uses its signing key to sign a challenge that is submitted with all requests to the kanidm server.
Extra
- Machines should be able to "re-join" with an alternate join token, moving their machine account join token relationship.
- Machines must be able to self-enroll newer keys which may have stronger cryptographic requirements.
Details
Join Token Creation
Join tokens are persisted in the database allowing tracing back to the usage of the token.
Every machine that is joined by that token will related back to that token. This allows auditing of which token was used to join which machine.
Machines may re-enroll with an alternate token.
The join token should be signed. The JWK pub key should be available at a known HTTPS uri so that the client can use it to validate the join token and its content. This may allow policy to be embedded into the join token for the client to self-adhere to in the join process.
Machine Auditing
The machine should be audited to be in a secure state. It's not yet clear how to proceed here, but we should consider using TPM PCRs with secure boot to measure this and validate the machine state.
One possible way to achieve this could be with full disk encryption that is bound to secure boot and TPM PCRs. Kanidm-unixd could validate the same PCR's to start operating. The challenge here would be updates of the expected PCR values during a system update. Alternately, Kanidm could "assume" that if started, then the FDE must have passed and attestation of health "is out of scope" for us.
Public Key Submission
The private key should be generated and stored in a TPM/HSM. If possible, we should also submit attestation of this.
The submission of the public key should prevent replays, and should sign either a nonce or the current time. The current time must be valid to within a number of seconds. The nonce must be created by the server.
The machine must submit its public key, the time value and the signature. This should accompany the join token.
If the signature is valid, and the join token is correct, then the machine is joined and has a machine account created. The machine account is linked to the join token.
Machine Account
The machine account is a new form of account, similar to a service account. It should identify the machine, its hostname, and other properties. It should also contain the machine's public key id.
When the machine requests certain API's from Kanidm, it should submit signed requests that include the current time. The kid is used to find the machine account that is submitting the request. This then validates the identity of the caller, and then allows the action to proceed.
Elevation of Privilege Inside User Sessions
To improve user experience, we need to allow long lived sessions in browsers. This is especially important as a single sign on system, users tend to be associated 1 to 1 with devices, and by having longer lived sessions, they have a smoother experience.
However, we also don't want user sessions to have unbound write permissions for the entire (possibly unlimited) duration of their session.
Prior art for this is github, which has unbounded sessions on machines and requests a re-authentication when a modifying or sensitive action is to occur.
For us to implement this will require some changes to how we manage sessions.
Session Issuance
-
ISSUE: Sessions are issued identically for service-accounts and persons
-
CHANGE: service-accounts require a hard/short session expiry limit and always have elevated permissions
-
CHANGE: persons require no session expiry and must request elevation for privs.
-
ISSUE: Sessions currently indicate all read-write types as the same access scope type.
-
CHANGE: Split sessions to show rwalways, rwcapable, rwactive
-
ISSUE: Sessions currently are recorded identically between service-accounts, persons, and api tokens
-
CHANGE: Change the session storage types to have unique session types for these ✅
-
ISSUE: Access Scope types are confused by api session using the same types.
-
CHANGE: Use access scope only as the end result of current effective permission calculation and not as a method to convert to anything else. ✅
AccessScope { ReadOnly, ReadWrite, Synchronise }
// Bound by token expiry ApiTokenScope { ReadOnly, ReadWrite, Synchronise }
UatTokenScope { ReadOnly, // Want to avoid "read write" here to prevent dev confusion. PrivilegeCapable, PrivilegeActive { expiry }, ReadWrite, }
SessionScope { Ro, RwAlways, PrivCapable, }
ApiTokenScope { RO RW Sync }
AuthSession if service account rw always, bound expiry
if person priv cap, unbound exp - Should we have a "trust the machine flag" to limit exp though? - can we do other types of cryptographic session binding?
Session Validation
- CHANGE: Session with PrivCapable indicates that re-auth can be performed.
- CHANGE: Improve how Uat/Api Token scopes become Access Scopes
- CHANGE: Remove all AccessScope into other types. ✅
Session Re-Authentication
-
Must be performed by the same credential that issued the session originally
- This is now stored in the session metadata itself.
- Does it need to be in the cred-id?
-
CHANGE: Store the cred id in UAT so that a replica can process the operation in a replication sync failure?
- This would rely on re-writing the session.
-
CHANGE: Should we record in the session when priv-escalations are performed?
Misc
- CHANGE: Compact/shrink UAT size if possible.
Diagram
Set
┌───────────────────────PrivActive────────────────────┐
│ + Exp │
│ │
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ .───────────. ┌────────────────┐
│ ┌────────────────▶( If Priv Cap )───────▶│Re-Auth-Allowed │
│ │ │ │ `───────────' └────────────────┘
DB Content ┌ ─ ─ ─ ┼ ─ ┼ ─ ─ ─ ─ ─ ─ ─ ─
┌───────────────────┐ │ │ JWT │ │ │
│ │ │ ▼ │
│ AuthSession │ │ ┌──────────────┐ │ ┌──────────────┐ │
│ │ │SessionScope │ │ │UatScope │
│ Service Account │ │ │- RO │ │ │- RO │ │
│ -> RWAlways │──────────────────▶│- RW │─────────┼──▶│- RW │──────────────────────────┐
│ │ │ │- PrivCapable │ │ │- PrivCapable │ │ │
│ Person │ └──────────────┘ │ │- PrivActive │ │
│ -> PrivCap │ │ │ └──────────────┘ │ │
│ │ │ │
└───────────────────┘ │ │ │ ▼
│ ┌──────────────┐
│ │ │ │AccessScope │ ┌───────────────┐
│ │- RO │ │ │
│ │ │ │- RW │───────────▶ │Access Controls│
│ │- Sync │ │ │
┌───────────────────┐ │ ┌─────────────────┐ │ ┌──────────────┐ │ └──────────────┘ └───────────────┘
│ │ │ApiSessionScope │ │ │ApiTokenScope │ ▲
│ Create API Token │ │ │- RO │ │ │- RO │ │ │
│ │───────────────▶│- RW │────────┼───▶│- RW │─────────────────────────┘
│Access Based On Req│ │ │- Sync │ │ │- Sync │ │
│ │ └─────────────────┘ │ │ │
└───────────────────┘ │ │ └──────────────┘ │
│
│ │ │
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
TODO:
- Remove the ident-only access scope, it's useless! ✅
- Split tokens to have a dedicated session type separate to uat sessions. ✅
- Change uat session access scope recording to match service-account vs person intent.
- Change UAT session issuance to have the uat purpose reflect the readwrite or readwrite-capable nature of the session, based on auth-type that was used.
- Based on auth-type, limit or unlimit expiry to match the intent of the session.
OAuth2 Device Flow
The general idea is that there's two flows.
Device/Backend
- Start an auth flow
- Prompt the user with the link
- On an interval, check the status
- Still pending? Wait.
- Otherwise, handle the result.
User
- Go to the "check user code" page
- Ensure user is authenticated
- Confirm that the user's happy for this auth session to happen
- This last step is the usual OAuth2 permissions/scope prompt
flowchart TD DeviceStatus -->|Pending| DeviceStatus D[Device] -->|Start Backend flow| BackendFlowStart(Prompt User with details) BackendFlowStart -->|User Clicks Link| DeviceGet BackendFlowStart -->|Check Status| DeviceStatus DeviceStatus -->|Result - error or success| End DeviceGet -->|Not Logged in, Valid Token| LoginFlow(Login Flow) DeviceGet -->|Invalid Token, Reprompt| DeviceGet LoginFlow --> DeviceGet DeviceGet -->|Logged in, Valid Token| ConfirmAccess(User Prompted to authorize) ConfirmAccess -->|Confirmed| End(Done!)
OAuth2 Refresh Tokens
Due to how Kanidm authentication sessions were originally implemented they had short session times (1 hour) due to the lack of privilege separation in tokens. Now with privilege separation being implemented session lengths have been extended to 8 hours with possible increases in the future.
However, this leaves us with an issue with OAuth2 - OAuth2 access tokens are considered valid until their expiry and we should not issue tokens with a validity of 8 hours or longer since that would allow rogue users to have a long window of usage of the token before they were forced to re-auth. It also means that in the case that an account must be forcefully terminated then the user would retain access to applications for up to 8 hours or more.
To prevent this, we need OAuth2 tokens to "check in" periodically to re-affirm their session validity.
This is performed with access tokens and refresh tokens. The access token has a short lifespan (proposed 15 minutes) and must be refreshed with Kanidm which can check the true session validity and if the session has been revoked. This creates a short window for revocation to propagate to OAuth2 applications since each OAuth2 application must periodically check in to keep their access token alive.
Risks
Refresh tokens are presented to the relying server where they receive an access token and an optional new refresh token. Because of this, it could be possible to present a refresh token multiple times to proliferate extra refresh and access tokens away from the system. Preventing this is important to limit where the tokens are used and monitor and revoke them effectively.
In addition, old refresh tokens should not be able to be used once exchanged, they should be "at most once". If this is not enforced then old refresh tokens can be used to gain access to sessions even if the associated access token was expired by many hours and its refresh token was already used.
This is supported by draft oauth security topics section 2.2.2 and draft oauth security topics refresh token protection
Refresh tokens must only be used by the client application associated. Kanidm strictly enforces this already with our client authorisation checks. This is discussed in RFC6749 section 10.4.
Design
┌─────────────────────────────────────────┐
│Kanidm │
│ │
│ ┌─────────┐ ┌─────────┐ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ Session │ 3. Update │ Session │ │
│ │ NIB 1 │─────NIB───────▶│ NIB 2 │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
│ └─────────┘ └─────────┘ │
│ │ │ │
└───┼───────────────────────────┼─────────┘
┌────┘ ▲ ┌────┘
│ │ │
│ │ │
1. Issued │ 4. Issued
│ │ │
│ │ │
│ │ │
▼ │ ▼
┌───────┐ │ ┌───────┐
│ │ │ │ │
│Access │ │ │Access │
│ + │ │ │ + │
│Refresh│──2. Refresh──┘ │Refresh│
│ IAT 1 │ │ IAT 2 │
│ │ │ │
└───────┘ └───────┘
In this design we associate a "not issued before" (NIB) timestamp to our sessions. For a refresh token to be valid for issuance, the refresh tokens IAT must be greater than or equal to the NIB.
In this example were the refresh token with IAT 1 reused after the second token was issued, then this condition would fail as the NIB has advanced to 2. Since IAT 1 is not greater or equal to NIB 2 then the refresh token must have previously been used for access token exchange.
In a replicated environment this system is also stable and correct even if a session update is missed.
2.
┌───────────────────────Replicate────────────────┐
│ │
│ │
┌───────┼─────────────────────────────────┐ ┌──────┼──────────────────────────────────┐
│Kanidm │ │ │Kanidm│ │
│ │ │ │ ▼ │
│ ┌─────────┐ ┌─────────┐ │ │ ┌─────────┐ ┌─────────┐ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ Session │ 4. Update │ Session │ │ │ │ Session │ 7. Update │ Session │ │
│ │ NIB 1 │─────NIB───────▶│ NIB 2 │ │ │ │ NIB 1 │ ─────NIB───────▶│ NIB 3 │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ └─────────┘ └─────────┘ │ │ └─────────┘ └─────────┘ │
│ │ │ │ │ ▲ │ │
└───┼───────────────────────────┼─────────┘ └──────┼────────────────────────┼─────────┘
┌────┘ ▲ ┌────┘ │ ┌────┘
│ │ │ │ │
│ │ │ │ │
1. Issued │ 5. Issued │ 8. Issued
│ │ │ │ │
│ │ │ │ │
│ │ │ │ │
▼ │ ▼ │ ▼
┌───────┐ │ ┌───────┐ │ ┌───────┐
│ │ │ │ │ │ │ │
│Access │ │ │Access │ │ │Access │
│ + │ │ │ + │ │ │ + │
│Refresh│──3. Refresh──┘ │Refresh│ │ │Refresh│
│ IAT 1 │ │ IAT 2 │─────6. Refresh──────────┘ │ IAT 3 │
│ │ │ │ │ │
└───────┘ └───────┘ └───────┘
In this example, we can see that the replication of the session with NIB 1 happens to the second Kanidm server, but the replication of session with NIB 2 has not occurred yet. If the token that was later issued with IAT 2 was presented to the second server it would still be valid and able to refresh since IAT 2 is greater or equal to NIB 1. This would also prompt the session to advance to NIB 3 such that when replication begun again, the session with NIB 3 would take precedence over the former NIB 2 session.
While this allows a short window where a former access token could be used on the second replica, this infrastructure being behind load balancers and outside of an attackers influence significantly hinders the ability to attack this for very little gain.
Attack Detection
draft oauth security topics section 4.14.2 specifically calls out that when refresh token reuse is detected then all tokens of the session should be canceled to cause a new authorisation code flow to be initiated.
Inactive Refresh Tokens
Similar draft oauth security topics section 4.14.2 also discusses that inactive tokens should be invalidated after a period of time. From the view of the refresh token this is performed by an internal exp field in the encrypted refresh token.
From the servers side we will require a "not after" parameter that is updated on token activity. This will also require inactive session cleanup in the server which can be extended into the session consistency plugin that already exists.
Since the act of refreshing a token is implied activity then we do not require other signaling mechanisms.
Questions
Currently with authorisation code grants and sessions we issue these where the sessions are recorded in an async manner. For consistency I believe the same should be true here but is there a concern with the refresh being issued but a slight delay before it's recorded? I think given the nature of our future replication we already have to consider the async/eventual nature of things, so this doesn't impact that further, and may just cause client latency in the update process.
However, we also don't want a situation where our async/delayed action queues become too full or overworked. Maybe queue monitoring/backlog issues are a separate problem though.
Replication Coordinator
Many other IDM systems configure replication on each node of the topology. This means that the administrator is responsible for ensuring all nodes are connected properly, and that agreements are bidirectional. As well this requires manual work for administrators to configure each node individually, as well as monitoring individually. This adds a significant barrier to "stateless" configurations.
In Kanidm we want to avoid this - we want replication to be coordinated to make deployment of replicas as easy as possible for new sites.
Kanidm Replication Coordinator
The intent of the replication coordinator (KRC) is to allow nodes to subscribe to the KRC which configures the state of replication across the topology.
1. Out of band - ┌────────────────┐
issue KRC ca + ────────────────┤ │
Client JWT. │ │
│ ┌──────────────▶│ │──────────────────────┐
│ │2. HTTPS │ Kanidm │ │
│ JWT in Bearer │ Replication │ 5. Issue repl config
│ Request repl config │ Coordinator │ with partner public
│ Send self signed ID │ │ key
│ │ cert │ │ │
│ │ ┌─────────│ │◀────────┐ │
│ │ │ │ │ 4. HTTPS │
│ │ │ └────────────────┘ JWT in Bearer │
│ │ 3. Issue Request repl config │
│ │ repl config Send self signed ID │
│ │ │ cert │
│ │ │ │ │
│ │ │ │ │
│ │ │ │ │
│ │ │ │ │
▼ │ ▼ │ ▼
┌────────────────┐ ┌─┴──────────────┐
│ │ │ │
│ │ │ │
│ │ 5. mTLS with self │ │
│ │──────────signed cert──────────▶│ │
│ Kanidm Server │ Perform replication │ Kanidm Server │
│ (node) │ │ (node) │
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
└────────────────┘ └────────────────┘
Kanidm Node Configuration
There are some limited cases where an administrator may wish to manually define replication configuration for their deployments. In these cases the admin can manually configure replication parameters in the Kanidm configuration.
A kanidm node for replication requires either:
- The URL to the KRC
- the KRC CA cert
- KRC issued configuration JWT
OR
- A replication configuration map
A replication configuration map contains a set of agreements and their direction of operation.
All replicas require:
- The direct url that other nodes can reach them on (this is NOT the origin of the server!)
Pull mode
This is the standard mode. The map contains for each node to pull replication data from. This logically maps to the implementation of the underlying replication mechanism.
- the url of the node's replication endpoint.
- The self-signed node certificate to be pinned for the connection.
- If a refresh required message is received, if automatic refresh should be carried out.
Push mode
This mode is unlikely to be developed as it does not match the way that replication works.
Worked examples
Manual configuration
There are two nodes, A and B.
The administrator configures both kanidm servers with replication urls.
# Server A
[replication]
origin = "repl://kanidmd_a:8444"
bindaddress = "[::]:8444"
# Server B
[replication]
origin = "repl://kanidmd_b:8444"
bindaddress = "[::]:8444"
The administrator extracts their replication certificates with the kanidmd binary admin features.
This will reflect the node_url
in the certificate.
kanidmd replication get-certificate
For each node, a replication configuration is created in json.
For A pulling from B.
[replication."repl://kanidmd_b:8444"]
type = "mutual-pull"
partner_cert = "M..."
automatic_refresh = false
For B pulling from A.
[replication."repl://kanidmd_a:8444"]
type = "mutual-pull"
partner_cert = "M..."
automatic_refresh = true
Notice that automatic refresh only goes from A -> B and not the other way around. This allows one server to be "authoritative".
KRC Configuration
The KRC is enabled as a replication parameter. This informs the node that it must not contact other nodes for its replication topology, and it prepares the node for serving that replication metadata. This is analogous to a single node operation configuration.
[replication]
origin = "repl://kanidmd_a:8444"
bindaddress = "[::]:8444"
krc_enable = true
# krc_url -- unset
# krc_ca_dir -- unset
All other nodes will have a configuration of:
[replication]
origin = "repl://kanidmd_b:8444"
bindaddress = "[::]:8444"
# krc_enable -- unset / false
# krc_url = https://private.name.of.krc.node
krc_url = https://kanidmd_a
# must contain ca that signs kanidmd_a's tls_chain.
krc_ca_dir = /path/to/ca_dir
The domain will automatically add a Default Site
. The KRC implies its own membership to "Default
Site" and it will internally add itself to the Default Site
.
The KRC can then issue Tokens that define which Site a new replica should join. Initially we will
only allow Default Site
(and will disallow creation of other sites).
The new replica will load its KRC token from the environment variable KANIDMD_KRC_TOKEN_PATH
. This
value will contain a file path where the JWT is stored. This is compatible with systemd credentials
and docker secrets. By default the value if unset will be defined by a profile default
(/etc/kanidm/krc.token
or /data/krc.token
).
A new replica can then contact the krc_url
validating the presented TLS chain with the roots from
krc_ca_dir
to assert the legitimacy of the KRC. Only once these are asserted, then the KRC token
can be sent to the instance as a Bearer
token. The new replica will also provide its mTLS
certificate and its server UUID.
Once validated, the KRC will create or update the server's replica entry. The replica entry in the database will contain the active mTLS cert of the replica and a reference to the replication site that the token referenced.
This will additionally add the "time first seen" to the server entry.
From this, for each server in the replication site associated to the token, the KRC will provide a replication config map to the new replica providing all URL's and mTLS certs.
Anytime the replica checks in, if the KRC replication map has changed a new one will be provided, or
the response will be None
for no changes.
To determine no changes we use a "generation". This is where any change to a replication site or server entries will increment the generation counter. This allows us to detect when a client requires a new configuration or not.
If a server's entry in the database is marked to be Revoked
then it will remain in the database,
but be ineligible for replication participation. This is to allow for forced removal of a
potentially compromised node.
The KRC will periodically examine its RUV. For any server entry whose UUID is not contained in the RUV, and whose "time first seen + time window" is less than now, then the server entry will be REMOVED for inactivity since it has now been trimmed from the RUV.
Moving the Replication Coordinator Role
Since the coordinator is part of a kanidmd server, there must be a process to move the KRC to another node.
Imagine the following example. Here, Node A is acting as the KRC.
┌─────────────────┐ ┌─────────────────┐
│ │ │ │
│ │ │ │
│ Node A │◀───────────────│ Node B │
│ │ │ │
│ │ │ │
└─────────────────┘ └─────────────────┘
▲ ▲
│ │
│ │
│ └────────────────────────────┐
│ │
│ │
│ │
┌─────────────────┐ ┌─────────────────┐
│ │ │ │
│ │ │ │
│ Node C │ │ Node D │
│ │ │ │
│ │ │ │
└─────────────────┘ └─────────────────┘
This would allow Node A to be aware of B, C, D and then create a full mesh.
We wish to decommission Node A and promote Node B to become the new KRC. Imagine at this point we cut over Node D to point its KRC at Node B.
┌─────────────────┐ ┌─────────────────┐
│ │ │ │
│ │ │ │
│ Node A │ │ Node B │
│ │ │ │
│ │ │ │
└─────────────────┘ └─────────────────┘
▲ ▲
│ │
│ │
│ │
│ │
│ │
│ │
┌─────────────────┐ ┌─────────────────┐
│ │ │ │
│ │ │ │
│ Node C │ │ Node D │
│ │ │ │
│ │ │ │
└─────────────────┘ └─────────────────┘
Since we still have the Server Entry records in the Default Site on both Node A and Node B, then all nodes will continue to participate in full mesh, and will update certificates as required.
Since all servers would still be updating their RUV's and by proxy, updating RUV's to their partners then no nodes will be trimmed from the topology.
This allows a time window where servers can be moved from Node A to Node B.
Gruesome Details
Server Start Up Process
Token is read from a file defined in the env - this works with systemd + docker secrets
Token is JWT with HS256. (OR JWE + AES-GCM)
Read the token
- if token domain_uuid != our domain_uuid -> set status to "waiting"
- empty replication config map
- if token domain_uuid == domain_uuid -> status to "ok"
- use cached replication config map
No TOKEN -> Implies KRC role.
- Set status to "ok", we are the domain_uuid source.
Client Process
-
Connect to KRC
- Provide token for site binding
- Submit my server_uuid
- Submit my public cert with the request
- Submit current domain_uuid + generation if possible
-
Reply from KRC -> repl config map.
- Config_map contains issuing KRC server uuid.
- If config_map generation > current config_map
- Reload config.
- If config_map == None
- Current map remains valid.
KRC Process
- Validate Token
- Is server_uuid present as a server entry?
- If no: add it with site association
- If yes: verify site associated to token
- Is server_uuid certificate the same as before?
- If no: replace it.
- Compare domain_uuid + generation
- If different supply config
- Else None (no change)
FUTURE: Possible Read Only nodes
For R/O nodes, we need to define how R/W will pass through. I can see a possibility like
No direct line
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ of sight─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
│ ▼
┌────────────┐ ┌─────────────┐────OOB Write────▶┌─────────────┐
│ │ │ Remote Kani │ │ │
│ Client │─────Write──────▶│ Server │ │ Main │
│ │ │ │ │ │
└────────────┘ └─────────────┘◀───Replication───└─────────────┘
This could potentially even have some filtering rules about what's allowed to proxy writes through. Generally though I think that RO will need a lot more thought, for now I want to focus on just simple cases like a pair / group of four replicas. 😅
Requirements
- Cryptographic (key) only authentication
- Node to Node specific authentication
- Scheduling of replication
- Multiple nodes
- Direction of traffic?
- Use of self-signed issued certs for nodes.
- Nodes must reject if incoming clients have the same certs.
Replication Design and Notes
Replication is a critical feature in an IDM system, especially when deployed at major sites and businesses. It allows for horizontal scaling of system read and write capacity, improves fault tolerance (hardware, power, network, environmental), and can improve client latency (by positioning replicas near clients).
Replication Background
Replication is a directed graph model, where each node (server) and directed edge (replication agreement) form a graph (topology). As the topology and direction can be seen, nodes of the graph can be classified based on their data transit properties.
NOTE: Historically many replication systems used the terms "master" and "slave". This has a number of negative cultural connotations, and is not used by this project.
Read-Write server
This is a server that is fully writable. It accepts external client writes, and these writes are propagated to the topology. Many read-write servers can be in a topology and written to in parallel.
Transport Hub
This is a server that is not writeable to clients, but can accept incoming replicated writes, and then propagates these to other servers. All servers that are directly after this server in the topology must not be a read-write, as writes may not propagate back from the transport hub. IE the following is invalid
RW 1 ---> HUB <--- RW 2
Note the replication direction in this, and that changes into HUB will not propagate back to RW 1 or RW 2.
Read-Only server
Also called a read-only replica, or in AD an RODC. This is a server that only accepts incoming replicated changes, and has no outbound replication agreements.
Replication systems are dictated by CAP theorem. This is a theory that states from "consistency, availability and partition tolerance" you may only have two of the three at any time.
Consistency
This is the property that a write to a server is guaranteed to be consistent and acknowledged to all servers in the replication topology. A change happens on all nodes or it does not happen at all, and clients contacting any server will always see the latest data.
Availability
This is the property that every request will receive a non-error response without the guarantee that the data is "up to date".
Partition Tolerance
This is the property that your topology in the face of partition tolerance will continue to provide functional services (generally reads).
Almost all systems expect partition tolerance, so the choice becomes between consistency and availability. These create a series of tradeoffs. Choosing consistency normally comes at significantly reduced write throughput due to the need for a majority of nodes to acknowledge changes. However, it avoids a need for complex conflict resolution systems. It also means that clients can be in a situation where they can't write. For IDM this would mean new sessions could not be created or accounts locked for security reasons.
Kanidm has chosen availability, as the needs of IDM dictate that we always function even in the face of partition tolerance, and when other failures occur. This comes at the cost of needing to manage conflict resolution. This AP selection is often called "eventually consistent" as nodes will convenge to an identical state over time.
Replication Phases
There are two phases of replication
- Refresh
This is when the content of a node is completely removed, and has the content of another node applied to replace it. No conflicts or issues can occur in this, as the refreshed node is now a "perfect clone" of the source node.
- Incremental
This is when differentials of changes are sent between nodes in the topology. By sending small diffs, it saves bandwidth between nodes and allows changes on all nodes to be merged and combined with other nodes. It is the handling of these incremental updates that can create conflicts in the data of the server.
Ordering of Writes - Change Identifiers
Rather than using an external coordinator to determine consistency, time is used to determine ordering of events. This allows any server to create a total-ordering of all events as though every write had occurred on a single server. This is how all nodes in replication will "arrive" at the same conclusion about data state, without the need for communication.
In order for time to be used in this fashion, it is important that the clock in use is always advancing and never stepping backwards. If a clock was to go backwards, it would cause an event on one node to be written in a different order than the way that other servers will apply the same writes. This creates data corruption.
As an aside, there are systems that do replication today and do not use always advancing clocks which can allow data corruption to seep in.
In addition it's also important that if an event happens at exactly the same moment on two nodes (down to the nanosecond) that a way of breaking the tie exists. This is why each server has an internal uuid, where the server uuid is used to order events if the timestamps are identical.
These points in time are represented by a changed identifier that contains the time of the event, and the server uuid that performed the event. In addition every write transaction of the server records the current time of the transaction, and if a subsequent transaction starts with a "time in the past", then the time is "dragged forward" to one nanosecond after the former transaction. This means CID's always advance - and never go backwards.
Conflict Resolution
Despite the ability to order writes by time, consistency is not a property that we can guarantee in an AP system. we must be able to handle the possibility of inconsistent data and the correct methods to bring all nodes into a consistent state with cross communication. These consistency errors are called conflicts. There are multiple types of conflict that can occur in a system like Kanidm.
Entry Conflicts
This is when the UUID of an entry is duplicated on a separate node. For example, two entries with UUID=A are created at the same time on two separate nodes. During replication one of these two nodes will persist and the other will become conflicted.
Attribute Conflicts
When entries are updated on two nodes at the same time, the changes between the entries need to be merged. If the same attribute is updated on two nodes at the same time the differences need to be reconciled. There are three common levels of resolution used for this. Lets consider an entry such as:
# Node A
attr_a: 1
attr_b: 2
attr_c: 3
# Node B
attr_b: 1
attr_c: 2
attr_d: 3
- Object Level
In object level resolution the entry that was "last written wins". The whole content of the last written entry is used, and the earlier write is lost.
In our example, if node B was the last write the entry would resolve as:
# OL Resolution
attr_b: 1
attr_c: 2
attr_d: 3
- Attribute Level
In attribute level resolution, the time of update for each attribute is tracked. If an attribute was written later, the content of that attribute wins over the other entries.
For example, if attr b was written last on node B, and attr c was written last on node A then the entry would resolve to:
# AL Resolution
attr_a: 1 <- from node A
attr_b: 1 <- from node B
attr_c: 3 <- from node A
attr_d: 3 <- from node B
- Value Level
In value level resolution, the values of each attribute is tracked for changes. This allows values to be merged, depending on the type of attribute. This is the most "consistent" way to create an AP system, but it's also the most complex to implement, generally requiring a changelog of entry states and differentials for sequential reapplication.
Using this, our entries would resolve to:
# VL Resolution
attr_a: 1
attr_b: 1, 2
attr_c: 2, 3
attr_d: 3
Each of these strategies has pros and cons. In Kanidm we have used a modified attribute level strategy where individual attributes can internally perform value level resolution if needed in limited cases. This allows fast and simple replication, while still allowing the best properties of value level resolution in limited cases.
Schema Conflicts
When an entry is updated on two nodes at once, it may be possible that the updates on each node individually are valid, but when combined create an inconsistent entry that is not valid with respect to the schema of the server.
Plugin Conflicts
Kanidm has a number of "plugins" that can enforce logical rules in the database such as referential integrity and attribute uniqueness. In cases that these rules are violated due to incremental updates, the plugins in some cases can repair the data. However in cases where this can not occur, entries may become conflicts.
Tracking Writes - Change State
To track these writes, each entry contains a hidden internal structure called a change state. The change state tracks when the entry was created, when any attribute was written to, and when the entry was deleted.
The change state reflects the lifecycle of the entry. It can either be:
- Live
- Tombstoned
A live entry is capable of being modified and written to. It is the "normal" state of an entry in the database. A live entry contains an "origin time" or "created at" timestamp. This allows unique identification of the entry when combined with the uuid of the entry itself.
A tombstoned entry is a "one way street". This represents that the entry at this uuid is deleted. The tombstone propagates between all nodes of the topology, and after a tombstone window has passed, is reaped by all nodes internally.
A live entry also contains a map of change times. This contains the maximum CID of when an attribute of the entry was updated last. Consider an entry like:
attr_a: 1
attr_b: 2
attr_c: 3
uuid: X
This entries changestate would show:
Live {
at: { server_uuid: A, cid: 1 },
attrs: {
attr_a: cid = 1
attr_b: cid = 1
attr_c: cid = 2
}
}
This shows us that the entry was created on server A, at cid time 1. At creation, the attributes a and b were created since they have the same cid.
attr c was either updated or created after this - we can't tell if it existed at cid 1, we can only know that a write of some kind occurred at cid 2.
Resolving Conflicts
With knowledge of the change state structure we can now demonstrate how the lower level entry and attribute conflicts are detected and managed in Kanidm.
Entry
An entry conflict occurs when two servers create an entry with the same UUID at the same time. This would be shown as:
Server A Server B
Time 0: create entry X
Time 1: create entry X
Time 2: <-- incremental --
Time 3: -- incremental -->
We can add in our entry change state for liveness here.
Time 0: create entry X cid { time: 0, server: A }
Time 1: create entry X cid { time: 1, server: B }
Time 2: <-- incremental --
Time 3: -- incremental -->
When the incremental occurs at time point 2, server A would consider these on a timeline as:
Time 0: create entry X cid { time: 0, server: A }
Time 1: create entry X cid { time: 1, server: B }
When viewed like this, we can see that if the second create had been performed on the same server, it would have been rejected as a duplicate entry. With replication enabled, this means that the latter entry will be moved to the conflict state instead.
The same process occurs with the same results when the reverse incremental operation occurs to server B where it receives the entry with the earlier creation from A. It will order the events and "conflict" its local copy of the entry.
Attribute
An attribute conflict occurs when two servers modify the same attribute of the same entry before an incremental replication occurs.
Server A Server B
Time 0: create entry X
Time 1: -- incremental -->
Time 2: modify entry X
Time 3: modify entry X
Time 4: <-- incremental --
Time 5: -- incremental -->
During an incremental operation, a modification to a live entry is allowed to apply provided the entries UUID and AT match the servers metadata. This gives the servers assurance that an entry is not in a conflict state, and that the node applied the change to the same entry. Were the AT values not the same, then the entry conflict process would be applied.
We can expand the metadata of the modifications to help understand the process here for the attribute.
Server A Server B
Time 0: create entry X
Time 1: -- incremental -->
Time 2: modify entry X attr A cid { time: 2, server: B }
Time 3: modify entry X attr A cid { time: 3, server: A }
Time 4: <-- incremental --
Time 5: -- incremental -->
When the incremental is sent in time 4 from B to A, since the modification of the attribute is earlier than the content of A, the incoming attribute state is discarded. (A future version of Kanidm may preserve the data instead).
At time 5 when the increment returns from A to B, the higher cid causes the value of attr A to be replaced with the content from server A.
This allows all servers to correctly order and merge changes between nodes.
Schema
An unlikely but possible scenario is a set of modifications that create incompatible entry states with regard to schema. For example:
Server A Server B
Time 0: create group X
Time 1: -- incremental -->
Time 2: modify group X into person X
Time 3: modify group X attr member
Time 4: <-- incremental --
Time 5: -- incremental -->
It is rare (if not will never happen) that an entry is morphed in place from a group to a person, from one class to a fundamentally different class. But the possibility exists so we must account for it.
In this case, what would occur is that the attribute of 'member' would be applied to a person, which is invalid for the kanidm schema. In this case, the entry would be moved into a conflict state since logically it is not valid for directory operations (even if the attributes and entry level replication requirements for consistency have been met).
Plugin
Finally, plugins allow enforcement of rules above schema. An example is attribute uniqueness. Consider the following operations.
Server A Server B
Time 0: create entry X create entry Y
Time 1: -- incremental -->
Time 2: <-- incremental --
Time 3: modify entry X attr name = A
Time 4: modify entry Y attr name = A
Time 5: <-- incremental --
Time 6: -- incremental -->
Here the entry is valid per the entry, attribute and schema rules. However, name is a unique attribute and can not have duplicates. This is the most likely scenario for conflicts to occur, since users can rename themself at any time.
In this scenario, in the incremental replication both entry Y and X would be move to the conflict state. This is because the name attribute may have been updated multiple times, or between incremental operations, meaning that either server can not reliably determine if X or Y is valid at this point in time, and wrt to future replications.
Incremental Replication
To this point, we have described "refresh" as a full clone of data between servers. This is easy to understand, and works as you may expect. The full set of all entries and their changestates are sent from a supplier to a consumer, replacing all database content on the consumer.
Incremental replication however requires knowledge of the state of the consumer and supplier to determine a difference of the entries between the pair.
To achieve this each server tracks a replication update vector (RUV), that describes the range of changes organised per server that originated the change. For example, the RUV on server B may contain:
|-----|----------|----------|
| | s_uuid A | s_uuid B |
|-----|----------|----------|
| min | T4 | T6 |
|-----|----------|----------|
| max | T8 | T16 |
|-----|----------|----------|
This shows that server B contains the set of data ranging from server A at time 4 and server B at time 6 to the latest values of server A at time 8 and server B at time 16.
During incremental replication the consumer sends it RUV to the supplier. The supplier calculates the difference between the consumer RUV and the supplier RUV. For example,
Server A RUV Server B RUV
|-----|----------|----------| |-----|----------|----------|
| | s_uuid A | s_uuid B | | | s_uuid A | s_uuid B |
|-----|----------|----------| |-----|----------|----------|
| min | T4 | T6 | | min | T4 | T6 |
|-----|----------|----------| |-----|----------|----------|
| max | T10 | T16 | | max | T8 | T20 |
|-----|----------|----------| |-----|----------|----------|
If A was the supplier, and B the consumer, when comparing these RUV's Server A would determine that
B required the changes A {T9, T10}
. Since B is ahead of A wrt to the server B changes, server A
would not supply these ranges. In the reverse, B would supply B {T17 -> T20}
.
If there were multiple servers, this allows replicas to proxy changes.
Server A RUV Server B RUV
|-----|----------|----------|----------| |-----|----------|----------|----------|
| | s_uuid A | s_uuid B | s_uuid C | | | s_uuid A | s_uuid B | s_uuid C |
|-----|----------|----------|----------| |-----|----------|----------|----------|
| min | T4 | T6 | T5 | | min | T4 | T6 | T4 |
|-----|----------|----------|----------| |-----|----------|----------|----------|
| max | T10 | T16 | T13 | | max | T8 | T20 | T8 |
|-----|----------|----------|----------| |-----|----------|----------|----------|
In this example, if A were supplying to B, then A would supply A {T9, T10}
and C {T9 -> T13}
.
This allows the replication to avoid full connection (where every node must contact every other
node).
In order to select the entries for supply, the database maintains an index of entries that are affected by any change for any cid. This allows range requests to be made for efficient selection of what entries were affected in any cid.
After an incremental replication is applied, the RUV is updated to reflect the application of these differences.
Lagging / Advanced Consumers
Replication relies on each node periodically communicating for incremental updates. This is because of deletes. A delete event occurs by a Live entry becoming a Tombstone. A tombstone is replicated over the live entry. Tombstones are then reaped by each node individually once the replication delay window has passed.
This delay window is there to allow every node the chance to have the tombstone replicated to it, so that all nodes will delete the tombstone at a similar time.
Once the delay window passes, the RUV is trimmed. This moves the RUV minimum.
We now need to consider the reason for this trimming process. Lets use these RUV's
Server A RUV Server B RUV
|-----|----------|----------| |-----|----------|----------|
| | s_uuid A | s_uuid B | | | s_uuid A | s_uuid B |
|-----|----------|----------| |-----|----------|----------|
| min | T10 | T6 | | min | T4 | T9 |
|-----|----------|----------| |-----|----------|----------|
| max | T15 | T16 | | max | T8 | T20 |
|-----|----------|----------| |-----|----------|----------|
The RUV for A on A does not overlap the range of the RUV for A on B (A min 10, B max 8).
This means that a tombstone could have been created at T9 and then reaped. This would mean that B would not have perceived that delete and then the entry would become a zombie - back from the dead, risen again, escaping the grave, breaking the tombstone. This could have security consequences especially if the entry was a group providing access or a user who was needing to be deleted.
To prevent this, we denote server B as lagging since it is too old. We denote A as advanced since it has data newer that can not be applied to B.
This will "freeze" B, where data will not be supplied to B, nor will data from B be accepted by other nodes. This is to prevent the risk of data corruption / zombies.
There is some harm to extending the RUV trim / tombstone reaping window. This window could be expanded even to values as long as years. It would increase the risk of conflicting changes however, where nodes that are segregated for extended periods have been accepting changes that may conflict with the other side of the topology.
REST Interface
note
This is a work in progress and not all endpoints have perfect schema definitions, but they're all covered!
We're generating an OpenAPI specification file and Swagger interface using utoipa.
The Swagger UI is available at /docs/swagger-ui
on your server (ie, if your origin is
https://example.com:8443
, visit https://example.com:8443/docs/swagger-ui
).
The OpenAPI schema is similarly available at /docs/v1/openapi.json
.
You can download the schema file using kanidm api download-schema <filename>
- it defaults to
./kanidm-openapi.json
.
Unixd MultiResolver Support
Up until July 2024 the purpose and motivation of the Kanidm Unixd component (unix_integration
in
the source tree) was to allow Unix-like platforms to authenticate and resolve users against a Kanidm
instance.
However, throughout 2023 and 2024 this project has expanded in scope - from the addition of TPM support to protect cached credentials (the first pam module to do so!), to use of the framework by himmelblau to enable Azure AD authentication.
We also have new features we want to add including LDAP backends (as an alternative to SSSD), the ability to resolve local system users, as well as support for PIV and CTAP2 for desktop login.
This has pushed the current design of the resolver to it's limits, and it's increasingly challenging to improve it as a result. This will necesitate a major rework of the project.
Current Architecture
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
┌───────┐ ┌───────┐ ┌───────┐ │ ┌───────────────────┐ │
│ │ │ │ │ │ │ │
│ NSS │ │ PAM │ │ CLI │ │ │ Tasks Daemon │ │
│ │ │ │ │ │ │ │
└───────┘ └───────┘ └───────┘ │ └───────────────────┘ │
▲ ▲ ▲ ▲
─ ─ ─ ─ ┼ ─ ─ ─ ─ ─│─ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ─ ─ ─ ┤
│ ▼ ▼ ▼ │
┌─────────────────────────────┐ ┌───────────┐ │
│ │ │ │ │
│ ClientCodec │ │ Tasks │ │
│ │ │ │ │
└─────────────────────────────┘ └───────────┘ │
┌ ─ ─ ─ ─ ─ ┘ ▲ ▲
│ │ │
│ ▼ │
┌───────────────┐ ┌────────────────────────────────┐ │ │
│ │ │ │ │ │
│ Kani Client │◀────▶│ Daemon / Event Loop │─────┘ │
│ │ │ │ │
└───────────────┘ └────────────────────────────────┘ │
│ ▲ ▲
│ │ │
│ ▼ ▼
┌──────────────────┐ ┌────────┐ │
│ │ │ │ │
│ DB / Cache │ │ TPM │ │
│ │ │ │ │
└──────────────────┘ └────────┘ │
│
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ── ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴
The current design treated the client as a trivial communication layer. The daemon/event loop contained all state including if the resolver was online or offline. Additionally the TPM and password caching operations primarily occurred in the daemon layer, which limited the access of these features to the client backend itself.
Future Features
Files Resolver
The ability to resolve and authenticate local users from /etc/{passwd,group,shadow}
. The classic
mechanisms to resolve this are considered "slow" since they require a full-file-parse each
operation.
In addition, these files are limited by their formats and can not be extended with future authentication mechanisms like CTAP2 or PIV.
Unixd already needs to parse these files to understand and prevent conflicts between local items and remote ones. Extending this functionality will allow us to resolve local users from memory.
Not only this, we need to store information permanently that goes beyore what /etc/passwd and similar can store. It would be damaging to users if their CTAP2 (passkeys) were deleted randomly on a cache clear!
Local Group Extension
An often requested feature is the ability to extend a local group with the members from a remote group. Often people attempt to achieve this by "overloading" a group remotely such as creating a group called "wheel" in Kanidm and then attempting to resolve it on their systems. This can introduce issues as different distributions may have the same groups but with different gidNumbers which can break systems, or it can cause locally configured items to be masked.
Instead, we should allow group extension. A local group can be nominated for extension, and paired to a remote group. For example this could be configured as:
[group."wheel"]
extend_from = "opensuse_wheel"
This allows the local group "wheel" to be resolved and extended with the members from the remote
group opensuse_wheel
.
Multiple Resolvers
We would like to support multiple backends simultaneously and in our source tree. This is a major motivator of this rework as the himmelblau project wishes to contribute their client layer into our source tree, while maintaining the bulk of their authentication code in a separate libhimmelblau project.
We also want to support LDAP and other resolvers too.
The major challenge here is that this shift the cache state from the daemon to the client. This requires each client to track it's own online/offline state and to self-handle that state machine correctly. Since we won't allow dynamic modules this mitigates the risk as we can audit all the source of interfaces committed to the project for correct behaviour here.
Resolvers That Can't Resolve Without Authentication Attempts
Some resolvers are unable to resolve accounts without actually attempting an authentication attempt such as Himmelblau. This isn't a limitation of Himmelblau, but of Azure AD itself.
This has consequences on how we performance authentication flows generally.
Domain Joining of Resolvers
Some Clients (and in the future Kanidm) need to be able to persist some state related to Domain Joining, where the client registers to the authentication provider. This provides extra functionality beyond the scope of this document, but a domain join work flow needs to be possible for the providers in some manner.
Encrypted Caches
To protect caches from offline modification content should be optionally encrypted / signed in the future.
CTAP2 / TPM-PIN
We want to allow local authentication with CTAP2 or a TPM with PIN. Both provide stronger assurances of both who the user is, and that they are in possession of a specific cryptographic device. The nice part of this is that they both implement hardware bruteforce protections. For soft-tpm we can emulate this with a strict bruteforce lockout prevention mechanism.
The weakness is that PIN's which are used on both CTAP2 and TPM, tend to be shorter, ranging from 4 to 8 characters, generally numeric. This makes them unsuitable for remote auth.
This means for SSH without keys, we must use a passphrase or similar instead. We must not allow SSH auth with PIN to a TPM as this can easily become a remote DOS via the bruteforce prevention mechanism.
This introduces it's own challenge - we are now juggling multiple potential credentials and need to account for their addition and removal, as well as changing.
Another significant challenge is that linux is heavily embedded in "passwords as the only factor" meaning that many systems are underdeveloped like gnome keyring - this expects stacked pam modules to unlock the keyring as it proceeds.
Local Users
Local Users will expect on login equivalent functionality that /etc/passwd
provides today, meaning
that local wallets and keyrings are unlocked at login. This necesitates that any CTAP2 or TPM unlock
need to be able to unlock the keyring.
This also has implications for how users expect to interact with the feature. A user will expect that changing their PIN will continue to allow unlock of their system. And a change of the users password should not invalidate their existing PIN's or CTAP devices. To achieve this we will need some methods to cryptographically protect credentials and allow these updates.
To achieve this, we need to make the compromise that the users password must be stored in a
reversible form on the system. Without this, the various wallets/keyrings won't work. This trade is
acceptable since pam_kanidm
is already a module that handles password material in plaintext, so
having a mechanism to securely retrieve this while the user is entering equivalent security
material is reasonable.
The primary shift is that rather than storing a kdf/hash of the users output, we will be storing an authenticated encrypted object where valid decryption of that object is proof that the password matches.
For the soft-tpm, due to PIN's short length, we will need to aggressively increase the KDF rounds and consider HMAC of the output.
HMAC-Secret
Password PIN output
│ │ │
│ │ │
│ │ │
▼ ▼ ▼
┌──────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ │ │ │ │ │
│ KDF │ │ PIN Object │ │ CTAP Object │
│ │ │ │ │ │
└──────────────────┘ └─────────────────┘ └─────────────────┘
│ │ ▲ │ ▲
│ │ │ │ │
│ Releases │ │
├───────KDF value──────┴─────┼───────────────┘ │
│
│ │ │
▼
┌──────────────────┐ │ │
│ │
│ Sealed Object │ │ │
│ │─ ─ ─ ─Unlocks─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
│ │
└──────────────────┘
│
Release
Password
│
▼
┌──────────────────┐
│ │
│pam_gnome_keyring │
│ pam_kwallet │
│ │
└──────────────────┘
Remote Users (such as Kanidm)
After a lot of thinking, the conclusion we arrived at is that trying to handle password stacking for later pam modules is out of scope at this time.
Initially, remote users will be expected to have a password they can use to access the system. In the future we may derive a way to distribute TPM PIN objects securely to domain joined machines.
We may allow PINs to be set on a per-machine basis, rather than syncing them via the remote source.
This would require that a change of the password remotely invalidates set PINs unless we think of some way around this.
We also think that in the case of things like password managers such as desktop wallets, these should have passwords that are the concern of the user, not the remote auth source so that our IDM has no knowledge of the material to unlock these.
Challenges
- The order of backend resolvers needs to be stable.
- Accounts/Groups should not move/flip-flop between resolvers.
- Resolvers need to uniquely identify entries in some manner.
- The ability to store persistent/permananent accounts in the DB that can not be purged in a cache clear.
- Simplicity of the interfaces for providers so that they don't need to duplicate effort.
- Ability to clear single items from the cache rather than a full clear.
- Resolvers that can't pre-resolve items
New Architecture
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
┌───────┐ ┌───────┐ ┌───────┐ │ ┌───────────────────┐ │
│ │ │ │ │ │ │ │
│ NSS │ │ PAM │ │ CLI │ │ │ Tasks Daemon │ │
│ │ │ │ │ │ │ │
└───────┘ └───────┘ └───────┘ │ └───────────────────┘ │
▲ ▲ ▲ ▲
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ─│─ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ─ ─ ─ ┤
▼ ▼ ▼ │
│ ┌─────────────────────────────┐ ┌───────────┐ │
│ │ │ │
│ │ ClientCodec │ │ Tasks │ │
┌──────────┐ │ │ │ │
│ │ │ └─────────────────────────────┘ └───────────┘ │
│ Files │◀────┐ ▲ ▲
│ │ │ │ │ │ │
└──────────┘ │ ▼ │
│ ┌───────────────┐│ ┌────────────────────────────────┐ │ │
│ │└─────┤ │ │
│ │ Kani Client │◀─┬──▶│ Daemon / Event Loop │─────┘ │
│ │ │ │ │
│ └───────────────┘◀─│┐ └────────────────────────────────┘ │
┌───────────────┐ │ ▲
│ │ │ ││ │ │
│ LDAP Client │◀─┤ ▼
│ │ │ ││ ┌────────┐ ┌──────────────────┐ │
└───────────────┘◀ ┼ │ │ │ │
│ ┌───────────────┐ │└ ─ ─│ TPM │ │ DB / Cache │ │
│ Himmleblau │ │ │ │ │ │
│ │ Client │◀─┘ └────────┘ └──────────────────┘ │
│ │
└ ┴───────────────┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴
Online/Offline State Machine
The major change that that this diagram may not clearly show is that the online/offline state machine moves into each of the named clients (excluding files). This also has some future impacts on things like pre-emptive item reloading and other task scheduling. This will require the backends to "upcall" into the daemon, as the TPM transaction needs to be passed from the daemon back down to the provider. Alternately, the provider needs to be able to register scheduled tasks into the daemon with some generic interface.
Resolution Flow
The most important change is that with multiple resolvers we need to change how accounts resolve. In pseudo code the "online" flow (ignoring caches) is:
if files.contains(item_id):
if item_id.is_extensible:
# This only seeks items from the providers, not files for extensibility.
item += resolver.get(item_id.extended_from)
return item
# Providers are sorted by priority.
for provider in providers:
if provider.contains(item_id)
return item
return None
Key points here:
- One provider is marked as "default".
- Providers are sorted by priority from highest to lowest.
- Default always sorts as "highest".
- The default provider returns items with Name OR SPN.
- Non-default providers always return by SPN.
Once at item is located it is then added to the cache. The provider marks the item with a cache timeout that the cache respects. The item is also marked to which provider is the origin of the item.
Once an item-id exists in the cache, it may only be serviced by the corresponding origin provider. This prevents an earlier stacked provider from "hijacking" an item from another provider. Only if the provider indicates the item no longer exists OR the cache is cleared of that item (either by single item or full clear) can the item change provider as the item goes through the general resolution path.
If we consider these behaviours now with the cache, the flow becomes:
def resolve:
if files.contains(item_id):
if item_id.is_extensible:
# This only seeks items from the providers, not files for extensibility.
item += resolver.get(item_id.extended_from)
return item
resolver.get(item_id)
def resolver.get:
# Hot Path
if cache.contains(item):
if item.expired:
provider = item.provider
# refresh if possible
let refreshed_item = provider.refresh(item)
match refreshed_item {
Missing => break; # Bail and let other providers have at it.
Offline => Return the cached item
Updated => item = refreshed_item
};
return item
# Cold Path
#
# Providers are sorted by priority. Default provider is first.
#
# Providers are looped *only* if an item isn't already in
# the cache in some manner.
let item = {
for provider in providers:
if provider.contains(item_id)
if provider.is_default():
item.id = name || spn
else:
item.id = spn
break item
}
cache.add(item)
return None
Cache and Database Persistence
The existing cache has always been considered ephemeral and able to be deleted at any time. With a move to Domain Join and other needs for long term persistence our cache must now also include elements that are permanent.
The existing cache of items also is highly limited by the fact that we "rolled our own" db schema and rely on sqlite heavily.
We should migrate to a primarily in-memory cache, where sqlite is used only for persistence. The sqlite content should be optionally able to be encrypted by a TPM bound key.
To obfuscate details, the sqlite db should be a single table of key:value where keys are uuids associated to the item. The uuid is a local detail, not related to the provider.
The cache should move to a concread based concurrent tree which will also allow us to multi-thread the resolver for high performance deployments. Mail servers is an often requested use case for Kanidm in this space.
Extensible Entries
Currently UserToken and GroupTokens are limited and are unable to contain provider specific keys. We should allow a generic BTreeMap of Key:Values. This will allow providers to extend entries as required
Offline Password/Credential Caching
The caching of credentials should move to be a provider specific mechanism supported by the presence of extensible UserToken entries. This also allows other types of credentials to be stored that can be consumed by the User.
Alternate Credential Caching
A usecase is that for application passwords a mail server may wish to cache and locally store the application password. Only domain joined systems should be capable of this, and need to protect the application password appropriately.
Python Module
So far it includes:
- asyncio methods for all calls, leveraging aiohttp
- every class and function is fully python typed (test by running
make test/pykanidm/mypy
) - test coverage for 95% of code, and most of the missing bit is just when you break things
- loading configuration files into nice models using pydantic
- basic password authentication
- pulling RADIUS tokens
TODO: a lot of things.
Setting up your dev environment.
Setting up a dev environment can be a little complex because of the mono-repo.
- Install poetry:
python -m pip install poetry
. This is what we use to manage the packages, and allows you to set up virtual python environments easier. - Build the base environment. From within the
pykanidm
directory, run:poetry install
This'll set up a virtual environment and install all the required packages (and development-related ones) - Start editing!
Most IDEs will be happier if you open the kanidm_rlm_python or pykanidm directories as the base you are working from, rather than the kanidm repository root, so they can auto-load integrations etc.
Building the documentation
To build a static copy of the docs, run:
make docs/pykanidm/build
You can also run a local live server by running:
make docs/pykanidm/serve
This'll expose a web server at http://localhost:8000.
RADIUS Module Development
Release Checklist
Pre-Reqs
cargo install cargo-audit
cargo install cargo-outdated
cargo install cargo-udeps
cargo install cargo-machete
Pre Release Check List
Start a release
- git checkout -b YYYYMMDD-pre-release
Cargo Tasks
-
Update MSRV if applicable
-
cargo update
-
RUSTC_BOOTSTRAP=1 cargo udeps
-
cargo machete
-
cargo outdated -R
-
cargo audit
-
cargo test
-
setup a local instance and run orca (TBD)
-
store a copy an an example db (TBD)
Code Changes
- upgrade crypto policy values if required
- check for breaking db entry changes.
Administration
-
Update
RELEASE_NOTES.md
-
Update
README.md
- cargo test
- git commit -a -m "Release Notes"
- git push origin YYYYMMDD-pre-release
- Merge PR
Git Management
-
git checkout master
-
git pull
-
git checkout -b 1.x.0 (Note no v to prevent ref conflict)
-
update version to set pre tag in ./Cargo.toml
-
git commit -m "Release 1.x.0-pre"
-
git tag v1.x.0-pre
-
Final inspect of the branch
-
git push origin 1.x.0 --tags
-
github -> Ensure release branch is protected
Follow up
- git checkout master
- git pull
- git checkout -b YYYYMMDD-dev-version
- update version to +1 and add dev tag in ./Cargo.toml
-
update
DOMAIN_*_LEVEL
in server/lib/src/constants/mod.rs - update and add new migrations
Final Release Check List
Git Management Part Deux
-
git checkout 1.x.0
-
git pull origin 1.x.0
-
update version to remove pre tag in ./Cargo.toml
-
update Makefile to set docker image to latest
-
git commit -a -m 'Release 1.x.0'
-
git tag v1.x.0
-
git push origin 1.x.0 --tags
-
github -> create new release based on tag (not branch) - use tag because then tools will get the tag + patches we apply.
Community
- Publish release announcement
Cargo publish
-
publish
kanidm_proto
-
publish
sketching
-
publish
kanidm_utils_users
-
publish
kanidm_lib_file_permissions
-
publish
kanidm_lib_crypto
-
publish
kanidm_build_profiles
-
publish
kanidm_client
-
publish
kanidm_tools
Docker
- docker buildx use cluster
-
make buildx
- Update the readme on docker https://hub.docker.com/repository/docker/kanidm/server
Distro
- vendor and release to build.opensuse.org
Packaging
This chapter presents the alternative packages and how to build your own.
To ease packaging for your distribution, the Makefile
has targets for sets of binary outputs.
Target | Description |
---|---|
release/kanidm | Kanidm's CLI |
release/kanidmd | The server daemon |
release/kanidm-ssh | SSH-related utilities |
release/kanidm-unixd | UNIX tools, PAM/NSS modules |
Debian / Ubuntu Packaging
Building packages
- Debian packaging is complex enough that it lives in a separate repository: kanidm/kanidm_ppa_automation.
- While official packages are available at https://kanidm.github.io/kanidm_ppa/ these instructions will guide you through replicating the same process locally, using cross & Docker to isolate the build process from your normal computer and allow building packages for multiple architectures.
- While the examples below will use
aarch64-unknown-linux-gnu
akaarm64
, the same process works forx86_64-unknown-linux-gnu
akaamd64
as well.
- Start in the root directory of the main kanidm/kanidm repository.
- Install cross:
cargo install cross
- Pull in the separate deb packaging submodule:
git submodule update platform/debian/kanidm_ppa_automation
- Launch your desired crossbuild target. Do note the script assumes you use rustup!
# See valid targets: platform/debian/kanidm_ppa_automation/scripts/crossbuild.sh # Launch a target: platform/debian/kanidm_ppa_automation/scripts/crossbuild.sh debian-12-aarch64-unknown-linux-gnu # You can also specify multiple targets within the same distribution: platform/debian/kanidm_ppa_automation/scripts/crossbuild.sh debian-12-{aarch64,x86_64}-unknown-linux-gnu
- Go get a drink of your choice while the build completes.
- Create a sacrificial deb builder container to avoid changing your own system:
docker run --rm -it -e CI=true \ --mount "type=bind,src=$PWD,target=/src" \ --workdir /src \ rust:bookworm
- In the container install dependencies with:
# The parameter given is which additional target debian architecture to enable (amd64, arm64, etc.) # If your native platform is amd64, running with arm64 is enough to cover both archs. platform/debian/kanidm_ppa_automation/scripts/install_ci_build_dependencies.sh arm64
- In the container launch the deb build:
platform/debian/kanidm_ppa_automation/scripts/build_debs.sh aarch64-unknown-linux-gnu # Again, multiple targets also work: platform/debian/kanidm_ppa_automation/scripts/build_debs.sh {aarch64,x86_64}-unknown-linux-gnu
- You can now exit the container, the package paths displayed at the end under
target
will persist.
Adding or amending a deb package
The rough overview of steps is:
- Add cargo-deb specific metadata to the rust package and any static assets. Submit your changes as a PR.
- Add build instructions to the separate packaging repo. Submit your changes as a PR.
- Go back to the main repo to update the packaging submodule reference to aid running manual dev builds of the new package.
In theory steps 1 & 3 could be done in one PR, but this way is simpler.
Configuration in the main repo
- The repo is: kanidm/kanidm
- Packages are primarily based on their package specific
Cargo.toml
definition read bycargo-deb
. For an example, seeunix_integration/resolver/Cargo.toml
- A package specific
debian
folder is used for static assets. See:unix_integration/resolver/debian
for an example. - The debian folder may house needed
postinst
,prerm
etc hook definitions. They must include the#DEBHELPER#
comment after any custom actions. - The package debian folder is also used for any systemd unit files. The file naming pattern is very specific, refer to
cargo-deb
documentation for details.
Configuration in the kanidm_ppa_automation repo
- The repo is: kanidm/kanidm_ppa_automation
- Changes are needed if a new binary and/or package is added, or if build time dependencies change.
- Amend
scripts/crossbuild.sh
build rules to include new binaries or packages with shared libraries. Search for the lines starting withcross build
. - Add any new build time system dependencies to
scripts/install_ci_build_dependencies.sh
, be aware of any difference in package names between Debian & Ubuntu. - Add any new packages to
scripts/build_debs.sh
, search for the line starting withfor package in
. - Finally, once your changes have been approved go back to the main
kanidm/kanidm
repo and update the submodule reference and PR the reference update. This is not needed for official builds but helps anyone doing dev builds themselves:cd platform/debian/kanidm_ppa_automation git pull cd - git add platform/debian/kanidm_ppa_automation git commit -m "Update kanidm_ppa_automation reference to latest"
Kanidm PPA Packages
-
The Kanidm PPA repository contains Debian & Ubuntu packages built from the main Kanidm repository.
-
Two separate components are available,
stable
for released versions andnightly
which only provides the latest bleeding edge, refreshed once a day. -
Packages are distributed for current LTS versions of Debian & Ubuntu that natively package the required dependencies;
- Ubuntu: 22.04 aka
jammy
& 24.04 akanoble
. - Debian 12 aka
bookworm
.
- Ubuntu: 22.04 aka
-
Please note that while the spirit of the commands below should also work on other Debian-based distributions, the codename detection will not work and you will need to manually choose which distribution is the closest to yours. The methods for adding repositories may also vary, for example Pop OS, requires an altered setup in line with their instructions.
Adding it to your system
Make sure you have a “trusted GPG” directory for storing signing keys.
sudo mkdir -p /etc/apt/trusted.gpg.d/
Download the Kanidm PPA GPG public key.
curl -s "https://kanidm.github.io/kanidm_ppa/kanidm_ppa.asc" \
| sudo tee /etc/apt/trusted.gpg.d/kanidm_ppa.asc >/dev/null
Add the Kanidm PPA to your local APT configuration, with autodetection of Ubuntu vs. Debian.
Please adjust accordingly if you want the nightly
component instead of the default stable
.
curl -s "https://kanidm.github.io/kanidm_ppa/kanidm_ppa.list" \
| grep $( ( . /etc/os-release && echo $VERSION_CODENAME) ) | grep stable \
| sudo tee /etc/apt/sources.list.d/kanidm_ppa.list
Update your local package cache.
sudo apt update
Listing Packages
Use apt search
to list the packages available:
apt search kanidm
Installing stable on top of nightly
If you previously had the alpha version kanidm nightly packages installed or are switching from nightly down to stable, it may be difficult to remove the previous versions safely without losing for example Kanidm backed sudo in the middle. This snippet is intended to help with that:
sudo bash <<EOT
dpkg --remove kanidm kanidm-unixd libnss-kanidm libpam-kanidm
apt install -y kanidm kanidm-unixd
EOT
If anything goes wrong during the snippet, you may need to fall back to other methods of gaining root to complete the transition!
Community Packages
There are several community maintained packages that you may use in your system. However, they are not officially supported and may not function identically.