Skip to content

Remove support for shadow(5)'s sp_min#1482

Merged
alejandro-colomar merged 6 commits into
shadow-maint:masterfrom
alejandro-colomar:sp_min
Jul 11, 2026
Merged

Remove support for shadow(5)'s sp_min#1482
alejandro-colomar merged 6 commits into
shadow-maint:masterfrom
alejandro-colomar:sp_min

Conversation

@alejandro-colomar

@alejandro-colomar alejandro-colomar commented Jan 6, 2026

Copy link
Copy Markdown
Collaborator

Password expiry was deprecated in 4.19.

sp_min (password minimum age) doesn't seem to be regulated, so it seems we can remove it already.

Link: #1432


Revisions:

v1b
  • Rebase
$ git rd 
1:  d23f4fd515f2 = 1:  cedee3d5c4fb src/chage.c: Remove interactive -m
2:  df14c804a6a7 = 2:  aa27a3c7d6f5 */: chage(1): -m,--mindays: Remove option
3:  66cf7e9ab2ec = 3:  99023e27937c */: passwd(1): -n,--mindays: Remove option
4:  dacb4880dd83 ! 4:  ba480ec8b60e */: login.defs(5): PASS_MIN_DAYS: Remove configuration variable
    @@ man/login.defs.5.xml
            HOME_MODE
     -      PASS_MAX_DAYS PASS_MIN_DAYS PASS_WARN_AGE
     +      PASS_MAX_DAYS PASS_WARN_AGE
    -       <phrase condition="sha_crypt">SHA_CRYPT_MAX_ROUNDS
    -       SHA_CRYPT_MIN_ROUNDS</phrase>
    +       SHA_CRYPT_MAX_ROUNDS SHA_CRYPT_MIN_ROUNDS
            SUB_GID_COUNT SUB_GID_MAX SUB_GID_MIN
    +       SUB_UID_COUNT SUB_UID_MAX SUB_UID_MIN
     @@
        <term>pwck</term>
        <listitem>
5:  c8f041d71528 = 5:  61639063c123 */: shadow(5): sp_min: Ignore field, and clear it
v2
  • Rebase
$ git rd 
1:  cedee3d5 = 1:  76082f17 src/chage.c: Remove interactive -m
2:  aa27a3c7 ! 2:  36d2615f */: chage(1): -m,--mindays: Remove option
    @@ tests/chage/16_chage-m_no_shadow_entry/config/etc/login.defs (deleted)
     -#
     -#CONSOLE_GROUPS           floppy:audio:cdrom
     -
    --#
    --# Only works if compiled with MD5_CRYPT defined:
    --# If set to "yes", new passwords will be encrypted using the MD5-based
    --# algorithm compatible with the one used by recent releases of FreeBSD.
    --# It supports passwords of unlimited length and longer salt strings.
    --# Set to "no" if you need to copy encrypted passwords to other systems
    --# which don't understand the new algorithm.  Default is "no".
    --#
    --# This variable is used by chpasswd, gpasswd and newusers.
    --#
    --#MD5_CRYPT_ENAB   no
    --
     -################# OBSOLETED BY PAM ##############
     -#                                         #
     -# These options are now handled by PAM. Please    #
3:  99023e27 = 3:  feb66370 */: passwd(1): -n,--mindays: Remove option
4:  ba480ec8 ! 4:  b5f6d93a */: login.defs(5): PASS_MIN_DAYS: Remove configuration variable
    @@ man/login.defs.5.xml
              existing accounts.
     @@
            GID_MAX GID_MIN
    -       MAX_MEMBERS_PER_GROUP MD5_CRYPT_ENAB
    +       MAX_MEMBERS_PER_GROUP
            HOME_MODE
     -      PASS_MAX_DAYS PASS_MIN_DAYS PASS_WARN_AGE
     +      PASS_MAX_DAYS PASS_WARN_AGE
    @@ man/login.defs.d/PASS_MIN_DAYS.xml (deleted)
     
      ## man/newusers.8.xml ##
     @@
    + <!ENTITY HOME_MODE             SYSTEM "login.defs.d/HOME_MODE.xml">
      <!ENTITY MAX_MEMBERS_PER_GROUP SYSTEM "login.defs.d/MAX_MEMBERS_PER_GROUP.xml">
    - <!ENTITY MD5_CRYPT_ENAB        SYSTEM "login.defs.d/MD5_CRYPT_ENAB.xml">
      <!ENTITY PASS_MAX_DAYS         SYSTEM "login.defs.d/PASS_MAX_DAYS.xml">
     -<!ENTITY PASS_MIN_DAYS         SYSTEM "login.defs.d/PASS_MIN_DAYS.xml">
      <!ENTITY PASS_WARN_AGE         SYSTEM "login.defs.d/PASS_WARN_AGE.xml">
    @@ tests/chroot/usermod/01_usermod--root/config_chroot/etc/login.defs: UMASK                022
      
      #
     
    - ## tests/crypt/login.defs_DES-MD5_CRYPT_ENAB/config/etc/login.defs ##
    -@@ tests/crypt/login.defs_DES-MD5_CRYPT_ENAB/config/etc/login.defs: KILLCHAR       025
    - # Password aging controls:
    - #
    - # PASS_MAX_DAYS   Maximum number of days a password may be used.
    --# PASS_MIN_DAYS   Minimum number of days allowed between password changes.
    - # PASS_WARN_AGE   Number of days warning given before a password expires.
    - #
    - PASS_MAX_DAYS     99999
    --PASS_MIN_DAYS     0
    - PASS_WARN_AGE     7
    - 
    - #
    -
      ## tests/crypt/login.defs_DES/config/etc/login.defs ##
     @@ tests/crypt/login.defs_DES/config/etc/login.defs: KILLCHAR      025
      # Password aging controls:
    @@ tests/crypt/login.defs_MD5/config/etc/login.defs: KILLCHAR       025
      
      #
     
    - ## tests/crypt/login.defs_MD5_CRYPT_ENAB/config/etc/login.defs ##
    -@@ tests/crypt/login.defs_MD5_CRYPT_ENAB/config/etc/login.defs: KILLCHAR   025
    - # Password aging controls:
    - #
    - # PASS_MAX_DAYS   Maximum number of days a password may be used.
    --# PASS_MIN_DAYS   Minimum number of days allowed between password changes.
    - # PASS_WARN_AGE   Number of days warning given before a password expires.
    - #
    - PASS_MAX_DAYS     99999
    --PASS_MIN_DAYS     0
    - PASS_WARN_AGE     7
    - 
    - #
    -
      ## tests/crypt/login.defs_SHA256-round-max/config/etc/login.defs ##
     @@ tests/crypt/login.defs_SHA256-round-max/config/etc/login.defs: KILLCHAR 025
      # Password aging controls:
