Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use structured settings for PAM configuration #105098

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from

Conversation

kwohlfahrt
Copy link
Contributor

Motivation for this change

Originally, this was brought on by the lack of support for the crypt option to pam_ldap, with no easy way of changing it. #90640 then provided motivation for a more thorough rework. So far, this PR keep the external interface, but replaces security.pam.services.<name>.text with .entries, consisting of a list of structured entries.

I'm opening the PR now for discussion, because this is a large change and @rissson was interested in working together.

TODO

  • Priorities for PAM service entries (see nixos/nsswitch: Make databases more configurable #85998 for reference)
  • Move service-specific options to their respective services, out of the PAM module
  • use mkRenamedOptionModule where appropriate
  • ??? any other changes we want to make to the interface

Things done

  • Tested using sandboxing (nix.useSandbox on NixOS, or option sandbox in nix.conf on non-NixOS linux)
  • Built on platform(s)
    • NixOS
    • macOS
    • other Linux distributions
  • Tested via one or more NixOS test(s) if existing and applicable for the change (look inside nixos/tests)
  • Tested compilation of all pkgs that depend on this change using nix-shell -p nixpkgs-review --run "nixpkgs-review wip"
  • Tested execution of all binary files (usually in ./result/bin/)
  • Determined the impact on package closure size (by running nix path-info -S before and after)
  • Ensured that relevant documentation is up to date
  • Fits CONTRIBUTING.md.

Coalesce lines where possible, to reduce the number of screens of
options
This is a more structured form, that will allow easier editing later.
@rissson
Copy link
Member

rissson commented Nov 27, 2020

ping @Rudi9719 @flokli who were part of the original discussion

I think this is already a huge step forward, i.e. we now have some structure for entries! Though I'm not too fond of the structure used (a list) to store those entries.

I would have imagined something like this:

security.pam.services.whatever = {
  auth = {
    "pam_unix.so" = {
      control = "required";
      extraArgs = [ "whatever" ];
    };
  };

  account = {
    /* ... */
  };

  password = {
    /* ... */
  };

  session = {
    /* ... */
  };
};

But this stops working when you want to use the same module several times, like pam_succeed_if.so to validate several conditions.
Also, overriding orders in a list is hard. For instance, if the default in nixpkgs is

# Let's put all of those in the session module, for the sake of the example
[
  { path = "pam_unix.so"; control = "required"; }
  { path = "pam_krb5.so"; control = "sufficient"; }
]

But I want to change that to invert the order, for whatever reason, how would I go about it? Overriding order in sets is easy, as we just have to change an order attribute to the required value, without having to rewrite the whole pam logic as we would have to for lists.

A solution to that would be to use sets like this:

  auth = {
    "unix" = {
      path = "pam_unix.so";
      control = "required";
      order = 10;
      extraArgs = [ "whatever" ];
    };
    "krb5" = {
      path = "pam_krb5.so";
      control = "sufficient";
      order = 20;
    };
  };

and then we could do something like security.pam.services.login.auth.krb5.order = 5; to easily change to order in which the modules get rendered to pam's config. I don't feel good neither bad with having an attribute name we just discard and that is only used for identifying the rule you want to change, but it is an idea and maybe a solution.

@rissson
Copy link
Member

rissson commented Nov 27, 2020

Also, you said that your current changes keep the external interface, but we shouldn't feel blocked by this. It isn't used widely in nixpkgs, and people tweaking it in their config would either be happy with an improvement of the pam situation, or they're already overriding the whole module like I do. Either way, it'll probably end up in the release notes anyway.

@aanderse
Copy link
Member

@rissson your suggestions are good 👍 The nginx module does the exact same thing, as you are aware, so your proposed changes fit in well with nixpkgs and provide the highest level of flexibility. Especially if combined with a small section in the manual on how to make customizations using this module, with some examples, this would be amazing.

@kwohlfahrt
Copy link
Contributor Author

Though I'm not too fond of the structure used (a list) to store those entries.

Agreed - I think we can do better here, I just wasn't sure enough on how to begin. In fact, the current implementation wouldn't necessarily solve my original issue ;) Note the proposed orderOf in #97392 might be applicable here. But until that is merged, the suggestion above seems reasonable.

