Automating Linux User Management and Permissions with Bash Scripting
Published: 2024-07-05 19:19:43

In this article, I'll be walking through the steps to create a user management bash script to meet some predefined requirements. The main goal of the script is to allow an administrator to create users and groups for different purposes within a linux environment, so without plenty talk lets get into it 

## Requirements

1. Users and their groups are defined in a text file that will be supplied to the script as an argument
2. A corresponding home directory will be created for each user
3. User passwords should be stored securely in a file with path /car/secure/user_passwords.txt
4. Logs of all actions should be logged to /var/log/user_management.log
5. Only the owner, in this case root, should be able to access the user_password.txt file
6. Errors should be gracefully handled

## Creating users
    
To create a user, you can use the useradd command. This command can be set to create a user, create their home directory, and set their password. If you simply wish to add a user to the linux system, you can run:

```
sudo useradd <username>
```

`<username>` here is the name of the user you wish to add. However, if you wish to create a home directory and add a password, you can run this instead:

```
sudo useradd -m -p $(openssl passwd -6 "$password") <username>
```

This command uses the `-m` flag to add a home directory and the `-p` flag to add a password (encrypted using openssl) for the user.

## Adding users to groups

By default, when a user is created, a personal group with their username is also created. This means you won't need to explicitly create this. However, to add a user to group, say sudo, you must use the usermod command. Find the basic command structure below:
```
sudo usermod -aG "<group>" "<user>"
```

The -a flag is used to append the user to the new group without removing them from existing groups. The -G flag on the other hand specifies the group that the user will be added to, in this case, .

## Creating groups

When a group doesn't exist, it should be created before a user is added to it. Groups are typically created using the groupadd command. Here's an example of the command in action:
```
sudo groupadd "<group>"
```

## Combining user creation, group creation, and group addition
You can command user creation, group creation, and adding a user to a group in a script. Say you have your users and groups defined in a semi-colon delimited script like this:

```
user1; group1, group2
user2; group3,group6
user3;group2,group3
```

You can write a script that loops through the file, extracts the relevant information, and creates the users and groups.

```
USERS_FILE=$1

mapfile -t lines < "$USERS_FILE"

# loop over each line in the array
for line in "${lines[@]}"; do
    # Remove leading and trailing whitespaces
    line=$(echo "$line" | xargs)

    # Split line by ';' and store the second part
    IFS=';' read -r user groups <<< "$line"

    # Remove leading and trailing whitespaces from the second part
    groups=$(echo "$groups" | xargs)

    # Create a variable groupsArray that is an array from spliting the groups of each user
    IFS=',' read -ra groupsArray <<< "$groups"

    # Generate a 6-character password using pwgen
    password=$(pwgen -sBv1 6 1)

    # Create the user with the generated password
    sudo useradd -m -p $(openssl passwd -6 "$password") "$user"

    # loop over each group in the groups array
    for group in "${groupsArray[@]}"; do
        group=$(echo "$group" | xargs)

        # Check if group exists, if not, create it
        if ! grep -q "^$group:" /etc/group; then
            sudo groupadd "$group"
            echo "Created group $group"
        fi

        # Add user to the group
        sudo usermod -aG "$group" "$user"
        echo "Added $user to $group"
    done

    echo "User $user created and added to appropriate groups"
done
```

Now, the script above does the following:

1. It takes in a single argument, expressed using $1. It then sets this argument as the variable, $USERS_FILE.
2. It uses the mapfile command to load the content of the $USERS_FILE into an array called lines.
3. It loops through each lines of lines and extracts the user and groups using the Internal Field Separator (IFS) shell command.
4. It generates a 6-character password using pwgen. pwgen is linux package that allows you to create passwords to your exact specification.
5. It loops over the groups, after splitting each group into groups using IFS, creates the group if it doesn't exist, and adds the user to the group.

## Securing the script: Hashing passwords with openssl

While the script above performs all the operations needed to create users and groups, and then add the users to groups, it does not consider security. The major security issue is that the generated passwords are added to users in plaintext format. To solve this problem, you can utilize openssl to hash the password. You can simply run openssl passwd -6 (generated_password) to achieve hashing. This command uses the SHA 512 algorithm for hashing. It's security is comparable to SHA 256 which is the most prominent hashing algorithm on the internet.

## Encrypting and storing the passwords
Since this script creates users, it is wise to capture the generated passwords in a file. But to do that securely, the passwords must be encrypted. Password encryption can also be done using openssl. But it'll require and encryption key. You can use the command below to generate, encrypt, and store a password.

```
# Generate a 6-character password using pwgen
    password=$(pwgen -sBv1 6 1)

    # Encrypt the password before storing it
    encrypted_password=$(encrypt_password "$password" "$PASSWORD_ENCRYPTION_KEY")

    # Store the encrypted password in the file
    echo "$user:$encrypted_password" >> "$PASSWORD_FILE"
```

The `$PASSWORD_ENCRYPTION_KEY` and `$PASSWORD_FILE` must be defined for this operation to complete successfully.

A look at the secure script
Here's the updated script with the password encryption and password hashing functionalities:


