feat!: implement complete password auth chain (+)
This is a final implementation, it completely works, is safe and audited. There are several changes overall (most explained in the README): - Added `debuggable` cargo feature to disable debug ability, reducing a bit library size. - Completed README with setup, concernsm, features and more. - Add `asdeny` argument to behave as a PAM barrier. Updated nixos configuration accordingly. - Modularity, separated config from API client and CURL client, each can take a `debug` option for a clean function call interface. - Unified logging methods, there is a current `e/println!` call in all the codebase, behind a macro, to allow for consistent log formatting. - Readability, separated and broke down cognitive complicated structures for ease of readability, methods are as simple as I could get them to be and reusable across `authentication` and `password` flows. - HTTP headers now are macro'ed to prevent typos and have unified casing and representations. - Simplified CURL interface so it integrates perfectly with rust type system for just what's necessary, it's more readable and loggable. - Checked PAM returned errors to see if they are sensible enough. - Checked this interacts with `pam_unix.so` as expected.
This commit is contained in:
@@ -4,9 +4,35 @@ Tiny PAM module to perform authentication against a keycloak instance.
|
||||
|
||||
Testing and development are done within a nix environment, if you don't plan to use nix extract the pieces you care about, but we only support nix configuration.
|
||||
|
||||
# Features
|
||||
|
||||
- Supports authentication and setting/changing one's password.
|
||||
- Can be restrained to only a group of users, so it won't interfere with system users.
|
||||
- Debuggable, if the option is set, you will get useful logs when using the module.
|
||||
- Error ready, extensive logging and descriptive errors to discover what and where went wrong.
|
||||
- Minimal, made in rust with a tiny dependency tree, avoiding unnecessary dynamic memory use, all methods are straight-forward and lightweight.
|
||||
- Reliable, made in rust without the use of `unwrap`, `expect`, slice indexing, etc; and graceful error handling.
|
||||
|
||||
> [!INFO]
|
||||
> Debuggability can be turned off with a rust feature flag to slightly decrease library size.
|
||||
|
||||
# System Configuration
|
||||
|
||||
This is being worked on, refer to the test configuration at [`tests/nixos/configuration.nix`](./tests/nixos/configuration.nix).
|
||||
For examples, refer to the test nix configuration at [`tests/nixos/configuration.nix`](./tests/nixos/configuration.nix).
|
||||
|
||||
To explain the basic PAM flow, I will assume some PAM knowledge, but still explain most details, refer to PAM documentation if you have doubts.
|
||||
|
||||
All parts of the module should have a PAM priority slightly prior to the `pam_unix.so` counterpart.
|
||||
|
||||
The PAM module can work as two rule types: `auth` and `password`; but both behave in a similar way. Both do different stuff, but I will call it their "action" as they match for most of the flow.
|
||||
|
||||
For correct behavior, the module has to be staged:
|
||||
|
||||
- The first stage is `sufficient`, if the user is meant to be handled (refer to the configuration) the PAM module will perform its action or fail.
|
||||
One issue with `sufficient` is that failure will fall through the next module (`pam_unix.so` if configured as intended).
|
||||
- To fix this, the module has to be loaded again as `requisite` (so failures don't trigger the rest of the `required` chain) with an argument that will make it behave differently, this will behave analogous to `pam_deny.so`, but only for users meant to be handled. Essentially acts as a barrier preventing keycloak users from falling to additional authentication methods.
|
||||
|
||||
This means that normal users exempt from keycloak authentication will use PAM as if keycloak wasn't there. But users meant to be handled won't be able to pass onto other authentication methods.
|
||||
|
||||
# Arguments
|
||||
|
||||
@@ -18,11 +44,21 @@ You can `cargo doc` to find the documentation for this at `keycloak_pam > args >
|
||||
|
||||
With the purpose of being tiny and minimal, the module uses libcurl for the few requests it makes, so HTTP/S and all networking behavior is offloaded to it.
|
||||
|
||||
This is also made in rust, which means default `String` and `&str` are guaranteed to be UTF-8 encoded and invalid UTF-8 input might fail. I did try to use OS and C strings for all input, but I recommend still avoiding non-UTF8 input. Password change needs to convert to `String` at some point and UTF-8 input is required.
|
||||
This is also made in rust, which means default `String` and `&str` are guaranteed to be UTF-8 encoded, and invalid UTF-8 input might fail. I did try to use OS and C strings for all input, but I recommend still avoiding non-UTF8 input. For instance, currently, password change needs to convert to `String` at some point and UTF-8 input is required.
|
||||
|
||||
The code should **NEVER** panic (aside from memory management issues), no `unwrap`, `expect` or such methods should ever be used, slice indexing is avoided and errors are gracefully managed.
|
||||
|
||||
To check the group, keycloak-pam uses libc's `getgrnam_r`, this takes a buffer we hardcoded to 4 KiB, if there happens to be a group bigger than this, keycloak-pam won't be able to handle and will fail explaining this.
|
||||
To check the group, keycloak-pam uses libc's `getgrnam_r`, this takes a buffer we hardcoded to 4 KiB, if there happens to be a group bigger than this, keycloak-pam won't be able to handle it and will fail gracefully.
|
||||
|
||||
# Security
|
||||
|
||||
Important security concerns or questions might be:
|
||||
|
||||
- `faillock`: Faillock is a `required` PAM module that runs well before unix and this module. This service does not implement rate-limiting or similar mechanisms, but faillock should be there to handle this for us.
|
||||
- Setting a password as another user. This module doesn't check the user calling it, and it can be used to set password of uninitialized users, so if things were passed stright to the module, `passwd test` as `anotheruser` would just ask for a new password for `test`. Tested with `passwd` and **this is not the case**, PAM or `passwd` check the user you are trying to change the password to and your current one to prevent things like this.
|
||||
- Falling back to `pam_unix`, as explained in [configuration](#system-configuration), the module can be set up as a barrier to itself so failures of it in `sufficient` mode won't fall back to unix. If this were the case, it could create inconsistencies with `/etc/passwd` and keycloak, allowing arbitrary login with either module.
|
||||
|
||||
As the project evolves, it's important to revisit these concerns as it's a bit harder to guarantee them and could be potential breach points.
|
||||
|
||||
# Policy
|
||||
|
||||
@@ -34,20 +70,22 @@ Must check the subject user against the configured group. If it's not a member,
|
||||
|
||||
Here things differ a bit from login to password change:
|
||||
|
||||
## Login
|
||||
## Authentication
|
||||
|
||||
This is simple, will only allow login if the password used to log in is not temporary, any error will fail with an error.
|
||||
|
||||
The client used for this is still not definitive, currently it uses a client ID and client secret for this, `account` by default, but this might change.
|
||||
|
||||
## Registration
|
||||
## Password
|
||||
|
||||
Even then, a user might and should be able to login if their account is not set up (e.g. through ssh).
|
||||
Even then, a user might and should be able to login ito their unix account if their keycloak account is not set up (e.g. through ssh).
|
||||
|
||||
If they try to change their password without having one set up, it won't ask for their current one and will use the admin REST API to set the first one, this one won't be a temporary password and will fully set up their accounts.
|
||||
|
||||
# Behavior
|
||||
|
||||
## User Authentication
|
||||
|
||||
To authenticate users, the module uses the endpoint `{BASE_URL}/realms/{realm}/protocol/openid-connect/token` as a configured client. This needs a client ID and its secret that you need to get and configure per realm.
|
||||
|
||||
A curl request you can use anywhere to test this is:
|
||||
@@ -65,6 +103,16 @@ The secret and id there are just an example (`account` is a client configurd by
|
||||
|
||||
This is the simplest way to check credentials but also has the consequence of leaking unhandled oauth2/OIDC user tokens. I'm unsure if this could have further consequences but it's a risk worth considering.
|
||||
|
||||
## Password Change
|
||||
|
||||
This is a bit more convoluted and frail.
|
||||
|
||||
First of all, when a user wants to change their password, this module fetches their current passwords to see if the user has any.
|
||||
|
||||
If it has some, it will authenticate the user in the same way that [`User Authentication`](#user-authentication) does, and if it succeeds, change the password in the same way a user with none would.
|
||||
|
||||
If the user has no password it will ask for one and use the admin API to `PUT` a new one with the [`reset-password`](https://www.keycloak.org/docs-api/latest/rest-api/index.html#_put_adminrealmsrealmusersuser_idreset_password) endpoint.
|
||||
|
||||
# Development
|
||||
|
||||
The nix test configuration relies on bridging the VM IP, for this it relies on the default `libvirt`'s interface and needs the `qemu-bridge-helper`. This is found in a nix shell hook.
|
||||
|
||||
Reference in New Issue
Block a user