5:  61639063 ! 5:  8bbd9705 */: shadow(5): sp_min: Ignore field, and clear it
    @@ src/passwd.c: static void check_password (const struct passwd *pw, const struct
     -                  (void) fprintf (stderr,
     -                                  _("The password for %s cannot be changed yet.\n"),
     -                                  sp->sp_namp);
    --                  SYSLOG ((LOG_WARN, "now < minimum age for '%s'", sp->sp_namp));
    +-                  SYSLOG(LOG_WARN, "now < minimum age for '%s'", sp->sp_namp);
     -                  closelog ();
     -                  fail_exit(E_NOPERM, process_selinux);
     -          }
v2b
  • Rebase
$ git rd 
1:  76082f17 = 1:  3aad7591 src/chage.c: Remove interactive -m
2:  36d2615f = 2:  f4ad4227 */: chage(1): -m,--mindays: Remove option
3:  feb66370 = 3:  0daeea49 */: passwd(1): -n,--mindays: Remove option
4:  b5f6d93a ! 4:  e757346e */: login.defs(5): PASS_MIN_DAYS: Remove configuration variable
    @@ man/login.defs.5.xml
            &PASS_MAX_DAYS;
     -      &PASS_MIN_DAYS;
            &PASS_WARN_AGE;
    -       <para> 
    +       <para>
     -        <option>PASS_MAX_DAYS</option>, <option>PASS_MIN_DAYS</option> and
     +        <option>PASS_MAX_DAYS</option>, and
              <option>PASS_WARN_AGE</option> are only used at the
    @@ man/login.defs.5.xml
     -      PASS_MAX_DAYS PASS_MIN_DAYS PASS_WARN_AGE
     +      PASS_MAX_DAYS PASS_WARN_AGE
            SHA_CRYPT_MAX_ROUNDS SHA_CRYPT_MIN_ROUNDS
    -       SUB_GID_COUNT SUB_GID_MAX SUB_GID_MIN
    -       SUB_UID_COUNT SUB_UID_MAX SUB_UID_MIN
    +       SUB_GID_COUNT SUB_GID_MAX SUB_GID_MIN SUB_GID_DETERMINISTIC
    +       SUB_GID_STORE_BY_UID
     @@
        <term>pwck</term>
        <listitem>
    @@ man/login.defs.5.xml
            MAIL_DIR MAX_MEMBERS_PER_GROUP
     -      PASS_MAX_DAYS PASS_MIN_DAYS PASS_WARN_AGE
     +      PASS_MAX_DAYS PASS_WARN_AGE
    -       SUB_GID_COUNT SUB_GID_MAX SUB_GID_MIN
    -       SUB_UID_COUNT SUB_UID_MAX SUB_UID_MIN
    -       SYS_GID_MAX SYS_GID_MIN SYS_UID_MAX SYS_UID_MIN UID_MAX UID_MIN
    +       SUB_GID_COUNT SUB_GID_MAX SUB_GID_MIN SUB_GID_DETERMINISTIC
    +       SUB_GID_STORE_BY_UID
    +       SUB_UID_COUNT SUB_UID_MAX SUB_UID_MIN SUB_UID_DETERMINISTIC
     
      ## man/login.defs.d/PASS_MIN_DAYS.xml (deleted) ##
     @@
    @@ tests/grouptools/groupdel/11_groupdel_invalid_option/config/etc/login.defs: KILL
      
      #
     
    - ## tests/grouptools/groupmems/54_groupmems_usage_invalid_option/config/etc/login.defs ##
    -@@ tests/grouptools/groupmems/54_groupmems_usage_invalid_option/config/etc/login.defs: KILLCHAR    025
    - # Password aging controls:
    - #
    - # PASS_MAX_DAYS   Maximum number of days a password may be used.
    --# PASS_MIN_DAYS   Minimum number of days allowed between password changes.
    - # PASS_WARN_AGE   Number of days warning given before a password expires.
    - #
    - PASS_MAX_DAYS     99999
    --PASS_MIN_DAYS     0
    - PASS_WARN_AGE     7
    - 
    - #
    -
    - ## tests/grouptools/groupmems/55_groupmems_usage-a-d/config/etc/login.defs ##
    -@@ tests/grouptools/groupmems/55_groupmems_usage-a-d/config/etc/login.defs: KILLCHAR       025
    - # Password aging controls:
    - #
    - # PASS_MAX_DAYS   Maximum number of days a password may be used.
    --# PASS_MIN_DAYS   Minimum number of days allowed between password changes.
    - # PASS_WARN_AGE   Number of days warning given before a password expires.
    - #
    - PASS_MAX_DAYS     99999
    --PASS_MIN_DAYS     0
    - PASS_WARN_AGE     7
    - 
    - #
    -
    - ## tests/grouptools/groupmems/56_groupmems_usage_extra_arg/config/etc/login.defs ##
    -@@ tests/grouptools/groupmems/56_groupmems_usage_extra_arg/config/etc/login.defs: KILLCHAR 025
    - # Password aging controls:
    - #
    - # PASS_MAX_DAYS   Maximum number of days a password may be used.
    --# PASS_MIN_DAYS   Minimum number of days allowed between password changes.
    - # PASS_WARN_AGE   Number of days warning given before a password expires.
    - #
    - PASS_MAX_DAYS     99999
    --PASS_MIN_DAYS     0
    - PASS_WARN_AGE     7
    - 
    - #
    -
      ## tests/grouptools/groupmod/01_groupmod_change_gid/config/etc/login.defs ##
     @@ tests/grouptools/groupmod/01_groupmod_change_gid/config/etc/login.defs: KILLCHAR        025
      # Password aging controls:
5:  8bbd9705 = 5:  07061db4 */: shadow(5): sp_min: Ignore field, and clear it
v2c
  • Fix previous rebase. A new use of nflg had been added recently.
$ git rd 
1:  3aad7591 = 1:  3aad7591 src/chage.c: Remove interactive -m
2:  f4ad4227 = 2:  f4ad4227 */: chage(1): -m,--mindays: Remove option
3:  0daeea49 ! 3:  ccd00d0c */: passwd(1): -n,--mindays: Remove option
    @@ src/passwd.c: static void update_shadow(bool process_selinux)
        if (wflg) {
                nsp->sp_warn = warn;
        }
    +@@ src/passwd.c: static void update_shadow(bool process_selinux)
    +                        ret ? SHADOW_AUDIT_SUCCESS : SHADOW_AUDIT_FAILURE);
    +   }
    +   /* Audit aging parameter changes if any were modified */
    +-  if (xflg || nflg || wflg || iflg) {
    ++  if (xflg || wflg || iflg) {
    +           char aging_msg[256];
    +           stprintf_a(aging_msg,
    +                      "changed-password-aging min=%ld max=%ld warn=%ld inact=%ld",
     @@ src/passwd.c: static void update_shadow(bool process_selinux)
       *        -i #    set sp_inact to # days (*)
       *        -k      change password only if expired
4:  e757346e = 4:  d4202fdc */: login.defs(5): PASS_MIN_DAYS: Remove configuration variable
5:  07061db4 = 5:  f1d901d7 */: shadow(5): sp_min: Ignore field, and clear it
v2d
  • Rebase
$ git rd 
1:  3aad759186ae = 1:  4987b3b45c51 src/chage.c: Remove interactive -m
2:  f4ad42274d31 = 2:  00313552b612 */: chage(1): -m,--mindays: Remove option
3:  ccd00d0cce2c = 3:  0b98d5866537 */: passwd(1): -n,--mindays: Remove option
4:  d4202fdcfee0 = 4:  461b3754c14f */: login.defs(5): PASS_MIN_DAYS: Remove configuration variable
5:  f1d901d78c36 = 5:  b4342405f966 */: shadow(5): sp_min: Ignore field, and clear it
v3
  • Remove sp_min tests.
$ git rd 
1:  4987b3b45c51 = 1:  4987b3b45c51 src/chage.c: Remove interactive -m
2:  00313552b612 = 2:  00313552b612 */: chage(1): -m,--mindays: Remove option
3:  0b98d5866537 = 3:  0b98d5866537 */: passwd(1): -n,--mindays: Remove option
4:  461b3754c14f = 4:  461b3754c14f */: login.defs(5): PASS_MIN_DAYS: Remove configuration variable
5:  b4342405f966 = 5:  b4342405f966 */: shadow(5): sp_min: Ignore field, and clear it
-:  ------------ > 6:  2588cef421b8 tests/system/tests/: sp_min: Ignore field
v4
  • Adapt tests, instead of removing them.
$ git rd --creation-factor=99
1:  4987b3b45c51 = 1:  4987b3b45c51 src/chage.c: Remove interactive -m
2:  00313552b612 = 2:  00313552b612 */: chage(1): -m,--mindays: Remove option
3:  0b98d5866537 = 3:  0b98d5866537 */: passwd(1): -n,--mindays: Remove option
4:  461b3754c14f = 4:  461b3754c14f */: login.defs(5): PASS_MIN_DAYS: Remove configuration variable
5:  b4342405f966 = 5:  b4342405f966 */: shadow(5): sp_min: Ignore field, and clear it
6:  2588cef421b8 ! 6:  719cd6318fb0 tests/system/tests/: sp_min: Ignore field
    @@ tests/system/tests/test_chpasswd.py: def test_chpasswd__change_passwords_interac
          assert re.match(shadow_password_pattern(), shadow_entry1.password), "Incorrect password"
          assert shadow_entry1.last_changed == days_since_epoch(), "Incorrect last changed"
     -    assert shadow_entry1.min_days == 0, "Incorrect min days"
    ++    assert shadow_entry1.min_days is None, "Incorrect min days"
          assert shadow_entry1.max_days == 99999, "Incorrect max days"
          assert shadow_entry1.warn_days == 7, "Incorrect warn days"
      
    @@ tests/system/tests/test_chpasswd.py: def test_chpasswd__change_passwords_interac
          assert re.match(shadow_password_pattern(), shadow_entry2.password), "Incorrect password"
          assert shadow_entry2.last_changed == days_since_epoch(), "Incorrect last changed"
     -    assert shadow_entry2.min_days == 0, "Incorrect min days"
    ++    assert shadow_entry2.min_days is None, "Incorrect min days"
          assert shadow_entry2.max_days == 99999, "Incorrect max days"
          assert shadow_entry2.warn_days == 7, "Incorrect warn days"
      
    @@ tests/system/tests/test_chpasswd.py: def test_chpasswd__change_passwords_from_fi
          assert re.match(shadow_password_pattern(), shadow_entry1.password), "Incorrect password"
          assert shadow_entry1.last_changed == days_since_epoch(), "Incorrect last changed"
     -    assert shadow_entry1.min_days == 0, "Incorrect min days"
    ++    assert shadow_entry1.min_days is None, "Incorrect min days"
          assert shadow_entry1.max_days == 99999, "Incorrect max days"
          assert shadow_entry1.warn_days == 7, "Incorrect warn days"
      
    @@ tests/system/tests/test_chpasswd.py: def test_chpasswd__change_passwords_from_fi
          assert re.match(shadow_password_pattern(), shadow_entry2.password), "Incorrect password"
          assert shadow_entry2.last_changed == days_since_epoch(), "Incorrect last changed"
     -    assert shadow_entry2.min_days == 0, "Incorrect min days"
    ++    assert shadow_entry2.min_days is None, "Incorrect min days"
          assert shadow_entry2.max_days == 99999, "Incorrect max days"
          assert shadow_entry2.warn_days == 7, "Incorrect warn days"
     
    - ## tests/system/tests/test_newusers.py ##
    -@@ tests/system/tests/test_newusers.py: def test_newusers__create_users_from_stdin(shadow: Shadow):
    -     assert shadow_entry.name == "tuser1", "Incorrect username"
    -     assert shadow_entry.password is not None, "Incorrect password"
    -     assert shadow_entry.last_changed == days_since_epoch(), "Incorrect last changed"
    --    assert shadow_entry.min_days is None, "Incorrect min days"
    -     assert shadow_entry.max_days == 99999, "Incorrect max days"
    -     assert shadow_entry.warn_days == 7, "Incorrect warn days"
    - 
    -@@ tests/system/tests/test_newusers.py: def test_newusers__create_users_from_stdin(shadow: Shadow):
    -     assert shadow_entry.name == "tuser2", "Incorrect username"
    -     assert shadow_entry.password is not None, "Incorrect password"
    -     assert shadow_entry.last_changed == days_since_epoch(), "Incorrect last changed"
    --    assert shadow_entry.min_days is None, "Incorrect min days"
    -     assert shadow_entry.max_days == 99999, "Incorrect max days"
    -     assert shadow_entry.warn_days == 7, "Incorrect warn days"
    - 
    -@@ tests/system/tests/test_newusers.py: def test_newusers__create_users_from_file(shadow: Shadow):
    -     assert shadow_entry.name == "tuser1", "Incorrect username"
    -     assert shadow_entry.password is not None, "Incorrect password"
    -     assert shadow_entry.last_changed == days_since_epoch(), "Incorrect last changed"
    --    assert shadow_entry.min_days is None, "Incorrect min days"
    -     assert shadow_entry.max_days == 99999, "Incorrect max days"
    -     assert shadow_entry.warn_days == 7, "Incorrect warn days"
    - 
    -@@ tests/system/tests/test_newusers.py: def test_newusers__create_users_from_file(shadow: Shadow):
    -     assert shadow_entry.name == "tuser2", "Incorrect username"
    -     assert shadow_entry.password is not None, "Incorrect password"
    -     assert shadow_entry.last_changed == days_since_epoch(), "Incorrect last changed"
    --    assert shadow_entry.min_days is None, "Incorrect min days"
    -     assert shadow_entry.max_days == 99999, "Incorrect max days"
    -     assert shadow_entry.warn_days == 7, "Incorrect warn days"
    - 
    -
      ## tests/system/tests/test_passwd.py ##
     @@ tests/system/tests/test_passwd.py: def test_passwd__change_password_as_root_with_stdin(shadow: Shadow):
          assert shadow_entry.password is not None, "Password should not be None"
          assert re.match(shadow_password_pattern(), shadow_entry.password), "Incorrect password"
          assert shadow_entry.last_changed == days_since_epoch(), "Incorrect last changed"
     -    assert shadow_entry.min_days == 0, "Incorrect min days"
    ++    assert shadow_entry.min_days is None, "Incorrect min days"
          assert shadow_entry.max_days == 99999, "Incorrect max days"
          assert shadow_entry.warn_days == 7, "Incorrect warn days"
      
    @@ tests/system/tests/test_passwd.py: def test_passwd__change_password_as_root_inte
          assert re.match(shadow_password_pattern(), shadow_entry.password), "Incorrect password"
          assert shadow_entry.last_changed == days_since_epoch(), "Incorrect last changed"
     -    assert shadow_entry.min_days == 0, "Incorrect min days"
    -     assert shadow_entry.max_days == 99999, "Incorrect max days"
    -     assert shadow_entry.warn_days == 7, "Incorrect warn days"
    - 
    -
    - ## tests/system/tests/test_useradd.py ##
    -@@ tests/system/tests/test_useradd.py: def test_useradd__add_user(shadow: Shadow):
    -     assert shadow_entry.name == "tuser", "Incorrect username"
    -     assert shadow_entry.password == "!", "Incorrect password"
    -     assert shadow_entry.last_changed == days_since_epoch(), "Incorrect last changed"
    --    assert shadow_entry.min_days is None, "Incorrect min days"
    ++    assert shadow_entry.min_days is None, "Incorrect min days"
          assert shadow_entry.max_days == 99999, "Incorrect max days"
          assert shadow_entry.warn_days == 7, "Incorrect warn days"
      
    @@ tests/system/tests/test_vipw.py: def test_vipw__add_user_to_shadow(shadow: Shado
          assert shadow_entry.password == "!", "Incorrect password"
          assert shadow_entry.last_changed == 20342, "Incorrect last changed"
     -    assert shadow_entry.min_days == 0, "Incorrect min days"
    ++    assert shadow_entry.min_days is None, "Incorrect min days"
          assert shadow_entry.max_days == 99999, "Incorrect max days"
          assert shadow_entry.warn_days == 7, "Incorrect warn days"
      
v4b
  • Rebase
$ git rd 
1:  4987b3b45c51 = 1:  38e88db5eb61 src/chage.c: Remove interactive -m
2:  00313552b612 = 2:  33d75c7ffe73 */: chage(1): -m,--mindays: Remove option
3:  0b98d5866537 = 3:  11f4618d7afc */: passwd(1): -n,--mindays: Remove option
4:  461b3754c14f = 4:  aaf3e0c61a0b */: login.defs(5): PASS_MIN_DAYS: Remove configuration variable
5:  b4342405f966 = 5:  fe44b2d28c7e */: shadow(5): sp_min: Ignore field, and clear it
6:  719cd6318fb0 = 6:  678e39163cf0 tests/system/tests/: sp_min: Ignore field
v4c
$ git rd 
1:  38e88db5eb61 ! 1:  2595ac772850 src/chage.c: Remove interactive -m
    @@ Metadata
      ## Commit message ##
         src/chage.c: Remove interactive -m
     
    +    Reviewed-by: Serge Hallyn <serge@hallyn.com>
         Signed-off-by: Alejandro Colomar <alx@kernel.org>
     
      ## src/chage.c ##
2:  33d75c7ffe73 ! 2:  3ff1eee9f1e4 */: chage(1): -m,--mindays: Remove option
    @@ Metadata
      ## Commit message ##
         */: chage(1): -m,--mindays: Remove option
     
    +    Reviewed-by: Serge Hallyn <serge@hallyn.com>
         Signed-off-by: Alejandro Colomar <alx@kernel.org>
     
      ## man/chage.1.xml ##
3:  11f4618d7afc ! 3:  7bb76d15f484 */: passwd(1): -n,--mindays: Remove option
    @@ Commit message
         changes its password, and 5 minutes later the password is leaked, one
         should be able to change the password immediately.
     
    +    Reviewed-by: Serge Hallyn <serge@hallyn.com>
         Signed-off-by: Alejandro Colomar <alx@kernel.org>
     
      ## man/passwd.1.xml ##
4:  aaf3e0c61a0b ! 4:  9e0c541ba1bf */: login.defs(5): PASS_MIN_DAYS: Remove configuration variable
    @@ Metadata
      ## Commit message ##
         */: login.defs(5): PASS_MIN_DAYS: Remove configuration variable
     
    +    Reviewed-by: Serge Hallyn <serge@hallyn.com>
         Signed-off-by: Alejandro Colomar <alx@kernel.org>
     
      ## etc/login.defs ##
5:  fe44b2d28c7e ! 5:  7d56419aa326 */: shadow(5): sp_min: Ignore field, and clear it
    @@ Commit message
         representation of an empty field).  Whenever we were writing it, let's
         write a -1.
     
    +    Reviewed-by: Serge Hallyn <serge@hallyn.com>
         Signed-off-by: Alejandro Colomar <alx@kernel.org>
     
      ## lib/age.c ##
6:  678e39163cf0 = 6:  0da7692eff7a tests/system/tests/: sp_min: Ignore field
v5
  • Remove tests, since I can't make them succeed, and anyway they test a feature that we don't support anymore, so whatever happens, we don't really care much.
$ git rd --creation-factor=99
1:  2595ac772850 = 1:  2595ac772850 src/chage.c: Remove interactive -m
2:  3ff1eee9f1e4 = 2:  3ff1eee9f1e4 */: chage(1): -m,--mindays: Remove option
3:  7bb76d15f484 = 3:  7bb76d15f484 */: passwd(1): -n,--mindays: Remove option
4:  9e0c541ba1bf = 4:  9e0c541ba1bf */: login.defs(5): PASS_MIN_DAYS: Remove configuration variable
5:  7d56419aa326 = 5:  7d56419aa326 */: shadow(5): sp_min: Ignore field, and clear it
6:  0da7692eff7a ! 6:  c94dd097b842 tests/system/tests/: sp_min: Ignore field
    @@ tests/system/tests/test_chpasswd.py: def test_chpasswd__change_passwords_interac
          assert re.match(shadow_password_pattern(), shadow_entry1.password), "Incorrect password"
          assert shadow_entry1.last_changed == days_since_epoch(), "Incorrect last changed"
     -    assert shadow_entry1.min_days == 0, "Incorrect min days"
    -+    assert shadow_entry1.min_days is None, "Incorrect min days"
          assert shadow_entry1.max_days == 99999, "Incorrect max days"
          assert shadow_entry1.warn_days == 7, "Incorrect warn days"
      
    @@ tests/system/tests/test_chpasswd.py: def test_chpasswd__change_passwords_interac
          assert re.match(shadow_password_pattern(), shadow_entry2.password), "Incorrect password"
          assert shadow_entry2.last_changed == days_since_epoch(), "Incorrect last changed"
     -    assert shadow_entry2.min_days == 0, "Incorrect min days"
    -+    assert shadow_entry2.min_days is None, "Incorrect min days"
          assert shadow_entry2.max_days == 99999, "Incorrect max days"
          assert shadow_entry2.warn_days == 7, "Incorrect warn days"
      
    @@ tests/system/tests/test_chpasswd.py: def test_chpasswd__change_passwords_from_fi
          assert re.match(shadow_password_pattern(), shadow_entry1.password), "Incorrect password"
          assert shadow_entry1.last_changed == days_since_epoch(), "Incorrect last changed"
     -    assert shadow_entry1.min_days == 0, "Incorrect min days"
    -+    assert shadow_entry1.min_days is None, "Incorrect min days"
          assert shadow_entry1.max_days == 99999, "Incorrect max days"
          assert shadow_entry1.warn_days == 7, "Incorrect warn days"
      
    @@ tests/system/tests/test_chpasswd.py: def test_chpasswd__change_passwords_from_fi
          assert re.match(shadow_password_pattern(), shadow_entry2.password), "Incorrect password"
          assert shadow_entry2.last_changed == days_since_epoch(), "Incorrect last changed"
     -    assert shadow_entry2.min_days == 0, "Incorrect min days"
    -+    assert shadow_entry2.min_days is None, "Incorrect min days"
          assert shadow_entry2.max_days == 99999, "Incorrect max days"
          assert shadow_entry2.warn_days == 7, "Incorrect warn days"
     
    + ## tests/system/tests/test_newusers.py ##
    +@@ tests/system/tests/test_newusers.py: def test_newusers__create_users_from_stdin(shadow: Shadow):
    +     assert shadow_entry.name == "tuser1", "Incorrect username"
    +     assert shadow_entry.password is not None, "Incorrect password"
    +     assert shadow_entry.last_changed == days_since_epoch(), "Incorrect last changed"
    +-    assert shadow_entry.min_days is None, "Incorrect min days"
    +     assert shadow_entry.max_days == 99999, "Incorrect max days"
    +     assert shadow_entry.warn_days == 7, "Incorrect warn days"
    + 
    +@@ tests/system/tests/test_newusers.py: def test_newusers__create_users_from_stdin(shadow: Shadow):
    +     assert shadow_entry.name == "tuser2", "Incorrect username"
    +     assert shadow_entry.password is not None, "Incorrect password"
    +     assert shadow_entry.last_changed == days_since_epoch(), "Incorrect last changed"
    +-    assert shadow_entry.min_days is None, "Incorrect min days"
    +     assert shadow_entry.max_days == 99999, "Incorrect max days"
    +     assert shadow_entry.warn_days == 7, "Incorrect warn days"
    + 
    +@@ tests/system/tests/test_newusers.py: def test_newusers__create_users_from_file(shadow: Shadow):
    +     assert shadow_entry.name == "tuser1", "Incorrect username"
    +     assert shadow_entry.password is not None, "Incorrect password"
    +     assert shadow_entry.last_changed == days_since_epoch(), "Incorrect last changed"
    +-    assert shadow_entry.min_days is None, "Incorrect min days"
    +     assert shadow_entry.max_days == 99999, "Incorrect max days"
    +     assert shadow_entry.warn_days == 7, "Incorrect warn days"
    + 
    +@@ tests/system/tests/test_newusers.py: def test_newusers__create_users_from_file(shadow: Shadow):
    +     assert shadow_entry.name == "tuser2", "Incorrect username"
    +     assert shadow_entry.password is not None, "Incorrect password"
    +     assert shadow_entry.last_changed == days_since_epoch(), "Incorrect last changed"
    +-    assert shadow_entry.min_days is None, "Incorrect min days"
    +     assert shadow_entry.max_days == 99999, "Incorrect max days"
    +     assert shadow_entry.warn_days == 7, "Incorrect warn days"
    + 
    +
      ## tests/system/tests/test_passwd.py ##
     @@ tests/system/tests/test_passwd.py: def test_passwd__change_password_as_root_with_stdin(shadow: Shadow):
          assert shadow_entry.password is not None, "Password should not be None"
          assert re.match(shadow_password_pattern(), shadow_entry.password), "Incorrect password"
          assert shadow_entry.last_changed == days_since_epoch(), "Incorrect last changed"
     -    assert shadow_entry.min_days == 0, "Incorrect min days"
    -+    assert shadow_entry.min_days is None, "Incorrect min days"
          assert shadow_entry.max_days == 99999, "Incorrect max days"
          assert shadow_entry.warn_days == 7, "Incorrect warn days"
      
    @@ tests/system/tests/test_passwd.py: def test_passwd__change_password_as_root_inte
          assert re.match(shadow_password_pattern(), shadow_entry.password), "Incorrect password"
          assert shadow_entry.last_changed == days_since_epoch(), "Incorrect last changed"
     -    assert shadow_entry.min_days == 0, "Incorrect min days"
    -+    assert shadow_entry.min_days is None, "Incorrect min days"
    +     assert shadow_entry.max_days == 99999, "Incorrect max days"
    +     assert shadow_entry.warn_days == 7, "Incorrect warn days"
    + 
    +
    + ## tests/system/tests/test_useradd.py ##
    +@@ tests/system/tests/test_useradd.py: def test_useradd__add_user(shadow: Shadow):
    +     assert shadow_entry.name == "tuser", "Incorrect username"
    +     assert shadow_entry.password == "!", "Incorrect password"
    +     assert shadow_entry.last_changed == days_since_epoch(), "Incorrect last changed"
    +-    assert shadow_entry.min_days is None, "Incorrect min days"
          assert shadow_entry.max_days == 99999, "Incorrect max days"
          assert shadow_entry.warn_days == 7, "Incorrect warn days"
      
    @@ tests/system/tests/test_vipw.py: def test_vipw__add_user_to_shadow(shadow: Shado
          assert shadow_entry.password == "!", "Incorrect password"
          assert shadow_entry.last_changed == 20342, "Incorrect last changed"
     -    assert shadow_entry.min_days == 0, "Incorrect min days"
    -+    assert shadow_entry.min_days is None, "Incorrect min days"
          assert shadow_entry.max_days == 99999, "Incorrect max days"
          assert shadow_entry.warn_days == 7, "Incorrect warn days"
      
v5b
  • Add link to this PR, and copy the draft for the CVE into a commit message.
$ git rd 
1:  2595ac772850 ! 1:  62c699314e9e src/chage.c: Remove interactive -m
    @@ Metadata
      ## Commit message ##
         src/chage.c: Remove interactive -m
     
    +    Link: <https://github.com/shadow-maint/shadow/pull/1482>
         Reviewed-by: Serge Hallyn <serge@hallyn.com>
         Signed-off-by: Alejandro Colomar <alx@kernel.org>
     
2:  3ff1eee9f1e4 ! 2:  cc2374f98d6f */: chage(1): -m,--mindays: Remove option
    @@ Metadata
      ## Commit message ##
         */: chage(1): -m,--mindays: Remove option
     
    +    Link: <https://github.com/shadow-maint/shadow/pull/1482>
         Reviewed-by: Serge Hallyn <serge@hallyn.com>
         Signed-off-by: Alejandro Colomar <alx@kernel.org>
     
3:  7bb76d15f484 ! 3:  1e77d4f1de69 */: passwd(1): -n,--mindays: Remove option
    @@ Commit message
         changes its password, and 5 minutes later the password is leaked, one
         should be able to change the password immediately.
     
    +    Link: <https://github.com/shadow-maint/shadow/pull/1482>
         Reviewed-by: Serge Hallyn <serge@hallyn.com>
         Signed-off-by: Alejandro Colomar <alx@kernel.org>
     
4:  9e0c541ba1bf ! 4:  2ac5c28e0dfa */: login.defs(5): PASS_MIN_DAYS: Remove configuration variable
    @@ Metadata
      ## Commit message ##
         */: login.defs(5): PASS_MIN_DAYS: Remove configuration variable
     
    +    Link: <https://github.com/shadow-maint/shadow/pull/1482>
         Reviewed-by: Serge Hallyn <serge@hallyn.com>
         Signed-off-by: Alejandro Colomar <alx@kernel.org>
     
5:  7d56419aa326 ! 5:  31b3783170af */: shadow(5): sp_min: Ignore field, and clear it
    @@ Commit message
         representation of an empty field).  Whenever we were writing it, let's
         write a -1.
     
    +    I think this would be worth a CVE.  I've contacted Debian, in case they
    +    can issue a CVE.  Here's the draft I've sent them for the CVE:
    +
    +            shadow-utils: inability to change a compromised password
    +
    +    Description
    +            A flaw was found in /etc/shadow, and the programs that handle
    +            the file.
    +
    +            The /etc/shadow database (documented in shadow(5)) entries
    +            contain a "minimum password age" field.  This field imposes
    +            a minimum password age before the password can be changed.
    +
    +            If a user's password is compromised before that period expires,
    +            the user must contact the administrator to be able to change the
    +            password.  This can usually take hours or days, and during these
    +            hours or days, an attacker would have unlimited access to the
    +            credentials.
    +
    +            One option of the user would be to lock its own password with
    +            'passwd -l', thus being locked out of its own account.
    +
    +            The history of this 4th field of /etc/shadow entries is
    +            considered dubious in retrospective, and the recommended action
    +            is to never set this field.  If the field is set, the
    +            recommended action is to ignore the field, and if possible,
    +            remove it.
    +
    +    Link: <https://github.com/shadow-maint/shadow/pull/1482>
         Reviewed-by: Serge Hallyn <serge@hallyn.com>
         Signed-off-by: Alejandro Colomar <alx@kernel.org>
     
6:  c94dd097b842 ! 6:  d6462d57cc9f tests/system/tests/: sp_min: Ignore field
    @@ Metadata
      ## Commit message ##
         tests/system/tests/: sp_min: Ignore field
     
    +    Link: <https://github.com/shadow-maint/shadow/pull/1482>
         Signed-off-by: Alejandro Colomar <alx@kernel.org>
     
      ## tests/system/tests/test_chpasswd.py ##

@patrakov

patrakov commented Jan 6, 2026

Copy link
Copy Markdown

The use case for the minimum password age is to prevent lazy people in environments where periodic password changes are required, knowing that they can't reuse 10 last passwords, from changing the password 10 times to throwaway values and then back to the original. In practice, even though it is not explicitly regulated, auditors view it as a part of enforcement of the password history requirement.

@alejandro-colomar

alejandro-colomar commented Jan 6, 2026

Copy link
Copy Markdown
Collaborator Author

The use case for the minimum password age is to prevent lazy people,

That's actually not lazy people, but intelligent people that know ways of enforcing security even under regulations that actively try them to decrease security.

knowing that they can't reuse 10 last passwords, from changing the password 10 times to throwaway values and then back to the original. In practice, even though it is not explicitly regulated, auditors view it as a part of enforcement of the password history requirement.

Auditors can come here and talk with us. :)

@alejandro-colomar

Copy link
Copy Markdown
Collaborator Author

Cc: @hallyn , @ikerexxe

@alejandro-colomar alejandro-colomar force-pushed the sp_min branch 2 times, most recently from 719cd63 to 678e391 Compare July 10, 2026 12:30
@hallyn

hallyn commented Jul 10, 2026

Copy link
Copy Markdown
Member

So I like this for simplifying the code, and for not encouraging dumb policies. But I don't like enforcing my own opinions on other people or organizations. If there were a request to enable it and we didn't yet support it, I'd be happy to say no. But removing existing support and breaking people's current scripts is a different thing.

(Not nacking outright, let's talk...)

@alejandro-colomar

Copy link
Copy Markdown
Collaborator Author

So I like this for simplifying the code, and for not encouraging dumb policies. But I don't like enforcing my own opinions on other people or organizations. If there were a request to enable it and we didn't yet support it, I'd be happy to say no.

But removing existing support and breaking people's current scripts is a different thing.

I think this shouldn't break anyone's scripts:

  • If some script sets the minimum time, we'll just ignore it (and overwrite it with an empty field).
  • If some script reads the minimum time, it'll still read a valid value (the empty string is a valid value).

Those scripts will not produce the intended effects, which are to prevent users from changing their passwords, but I wouldn't say that's fundamentally breaking the scripts.

Another point of view is that this whole feature is a security bug:

  • Let's say a password was changed in day 0.
  • Let's say sp_min is set to 10 days.
  • Let's say in day 5 you realize your password was compromised.
  • If you try to change your password, you won't be able; you need to contact the administrator, which might take hours or days, to change your password. In the meantime, the attacker might use your credentials freely.
  • If sp_min was removed, you could change the password immediately.

I think this case would warrant removing the feature, even if it may ignore the effects of some existing scripts.

Does this sound reasonable?

(Not nacking outright, let's talk...)

@alejandro-colomar

alejandro-colomar commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator Author

@ikerexxe , would you mind having a look at the failing tests? They were added recently, and I'm not sure why they're failing.

Edit: In v3, I removed those tests, which worked, but in v4, I tried to keep them (and adapt them), which seems to fail for unprivileged tests.

@alejandro-colomar

alejandro-colomar commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator Author

So I like this for simplifying the code, and for not encouraging dumb policies. But I don't like enforcing my own opinions on other people or organizations. If there were a request to enable it and we didn't yet support it, I'd be happy to say no.

But removing existing support and breaking people's current scripts is a different thing.

I think this shouldn't break anyone's scripts:

* If some script sets the minimum time, we'll just ignore it (and overwrite it with an empty field).

* If some script reads the minimum time, it'll still read a valid value (the empty string is a valid value).

Those scripts will not produce the intended effects, which are to prevent users from changing their passwords, but I wouldn't say that's fundamentally breaking the scripts.

Another point of view is that this whole feature is a security bug:

* Let's say a password was changed in day 0.

* Let's say sp_min is set to 10 days.

* Let's say in day 5 you realize your password was compromised.

* If you try to change your password, you won't be able; you need to contact the administrator, which might take hours or days, to change your password.  In the meantime, the attacker might use your credentials freely.

* If sp_min was removed, you could change the password immediately.

I think this case would warrant removing the feature, even if it may ignore the effects of some existing scripts.

Does this sound reasonable?

(Not nacking outright, let's talk...)

Self correction:

I was thinking of scripts that handle /etc/shadow directly. This change would indeed break scripts that call these tools with the flags we'd be removing.

We could accept the flags and ignore them, but I think that'd be worse.

I think this breakage would be acceptable, considering the security problem of minimum periods of changing passwords.

In fact, I think this deserves a CVE. @ikerexxe , would you mind contacting Red Hat for issuing a CVE against shadow for the sp_min field?

Here's a draft they can use:

Title: shadow-utils: inability to change a compromised password

Description
	A flaw was found in /etc/shadow, and the programs that handle
	the file.

	The /etc/shadow database (documented in shadow(5)) entries
	contain a "minimum password age" field.  This field imposes
	a minimum password age before the password can be changed.

	If a user's password is compromised before that period expires,
	the user must contact the administrator to be able to change the
	password.  This can usually take hours or days, and during these
	hours or days, an attacker would have unlimited access to the
	credentials.

	One option of the user would be to lock its own password with
	'passwd -l', thus being locked out of its own account.

	The history of this 4th field of /etc/shadow entries is
	considered dubious in retrospective, and the recommended action
	is to never set this field.  If the field is set, the
	recommended action is to ignore the field, and if possible,
	remove it.

Edit: I've sent an email to Debian security@, in case they can issue the CVE.

@alejandro-colomar alejandro-colomar force-pushed the sp_min branch 2 times, most recently from 0da7692 to c94dd09 Compare July 11, 2026 14:29
@alejandro-colomar

Copy link
Copy Markdown
Collaborator Author

Thanks!! :)

Link: <shadow-maint#1482>
Reviewed-by: Serge Hallyn <serge@hallyn.com>
Signed-off-by: Alejandro Colomar <alx@kernel.org>
Link: <shadow-maint#1482>
Reviewed-by: Serge Hallyn <serge@hallyn.com>
Signed-off-by: Alejandro Colomar <alx@kernel.org>
It makes no sense to limit the frequency of password change.  If one
changes its password, and 5 minutes later the password is leaked, one
should be able to change the password immediately.

Link: <shadow-maint#1482>
Reviewed-by: Serge Hallyn <serge@hallyn.com>
Signed-off-by: Alejandro Colomar <alx@kernel.org>
Link: <shadow-maint#1482>
Reviewed-by: Serge Hallyn <serge@hallyn.com>
Signed-off-by: Alejandro Colomar <alx@kernel.org>
Whenever we were reading it, let's assume it contains a -1 (the integer
representation of an empty field).  Whenever we were writing it, let's
write a -1.

I think this would be worth a CVE.  I've contacted Debian, in case they
can issue a CVE.  Here's the draft I've sent them for the CVE:

	shadow-utils: inability to change a compromised password

Description
	A flaw was found in /etc/shadow, and the programs that handle
	the file.

	The /etc/shadow database (documented in shadow(5)) entries
	contain a "minimum password age" field.  This field imposes
	a minimum password age before the password can be changed.

	If a user's password is compromised before that period expires,
	the user must contact the administrator to be able to change the
	password.  This can usually take hours or days, and during these
	hours or days, an attacker would have unlimited access to the
	credentials.

	One option of the user would be to lock its own password with
	'passwd -l', thus being locked out of its own account.

	The history of this 4th field of /etc/shadow entries is
	considered dubious in retrospective, and the recommended action
	is to never set this field.  If the field is set, the
	recommended action is to ignore the field, and if possible,
	remove it.

Link: <shadow-maint#1482>
Reviewed-by: Serge Hallyn <serge@hallyn.com>
Signed-off-by: Alejandro Colomar <alx@kernel.org>
Link: <shadow-maint#1482>
Signed-off-by: Alejandro Colomar <alx@kernel.org>
@alejandro-colomar alejandro-colomar merged commit 34f78dc into shadow-maint:master Jul 11, 2026
13 of 15 checks passed
alejandro-colomar added a commit that referenced this pull request Jul 11, 2026
Link: <#1482>
Reviewed-by: Serge Hallyn <serge@hallyn.com>
Signed-off-by: Alejandro Colomar <alx@kernel.org>
alejandro-colomar added a commit that referenced this pull request Jul 11, 2026
Link: <#1482>
Reviewed-by: Serge Hallyn <serge@hallyn.com>
Signed-off-by: Alejandro Colomar <alx@kernel.org>
alejandro-colomar added a commit that referenced this pull request Jul 11, 2026
It makes no sense to limit the frequency of password change.  If one
changes its password, and 5 minutes later the password is leaked, one
should be able to change the password immediately.

Link: <#1482>
Reviewed-by: Serge Hallyn <serge@hallyn.com>
Signed-off-by: Alejandro Colomar <alx@kernel.org>
alejandro-colomar added a commit that referenced this pull request Jul 11, 2026
Link: <#1482>
Reviewed-by: Serge Hallyn <serge@hallyn.com>
Signed-off-by: Alejandro Colomar <alx@kernel.org>
alejandro-colomar added a commit that referenced this pull request Jul 11, 2026
Whenever we were reading it, let's assume it contains a -1 (the integer
representation of an empty field).  Whenever we were writing it, let's
write a -1.

I think this would be worth a CVE.  I've contacted Debian, in case they
can issue a CVE.  Here's the draft I've sent them for the CVE:

	shadow-utils: inability to change a compromised password

Description
	A flaw was found in /etc/shadow, and the programs that handle
	the file.

	The /etc/shadow database (documented in shadow(5)) entries
	contain a "minimum password age" field.  This field imposes
	a minimum password age before the password can be changed.

	If a user's password is compromised before that period expires,
	the user must contact the administrator to be able to change the
	password.  This can usually take hours or days, and during these
	hours or days, an attacker would have unlimited access to the
	credentials.

	One option of the user would be to lock its own password with
	'passwd -l', thus being locked out of its own account.

	The history of this 4th field of /etc/shadow entries is
	considered dubious in retrospective, and the recommended action
	is to never set this field.  If the field is set, the
	recommended action is to ignore the field, and if possible,
	remove it.

Link: <#1482>
Reviewed-by: Serge Hallyn <serge@hallyn.com>
Signed-off-by: Alejandro Colomar <alx@kernel.org>
@ikerexxe

Copy link
Copy Markdown
Collaborator

Edit: In v3, I removed those tests, which worked, but in v4, I tried to keep them (and adapt them), which seems to fail for unprivileged tests.

The latest version looks good. If we are to ignore the attribute, then let's stop checking it.

Edit: I've sent an email to Debian security@, in case they can issue the CVE.

Since you’ve already got in touch with the Debian security team, let’s continue along those lines. At this stage, I assume that all the teams dealing with CVEs are swamped at the moment, and I don’t want to add to their workload any more than they already have

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.

4 participants