# Roles and single sign-on

> Log people in through an LDAP directory or an OpenID Connect provider, and give them roles that say which commands and keys they can use.

URL: https://docs.hotkv.com/operate/iam/


[ACL users](https://docs.hotkv.com/security/index.md#acl) have a password you keep on the server. With
**IAM** (identity and access management) people log in with the account they
already have, through your **LDAP** directory (OpenLDAP, Active Directory) or an
**OpenID Connect** identity provider (Okta, Microsoft Entra ID, Keycloak,
Google and others), and what they can do comes from **roles** you define once.
IAM needs the `enterprise-iam` license feature and is for On-Premise
installations.

A login runs as its roles and as nothing else. Each role is a set of commands
and keys, checked the way ACL rules are, with the same
`NOPERM` errors and the same entries in [ACL LOG](https://docs.hotkv.com/commands/acl-log/index.md). A
connection that does not log in through IAM (the `default` user, an ACL user)
is checked exactly as before.

> **Caution: Turn the default user off**
> A connection that never logs in is the `default` user, which can do
> everything unless you change that. Once logins are set up, run
> `ACL SETUSER default off` (or give `default` a password) so that logging in is
> the only way in. HotKV logs a warning at start if the default user is open and
> IAM logins are on.

## How a login works

1. The client connects and logs in. For an LDAP user that is
   `AUTH alice password`, which every client library can send at connect. For
   a token it is `IAM.AUTH oidc - <token>`.
2. HotKV asks the directory or the provider whether the credentials are right.
   The client waits; other clients are served meanwhile.
3. HotKV works out the roles of the login: the roles given to the user name,
   to `ldap:name` or `oidc:name`, and to each group the login is in
   (`group:name`).
4. A login with at least one role becomes a **session**, and the connection
   runs as an identity named after it, such as `ldap:alice#7`
   ([ACL WHOAMI](https://docs.hotkv.com/commands/acl-whoami/index.md) shows it, and so do `CLIENT LIST` and the
   audit log). A login with no role is refused, with `NOPERM`.

Changes apply at once. When you take a role from a user, change it, or delete
it, the sessions that had it lose what it gave on their next command, on every
node of a cluster.

## Roles

```
IAM.ROLE.CREATE app "@read,@write,@connection,-@dangerous" KEY_PREFIXES app:
IAM.ROLE.CREATE reporting "get,mget,scan,ping" KEY_PREFIXES report:,shared:* RATE_LIMIT 500
IAM.ROLE.CREATE office "@all" IP_ALLOWLIST 10.0.0.0/8,fd00::/8
```

The second argument lists what the role allows, separated by commas:

| Entry | Meaning |
| --- | --- |
| `get`, `config\|get` | Allow a command, or one subcommand of it |
| `@read`, `@write`, `@connection`, `@all` | Allow a [category of commands](https://docs.hotkv.com/security/index.md#acl) (`*` is `@all`) |
| `-flushall`, `-@dangerous` | Never allow it, whatever else the role allows |

`KEY_PREFIXES` lists the keys the role reaches. An entry with no pattern
character is a prefix (`app:` means `app:*`), and one with `*`, `?` or `[` is a
pattern. Channels follow the keys: a role limited to `app:` can publish and
subscribe on `app:` channels only. With no `KEY_PREFIXES`, the role reaches all
keys. `RATE_LIMIT` is commands per second for all the connections of one login
together. `IP_ALLOWLIST` lists the addresses or networks (CIDR) a login must
come from to have the role; from anywhere else it does not get it.

Most client libraries send a few commands of their own at connect (`PING`,
`CLIENT SETINFO`, `SELECT`), so an application role usually wants `@connection`.

A role is checked when you create it: an unknown command or category, a bad
network, or a role that allows nothing is an error. So is a role that would let
its users run HotKV's administrative commands that change the server, such as
`IAM.USER.ASSIGN` or `TENANT.CREATE`, which `@write` includes: that would let
them give themselves any role. Remove them with `-@admin` or `-@dangerous`, or
name `@admin` or `@all` if an administrator's role is what you mean.
`IAM.ROLE.INFO` shows a role in the form you created it, `IAM.ROLE.LIST` lists
them, and `IAM.ROLE.DELETE` removes one.

Every server has two roles to begin with: `admin` (every command on every key)
and `readonly` (the read and connection commands, on every key, and none of the
administrative ones).

> **Note**
> A login with several roles may do what any one of them allows, and each role
> keeps its own keys. A role that allows `get` on `a:*` and another that allows
> `set` on `b:*` give `GET a:1` and `SET b:1`, but not `SET a:1`, and not
> `MGET a:1 b:1`.

## Who gets which role

```
IAM.USER.ASSIGN alice app
IAM.USER.ASSIGN group:developers app
IAM.USER.ASSIGN "group:cn=ops,ou=groups,dc=example,dc=com" admin
IAM.USER.ASSIGN oidc:bob reporting
IAM.USER.UNASSIGN alice app
```

The first argument is one of:

| Form | Matches |
| --- | --- |
| `alice` | The user of that name, whichever way they logged in |
| `ldap:alice`, `oidc:alice` | The user, only through that source |
| `group:name` | Everyone the directory or the token lists in that group, by its name (`cn=ops,...` is `ops`) or by its full DN |

`IAM.USER.LIST` shows who has what. Names are matched exactly as typed at
login, including case.

## LDAP

```
iam.ldap.enabled yes
iam.ldap.url ldaps://ldap.example.com
iam.ldap.bind-dn "cn=hotkv,ou=service,dc=example,dc=com"
iam.ldap.bind-password "..."
iam.ldap.search-base "ou=people,dc=example,dc=com"
```

HotKV finds the user with a search (as the service account `bind-dn`), then
checks the password by logging in to the directory as that user. The directory's certificate is checked against
`iam.ldap.ca-file` (the system's authorities if you give none) and against the
host in the URL. With `ldap://` HotKV upgrades the connection with STARTTLS
before any password is sent. For Active Directory, set
`iam.ldap.user-filter "(sAMAccountName={username})"`.

`AUTH <user> <password>` goes to the directory for a name the ACL does not
have. A name the ACL has is always the ACL's, so an LDAP user can never take
the place of an ACL user. `HELLO ... AUTH` takes ACL users only: log in with
`AUTH` or `IAM.AUTH ldap <user> <password>` instead.

| Directive | Default | CONFIG SET | Description |
| --- | --- | --- | --- |
| `iam.ldap.enabled` (TOML `[iam.ldap] enabled`; type yes/no) | `no` | No | Turn LDAP login on (`AUTH user password` for a name the ACL does not have, and `IAM.AUTH ldap`). |
| `iam.ldap.url` (TOML `[iam.ldap] url`; type string) | `(empty)` | No | `ldap://host[:389]` (upgraded to TLS with STARTTLS before any password is sent) or `ldaps://host[:636]`. |
| `iam.ldap.bind-dn` (TOML `[iam.ldap] bind_dn`; type string) | `(empty)` | No | The service account that searches for the user. Empty means an anonymous search, which many directories refuse. |
| `iam.ldap.bind-password` (TOML `[iam.ldap] bind_password`; type string) | `(empty)` | No | Password of `iam.ldap.bind-dn`. |
| `iam.ldap.search-base` (TOML `[iam.ldap] search_base`; type string) | `(empty)` | No | Where users are searched for, in the whole subtree below it. |
| `iam.ldap.user-filter` (TOML `[iam.ldap] user_filter`; type string) | `(uid={username})` | No | The search that finds one user. `{username}` is the login name, escaped. For Active Directory use `(sAMAccountName={username})`. |
| `iam.ldap.user-dn-template` (TOML `[iam.ldap] user_dn_template`; type string) | `(empty)` | No | Instead of a search, the DN of every user, such as `uid={username},ou=people,dc=example,dc=com` (`{username}` is escaped as a DN value). No service account is needed. |
| `iam.ldap.group-attribute` (TOML `[iam.ldap] group_attribute`; type string) | `memberOf` | No | The attribute of the user's entry that lists their groups (`memberOf` in Active Directory and OpenLDAP with the memberof overlay). Empty means none. |
| `iam.ldap.group-search-base` (TOML `[iam.ldap] group_search_base`; type string) | `(empty)` | No | For a directory with no group attribute: search below this base for the groups that list the user. Empty turns the group search off. |
| `iam.ldap.group-filter` (TOML `[iam.ldap] group_filter`; type string) | `(member={dn})` | No | The group search. `{dn}` is the user's DN, escaped. |
| `iam.ldap.group-name-attribute` (TOML `[iam.ldap] group_name_attribute`; type string) | `cn` | No | The attribute that names a group found by the group search. |
| `iam.ldap.start-tls` (TOML `[iam.ldap] start_tls`; type yes/no) | `yes` | No | Upgrade `ldap://` to TLS before any password is sent. `no` sends passwords in the clear. `ldaps://` is TLS from the first byte either way. |
| `iam.ldap.tls-verify` (TOML `[iam.ldap] tls_verify`; type yes/no) | `yes` | No | Check the directory's certificate against `iam.ldap.ca-file` and the host in the URL. |
| `iam.ldap.ca-file` (TOML `[iam.ldap] ca_file`; type path) | `(empty)` | No | Certificate authorities that vouch for the directory (PEM). Empty means the system's. |
| `iam.ldap.timeout-ms` (TOML `[iam.ldap] timeout_ms`; type milliseconds) | `5000` | No | Time allowed for a whole login: connecting, TLS and every reply. |
| `iam.ldap.cache-ttl` (TOML `[iam.ldap] cache_ttl`; type seconds) | `300` | No | How long a password the directory accepted is remembered. `0` means every login asks the directory. A changed password or group applies after at most this long. |

## OpenID Connect

```
iam.oidc.enabled yes
iam.oidc.issuer https://login.example.com/
iam.oidc.audience hotkv
iam.oidc.jwks-uri https://login.example.com/.well-known/jwks.json
```

The client gets a token from your provider, then sends it:

```
IAM.AUTH oidc - <token>
```

HotKV accepts a token only if all of this holds: it is signed with a key the
provider publishes at `jwks-uri` (RS256, RS384, RS512, PS256, PS384, PS512,
ES256, ES384 or ES512), its `iss` is `iam.oidc.issuer` exactly, its `aud` names
`iam.oidc.audience`, it has not expired (`exp`) and is already valid (`nbf`).
The name after `oidc` can be `-`, or the name the token must carry.

The session ends when the token's `exp` passes, and the connection is closed;
the client gets a new token and logs in again.

| Directive | Default | CONFIG SET | Description |
| --- | --- | --- | --- |
| `iam.oidc.enabled` (TOML `[iam.oidc] enabled`; type yes/no) | `no` | No | Turn OpenID Connect login on (`IAM.AUTH oidc`). |
| `iam.oidc.issuer` (TOML `[iam.oidc] issuer`; type string) | `(empty)` | No | The `iss` a token must have, exactly as the provider writes it. |
| `iam.oidc.audience` (TOML `[iam.oidc] audience`; type string) | `(empty)` | No | The `aud` a token must name: this server's client ID at the provider. Required: without it OIDC login stays off. |
| `iam.oidc.jwks-uri` (TOML `[iam.oidc] jwks_uri`; type string) | `(empty)` | No | Where the provider publishes the keys its tokens are signed with. `https://` only. |
| `iam.oidc.jwks-file` (TOML `[iam.oidc] jwks_file`; type path) | `(empty)` | No | A file with the same key set, instead of `iam.oidc.jwks-uri`: for a provider you cannot reach, or keys you pin. |
| `iam.oidc.username-claim` (TOML `[iam.oidc] username_claim`; type string) | `sub` | No | The claim that names the user. `preferred_username` and `email` are common. |
| `iam.oidc.groups-claim` (TOML `[iam.oidc] groups_claim`; type string) | `groups` | No | The claim that lists the user's groups (a list, or one string). |
| `iam.oidc.leeway` (TOML `[iam.oidc] leeway`; type seconds) | `60` | No | How far a clock may be off, for the `exp` and `nbf` of a token. |
| `iam.oidc.jwks-cache-ttl` (TOML `[iam.oidc] jwks_cache_ttl`; type seconds) | `3600` | No | How long the provider's keys are kept before they are fetched again. A token signed with a key HotKV does not know fetches them at once, at most every ten seconds. |
| `iam.oidc.timeout-ms` (TOML `[iam.oidc] timeout_ms`; type milliseconds) | `5000` | No | Time allowed for fetching the provider's keys. |
| `iam.oidc.ca-file` (TOML `[iam.oidc] ca_file`; type path) | `(empty)` | No | Certificate authorities that vouch for the provider's HTTPS certificate (PEM). Empty means the system's. |

## Sessions

`IAM.SESSION.LIST` shows every open session on the node: who, from where,
through which source, with which roles, and when it ends. A session ends when
its connection closes, when its token expires, when the client logs in again
(with `AUTH`, `IAM.AUTH` or `RESET`), and when you end it:

```
IAM.SESSION.KILL 7        # one session, and its connection is closed
IAM.SESSION.KILL alice    # every session of a user
```

Sessions belong to the node they are on. Roles and who has them are the same
on every node of a [cluster](https://docs.hotkv.com/clustering/index.md): a change made on one node reaches
the others within about a second.

## Guarding against guessing

An address that fails `iam.max-login-failures` logins (default 10) in a
minute is turned away for the rest of it. Failed logins are in
[ACL LOG](https://docs.hotkv.com/commands/acl-log/index.md) and count in `acl_access_denied_auth`. The
password or token of `IAM.AUTH` is never shown by `SLOWLOG` or `MONITOR`. If
many clients share one address, for instance behind a proxy, raise the limit or
set it to `0`.

At most `iam.auth-queue` logins (default 64) wait for the `iam.auth-threads`
threads (default 2) that ask the directory or the provider. More are told
`BUSY`. A directory that does not answer is given up on after its timeout.

| Directive | Default | CONFIG SET | Description |
| --- | --- | --- | --- |
| `iam.auth-threads` (TOML `[iam] auth_threads`; type integer) | `2` | No | Threads that ask a directory or an identity provider whether a login is right, so a slow one never holds a thread that serves clients. |
| `iam.auth-queue` (TOML `[iam] auth_queue`; type integer) | `64` | No | Logins that may wait for one of those threads. More are told `BUSY`. |
| `iam.max-login-failures` (TOML `[iam] max_login_failures`; type integer) | `10` | No | Failed `IAM.AUTH` from one address in a minute, after which that address is turned away for the rest of the minute. `0` means no limit. |

In `hotkv.toml` the settings are under `[iam]`, `[iam.ldap]` and `[iam.oidc]`,
without the prefix and with underscores (`bind_dn`, `search_base`,
`jwks_uri`, `auth_threads`, ...).

## What is not supported

- **A memory limit on a role.** Nothing counts the memory of one login. Use
  `maxmemory` for the server.
- **Tokens in `AUTH` and `HELLO`.** A token goes in `IAM.AUTH`. `HELLO ... AUTH`
  takes ACL users only.

## Next steps

- [Security](https://docs.hotkv.com/security/index.md) for TLS, ACL users, encryption at rest and the audit log
- [IAM.ROLE.CREATE](https://docs.hotkv.com/commands/iam-role-create/index.md), [IAM.USER.ASSIGN](https://docs.hotkv.com/commands/iam-user-assign/index.md)
  and [IAM.AUTH](https://docs.hotkv.com/commands/iam-auth/index.md)