Smart SysAdmin: Automating User Management on Linux with Bash Scripts
Published: 2024-07-03 21:00:16

### 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

```bash
#!/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.

```bash
# 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.

```bash
# 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.

```bash
# 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.

```bash
# 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 `true`command 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.

```bash
# 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:**
   ```bash
   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:**
   ```bash
   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:**
   ```bash
   [ -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**

```bash
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**

```bash
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**

```bash
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**

```bash
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**

```bash
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:

```bash
# 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:**
   ```bash
   generate_password() {
   ```
   - This line defines a shell function named `generate_password`. Functions in bash allow you to encapsulate and reuse code.

2 **Local Variable:**
   ```bash
   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:**
   ```bash
   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

```bash
# 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**

```bash
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**

```bash
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**

```bash
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**

```bash
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](https://github.com/Data-Bishop/Linux-User-Creation-Bash-Script/).

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

Thank you for reading.

---