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

nixos/users: validate password hashes #83171

Merged
merged 1 commit into from Jun 17, 2020
Merged

nixos/users: validate password hashes #83171

merged 1 commit into from Jun 17, 2020

Conversation

rnhmjoj
Copy link
Contributor

@rnhmjoj rnhmjoj commented Mar 23, 2020

Motivation for this change

Fixes #12008

This is a more basic check than actually running crypt on the hash but it's simplier and can be done at evaluation time without worrying about hashes getting into derivations.

Things done
  • Tested using sandboxing (nix.useSandbox on NixOS)
  • Built on platform(s)
    • NixOS
    • macOS
    • other Linux distributions
  • Tested via nixosTests.pam-oath-login and manually
  • Ensured that relevant documentation is up to date
  • Fits CONTRIBUTING.md.

@rnhmjoj
Copy link
Contributor Author

rnhmjoj commented Mar 23, 2020

It would definitely be shorter but I'm afraid of false positives when matching a stricter regex.

@rnhmjoj rnhmjoj closed this Apr 7, 2020
@rnhmjoj rnhmjoj deleted the hash branch April 7, 2020 12:41
@rnhmjoj rnhmjoj restored the hash branch April 7, 2020 12:47
@rnhmjoj rnhmjoj reopened this Apr 7, 2020
@infinisil
Copy link
Member

I'm also in favor of using builtins.match for this, seems like the perfect use-case. It will also be faster to execute (haven't checked, but most likely, especially since splitString should be avoided due to its slowness).

@rnhmjoj
Copy link
Contributor Author

rnhmjoj commented Apr 14, 2020

So, I implemented the check using builtins.match. It wasn't easy because there is no actual standard behind the format used for the hashes and I had to come to a compromise between supporting obscure (read deprecated) schemes and catching common mistakes. Here's the expression I used to test the regex.

rec {  
  regex =
    let
      sep = "\\$";
      base64 = "[a-zA-Z0-9./]+";
      scheme = "[a-z0-9-]+";
      content = "${base64}${sep}${base64}";
    in
      "^${sep}${scheme}${sep}${content}$";

  validate = hash: builtins.match regex hash != null;

  results = map validate
  [ # valid but should fail
    "Kyq4bCxAXJkbg" # the original crypt() hash
    "$sha1$40000$jtNX3nZ2$hBNaIXkt4wBI2o5rsi8KejSjNqIq" #  PBKDF2 unsupported on gnu/linux

    # valid and should match
    "$1$etNnh7FA$OlM7eljE/B7F1J4XYNnk81"
    "$2a$10$VIhIOofSMqgdGlL4wzE//e.77dAQGqntF/1dT7bqCrVtquInWy2qi"
    "$5$9ks3nNEqv31FX.F$gdEoLFsCRsn/WRN3wxUnzfeZLoooVlzeF4WjLomTRFD"
    "$6$qoE2letU$wWPRl.PVczjzeMVgjiA8LLy2nOyZbf7Amj3qLIL978o18gbMySdKZ7uepq9tmMQXxyTIrS12Pln.2Q/6Xscao0"

    # invalid and should fail
    "Still not giving it away :P"
    "aNonHashedPasssword3!"
    "aNonHa$hedPa$$swordWith$s"
  ];

}

@infinisil
Copy link
Member

Hm, looking at man $(nix-build --no-out-link '<nixpkgs>' -A manpages)/share/man/man3/crypt.3.gz, it seems that there's also formats with 4 $'s. And why should the original crypt hash and PBKDF2 fail when they could be supported? I know I asked for a regex implementation, but I didn't know it was this complex!

It might make sense to be precise with this though, since failure to check correctly could lead to users being locked out, or to users getting errors even when they have a working password. But now I'm not even sure if this is achievable at all during eval time.

@rnhmjoj
Copy link
Contributor Author

rnhmjoj commented Apr 14, 2020

but I didn't know it was this complex!

Yeah, that's why I initially went for some simplier sanity check.

