How to disable GNU/Linux user accounts is a topic that gets asked of me once every few years. And then as I have slept since the last time I needed to know this I always must go read through all of the documentation all over again and make some test cases to verify how everything works. This just happened again. This time I am documenting this for my own purposes for the next time.
This does NOT APPLY to BSD or legacy Unix systems. FreeBSD, NetBSD, and OpenBSD are different with a different account password management system.
To disable an account.
# passwd -l ACCOUNTNAME
# usermod -e 1 ACCOUNTNAME
# crontab -u ACCOUNTNAME -l > crontab.saved.ACCOUNTNAME
# crontab -u ACCOUNTNAME -r
# mv ~ACCOUNTNAME/.ssh/authorized_keys ~ACCOUNTNAME/.ssh/authorized_keys.disabled
To re-enable a disabled account.
# passwd -u ACCOUNTNAME
# usermod -e '' ACCOUNTNAME
# crontab -u ACCOUNTNAME - < crontab.saved.ACCOUNTNAME
# mv ~ACCOUNTNAME/.ssh/authorized_keys.disabled ~ACCOUNTNAME/.ssh/authorized_keys
The passwd
command.
PASSWD(1) User Commands PASSWD(1)
NAME
passwd - change user password
...
-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).
Note that this does not disable the account. The user may still be
able to login using another authentication token (e.g. an SSH key).
To disable the account, administrators should use usermod
--expiredate 1 (this set the account's expire date to Jan 2, 1970).
Users with a locked password are not allowed to change their
password.
-u, --unlock
Unlock the password of the named account. This option re-enables a
password by changing the password back to its previous value (to
the value before using the -l option).
The usermod
command.
USERMOD(8) System Management Commands USERMOD(8)
NAME
usermod - modify a user account
...
-e, --expiredate EXPIRE_DATE
The date on which the user account will be disabled. The date is
specified in the format YYYY-MM-DD.
An empty EXPIRE_DATE argument will disable the expiration of the
account.
This option requires a /etc/shadow file. A /etc/shadow entry will
be created if there were none.
The shadow
file from a GNU/Linux system.
SHADOW(5) File Formats and Conversions SHADOW(5)
NAME
shadow - shadowed password file
DESCRIPTION
shadow is a file which contains the password information for the
system's accounts and optional aging information.
This file must not be readable by regular users if password security is
to be maintained.
Each line of this file contains 9 fields, separated by colons (":"), in
the following order:
...
account expiration date
The date of expiration of the account, expressed as the number of
days since Jan 1, 1970.
Note that an account expiration differs from a password expiration.
In case of an account expiration, the user shall not be allowed to
login. In case of a password expiration, the user is not allowed to
login using her password.
An empty field means that the account will never expire.
The value 0 should not be used as it is interpreted as either an
account with no expiration, or as an expiration on Jan 1, 1970.
The crontab
command.
CRONTAB(1) General Commands Manual CRONTAB(1)
NAME
crontab - maintain crontab files for individual users (Vixie Cron)
SYNOPSIS
crontab [ -u user ] file
crontab [ -u user ] [ -i ] { -e | -l | -r }
...
The first form of this command is used to install a new crontab from
some named file or standard input if the pseudo-filename ``-'' is
given.
The -l option causes the current crontab to be displayed on standard
output. See the note under DEBIAN SPECIFIC below.
The -r option causes the current crontab to be removed.
...
Let's dig into the details of the above and interpret and summarize.
The password is stored as a hash. When matching passwords the entry
is hashed in the same way and then the hash string compared. Two
characters that are not part of the hash output and therefore if
present can never match are "*
" and "!". If either of those
characters are present in the hash string then it is impossible for
the password to match.
This makes using "*
" a very typical character to use in the hashed
password field since the beginning. Admins will somethings refer to
it this way. "I starred out the password." That means they either
added a "*
" or replaced the entire hash with a "*
" to disable the
password.
Programs used for automation also need to edit this file. And it is
convenient to be able to make the edit reversible. Therefore the
passwd
program adds a "!" character to the start of the hash with
-l
and remove a leading "!" character with a -u
option. This
makes the program usage to automatically disable and to reversibly
enable accounts possible.
# passwd -u test1
# passwd -l test1
Logged to the /var/log/auth.log
file.
passwd[20415]: password for 'test1' changed by 'root'
passwd[20817]: password for 'test1' changed by 'root'
The documentation for the shadow file on a GNU/Linux system says "The value 0 should not be used as it is interpreted as either an account with no expiration, or as an expiration on Jan 1, 1970." And therefore I should conclude that different systems do this differently. And BSD systems are examples that do this differently. On a FreeBSD system the man 5 passwd page says.
The expire field is the number of seconds from the epoch, UTC, until the
account expires. This field may be left empty to turn off the account
aging feature.
Therefore on FreeBSD, NetBSD, and OpenBSD the field should definitely be 1 and not 0. Also there are many other differences and this document specifically only deals with GNU/Linux systems. Above we see that BSD stores this in seconds for one. I checked HP-UX for a different legacy system and on HP-UX it is stored as the number of weeks which is yet again different.
Testing with these following commands I find the following results.
# usermod -e 0 test1
# su - test1
Your account has expired; please contact your system administrator.
su: Authentication failure
$ ssh test1@localhost
test1@localhost's password:
Your account has expired; please contact your system administrator.
Connection closed by 127.0.0.1 port 22
# usermod -e 1 test1
# su - test1
Your account has expired; please contact your system administrator.
su: Authentication failure
$ ssh test1@localhost
test1@localhost's password:
Your account has expired; please contact your system administrator.
Connection closed by 127.0.0.1 port 22
Then in the log the following is logged to the /var/log/auth.log
file.
usermod[854]: change user 'test1' expiration from 'never' to '1970-01-01'
usermod[886]: change user 'test1' expiration from '1970-01-01' to '1970-01-02'
su: pam_unix(su-l:account): account test1 has expired (account expired)
sshd[19005]: pam_unix(sshd:account): account test1 has expired (account expired)
sshd[19005]: Failed password for test1 from 127.0.0.1 port 51028 ssh2
sshd[19005]: fatal: Access denied for user test1 by PAM account configuration [preauth]
su: pam_unix(su-l:account): account test1 has expired (account expired)
sshd[20211]: pam_unix(sshd:account): account test1 has expired (account expired)
sshd[20211]: Failed password for test1 from 127.0.0.1 port 51050 ssh2
sshd[20211]: fatal: Access denied for user test1 by PAM account configuration [preauth]
If the user had an installed crontab file then cron will continue to attempt to run the contents of that cronjob file. If the account is disabled then this will be rejected. Therefore disabing an account (the usermod -e 1 part of the above) will also prevent any of that users cronjob tasks from running. However when cron attempts to run the task the result will be logged into
If the user has commands in their crontab file then the following gets
logged to the /var/log/auth.log
file every time it would have run.
CRON[21493]: pam_unix(cron:account): account test1 has expired (account expired)
Therefore to silence that log action it is best to remove the file. But obviously removing the file is not a reversible action. Therefore I suggest saving the file off prior to removing it. Then to reverse the action the saved file can be installed. This feels better to me than to comment out the entire file. Because the file might already be commented out and reversing such an edit is not completely trivial to do.
# crontab -u test1 -l > /tmp/test.crontab.out
# crontab -u test1 -r
# crontab -u test1 -l
no crontab for test1
# crontab -u test1 - < /tmp/test.crontab.out
# crontab -u test1 -l
# This is a comment in test1 crontab.
* * * * * echo hello > /var/tmp/hello.txt
# crontab -u test1 -r
# crontab -u test1 -l
no crontab for test1
SSH logins are immediately disabled when the account is expired. Therefore there is no concern that a left-behind authorizedkeys file will allow access by itself. However if the account were to be enabled again by accident then the authorizedkeys file is once again active if it is present. Therefore it is best to disable it.
# mv ~ACCOUNTNAME/.ssh/authorized_keys ~ACCOUNTNAME/.ssh/authorized_keys.disabled
Once again I choose to make these changes reversible. However if it is known that this will never need to be reversed then removing the file is a good cleanup rather than leaving behind a public key file that can never be used again. In which case it would just be lint.
# rm -f ~ACCOUNTNAME/.ssh/authorized_keys
Copyright (C) 2021 Bob Proulx
Copying and distribution of this file, with or without modification, are permitted in any medium without royalty provided the copyright notice and this notice are preserved. This file is offered as-is, without any warranty.