My only question is how we should assign orderings to our default values - should we leave a gap (say 100) between each default entry? Or should we group them into blocks with the same priority? Perhaps certain rules should generally move together, and a more tree-like structure is called for (see include and submodule thoughts below)?

Regarding structuring the attributes as { auth = ...; session = ...; ... } with one key per module type - this seems reasonable, as long as the different module types are independent (i.e. auth rules can't interfere with password rules and vice versa) - I think they are, but this is not spelled out anywhere in the PAM guides.

I also plan to improve support for include and submodule rules. It might be interesting to explore a more tree-like organization, but I'm not enough of an expert with PAM to see if this would have any advantages at a glance?

@rissson
Copy link
Member

rissson commented Nov 28, 2020

Just so you know @kwohlfahrt, I started building something with orderOf on top of your PR. Will probably take another day before I have something to show.


{
makePAMService = name: service: let
argumentToString = value: if hasInfix " " value then "[${escape [ "]" ] value}]" else value;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need the escape here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nevermind, I RTFMed

Note, if you wish to include spaces in an argument, you should surround that argument with square brackets.

When using this convention, you can include [' characters inside the string, and if you wish to include a ]' character inside the string that will survive the argument parsing, you should use `]'. In other words: [..[..]..] --> ..[..]..

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it should be up to the user to escape any ] in a single argument that contains spaces. I'm guessing we will miss something here and it might break stuff for people. Let's not do backend stuff like this, users getting to this level of configuration already know about this or will find out from the manpage like I did.

Copy link
Member

@rissson rissson Nov 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I soooo wish I could think of a way to make this more Nixy than a list of strings. For example, if I want to override the umask in the mkHomeDir module, I'd have to override the whole list, and I wouldn't be able to do something like mkHomeDir.args.umask = "0027"; (random value, use at your own risk). Anyone else has a take on this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not do backend stuff like this, users getting to this level of configuration already know about this or will find out from the manpage like I did.

I disagree pretty strongly with this take, because accidentally adding an argument with spaces doesn't result in an error, it results in a silently different configuration (with that argument treated as multiple separate arguments). You clearly have been configuring PAM modules, and weren't aware of the need to escape spaces. If you had added an argument that contained spaces, it would have silently been accepted but then treated as multiple separate arguments, which could have resulted in an insecure configuration.

I'm also unable to think of a situation in which case us doing the escaping would cause trouble, apart from perhaps double-escaping. But I think people are more likely to read the nix option docs, where we can note that we do this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I soooo wish I could think of a way to make this more Nixy than a list of strings.

I agree here, but there are lots of options that are just a single string... 🤷 It'd be nice if they had to be key-value pairs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I have a thought. We can use types.attrsOf (types.either types.str types.bool). Then, if the value is bool, we add the key if it is true, omit the key if it is false. If the value is str, we use key=value syntax. Could this work?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't see how that would work. Could you give an example?
attrsOf (nullOr str) might be better. It would look like this, assuming we have a render function outputting the result from a single argument (name, value pair):

>>> render { name = "debug"; value = null; }
"debug"
>>> render { name = "umask"; value = "0027"; }
"umask=0027"

Usage:

args = {
  debug = null;
  umask = "0027";
};

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I missed this comment, I was thinking as follows:

args = {
  debug = true;
  umask = "0027";
}

which would render as "debug umask=0027". The render function would check whether the attr value is a string, in which case i renders as "key=value" or a boolean, in which case it renders "key" if the value is true, otherwise "". The advantage of this is it allows removing a key, by doing { args.debug = mkForce false; }, I'm not sure theres a nice way of unsetting a key from an attrset?

{
makePAMService = name: service: let
argumentToString = value: if hasInfix " " value then "[${escape [ "]" ] value}]" else value;
controlToString = value:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to use the apply arg to mkOption to achieve this

Copy link
Member

@rissson rissson Nov 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like this:

      control = mkOption {
        type = controlType;                                                                    
        apply = value: if isString value then value else
                       concatStringsSep " " (mapAttrsToList (n: v: "${n}=${toString v}") value);                      
      };                                                   
      args = mkOption {
        default = [];
        apply = value: concatStringsSep " " (map toString value);
      };

EDIT: it didn't work before

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nixos/modules/security/pam.nix Outdated Show resolved Hide resolved
{ control = "required"; path = "pam_deny.so"; }];
in listToAttrs (map (t: nameValuePair t rules) [ "auth" "account" "password" "session" ]);

# Most of these should be moved to specific modules.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder where those would end up...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the original intent was that the kerberos rules would live in their respective packages (e.g. kerberos, LDAP, etc).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed.

nixos/modules/security/pam.nix Outdated Show resolved Hide resolved
@kwohlfahrt
Copy link
Contributor Author

Just so you know @kwohlfahrt, I started building something with orderOf on top of your PR. Will probably take another day before I have something to show.

Oops, sorry for the merge conflicts :P

I'm interested to see what you come up with, the downside to using orderOf is that it makes it difficult to refer to existing rules (so I can't add an argument to an existing rule added by say the LDAP module). Having named rule-sets with priorities would allow this, so perhaps that's a better approach? Like this:

services.pam.foo.entries = {
  # This attr-name is semantically meaningful, it sets the module type
  auth = {
    # This attr-name is ignored
    ldapRules = {
      entry = { ...; }; # Can use include/substack for multiple rules in a block
      priority = 100;
    };
  };
};

I think combined with the include/substack support I just added, this could be promising?

@rissson
Copy link
Member

rissson commented Nov 28, 2020

I still have to figure out how to integrate the include/substack functionality of PAM in my implementation. I wonder if it is relevant to have it in NixOS. Maybe let's table that discussion for later? We won't lose any functionality anyway...

@rissson
Copy link
Member

rissson commented Nov 29, 2020

I'm interested to see what you come up with, the downside to using orderOf is that it makes it difficult to refer to existing rules (so I can't add an argument to an existing rule added by say the LDAP module). Having named rule-sets with priorities would allow this, so perhaps that's a better approach? Like this:

services.pam.foo.entries = {
  # This attr-name is semantically meaningful, it sets the module type
  auth = {
    # This attr-name is ignored
    ldapRules = {
      entry = { ...; }; # Can use include/substack for multiple rules in a block
      priority = 100;
    };
  };
};

I just included the priority (or order as I named it) inside the entry. So in my implementation, your example would look like this:

security.pam.services.foo = {
  auth = {
    ldap = {
      # control, path, ...
      order = 100;
    };
  };
};

I think combined with the include/substack support I just added, this could be promising?

I think include and substack should just be another control type, without enforcing anything. What if someone wants to do:

# This could come from any number of sources and doesn't have to be defined in NixOS configuration
environment.etc."pam.d/example".text = "wathever";

security.pam.services.myService = {
  auth = {
    myEntry = {
      control = "include";
      path = "example";
      order = 42;
    };
  };
};

@kwohlfahrt
Copy link
Contributor Author

I think include and substack should just be another control type, without enforcing anything. What if someone wants to do:

Passing a string path is supported, it just also supports passing more entries, in which case they will be written to a file and that will be included. They use the same attribute, so you can entries = "/foo/bar" or entries = { auth = ...; }

@rissson
Copy link
Member

rissson commented Nov 29, 2020

Passing a string path is supported, it just also supports passing more entries, in which case they will be written to a file and that will be included. They use the same attribute, so you can entries = "/foo/bar" or entries = { auth = ...; }

That's nice. But that means there would be two ways of using include/substack, and I don't think we want that. Also entries = "/foo/bar", is that include or substack, it isn't explicit.

@kwohlfahrt
Copy link
Contributor Author

kwohlfahrt commented Nov 30, 2020

My example wasn't clear, the directive is still explicitly required:

services.pam.foo.entries = {
  # I agree these shouldn't be a list, but named attrsets (TODO)
  auth = [{
    control = "include";
    entries = "/foo/bar";
  } {
    control = "include";
    # These entries could be implicitly all auth to reduce boilerplate
    entries = { auth = [ ... ]; };
  }];

This way I think it looks quite similar to what you were proposing (except path is renamed to entries, and supports a string path, or an attrset which will be automatically written to a file and then the store path is included).

I think supporting nested rules via nix expressions is useful, because substack has semantic meaning that can't be emulated otherwise. Similarly, supporting nested rules via direct paths is also useful, because people might have pre-written rules they just want to import. In the ideal world everything would be added via nix expressions, but I think this is still useful until all tutorials embrace Nix 😄

@rissson
Copy link
Member

rissson commented Nov 30, 2020

I think supporting nested rules via nix expressions is useful, because substack has semantic meaning that can't be emulated otherwise.

What about creating another PAM service that you then use in your substack?

 Similarly, supporting nested rules via direct paths is also useful, because people might have pre-written rules they just want to import.

Yes, that has to be supported, of course.

@kwohlfahrt
Copy link
Contributor Author

I think supporting nested rules via nix expressions is useful, because substack has semantic meaning that can't be emulated otherwise.

What about creating another PAM service that you then use in your substack?

I'm not sure I follow here. The substack entries could refer to another services rules like this, but I'm not sure that's what you mean...

security.pam.services.foo.entries = {
  auth = [{
    control = "substack";
    entries = security.pam.services.bar.entries.auth;
  }]
}

The actual file containing the rules will be a store path, not a service path in /etc/pam.d.

@rissson
Copy link
Member

rissson commented Dec 2, 2020

I'm not sure I follow here. The substack entries could refer to another services rules like this, but I'm not sure that's what you mean...

security.pam.services.foo.entries = {
  auth = [{
    control = "substack";
    entries = security.pam.services.bar.entries.auth;
  }]
}

What I meant is that your bar service would end up in /etc/pam.d/bar anyway, so for your foo service, you could do:

security.pam.services.foo.entries = {
  auth = [{
    control = "substack";
    path = "bar";
  }];
};

@kwohlfahrt
Copy link
Contributor Author

kwohlfahrt commented Dec 2, 2020

You could, but that would be relying on a NixOS implementation detail. Nothing is stopping us from migrating to having all service configs together in /etc/pam.conf instead.

Not suggesting we do that, but that seems like a fragile configuration, that just so happens to work because of the details of the current implementation (and should not be encouraged, but I don't see any way to prevent it).

@stale
Copy link

stale bot commented Nov 16, 2021

I marked this as stale due to inactivity. → More info

@stale stale bot added the 2.status: stale https://github.com/NixOS/nixpkgs/blob/master/.github/STALE-BOT.md label Nov 16, 2021
@flokli
Copy link
Contributor

flokli commented Nov 16, 2021

I'd still love to see this getting in :-)

@stale stale bot removed the 2.status: stale https://github.com/NixOS/nixpkgs/blob/master/.github/STALE-BOT.md label Nov 16, 2021
@aanderse
Copy link
Member

No better time than in a few weeks, once 21.11 is released.

@stale stale bot added the 2.status: stale https://github.com/NixOS/nixpkgs/blob/master/.github/STALE-BOT.md label Jun 19, 2022
@PolarizedIons
Copy link

Sorry to bump, but is there anything that can be done to help?

@stale stale bot removed the 2.status: stale https://github.com/NixOS/nixpkgs/blob/master/.github/STALE-BOT.md label Sep 4, 2023
@nixos-discourse
Copy link

This pull request has been mentioned on NixOS Discourse. There might be relevant details there:

https://discourse.nixos.org/t/what-do-you-want-from-pam-security-pam-in-nixos/33265/4

@wegank wegank added 2.status: stale https://github.com/NixOS/nixpkgs/blob/master/.github/STALE-BOT.md 2.status: merge conflict labels Mar 19, 2024
@stale stale bot removed the 2.status: stale https://github.com/NixOS/nixpkgs/blob/master/.github/STALE-BOT.md label Mar 20, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants