Smart SysAdmin: Automating User Management on Linux with Bash Scripts

RMAG news

Introduction

This article is a step-by-step walkthrough of a project I recently worked on, where I needed to automate user management in Linux This project was one of the tasks for the HNG internship, a program designed to accelerate learning and development in the tech industry.

This guide will provide an in-depth look at how I developed this bash script, explaining the reasoning behind each line of code and the approach I took. By sharing my thought process and important considerations, I aim to demonstrate my proficiency in bash scripting and system administration. Whether you are a beginner or an experienced sysadmin, this guide will help you understand the nuances of automating user management in Linux.

Understanding the Requirements

Overview:

Write a bash script called create_users.sh.
The script reads a text file containing usernames and groups.
Create users and groups as specified.
Set up home directories with appropriate permissions and ownership.
Generate random passwords for the users.
Log all actions to /var/log/user_management.log.
Store generated passwords securely in /var/secure/user_passwords.csv.

Input Format:

Each line in the input file is formatted as user;groups.
Multiple groups are separated by commas ,.
Usernames and groups are separated by a semicolon ;.

Criteria:

Users should be created and assigned to their groups.
Logging actions to /var/log/user_management.log.
Storing passwords in /var/secure/user_passwords.csv.

Detailed Script Execution

Let’s break down the code step by step:

A. Ensuring Root Privileges

#!/bin/bash

This is the shebang line that specifies the script should be run using the bash shell. It ensures the script is executed in the correct shell environment.

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

This section checks if the script is being run as the root user. The id -u command returns the user ID of the current user. The root user has a user ID of 0. If the script is not run as root (i.e., the user ID is not 0), it prints an error message and exits with a status of 1. Running as root is necessary because creating users and modifying system files requires superuser privileges.

Alternatives: Instead of exiting, you could prompt the user to re-run the script with sudo or use a function to elevate privileges automatically.

B. Setting Up Log and Secure Directory

Logging actions and securely storing generated passwords are crucial for monitoring and auditing purposes.

# Log file
LOG_FILE=“/var/log/user_management.log”
SECURE_DIR=“/var/secure”
PASSWORD_FILE=$SECURE_DIR/user_passwords.csv”

These lines define variables for the paths of the log file and the secure directory.

LOG_FILE is the path where the script will log its actions.

SECURE_DIR is the directory where the password file will be stored.

PASSWORD_FILE is the file where generated passwords will be securely stored.

# Create secure directory if it doesn’t exist
mkdir -p $SECURE_DIR
chmod 700 $SECURE_DIR

Explanation:

mkdir -p “$SECURE_DIR”: This command creates the SECURE_DIR directory if it doesn’t already exist. The -p option ensures that no error is reported if the directory already exists, and it creates any parent directories as needed.

chmod 700 “$SECURE_DIR”: This command sets the permissions of the SECURE_DIR directory to 700. This means:
The owner (root) has read, write, and execute permissions.
No permissions are granted to the group or others. This ensures the directory is secure and only accessible by the root user.

# Clear the log and password files
true > $LOG_FILE
true > $PASSWORD_FILE
chmod 600 $PASSWORD_FILE

Explanation:

true > “$LOG_FILE” and true > “$PASSWORD_FILE”: These commands clear the contents of the log file and the password file, respectively. Using > truncates the file to zero length if it exists, effectively clearing it. If the file does not exist, it is created.
Note the importance of the truecommand as it acts as a no-op (no operation) command and always ensures that the redirection operator has a valid command associated with it, and the files will be truncated as intended.

chmod 600 “$PASSWORD_FILE”: This command sets the permissions of the PASSWORD_FILE to 600. This means:
The owner (root) has read and write permissions.
No permissions are granted to the group or others. This ensures that the password file is secure and only readable and writable by the root user.

Alternatives: One can use a logging framework or tool for more advanced logging capabilities and security measures.

C. Processing the Input File

The section reads an input file containing usernames and group assignments, and processes each line as needed.

# Read the input file line by line
while IFS=‘;’ read -r username groups; do
# Remove any leading or trailing whitespace
username=$(echo $username | xargs)
groups=$(echo $groups | xargs)

# Skip empty lines
[ -z $username ] && continue

Explanation
1 Reading the Input File Line by Line:

while IFS=‘;’ read -r username groups; do

while …; do … done: This is a loop that continues to execute as long as the read command successfully reads a line from the input.

IFS=’;’: IFS stands for Internal Field Separator. By setting it to ;, we are telling the read command to use ; as the delimiter for splitting each line into fields.

read -r username groups: The read command reads a line from the input file, splits it into two parts using ; as the delimiter, and assigns the first part to the username variable and the second part to the groups variable.

-r: This option prevents backslashes from being interpreted as escape characters.

2 Removing Leading or Trailing Whitespace:

username=$(echo $username | xargs)
groups=$(echo $groups | xargs)

$(…): This is command substitution, which captures the output of the command inside the parentheses.

echo “$username” | xargs:

echo “$username”: Prints the value of the username variable.
The output of echo is piped to xargs.

xargs: This command trims any leading or trailing whitespace from its input.

The same process is applied to the groups variable to remove any leading or trailing whitespace.

3 Skipping Empty Lines:

[ -z $username ] && continue

[ -z “$username” ]: This is a test condition that checks if the username variable is empty. -z returns true if the string is of zero length.

&& continue: If the username variable is empty, the continue statement is executed. This causes the loop to skip the current iteration and proceed to the next line of input. This effectively ignores empty lines in the input file.

Alternatives: Use more sophisticated parsing techniques or external libraries for handling input files.

D. User and Group Management

Creating users, assigning them to groups, and setting permissions are the core functionalities of the script.

Checking and Creating User

if id $username &>/dev/null; then
echo “User $username already exists, skipping…” | tee -a $LOG_FILE
else
useradd -m -s /bin/bash -G $username $username
echo “Created user $username with personal group $username | tee -a $LOG_FILE
fi

if id “$username” &>/dev/null; then:

The id command checks if a user exists. It returns 0 if the user exists and non-zero if the user does not exist.

&>/dev/null redirects both stdout and stderr to /dev/null, effectively discarding any output from the id command.
If the user exists, the script proceeds to the then block; otherwise, it goes to the else block.

echo “User $username already exists, skipping…” | tee -a “$LOG_FILE”:

If the user exists, this message is logged. The tee command outputs the message to both the terminal and the log file ($LOG_FILE).

useradd -m -s /bin/bash -G “$username” “$username”:

If the user does not exist, the useradd command creates the user.

-m: Creates a home directory for the user.

-s /bin/bash: Sets the user’s default shell to /bin/bash.

-G “$username”: Creates a personal group for the user with the same name as the username and adds the user to this group.
The username is specified twice because the first instance specifies the group and the second specifies the username.

echo “Created user $username with personal group $username” | tee -a “$LOG_FILE”:

Logs the creation of the new user and their personal group.

Adding User to Additional Groups

if [ -n $groups ]; then
IFS=‘,’ read -ra ADDR <<<$groups
for group in ${ADDR[@]}; do
group=$(echo $group | xargs) # Remove whitespace
if ! getent group $group >/dev/null; then
groupadd $group
echo “Created group $group | tee -a $LOG_FILE
fi
usermod -aG $group $username
echo “Added user $username to group $group | tee -a $LOG_FILE
done
fi

if [ -n “$groups” ]; then:

Checks if the groups variable is non-empty. -n returns true if the length of the string is non-zero.

IFS=’,’ read -ra ADDR <<<“$groups”:

Sets the Internal Field Separator (IFS) to , to split the groups string into an array (ADDR) using commas as delimiters.

<<<“$groups”: This is a here-string, which feeds the value of groups into the read command.

for group in “${ADDR[@]}”; do:

Iterates over each group name in the ADDR array.

group=$(echo “$group” | xargs):

Removes any leading or trailing whitespace from the group name using xargs.

if ! getent group “$group” >/dev/null; then:

The getent group “$group” command checks if the group exists.

! negates the result, so the condition is true if the group does not exist.

>/dev/null discards the output of the getent command.

groupadd “$group”:

Creates the group if it does not exist.

echo “Created group $group” | tee -a “$LOG_FILE”:

Logs the creation of the new group.

usermod -aG “$group” “$username”:

Adds the user to the specified group using the usermod command.

-aG: Appends the user to the supplementary group(s) without removing them from other groups.

echo “Added user $username to group $group” | tee -a “$LOG_FILE”:

Logs the addition of the user to the group.

Alternatives: One can use more advanced user management tools or scripts for handling user and group assignments.

E. Setting Permissions and Ownership for the home directory

Setting Permissions for the Home Directory

chmod 700 “/home/$username

chmod 700 “/home/$username”:

chmod is a command used to change the file mode (permissions) of a file or directory.

700 sets the permissions of the directory to:

7 (read, write, and execute) for the owner (username).

0 (no permissions) for the group.

0 (no permissions) for others.

“/home/$username” specifies the path of the home directory for the user. The $username variable is replaced with the actual username.
This ensures that only the user can access their home directory, providing security and privacy.

Changing Ownership of the Home Directory

chown $username:$username “/home/$username

chown “$username”:”$username” “/home/$username”:

chown is a command used to change the ownership of a file or directory.

“$username”:”$username” sets both the user owner and the group owner to the specified username.

“/home/$username” specifies the path of the home directory for the user.
This ensures that the user owns their home directory and can manage its contents.

Logging the Action

echo “Set permissions for /home/$username | tee -a $LOG_FILE

echo “Set permissions for /home/$username”:

Prints the message “Set permissions for /home/$username” to the terminal.
The $username variable is replaced with the actual username, providing a specific log entry for each user.

| tee -a “$LOG_FILE”:

The tee command reads from standard input and writes to both standard output and the specified file.

-a option tells tee to append to the file rather than overwrite it.

“$LOG_FILE” specifies the path of the log file where the message will be recorded.
This logs the action of setting permissions for the user’s home directory, ensuring a record is kept of all actions performed by the script.

F. Generating Random Passwords

Let’s break down the function that generates random passwords:

# Function to generate random passwords
generate_password() {
local password_length=12
tr -dc A-Za-z0-9 </dev/urandom | head -c $password_length
}

Explanation

1 Function Definition:

generate_password() {

This line defines a shell function named generate_password. Functions in bash allow you to encapsulate and reuse code.

2 Local Variable:

local password_length=12

local is a keyword used to declare a variable with local scope within the function. This means that password_length is only accessible within the generate_password function.

password_length=12 sets the length of the generated password to 12 characters. You can adjust this value to generate passwords of different lengths.

3 Generating the Password:

tr -dc A-Za-z0-9 </dev/urandom | head -c $password_length

This line generates the random password using a combination of tr, head, and /dev/urandom:

/dev/urandom: This is a special file that provides random data. It’s commonly used for generating random numbers or strings.

tr -dc A-Za-z0-9: The tr command is used to translate or delete characters. In this case:

-d option deletes characters from the input that are not specified.

-c option complements the set of characters specified, effectively including only the characters in the specified set.

A-Za-z0-9 specifies the set of characters to include: uppercase letters (A-Z), lowercase letters (a-z), and digits (0-9).

The output of /dev/urandom is piped (|) to tr to filter out any characters not in the specified set.

head -c $password_length: The head command outputs the first part of files. The -c option specifies the number of bytes to output. Here, it outputs the first 12 characters ($password_length) from the filtered random data.

The result is a secure, random password of the specified length.

G. Generating and Storing User Passwords

# Generate and store the user’s password
password=$(generate_password)
echo $username,$password >> $PASSWORD_FILE
echo “Generated password for $username | tee -a $LOG_FILE
echo $username:$password | chpasswd

Generating user password

password=$(generate_password)

password=$(generate_password):

This line calls the generate_password function (defined earlier) to generate a random password.

$(…) is command substitution, which captures the output of the generate_password function and assigns it to the password variable.

Storing the Username and Password

echo $username,$password >> $PASSWORD_FILE

echo “$username,$password” >> “$PASSWORD_FILE”:

This line appends the username and the generated password to the password file.

“$username,$password” creates a comma-separated string containing the username and password.

>> is the append redirection operator, which appends the output to the specified file without overwriting its existing content.

“$PASSWORD_FILE” is the path to the file where the passwords are securely stored.

Logging the Password Generation

echo “Generated password for $username | tee -a $LOG_FILE

echo “Generated password for $username”:

Prints the message “Generated password for $username” to the terminal.
The $username variable is replaced with the actual username, providing a specific log entry for each user.

| tee -a “$LOG_FILE”:

The tee command reads from standard input and writes to both standard output and the specified file.

-a option tells tee to append to the file rather than overwrite it.

“$LOG_FILE” is the path of the log file where the message will be recorded.
This logs the action of generating a password for the user, ensuring a record is kept of all actions performed by the script.

Setting the User’s Password

echo $username:$password | chpasswd

echo “$username:$password”:

Creates a string in the format username:password, where $username and $password are replaced with the actual username and generated password.

| chpasswd:

The chpasswd command reads a list of username:password pairs from standard input and updates the passwords for the specified users.
This sets the password for the user to the generated password.

Alternatives: Use password management tools or integrate with centralized authentication systems for more secure password handling.

Conclusion

By breaking down the script into detailed steps, we demonstrated the importance of checking for root privileges, setting up logging and secure storage, processing input files, managing users and groups, and securely handling passwords. Each part of the script was explained with code examples and alternatives, showcasing the thought process and best practices behind the implementation.

As stated earlier, this project was undertaken as part of the HNG internship, a program designed to accelerate the learning and development of talented individuals in the tech industry. The HNG internship provides a platform for interns to work on real-world projects, enhancing their skills and preparing them for future careers.

For the complete source code and detailed documentation, you can visit the GitHub repository.

To learn more about the HNG internship and its opportunities, please visit https://hng.tech/internship or https://hng.tech/premium.

Thank you for reading.