Automating User Management on Linux using Bash Script

RMAG news

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

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

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

To open the Script File

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

nano create_users.sh

(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

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

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

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

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

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.

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

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. To avoid having passwords in plain text files, using /dev/urandom ensures your passwords are both random and secure. /dev/urandom 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
}

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

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:])

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.

Iteration 2: Check if usernames or groups are empty

if [[ z $username || z $groups ]]; then
echo Error: Username or groups missing in line: $line >> $LOG_FILE
continue
fi

[[ -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.

echo “Error: Username or groups missing in line: $line” >> “$LOG_FILE”: 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

# Check if the user already exists
if id $username &>/dev/null; then
echo User $username already exists, skipping. >> $LOG_FILE
else
# Create the user
useradd m s /bin/bash $username >> $LOG_FILE 2>&1
echo User $username created. >> $LOG_FILE

# Create the users personal group
groupadd “$username” >> “$LOG_FILE” 2>&1
echo “Group $username created.” >> “$LOG_FILE”
fi

if id “$username” &>/dev/null; then: id “$username” 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.

echo “User $username already exists, skipping.” >> “$LOG_FILE”: If the user already exists (id command succeeds), this message is appended to the $LOG_FILE indicating that 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.

useradd -m -s /bin/bash “$username” >> “$LOG_FILE” 2>&1″: This command creates a new user with the username $username. -m creates the user’s home directory if it does not exist, -s /bin/bash sets the user’s default shell to /bin/bash, and >> “$LOG_FILE” 2>&1 redirects both stdout and stderr of the useradd command to $LOG_FILE.

echo “User $username created.” >> “$LOG_FILE”: Logs a message indicating that the user $username was successfully created to the $LOG_FILE.

groupadd “$username” >> “$LOG_FILE” 2>&1: This command creates a new group with the same name as the user. >> “$LOG_FILE” 2>&1 redirects both stdout and stderr of the groupadd command to $LOG_FILE.

echo “Group $username created.” >> “$LOG_FILE”: Logs a message indicating that the group for user $username was successfully created to the $LOG_FILE.

Iteration 4: 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_FILE” 2>&1
echo “Group $group created.” >> “$LOG_FILE”
fi
# Add user to group
usermod -aG “$group” “$username” >> “$LOG_FILE” 2>&1
echo “User $username added to group $group.” >> “$LOG_FILE”
done

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” >> “$LOG_FILE” 2>&1: Creates the group $group if it does not already exist. >> “$LOG_FILE” appends the result (success or failure) to the $LOG_FILE. 2>&1 redirects stderr to stdout, ensuring any error messages are also captured in the $LOG_FILE.

usermod -aG “$group” “$username” >> “$LOG_FILE” 2>&1: 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_FILE” appends the result (success or failure) to the $LOG_FILE. 2>&1redirects stderr to stdout, ensuring any error messages are also captured in the $LOG_FILE.

echo “User $username added to group $group.” >> “$LOG_FILE”: Logs a message indicating that the user $username was successfully added to the group $group to the $LOG_FILE.

Iteration 5: Set home directory permissions

mkdir p /home/$username
chown R $username:$username /home/$username
chmod 755 /home/$username

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 6: Generate and store the password securely

password=$(generate_password)
echo $username:$password >> $PASSWORD_FILE
chmod 600 $PASSWORD_FILE
echo Password for $username stored in $PASSWORD_FILE. >> $LOG_FILE

password=$(generate_password): This captures and stores the output of the generate_password function within a variable named password.

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.

echo “Password for $username stored in $PASSWORD_FILE.” >> “$LOG_FILE”: Logs a message indicating that the password for $username has been stored in the password file ($PASSWORD_FILE).

End the Loop

At the end of the loop, add the below line to end it:

done < $INPUT_FILE

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

The complete script should be as follows:

#!/bin/bash

# Check if the script is run as root
if [[ $(id -u) -ne 0 ]]; then
echo “Please run as root”
exit 1
fi

# Check if the input file is provided as argument
if [[ $# -ne 1 ]]; then
echo “Usage: $0 <input-file>”
exit 1
fi

INPUT_FILE=$1
LOG_FILE=”/var/log/user_management.log”
PASSWORD_FILE=”/var/secure/user_passwords.txt”

# Ensure /var/secure directory exists
if [[ ! -d “/var/secure” ]]; then
mkdir -p /var/secure
chown root:root /var/secure
chmod 600 /var/secure
fi

# Function to generate a random password
generate_password() {
tr -dc ‘A-Za-z0-9!@#$%^&*()_+=-[]{}|;:<>,.?/~’ </dev/urandom | head -c 16
}

# Read the input file line by line
while IFS=’;’ read -r username groups; do
# Trim any leading/trailing whitespace
username=$(echo “$username” | tr -d ‘[:space:]’)
groups=$(echo “$groups” | tr -d ‘[:space:]’)

# Check if username or groups are empty
if [[ -z “$username” || -z “$groups” ]]; then
echo “Error: Username or groups missing in line: $line” >> “$LOG_FILE”
continue
fi

# Check if the user already exists
if id “$username” &>/dev/null; then
echo “User $username already exists, skipping.” >> “$LOG_FILE”
else
# Create the user
useradd -m -s /bin/bash “$username” >> “$LOG_FILE” 2>&1
echo “User $username created.” >> “$LOG_FILE”

# Create the user’s personal group
groupadd “$username” >> “$LOG_FILE” 2>&1
echo “Group $username created.” >> “$LOG_FILE”
fi

# 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_FILE” 2>&1
echo “Group $group created.” >> “$LOG_FILE”
fi
# Add user to group
usermod -aG “$group” “$username” >> “$LOG_FILE” 2>&1
echo “User $username added to group $group.” >> “$LOG_FILE”
done

# Set home directory permissions
mkdir -p “/home/$username”
chown -R “$username:$username” “/home/$username”
chmod 755 “/home/$username”

# Generate and store the password securely
password=$(generate_password)
echo “$username:$password” >> “$PASSWORD_FILE”
chmod 600 “$PASSWORD_FILE”
echo “Password for $username stored in $PASSWORD_FILE.” >> “$LOG_FILE”

done < “$INPUT_FILE”

echo “User creation script completed.”

Run the Script

For a Bash script to be run directly from the command line by typing its name, it must have the executable permission set.

To make the script executable:

chmod +x create_users.sh

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

./create_users.sh user_passwords.txt

Verify the Script Executed Tasks Successfully

To be certain the script executed all the user management tasks successfully, there are several checks that should be carried out.

To verify user existence, run:

id <username>

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

cat /var/secure/user_passwords.txt

To verify group existence, run:

getent group <username>

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

To verify that each user has a personal group with the same name as their username, run:

getent passwd <username>
getent group <username>

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.