-   Programming (
-   -   BASH script report invalid logins (

bkone 10-08-2007 05:04 PM

BASH script report invalid logins
I am trying to create a BASH script that will monitor users who login to a Linux machine locally. If they exceed the maximum number of logins their accounts will be locked and an email will be sent to the administrator. I've found applications that will do this but I want to see it in a BASH script to help me understand the process. I know I can use the faillog, at least that is what I am thinking, to track the say 5 unsuccessful logins. I just don't know how to take that information and lock them out and send an email to the admin. Any help would be greatly appreciated. I am really new to Linux mainly a Windows admin but trying to move off and into the Linux world because this OS is simply amazing. Thanks again.

bkone 10-09-2007 06:40 AM

Could someone maybe point me in the direction to accomplish this script?

matthewg42 10-09-2007 09:51 AM

Well, the lastb command can be used to list bad login attempts. The last command can be used to show successful attempts. You could try to parse these and extract the information that way.

A question arises... how do you want to limit the number of failed logins before the program locks the account? Would it be a certain number per day, or a certain number before a successful attempt, regardless of the date?

Is performance an issue? How many users are we talking here?

bkone 10-09-2007 11:16 AM

I want to track it by number of invalid attempts until successful attempt is made. In short, after 5 unsuccessful logins the user account is locked out and an email is sent to the administration stating Joe User's account is locked because he had 5 unsuccessful logins.

This would be one user per machine.

I hope that helps.

matthewg42 10-09-2007 06:29 PM

Well, there are three main parts to this:
  1. Getting the list of users to lock
  2. Locking the users
  3. Notifying the system administrator.

There are also a few things to watch out for... For example, you want the program to not lock the root user, or any other special-user accounts. If the program could do this, then a malicious user could lock you out of the machine just by trying to log in five times as root and getting the password wrong...

I'd probably want to separate out the three parts into separate functions, just to keep the code nice and tidy and understandable.

Shell scripts are read in a single pass, and so functions must be defined before they are used, so you cannot do something like this:



my_function () {
    # define function here

another_function () {
    # define function here

You can't do that because at the time the shell calls the functions my_function and another_function, they are not defined.

However, defining long set of functions first, and then calling them all from the bottom of the script makes it a little hard to read the script and understand it - something any good programmer should keep in mind.

To improve readability, I like to formulate long scripts like this:


main () {
    # The main flow of the program goes here

some_function () {
    # We can define this here because we _call_ main at the end of the file after this

main "$@"

This is just my preference, but it might help to explain why I structure the script as I do.

So, here goes, what I came up with. The email mechanism will need to be tailored to your system, depening on what command line mail tools you have available.



# This variable holds the number of consecutive failed attempts allowed
# before the action is taken

# Change this to send to whatever email address you like.

main () {

    for u in $users_to_lock; do
        lock_user $u && users_locked="$users_locked $u"

    if [  "$users_locked" != "" ]; then
        notify_admin $users_locked


get_list_users_to_lock () {
    # list the failed login attempts and successful attempts
    # with reformatting to make it possible to do a chronological
    # sort using the program "sort".

    # we will use a temporary file to store some stuff...

    # The cut command is used to print only the user name and
    # the login time
    lastb | cut -c1-8,40-56 | while read user attempt_date
        if [ "$user" = "" ]; then

        # use the date command to reformat the date string
        echo "$user $(date -d "$attempt_date" "+%Y%m%d%H%M%S") FAILURE"
    done  > "$tmp"

    last | cut -c1-8,40-56 | while read user attempt_date
        if [ "$user" = "" ]; then
        echo "$user $(date -d "$attempt_date" "+%Y%m%d%H%M%S") SUCCESS"
    done >> "$tmp"

    # Now we have a temp file with the username, date and status of
    # each login attempt.  Sorting this file will group the results
    # by user and then sort by time.

    # we'll use this counter to check how many failures in a row we
    # have found

    # OK, do the sort and read in the values into some variables.
    sort "$tmp" |grep -v "^reboot" |while read user date result; do
        case $result in
            let consecutive_failures+=1

        if [ $consecutive_failures -gt $FAILNUM ]; then
            echo $user
    done | sort -u

    # don't forget to clean up after ourselves
    rm -f "$tmp"

lock_user () {
    # get the UID (numerical ID for user)
    uid=$(grep "^$1:" /etc/passwd |cut -d: -f 3)
    if [ $? -ne 0 ]; then
        echo "UID lookup for user $1 failed" 1>&2
        return 1

    case "$uid" in
        # we want a numerical value...  don't do anything in this case
        echo nop > /dev/null
        echo "uid is not numeric - $uid - weirdness"
        return 1

    if [ "$uid" -lt 1000 ]; then
        echo "Will not lock system account: $1 (uid is $uid)"
        return 2

    # Check if the account is already locked
    # the password hash is prefixed with ! if the account is locked
    # the password is in the /etc/shadow file, so we can only do
    # this as root.
    # anyhow, if we find that the user if
    grep -q "^$1:\!" /etc/shadow
    case $? in
        # We found this line - the user is already locked
        echo "account for user $1 is already locked" 1>&2
        return 3
        # OK, the account is not already locked... lets do it
        usermod -L $1 && return 0 || return 4
        # The grep failed for some reason - probably permissions on the
        # /etc/shadow
        return 5

notify_admin () {
    cat <<EOD | mailx -s "$# account(s) locked because of failed login attempts" $NOTIFY_ADDRESS
The following user accounts have been locked because of multiple
FAILED login attempts to host $(hostname):


Your lovely script

main "$@"

The script would presumably be run from cron once every so often.

Now, the reasons not to use this script:
  1. This is not really the right approach to take at all. Your login program should be doing this, and should do it is real time. If you run the script once every 24 hours, that's a lot of time for an attacker to attack your machine without you being notified.
  2. There are already programs which do this sort of thing - don't re-invent the wheel.
  3. Calling date many times - once per line of last and lastb output is slow and loads the machine more than is necessary. A better script would parse the date internally, and thus a better choice of language would be Perl of Python, both of which have modules which can access the wtmp directly, have good date parsing functions, and thus will be a lot more efficient. This is especially important on machines which are going to have large wtmp. They can get very big - do not underestimate this.
  4. It is not thoroughly tested. Any program which automatically locks user accounts is dangerous, and you really shouldn't run it unless it's had serious testing.
  5. If I hired a programmer to write it, and got this back, I would not hire them again - wrong choice of language.
  6. DOS attack vector - anyone wanting to mess about another user can deliberately get their password wrong 5 times and have their account locked.
  7. UIDs of less than 1000 are quite OK and used for some user accounts. 1000 is commonly used on main stream Linux distros, but it is not a given - therefore you have a maintenance and portability problem.

bkone 10-10-2007 11:20 AM


How do you read which users are locked? How do you save their id to a variable?

matthewg42 10-10-2007 11:45 AM


Originally Posted by bkone (Post 2919738)

How do you read which users are locked?

In the /etc/shadow file, their password hash entry is prefixed with a ! if their account is locked already... From the usermod manual page:

-L, --lock
  Lock a user’s password. This puts a ’!’ in front of the
  encrypted password, effectively disabling the password.


Originally Posted by bkone (Post 2919738)
How do you save their id to a variable?

What do you mean?

schneidz 10-10-2007 01:19 PM

this might get you started:

All times are GMT -5. The time now is 06:27 PM.