DEV Community

Abasifreke Nkanang
Abasifreke Nkanang

Posted on

Smart SysAdmin: Automating User Management on Linux with Bash Scripts

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode
  • 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"
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

Explanation
1 Reading the Input File Line by Line:

   while IFS=';' read -r username groups; do
Enter fullscreen mode Exit fullscreen mode
  • 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)
Enter fullscreen mode Exit fullscreen mode
  • $(...): 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
Enter fullscreen mode Exit fullscreen mode
  • [ -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
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode
  • 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"
Enter fullscreen mode Exit fullscreen mode
  • 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"
Enter fullscreen mode Exit fullscreen mode
  • 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"
Enter fullscreen mode Exit fullscreen mode
  • 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
}
Enter fullscreen mode Exit fullscreen mode

Explanation

1 Function Definition:

   generate_password() {
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

Generating user password

password=$(generate_password)
Enter fullscreen mode Exit fullscreen mode
  • 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"
Enter fullscreen mode Exit fullscreen mode
  • 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"
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode
  • 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.


Top comments (0)