DEV Community

Daniel Favour
Daniel Favour

Posted on

Automating User Management on Linux using Bash Script

Efficient user management is important for maintaining security and productivity. Manual management of users and groups can be time-consuming, especially in larger organizations where administrators need to handle multiple accounts and permissions. Automating these tasks not only saves time but also reduces the risk of human error.

This guide discusses a practical approach to automating user management on a Linux machine using a Bash script. You will learn how to create users, assign groups, generate secure passwords for users, and log actions using a single bash script.

Overview of the Bash Script Functionality

Below is an overview of the tasks the Bash script will automate for efficient user management:

  • Read User Data: The script will read a text file containing employee usernames and their corresponding group names.
  • Create Users and Groups: It will create users and groups as specified in the text file.
  • Set Up Home Directories: The script will set up home directories for each user with appropriate permissions and ownership.
  • Generate Secure Passwords: It will generate random, secure passwords for the users.
  • Log Actions: All actions performed by the script will be logged to the /var/log/user_management.log directory.
  • Store Passwords Securely: Generated passwords will be securely stored in the /var/secure/user_passwords.txt directory.
  • Error Handling: The script will include error handling to manage scenarios such as existing users and groups.

Prerequisites

To get started with this tutorial, you must have the following:

  • A Linux machine with administrative privileges.
  • Basic knowledge of Linux commands.
  • A text editor of choice (vim, nano, etc)

Setting Up the User Data File

The first step is to create a text file containing the username for each employee and the groups to be assigned to each of them.

In your terminal, create a user_password.txt file:

touch user_passwords.txt
Enter fullscreen mode Exit fullscreen mode

Paste the below content into the file

john;qa
jane;dev,manager
robert;marketing
emily;design,research
michael;devops
olivia;design,research
william;support
sophia;content,marketing
daniel;devops,sre
ava;dev,qa
Enter fullscreen mode Exit fullscreen mode

The above is a list of usernames for the employees and their respective group(s).

Writing the Bash script

To start creating the Bash script, follow these steps in your terminal:

Create the Script file

Open your terminal and run the following command to create an empty file named create_users.sh:

touch create_users.sh
Enter fullscreen mode Exit fullscreen mode

Use your preferred text editor to open the create_users.sh file and begin writing the script:

nano create_users.sh
Enter fullscreen mode Exit fullscreen mode

(If you are using a different editor, replace nano with its command)

Add the Shebang Line

At the top of the create_users.sh file, include the shebang.

#!/bin/bash
Enter fullscreen mode Exit fullscreen mode

This line specifies the interpreter that will be used to execute the script. In this case,#!/bin/bash indicates that the script should be run using the Bash shell.

Check Root Privileges

Creating users and groups typically requires administrative privileges because it involves modifying system files and configurations. After the shebang line, add the below configuration to ensure that the Bash script is executed with root privileges:

if [[ $(id -u) -ne 0 ]]; then
    echo "This script must be run as root."
    exit 1
fi
Enter fullscreen mode Exit fullscreen mode

This checks if the script is running with root privileges. If the current user ID ($(id -u)) does not equal (-ne) 0, then the condition is true (indicating the script is not running as root), and the code within the then ... fi block will execute accordingly. In this case, it will output "This script must be run as root." to the terminal, and then the script will exit with a status of 1. This exit status is a signal to the operating system and any other processes that the script encountered an error and did not complete successfully.

Check that the Input File is Passed as an Argument

In Bash scripting, "arguments" are the values or parameters provided to a script or command when it is invoked from the command line. For example, if you have a Bash script named process_file.sh and you want to read an input file, data.txt provided as an argument, in the terminal, you will execute it with:

./process_file.sh data.txt
Enter fullscreen mode Exit fullscreen mode

Inside your Bash script (process_file.sh), you can access this argument using special variables like $1, $2, etc. $1 specifically refers to the first argument passed (data.txt in this case). Once the script captures the argument ($1), it can use it in various ways. For instance, it might open and read the file specified (data.txt), process its contents, or perform any other operation that the script is designed to do.

In this case, the create_users.sh script needs to read the user data file, user_passwords.txt containing the usernames and groups of the employees so it can perform certain actions.

Paste the below configuration in your script:

if [[ $# -ne 1 ]]; then
    echo "Usage: $0 <input-file>"
    exit 1
fi
Enter fullscreen mode Exit fullscreen mode
  • if [[ ... ]]; then ... fi: This is a conditional statement in Bash. The code inside the then ... fi block will execute only if the condition within the double square brackets [[ ... ]] evaluates to true.
  • if [[ $# -ne 1 ]]; then: This checks if the number of arguments ($#) passed to the script is not equal (-ne) to 1.
  • echo "Usage: $0 <input-file>": If the condition is true (meaning the wrong number of arguments were provided), this line prints a helpful message to the terminal explaining how the script should be used.
    • Usage: A standard keyword indicating the start of usage instructions.
    • $0: This is a special variable that holds the name of the script itself. It is automatically replaced with the actual name of the script when it runs (for example, "create_users.sh").
    • <input-file>: This placeholder communicates to the user that they need to provide the name of the input file (for example, "user_passwords.txt") as the argument when running the script.
  • exit 1: This terminates the script with an exit status of 1. An exit status of 1 signals that the script encountered an error and did not complete successfully.

Assign Variables

The next step is to assign variables for essential paths and files.

INPUT_FILE=$1
LOG_FILE="/var/log/user_management.log"
PASSWORD_FILE="/var/secure/user_passwords.txt"
Enter fullscreen mode Exit fullscreen mode
  • INPUT_FILE=$1: This line assigns the first command-line argument (the user_passwords.txt file) to the variable INPUT_FILE. This makes it easier to reference the filename throughout the script.
  • LOG_FILE="/var/log/user_management.log": This sets the variable LOG_FILE to the path where the script will write its log messages. This log will help track the actions performed by the script.
  • PASSWORD_FILE="/var/secure/user_passwords.txt": This sets the variable PASSWORD_FILE to the path where the generated passwords for the users will be stored securely.

Log Messages

Logging messages is a common practice in scripting and software development as it records what happens at each step in the script.

Add the below configuration to log messages in the $LOG_FILE directory:

# Function to log messages
log_message() {
    echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a $LOG_FILE
}
Enter fullscreen mode Exit fullscreen mode
  • log_message(): This function is a reusable piece of code designed to create formatted log entries and write them to a specified log file.
  • echo "$(date '+%Y-%m-%d %H:%M:%S') - $1": date '+%Y-%m-%d %H:%M:%S': This generates the current date and time in the format "YYYY-MM-DD HH:MM:SS". $1 represents the message you pass to the function as the first argument. This command combines the timestamp and the message, separated by a hyphen (-), creating the formatted log entry.
  • tee -a $LOG_FILE: The tee command reads from standard input (in this case, the output of the echo command) and writes it to both standard output (the terminal) and to one or more files. The -a option tells tee to append to the file ($LOG_FILE) instead of overwriting it. This ensures that previous log entries are preserved.

Ensure the /var/secure Directory Exists

Before proceeding with user creation, it's imperative to establish a secure environment for storing the generated passwords. The passwords will be stored in the /var/secure directory, so it is necessary to check if this directory exists and configure it with appropriate permissions to limit access to authorized users only.

if [[ ! -d "/var/secure" ]]; then
    mkdir -p /var/secure
    chown root:root /var/secure
    chmod 600 /var/secure
fi
Enter fullscreen mode Exit fullscreen mode
  • if [[ ! -d "/var/secure" ]]; then: This condition checks if the /var/secure directory exists. The -d flag checks if the path is a directory, and the ! negates the result, meaning the code within the then block will only execute if the directory does not exist.
  • mkdir -p /var/secure: This line creates the /var/secure directory if it does not exist. The -p option ensures that any necessary parent directories are also created.
  • chown root:root /var/secure: This changes the owner of the /var/secure directory to the root user and the root group. This is a security best practice, as sensitive data like passwords should only be accessible to the system administrator.
  • chmod 600 /var/secure: This changes the permissions of the /var/secure directory so that the owner (root) has full read and write access to the directory, no users in the root group (other than root itself) can access the directory's contents in any way and no other users on the system can access the directory's contents.
  • fi: It is used to close an if statement and indicates the end of the block of code that should be executed conditionally based on the evaluation of the if statement.

Generate the User Passwords

User passwords should be unique and secure. /dev/urandom ensures your passwords are both random and secure. It is a special file in Unix-like systems that provides a constant stream of high-quality random data, making it difficult to predict or replicate the generated passwords.

Paste the below configuration in the script:

generate_password() {
    tr -dc 'A-Za-z0-9!@#$%^&*()_+=-[]{}|;:<>,.?/~' </dev/urandom | head -c 16
}
Enter fullscreen mode Exit fullscreen mode
  • generate_password() {: This defines the start of a function named generate_password.
  • tr -dc: This command is used to delete all characters from the input that are not in the specified set. tr stands for "translate" and is used to delete or replace characters, the -d option specifies that characters should be deleted, and the -c option complements the set of characters. This means that instead of deleting the characters specified in the set, it will delete all characters that are not in the set.
  • 'A-Za-z0-9!@#$%^&*()_+=-[]{}|;:<>,.?/~': This is the set of characters allowed in the password, including uppercase letters (A-Z), lowercase letters (a-z), digits (0-9), and various special characters.
  • </dev/urandom |: /dev/urandom is a special file in Unix-like operating systems that provides random data. < is used to redirect the contents of /dev/urandom as input to the command on the left, and the file contents are passed through the pipe | to the next command.
  • head -c 16: The head command displays the first few lines of the file content that passes through the pipe, and the -c 16 option modifies head to output only the first 16 bytes of its input.

Process the Input File

The user_password.txt file that was passed in can now be used to carry out user management tasks. It will read the usernames and groups from the input file, create the users and their personal group, add them to their respective groups, set up their home directory, and generate and store their passwords. To execute these tasks efficiently, it's beneficial to keep them within a while loop so that each line of user data is processed sequentially, ensuring systematic user management operations.

To create the while loop, use:

# Read the input file line by line
while IFS=';' read -r username groups; do
Enter fullscreen mode Exit fullscreen mode
  • while ... do: This starts a loop that continues to read lines from the input until there are no more lines to read.
  • IFS=';': This sets the internal field separator (IFS) to a semicolon. It tells the read command to use semicolons as the delimiter for splitting input lines into fields.
  • read -r: Reads the input line into the variables username and groups.
  • tr -d '[:space:]' removes all whitespace characters from the username and groups.

In this loop, there will be several iterations that will be carried out:

Iteration 1: Trim any leading/trailing whitespace

    # Trim any leading/trailing whitespace
    username=$(echo "$username" | tr -d '[:space:]')
    groups=$(echo "$groups" | tr -d '[:space:]')
Enter fullscreen mode Exit fullscreen mode
  • username=$(echo "$username" | tr -d '[:space:]'): This line removes all whitespace characters from the beginning and end of the username value.
  • groups=$(echo "$groups" | tr -d '[:space:]'): This line does the exact same thing as the first line, but for the groups variable. It removes any leading or trailing whitespace from the list of groups associated with the user.

Logs the username and associated groups read from the input file to be certain the script is reading the user_password.txt file correctly:

    # Debug: Log the username and groups read from the file
    log_message "Read line: username='$username', groups='$groups'"
Enter fullscreen mode Exit fullscreen mode

This is useful for troubleshooting.

Iteration 2: Check if usernames or groups are empty

    if [[ -z "$username" || -z "$groups" ]]; then
        log_message "Error: Username or groups missing in line: $username"
        continue
    fi
Enter fullscreen mode Exit fullscreen mode
  • [[ -z "$username" || -z "$groups" ]]: [[ ... ]] is a conditional expression in Bash for testing. -z "$username" checks if the variable username is empty (has zero length). -z "$groups" checks if the variable groups is empty (has zero length). || is the logical OR operator, which means the condition is true if either $username or $groups (or both) are empty.
  • log_message "Error: Username or groups missing in line: $username": If either $username or $groups is empty, this command writes an error message to the $LOG_FILE.
  • continue: If the condition is true (i.e., either $username or $groups is empty), continue skips the rest of the current iteration of the loop. The script then moves on to the next iteration to process the next line from the input file.

Iteration 3: Check if the user already exists, otherwise create the user's personal group

    # Check if the user already exists
    if id "$username" &>/dev/null; then
        log_message "User $username already exists, skipping."
    else
        # Create the user's personal group
        if ! getent group "$username" >/dev/null; then
            groupadd "$username"
            log_message "Created group: $username"
        fi
Enter fullscreen mode Exit fullscreen mode
  • if id "$username" &>/dev/null; then: This if statement checks if the user $username exists by querying the user database. &>/dev/null redirects both stdout (standard output) and stderr (standard error) to /dev/null, discarding any output. If the user exists, the condition is true (id command succeeds), and the script proceeds inside the if block.
  • log_message "User $username already exists, skipping.": If the user already exists (id command succeeds), this message is logged and the user creation process is skipped for this user.
  • else: If the user does not exist (the id command fails), the script proceeds with user and group creation.
  • if ! getent group "$username" >/dev/null; then: This command checks if a group with the username already exists and proceeds only if it doesn't.
  • groupadd "$username": Creates a new group with the same name as the username. This group will serve as the user's primary group, providing some basic permissions and ownership settings.
  • log_message "Created group: $username": Calls the log_message function to record the action of creating the group. The message passed to the function includes the name of the group that was created.

Iteration 4: Create the users with their personal group
To create users with their personal groups, add:

        # Create the user with the personal group
        useradd -m -g "$username" "$username"
        log_message "Created user: $username"
Enter fullscreen mode Exit fullscreen mode
  • useradd -m -g "$username" "$username": This creates a new user account with the specified username, creates their home directory, and assigns the user to their personal group.
  • log_message "Created user: $username": Calls the logging function and logs the username of the newly created account.

Iteration 5: Assign passwords to users
For the users to have access, they should be assigned their own passwords each.

        # Generate a random password and set it for the user
        password=$(generate_password)
        echo "$username:$password" | chpasswd
        log_message "Set password for user: $username"
Enter fullscreen mode Exit fullscreen mode
  • password=$(generate_password): Calls the generate_password function to generate a password, and stores the output within a variable named password.
  • echo "$username:$password" | chpasswd: This takes the generated password, formats it correctly, and passes it to the chpasswd command to set the password for the user.
  • log_message "Set password for user: $username": The function logs the action taken, indicating that the password has been set for the user.

Iteration 6: Add the user to additional groups

        IFS=',' read -ra group_array <<< "$groups"
        for group in "${group_array[@]}"; do
            if ! getent group "$group" &>/dev/null; then
                groupadd "$group"
                log_message "Created group: $group"
            fi
            usermod -aG "$group" "$username"
            log_message "Added user $username to group: $group"
        done
Enter fullscreen mode Exit fullscreen mode
  • IFS=',' read -ra group_array <<< "$groups": IFS=',' sets the Internal Field Separator (IFS) to comma (,). This means that when the read command reads $groups, it will split it into multiple parts using comma as the delimiter. read -ra group_array <<< "$groups" reads the content of $groups into an array group_array, splitting it based on the comma delimiter (','), -r prevents backslashes from being interpreted as escape characters and -a group_array assigns the result to the array variable group_array.
  • for group in "${group_array[@]}"; do: Iterates over each element (group) in the group_array array.
  • if ! getent group "$group" &>/dev/null; then: getent group "$group" checks if the group $group exists in the system, ! negates the result, meaning if the group does not exist (! getent ...), the condition becomes true. &>/dev/null redirects both stdout and stderr to /dev/null, discarding any output. If the group does not exist, it proceeds with group creation.
  • groupadd "$group": Creates the group $group if it does not already exist.
  • log_message "Created group: $group": Logs a message indicating that the group $group was successfully created.
  • usermod -aG "$group" "$username": Adds the user $username to the group $group. -aG "$group": -a appends the user to the group without removing them from other groups -G specifies a list of supplementary groups.
  • log_message "Added user $username to group: $group": Logs a message indicating that the user $username was successfully added to the group $group.

Iteration 7: Create and set the home directory permissions
Users should have access to their individual home directories to perform certain actions. Add the below configuration in the script:

mkdir -p "/home/$username"
chown -R "$username:$username" "/home/$username"
chmod 755 "/home/$username"
Enter fullscreen mode Exit fullscreen mode
  • mkdir -p "/home/$username": This creates the home directory for each new user, where $username is the username of the user being processed.
  • chown -R "$username:$username" "/home/$username": This changes the ownership of the user's home directory and all files and subdirectories within it.
  • chmod 755 "/home/$username: This sets the permissions for the newly created user's home directory. 755 means the owner ($username) has read, write, and execute permissions (rwx). Users in the same group as the owner and other users have read and execute permissions (r-x).

Iteration 8: Store the username and password securely

        # Store the username and password securely
        echo "$username,$password" >> $PASSWORD_FILE
        chmod 600 "$PASSWORD_FILE"
        log_message "Password for $username stored in $PASSWORD_FILE."
    fi
Enter fullscreen mode Exit fullscreen mode
  • echo "$username:$password" >> "$PASSWORD_FILE": This line stores the username and its corresponding generated password in a file designated for storing passwords securely.
  • chmod 600 "$PASSWORD_FILE": This command restricts access to the password file to ensure it remains secure and confidential.
  • log_message "Password for $username stored in $PASSWORD_FILE.": Logs a message indicating that the password for $username has been stored in the password file ($PASSWORD_FILE).
  • fi: The fi keyword marks the end of the entire if...else conditional block, which started at iteration 3. It marks the end of the code block to execute if the condition is true.

End the Loop

End the loop using:

done < "$1"
Enter fullscreen mode Exit fullscreen mode

The expression done < "$1" signifies the end of the while loop and instructs it to read input from the file specified by the INPUT_FILE=$1 variable.

The complete script is available in this GitHub repository.

Run the Script

To execute the Bash script without calling Bash in your terminal, make the script executable:

chmod +x create_users.sh
Enter fullscreen mode Exit fullscreen mode

Once the script is executable, run it from the directory where it resides:

./create_users.sh user_passwords.txt
Enter fullscreen mode Exit fullscreen mode

Verify the Script Executed Tasks Successfully

Several checks should be carried out to ensure that the script executed all the user management tasks successfully.

To verify user existence, run:

id <username>
Enter fullscreen mode Exit fullscreen mode

To verify that the user password file was successfully created in the /var/secure/ directory, run:

cat /var/secure/user_passwords.txt
Enter fullscreen mode Exit fullscreen mode

To verify group existence, run:

getent group <username>
Enter fullscreen mode Exit fullscreen mode

To verify the log file as created and all actions are logged correctly without any errors or unexpected behaviors, run:

cat /var/log/user_management.log
Enter fullscreen mode Exit fullscreen mode
  • To verify that each user has a personal group with the same name as their username, run:
getent passwd <username>
getent group <username>
Enter fullscreen mode Exit fullscreen mode

Conclusion

This article highlights the automation of user management tasks using Bash scripts. It explores how scripting enhances efficiency in creating users, managing groups, setting permissions, and securing passwords on Linux systems.


This article is my stage 1 task at HNG internship program. HNG is an internship that helps people improve their tech skills. To learn more about the HNG internship, visit their website. If you are looking to hire talented Developers and Designers from the internship, visit HNG hire.

Top comments (0)