Hi all!
First, I know this PR looks scary, because it is. But I couldn't see …another way of making this module better without starting from scratch.
> Alternative names for this PR:
> * make the PAM module great again
> * ohno, I won't review this much
> * ah finally, but boy, is this big
Here is my solution to the current PAM problem in NixOS. The problem is described extensively in #90640 and all related issues and PRs. It basically boils down to the PAM module not being managed by module system, and thus integrates badly with other modules, resulting in PAM services being hardcoded by other modules (yes, I am looking at you display managers :eyes:, but you had no other choice :/).
##### Origin, requirements and blockers
This PR builds on top of #105098, which is a step forward, but still has some flaws regarding ordering and the ability to easily override something as a user. See my comments over there for what I think needs improving, and is basically the idea that started my work on this.
We also need #97023 to be able to use `mkRenamedOptionModule` inside submodules. This isn't a blocker for review, see below.
## Design
I'll start with how the PAM module can be interacted with. It is now divided into _top level_ options:
```
security.pam = {
modules = {
u2f = {
enable = true;
cue = true;
};
};
services = {
login = {
modules = {
u2f = { ... };
};
auth = {
u2f = {
control = "sufficient";
path = "pam_u2f.so";
args = [ "cue" ];
};
};
};
};
};
```
Under the hood, each module does... nothing! Actually, the toplevel `modules` configuration option only exists to provide default options to the modules defined in each service. And thus, the modules in the services do the work, meaning they will add entries to the relevant PAM types (`account`, `auth`, `password`, `session`) depending on the options that are set in it. To see how this is rigged, have a look inside `nixos/modules/security/pam/modules`. I guess the best explanation of how this all works at this point is with examples.
### Interacting with the PAM module
##### Creating a new service with the defaults
```
security.pam.services.newService = {};
```
##### A service overriding something in the modules
```
security.pam.services.newService = {
modules = {
rootOK = true;
};
};
```
##### Changing a module option for all services
```
security.pam.modules.u2f.enable = true;
```
##### Changing a module option for one service
```
security.pam.services.sshd.modules.u2f.enable = false;
```
##### Creating a custom service, using none of the defaults
This will be especially useful for display managers.
```
security.pam.services.myService = {
auth = {
myEntry = {
control = "sufficient";
path = "my_entry.so";
order = 100;
};
mySecondEntry = {
control = "required";
path = "my_second_entry.so";
order = 200;
};
};
# The following is required to explicitly remove any entries that are included by default, i.e. by the PAM modules.
excludeDefaults = [ "account" "auth" "password" "session" ];
# can also be written as excludeDefaults = utils.pam.entryTypes;
};
```
This will result in `/etc/pam.d/myService` containing:
```
auth sufficient my_entry.so
auth required my_second_entry.so
```
##### Changing the order in which the entries appear in the file
Looking at the orders table below, if we want to make `krb5` come before `ldap` and after `unix` in the `account` type for the `login` service, we can do something like this:
```
security.pam.services.login.account.krb5.order = mkForce 1500;
```
##### Include/substack
If we look at the `lightdm` PAM service on `master`, we can see it looks like this:
```
auth substack login
account include login
password substack login
session include login
```
To do the same with this PAM module, we can do the following:
```
security.pam.services.lightdm = {
# First let's get rid of the defaults.
excludeDefaults = [ "auth" "account" "password" "session" ];
# And then let's define our service the way we want it
auth.lightdm = {
control = "substack";
path = "login"; # no check is made on this
# even if we only have one entry, we still have to provide
# a default or the configuration will not evaluate
order = 1000;
};
account.lightdm = {
control = "include";
path = "login";
order = 1000;
};
password.lightdm = {
control = "substack";
path = "login";
order = 1000;
};
session.lightdm = {
control = "include";
path = "login";
order = 1000;
};
};
```
Which will generate the same PAM service file (modulo the order of the types (i.e. `account` will come before `auth`) and whitespace).
### Ordering of the PAM modules
This used to be done using `types.orderOf` (see #97392) and comparing the `order` value of each PAM entry, and then @roberth [suggested to me](https://github.com/NixOS/nixpkgs/pull/97392#issuecomment-736428795) that it could actually be done just by using `builtins.sort`. The lower that value, the higher the entry ends up in the file.
To avoid more breaking changes than necessary \s, I decided to keep the old order of PAM modules. More seriously, the goal is to leave users who don't interact with the PAM module alone, the interface might change, but they won't see it as they will get the same result.
<details>
<summary>Here's that table:</summary>
| Type | Module | Order |
|:--------:|:-------------------:|:------:|
| account | unix | 1000 |
| account | ldap | 2000 |
| account | sssd | 3000 |
| account | krb5 | 4000 |
| account | googleOsLoginDie | 10000 |
| account | googleOsLoginIgnore | 10500 |
| | | |
| auth | googleOsLogin | 10000 |
| auth | rootOK | 11000 |
| auth | requireWheel | 12000 |
| auth | logFailures | 13000 |
| auth | sshAgent | 14000 |
| auth | fprintd | 15000 |
| auth | p11 | 16000 |
| auth | u2f | 17000 |
| auth | usb | 18000 |
| auth | oath | 19000 |
| auth | yubico | 20000 |
| auth | unixRequired | 21000 |
| auth | ecryptfs | 22000 |
| auth | mount | 23000 |
| auth | kwallet | 24000 |
| auth | gnomeKeyring | 25000 |
| auth | gnupg | 26000 |
| auth | googleAuthenticator | 27000 |
| auth | duoSecurity | 28000 |
| auth | unix | 30000 |
| auth | otpw | 31000 |
| auth | ldap | 32000 |
| auth | sssd | 33000 |
| auth | krb5 | 34000 |
| auth | deny | 100000 |
| | | |
| password | unix | 1000 |
| password | ecryptfs | 2000 |
| password | mount | 3000 |
| password | ldap | 4000 |
| password | sssd | 5000 |
| password | krb5 | 6000 |
| password | gnomeKeyring | 10000 |
| | | |
| session | setEnvironment | 500 |
| session | unix | 1000 |
| session | setLoginUid | 2000 |
| session | makeHomeDir | 3000 |
| session | updateWtmp | 4000 |
| session | ecryptfs | 5000 |
| session | mount | 6000 |
| session | ldap | 7000 |
| session | sssd | 8000 |
| session | krb5 | 9000 |
| session | otpw | 10000 |
| session | startSession | 11000 |
| session | forwardXAuth | 12000 |
| session | limits | 13000 |
| session | motd | 14000 |
| session | appArmor | 15000 |
| session | kwallet | 16000 |
| session | gnomeKeyring | 17000 |
| session | gnupg | 18000 |
| session | lxcfs | 19000 |
</details>
The chosen values are complitely arbitrary, except that they conserve the current order of modules. They might seem excessive, I might have been over enthused, but at least they give people and us a lot of room to move stuff around.
### Some other improvements / stuff that changed
* PAM configuration has been regrouped under `nixos/modules/security/pam` and each PAM module has its own file under `modules/`, except for the PAM modules that have been moved to their related NixOS module
* The `fprintd` PAM module now uses `services.fprintd.package` instead of hardcoding `pkgs.fprintd`
* Some PAM modules names were changed to match their services names. If possible, the old names were preserved with `mkRenamedOptionModule`
* Some PAM modules' source code were moved to their related NixOS modules:
- `googleOsLogin
- `fprintd`
- `apparmor`
- `duosec`
- `gnome-keyring`
- `ldap`
- `krb5`
- `lxcfs`
### New and renamed PAM modules options
No new PAM modules or options have been added, as it is not the goal of this PR. However, due to the way this module is implemented, new options have appeared nonetheless, as modules are now configurable globally and per-service.
<details>
<summary>New options</summary>
* `security.pam.modules.apparmor.enable`
* `security.pam.modules.duosec.enable`
* `security.pam.modules.forwardXAuth`
* `security.pam.modules.fprintd.enable`
* `security.pam.modules.gnome-keyring.enable`
* `security.pam.modules.gnupg.*`
* `security.pam.modules.googleAuthenticator.enable`
* `security.pam.modules.googleOsLogin.*`
* `security.pam.modules.krb5.enable`
* `security.pam.modules.kwallet.enable`
* `security.pam.modules.logFailures`
* `security.pam.modules.lxcfs.enable`
* `security.pam.modules.makeHomeDir.enable`
* `security.pam.modules.motd.*`
* `security.pam.modules.requireWheel`
* `security.pam.modules.rootOK`
* `security.pam.modules.setEnvironment`
* `security.pam.modules.setLoginUid`
* `security.pam.modules.sssd.*`
* `security.pam.modules.startSession`
* `security.pam.modules.unix.*`
* `security.pam.services.<name>.account`
* `security.pam.services.<name>.auth`
* `security.pam.services.<name>.password`
* `security.pam.services.<name>.session`
* `security.pam.services.<name>.excludeDefaults`
* `security.pam.services.<name>.modules.ecryptfs.enable`
* `security.pam.services.<name>.modules.krb5`
* `security.pam.services.<name>.modules.ldap.enable`
* `security.pam.services.<name>.modules.limits`
* `security.pam.services.<name>.modules.lxcfs.enable`
* `security.pam.services.<name>.modules.makeHomeDir.skelDirectory`
* `security.pam.services.<name>.modules.motd.motdFile`
* `security.pam.services.<name>.modules.oath` (except `enable`)
* `security.pam.services.<name>.modules.p11` (except `enable`)
* `security.pam.services.<name>.modules.sssd.enable`
* `security.pam.services.<name>.modules.u2f` (except `enable`)
* `security.pam.services.<name>.modules.unix.debug`
* `security.pam.services.<name>.modules.yubico` (except `enable`)
</details>
<details>
<summary>Renamed options</summary>
* `security.pam.enableEcryptfs` -> `security.pam.modules.ecryptfs.enable`
* `users.ldap.loginPam` -> `security.pam.modules.ldap.enable`
* `security.pam.loginLimits` -> `security.pam.modules.limits`: type changed, no possibility of `mkRenamedOptionModule`
* `security.pam.makeHomeDir.skelDirectory` -> `security.pam.modules.makeHomeDir.skelDirectory`
* `security.pam.oath` -> `security.pam.modules.oath`
* `security.pam.enableOTPW` -> `security.pam.modules.otpw.enable`
* `security.pam.p11` -> `security.pam.modules.p11`
* `security.pam.mount` -> `security.pam.modules.pam_mount`
* `security.pam.enableSSHAgentAuth` -> `security.pam.modules.sshAgent.enable`
* `security.pam.services.<name>.sssdStrictAccess` -> `security.pam.services.<name>.modules.sssd.strictAccess`
* `security.pam.u2f` -> `security.pam.modules.u2f`
* `security.pam.usb.enable` -> `security.pam.modules.usb.enable`
* `security.pam.yubico` -> `security.pam.modules.yubico`
* `security.pam.services.<name>.enableAppArmor` -> `security.pam.services.<name>.apparmor.enable`
* `security.pam.services.<name>.duoSecurity.enable` -> `security.pam.services.<name>.modules.duosec.enable`
* `security.pam.services.<name>.forwardXAuth` -> `security.pam.services.<name>.modules.forwardXAuth`
* `security.pam.services.<name>.fprintAuth` -> `security.pam.services.<name>.modules.fprintd.enable`
* `security.pam.services.<name>.enableGnomeKeyring` -> `security.pam.services.<name>.modules.gnome-keyring.enable`
* `security.pam.services.<name>.gnupg` -> `security.pam.services.<name>.modules.gnupg`
* `security.pam.services.<name>.googleAuthenticator.enable` -> `security.pam.services.<name>.modules.googleAuthenticator.enable`
* `security.pam.services.<name>.googleOsLoginAccountVerification` -> `security.pam.services.<name>.modules.googleOsLogin.enableAccountVerification`
* `security.pam.services.<name>.googleOsLoginAuthentication` -> `security.pam.services.<name>.modules.googleOsLogin.enableAuthentication`
* `security.pam.services.<name>.enableKwallet` -> `security.pam.services.<name>.modules.kwallet.enable`
* `security.pam.services.<name>.logFailures` -> `security.pam.services.<name>.modules.logFailures`
* `security.pam.services.<name>.makeHomeDir` -> `security.pam.services.<name>.modules.makeHomeDir.enable`
* `security.pam.services.<name>.showMotd` -> `security.pam.services.<name>.modules.motd.enable`
* `security.pam.services.<name>.oathAuth` -> `security.pam.services.<name>.modules.oath.enable`
* `security.pam.services.<name>.otpwAuth` -> `security.pam.services.<name>.modules.otpw.enable`
* `security.pam.services.<name>.p11Auth` -> `security.pam.services.<name>.modules.p11.enable`
* `security.pam.services.<name>.pamMount` -> `security.pam.services.<name>.modules.pam_mount.enable`
* `security.pam.services.<name>.requireWheel` -> `security.pam.services.<name>.modules.requireWheel`
* `security.pam.services.<name>.rootOK` -> `security.pam.services.<name>.modules.rootOK`
* `security.pam.services.<name>.setEnvironment` -> `security.pam.services.<name>.modules.setEnvironment`
* `security.pam.services.<name>.setLoginUid` -> `security.pam.services.<name>.modules.setLoginUid`
* `security.pam.services.<name>.sshAgentAuth` -> `security.pam.services.<name>.modules.sshAgent.enable`
* `security.pam.services.<name>.startSession` -> `security.pam.services.<name>.modules.startSession`
* `security.pam.services.<name>.u2fAuth` -> `security.pam.services.<name>.u2fAuth`
* `security.pam.services.<name>.unixAuth` -> `security.pam.services.<name>.modules.unix.enableAuth`
* `security.pam.services.<name>.allowNullPassword` -> `security.pam.services.<name>.modules.unix.allowNullPassword`
* `security.pam.services.<name>.nodelay` -> `security.pam.services.<name>.modules.unix.nodelay`
* `security.pam.services.<name>.updateWtmp` -> `security.pam.services.<name>.modules.updateWtmp`
* `security.pam.services.<name>.usbAuth` -> `security.pam.services.<name>.modules.usb.enable`
* `security.pam.services.<name>.yubicoAuth` -> `security.pam.services.<name>.modules.yubico.enable`
</details>
## TODO
* [x] Add @kwohlfahrt as a co-author as I build on top of his changes
* [x] Allow other modules to integrate with PAM. This is done via submodules, see `nixos/modules/security/pam/modules/*` for examples
* [x] Keep the current order of PAM entries in order not to break existing configurations. I think this is done, I tripled checked it, but it wouldn't hurt if someone could check again. The current order of PAM modules is provided above, with the ordering values I chose. Checking that order against the current one could already be a huge help!
* [x] Refactor the modules in `nixos/modules/security/pam/modules` with a function that does the `submodule` part of the work. See https://github.com/NixOS/nixpkgs/pull/105319#issuecomment-735474918
* [x] Allow for creation of services with the defaults provided by the modules. See https://github.com/NixOS/nixpkgs/pull/105319#issuecomment-735470579
* [ ] Decide if we should `mkDefault` all the entries orders
* [ ] Add `mkRenamedOptionModule` imports, where relevant and possible. The list is available above, and we need #97023 to be able to `mkRenamedOptionModule` inside submodules. This isn't so much a blocker for review, as you can base that review on the list provided above.
* [ ] Write a proper commit message, this can wait the end when I squash all of them
### Services migration
To remove the `text = mkDefault [...]` in the `pamServiceSubmodule`, we have to migrate a bunch of modules that are still using the old interface. If this PR is accepted, I will create an issue with the following list and ping the respective maintainers of the modules so they can migrate, and I'll be happy to give them a hand.
* `services/x11/desktop-managers/gnome3.nix`
* `services/x11/display-managers/sddm.nix`
* `services/x11/display-managers/gdm.nix`
* `services/x11/display-managers/lightdm.nix`
* `services/networking/vsftpd.nix`
### Documentation
I still need to write some documentation for this, mainly release notes, and the PAM section of the manual. Most of this is just formatting and expanding the description of the PR above.
Here's a list of TODOs for the documentation:
* [x] Check that the module options display correctly.
* [ ] Write release notes
* [ ] Write a PAM section in the NixOS manual, probably under `III. Administration`
* [x] Update `nixos/doc/manual/development/writing-modules.xml` with the new path to the PAM module
## Testing
> Please, _please_, __please__, don't test this on your machine. Try it our in a VM before. As much as I have tested this, some edge cases might remain and you risk getting locked out of our machine. If that is the case, reboot and choose the configuration before you tried out those changes.
### How to test this
If you are using flakes, change your `nixpkgs` input to `github:rissson/nixpkgs/nixos-pam` and then `nix flake update --update-input nixpkgs && nixos-rebuild --flake .#myHost build-vm`.
If you're not, you can do something along the lines of `nixos-rebuild -I 'nixpkgs=https://github.com/rissson/nixpkgs/archive/nixos-pam.tar.gz build-vm`. I haven't tested this.
Then you can execute the commands it gives you to start the VM and try this out.
To make it a bit easier to report your finding, I made a small snippet you can copy paste when commenting on this PR (you can remove everything you didn't tick for a more concise output):
```
* [ ] I haven't any special PAM configuration.
* [ ] I have some special PAM config (please describe it).
* [ ] Types of auth tested
* [ ] username/password from `/etc/passwd` from a TTY. You won't test anything if you try this from a display manager, as it probably doesn't use the new PAM module.
* [ ] ssh
* [ ] krb5
* [ ] ldap
* [ ] sudo
* [ ] Other modules tested
* [ ] appArmor
* [ ] duoSecurity
* [ ] ecryptfs
* [ ] forwardXAuth
* [ ] fprintd
* [ ] gnomeKeyring
* [ ] gnupg
* [ ] googleAuthenticator
* [ ] googleOsLogin
* [ ] kwallet
* [ ] limits
* [ ] logFailures
* [ ] lxcfs
* [ ] makeHomeDir
* [ ] motd
* [ ] mount (pam_mount)
* [ ] oath
* [ ] otpw
* [ ] p11
* [ ] requireWheel
* [ ] rootOK
* [ ] setEnvironment
* [ ] setLoginUid
* [ ] sshAgent
* [ ] sssd
* [ ] startSession
* [ ] u2f
* [ ] updateWtmp
* [ ] usb
* [ ] yubico
* [ ] I ran NixOS tests. If yes, list them below, tick the box if they succeeded. If they didn't, provide the output.
```
I'll start:
* [x] I have some special PAM config (please describe it).
- PAM krb5 is enabled, but I'm not using it
- u2f is enabled and I use it. Its options are `cue = true; control = "sufficient";`
* [x] Types of auth tested
* [x] username/password from `/etc/passwd` from a TTY. You won't test anything if you try this from a display manager, as it probably doesn't use the new PAM module. Works for root, and for a user.
* [x] ssh
* [x] sudo
* [x] Other modules tested
* [x] limits
* [x] logFailures
* [x] motd
* [x] setEnvironment
* [x] startSession
* [x] u2f
* [x] lightdm-autologin
* [x] i3lock
* [x] I ran NixOS tests
* [x] `pam-u2f.nix`
* [x] `krb5`
* [x] `openssh.nix`
* [x] `ecryptfs.nix`
* [x] `lightdm.nix`
* [x] `pam-oath-login.nix`
##### CC.ing people
###### People part of the original conversation about reworking this module
@flokli @Rudi9719 @kwohlfahrt
###### People who previously worked or reviewed some related stuff
@Mic92 @aanderse
###### People working on the blockers for this PR
@Infinisil for `mkRenamedOptionModule` inside submodules