And why should the original crypt hash and PBKDF2 fail when they could be supported?

I could handle the PBKDF2 case by complicating a bix the regex, but allowing the original DES is really asking for false negatives: 8 bytes alphanumeric ASCII. It will match pretty much everything. Also I think the number of NixOS users having DES hashes in 2020 is 0.

@rnhmjoj
Copy link
Contributor Author

rnhmjoj commented Apr 21, 2020

Ok, this should handle the hashes with options

rec {  
  regex =
    let
      sep = "\\$";
      base64 = "[a-zA-Z0-9./]+";
      id = "[a-z0-9-]+";
      value = "[a-zA-Z0-9/+.-]+";
      options = "${id}(=${value})?(,${id}=${value})*";
      scheme  = "${id}(${sep}${options})?";
      content = "${base64}${sep}${base64}";
    in
      "^${sep}${scheme}${sep}${content}$";

  validate = hash: builtins.match regex hash != null;

  results = map validate
  [ 
    ## valid but should fail

    # original DES hash
    "Kyq4bCxAXJkbg"

    ## valid and should match

    # MD5
    "$1$etNnh7FA$OlM7eljE/B7F1J4XYNnk81"
    # bcrypt
    "$2a$10$VIhIOofSMqgdGlL4wzE//e.77dAQGqntF/1dT7bqCrVtquInWy2qi"
    # SHA2 hashes
    "$5$9ks3nNEqv31FX.F$gdEoLFsCRsn/WRN3wxUnzfeZLoooVlzeF4WjLomTRFD"
    "$6$qoE2letU$wWPRl.PVczjzeMVgjiA8LLy2nOyZbf7Amj3qLIL978o18gbMySdKZ7uepq9tmMQXxyTIrS12Pln.2Q/6Xscao0"
    # SHA-512 with custom rounds
    "$6$rounds=1000000$9ks3nNEqv31FX.F$9PJ4NaW6zmi/iuQdVtOvFXB3wdtNC7k5GOHRtnEtpJAMdBJ7asunZpCuqfjyx2vZdqRRaS/z33EXI9Z.nkNEb."
    # PBKDF2 (unsupported on gnu/linux)
    "$sha1$40000$jtNX3nZ2$hBNaIXkt4wBI2o5rsi8KejSjNqIq"

    ## invalid and should fail
    "Still not giving it away :P"
    "aNonHashedPasssword3!"
    "aNonHa$hedPa$$swordWith$s"
    "sha1$40000$jtNX3nZ2$hBNaIXkt4wBI2o5rsi8KejSjNqIq"
  ];

}

@rnhmjoj rnhmjoj force-pushed the hash branch 2 times, most recently from efd4115 to 660a701 Compare April 26, 2020 09:09
@rnhmjoj
Copy link
Contributor Author

rnhmjoj commented Apr 26, 2020

Fixed evaluation.

@Profpatsch
Copy link
Member

Profpatsch commented Jun 9, 2020 via email

@rnhmjoj
Copy link
Contributor Author

rnhmjoj commented Jun 15, 2020

Turned the assertion into a warning. It's not as effective as failing the build in protecting the users but it won't ever reject a valid hash.

Copy link
Member

@Profpatsch Profpatsch left a comment

Choose a reason for hiding this comment

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

Cool, LGTM.

I don’t know if the check is valid, but even if it is not we won’t break anybody hopefully.

@rnhmjoj
Copy link
Contributor Author

rnhmjoj commented Jun 17, 2020

I tested it with the above expression, which covers the most common and some rarely used but modern hashes. It should be ready to go.

@rnhmjoj rnhmjoj merged commit 4ddf9b7 into NixOS:master Jun 17, 2020
@rnhmjoj rnhmjoj deleted the hash branch June 19, 2020 08:11
@erictapen
Copy link
Member

@rnhmjoj My hashedPasswords are all generated using