```
#!/bin/bash

PASSWORD_FILE_DIRECTORY="/var/secure"
PASSWORD_FILE="/var/secure/user_passwords.txt"
PASSWORD_ENCRYPTION_KEY="secure-all-things"
USERS_FILE=$1

# Function to encrypt password
encrypt_password() {
    echo "$1" | openssl enc -aes-256-cbc -pbkdf2 -base64 -pass pass:"$2"
}

# Create the directory where the user's password file will be stored
sudo mkdir -p "$PASSWORD_FILE_DIRECTORY"
sudo touch "$PASSWORD_FILE"
sudo chmod 600 "$PASSWORD_FILE" # Set read permission for only the owner of the file
sudo chown root:root "$PASSWORD_FILE" # Set the owner as the root user

# load the content of the users.txt file into an array: lines
mapfile -t lines < "$USERS_FILE"

# loop over each line in the array
for line in "${lines[@]}"; do
    # Remove leading and trailing whitespaces
    line=$(echo "$line" | xargs)

    # Split line by ';' and store the second part
    IFS=';' read -r user groups <<< "$line"

    # Remove leading and trailing whitespaces from the second part
    groups=$(echo "$groups" | xargs)

    # Create a variable groupsArray that is an array from spliting the groups of each user
    IFS=',' read -ra groupsArray <<< "$groups"


    # Generate a 6-character password using pwgen
    password=$(pwgen -sBv1 6 1)

    # Encrypt the password before storing it
    encrypted_password=$(encrypt_password "$password" "$PASSWORD_ENCRYPTION_KEY")

    # Store the encrypted password in the file
    echo "$user:$encrypted_password" >> "$PASSWORD_FILE"

    # Create the user with the generated password
    sudo useradd -m -p $(openssl passwd -6 "$password") "$user"

    # loop over each group in the groups array
    for group in "${groupsArray[@]}"; do
        group=$(echo "$group" | xargs)

        # Check if group exists, if not, create it
        if ! grep -q "^$group:" /etc/group; then
            sudo groupadd "$group"
            echo "Created group $group"
        fi

        # Add user to the group
        sudo usermod -aG "$group" "$user"
        echo "Added $user to $group"
    done

    echo "User $user created and password stored securely"
done

# remove the created password from the current shell session
unset password
```

The script above includes the additional functionality as preventing non-root users from accessing the password storage file and also removing the password variable using unset password from the shell where it is run.

Adding logging to the script
The script can be further improved by logging the commands to a log file. This file can be defined as a variable at the top of the script then a redirection command can be added to redirect logs from the script to the log file. We can also direct errors that might occur to the std out, that's the normal output you see when you run commands without errors. Both log types will ultimately be sent to the log file. The command below illustrates this:

```
# Redirect stdout and stderr to log file
exec > >(tee -a "$LOG_FILE") 2>&1 
echo "Executing script... (note that this line will be logged twice)" | tee -a $LOG_FILE 
```

The echo "Executing script..." command is added so that the normal console shows the logs too. It's not wise to run a script without seeing an output. The addition of this line will ultimately mean it gets shown in the log file twice, but this is the compromise that has to be made.

Adding Error Handling
Errors can be handled and prevented using exception handling. We can add functions that check that both openssl and pwgen are installed, otherwise installs them. When can also add handlers that check if arguments are not passed to the script and if the argument passed for the user's file is a valid file. Here's a snippet with these exception handlers:

```
#!/bin/bash

LOG_FILE="/var/log/user_management.log"
PASSWORD_FILE_DIRECTORY="/var/secure"
PASSWORD_FILE="/var/secure/user_passwords.txt"
PASSWORD_ENCRYPTION_KEY="secure-all-things"
USERS_FILE=$1

# Function to display usage information
usage() {
    echo "Usage: $0 <user-data-file-path>"
    echo "    <user-data-file-path>: Path to the file containing user data."
    echo
    echo "The user data file should contain lines in the following format:"
    echo "    username;group1,group2,..."
    echo
    echo "Example:"
    echo "    light; dev,sudo"
    echo "    mayowa; www-data, admin"
    exit 1
}

# Check if script is run with sudo
if [ "$(id -u)" != "0" ]; then
    echo "This script must be run with sudo. Exiting..."
    exit 1
fi


# Check if an argument was provided
if [ $# -eq 0 ]; then
    echo "Error: No file path provided."
    usage
fi

# Check if the user's data file exists
if [ ! -e "$USERS_FILE" ]; then
    echo "Error: The provided user's data file does not exist: $USERS_FILE"
    usage
fi

# Function to check if a package is installed
is_package_installed() {
    dpkg -s "$1" >/dev/null 2>&1
}

# Check if openssl is installed
if ! is_package_installed openssl; then
    echo "openssl is not installed. Installing..."
    sudo apt-get update
    sudo apt-get install -y openssl
fi

# Check if pwgen is installed
if ! is_package_installed pwgen; then
    echo "pwgen is not installed. Installing..."
    sudo apt-get update
    sudo apt-get install -y pwgen
fi

# Check if the file exists
if [ ! -f "$USERS_FILE" ]; then
    echo "Error: $USERS_FILE not found."
    exit 1
fi
```

An exception is also added that checks if the script was run using the sudo command. This is because sudo is required to perform useradd and groupadd operations.


## Conclusion

You can find the complete script in this [GitHub repository](https://github.com/vinuch/bash-user-management). A big thanks to [HNG](https://hng.tech) for providing this chance to dive into advanced bash scripting through a practical example. To join an upcoming HNG internship, keep an eye on [their internship page](https://hng.tech/internship). You can also hire top talent for your project through the HNG network by visiting [HNG Hire](https://hng.tech/hire).