$ echo notsecret | mkpasswd --stdin -m sha-512
$6$.wR5JrSCCS4D69$zSf.X9l4nrAkFJNv8vz6zlcA.c5aAbxOzb0/BD.mRHRuMUnwPsUWyJ0kPIpxBhiBiTpXkvJV6AzLYr5rDIbfM1

as the documentation suggests. They all fail the regex and generate the warning. Do you have an suggestion for what would be best practice for generating the hash?

@rnhmjoj
Copy link
Contributor Author

rnhmjoj commented Jun 20, 2020

Your hash is perfectly fine and should be covered. The regular expression itself looks ok, I added this hash just to be sure and it matches: https://regexr.com/572ro.

I don't understand why you're getting the warning.

@rnhmjoj
Copy link
Contributor Author

rnhmjoj commented Jun 20, 2020

I built a NixOS VM with this configuration:

{...}:

{
  networking.hostName = "vmhost";

  users.users.vm = {
    isNormalUser = true;
    hashedPassword = "$6$.wR5JrSCCS4D69$zSf.X9l4nrAkFJNv8vz6zlcA.c5aAbxOzb0/BD.mRHRuMUnwPs";
  };

}

and I can't reproduce it. How are you writing the hash?

@erictapen
Copy link
Member

I'm so sorry, as I build all my NixOS machines in one eval, I misinterpreted the warnings. They happened not on machines with a hashedPassword field, but on those where the password was just set via users.users.root.password. But it can't be just that, as just doing that results in no warning. I will further minimize my config and report back!

@rnhmjoj
Copy link
Contributor Author

rnhmjoj commented Jun 20, 2020

Don't be, if you've seen the warning it already means something is not right.
Thank you for reporting it.

@tilpner
Copy link
Member

tilpner commented Jun 20, 2020

My servers(s) are also giving this warning, but they use SSH-only authentication. I've ignored the warning, deployed the update to them, and am still able to log in remotely.

Inspection with nix repl shows:

nix-repl> devices.talk.config.users.users.root.initialPassword
null

nix-repl> devices.talk.config.users.users.root.initialHashedPassword
"!"

nix-repl> devices.talk.config.users.users.root.password
null

nix-repl> devices.talk.config.users.users.root.hashedPassword
"!"

@rnhmjoj
Copy link
Contributor Author

rnhmjoj commented Jun 20, 2020

@tilpner: is that a literal "!"? What does it mean?

@erictapen
Copy link
Member

I've managed to build my testcase:

{ nixpkgs ? <nixpkgs> }:
(
  import "${nixpkgs}/nixos/lib/eval-config.nix" {
    modules = [
      (
        { config, pkgs, lib, ... }: {
          fileSystems."/".device = "x";
          boot.loader.grub.device = "nodev";

          users.users.root = {
            password = "notsecret";
            shell = pkgs.bashInteractive;
          };
          users.mutableUsers = false;
        }
      )
    ];
  }
).config.system.build.toplevel

It was mutableUsers that I still had on. I'll call this a day but may investigate tomorrow further.

@rnhmjoj
Copy link
Contributor Author

rnhmjoj commented Jun 21, 2020

It seems "!" has the special meaning "disable password login". This was documented in the option security.initialRootPassword but the description is gone after its deprecation (f496c3c). I still don't understand how or why this happens when mutableUsers = false.

EDIT:
passwd(1) says:

       -l, --lock
           Lock the password of the named account.
           This option disables a password by
           changing it to a value which matches no
           possible encrypted value (it adds a '!' at
           the beginning of the password).

Whatever the reason, locking the user out is intended so it should be ignored by the check. I'll open a PR soon.

then
''
The password hash of user "${name}" may be invalid. You must set a
valid hash or the user will be locked out of his account. Please
Copy link
Contributor

Choose a reason for hiding this comment

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

his → their

Copy link
Member

Choose a reason for hiding this comment

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

Good catch, let’s change that.

Copy link
Member

Choose a reason for hiding this comment

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

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.

Enforce no ":" in "hashedPassword" (perhaps a check)
7 participants