Automating User Management on Linux using Bash Script
Published: 2024-07-02 21:10:30


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:
```jsx
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`:
```jsx
touch create_users.sh
```
Use your preferred text editor to open the `create_users.sh` file and begin writing the script:

```jsx
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](https://linuxhandbook.com/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:
```jsx
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:
```jsx
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. 
```jsx
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.

### 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:
```jsx
# Function to log messages
log_message() {
    echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a $LOG_FILE
}
```
- **`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.

```jsx
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. `/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:
```jsx
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: 
```jsx
# 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**
```jsx
    # 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.

Logs the username and associated groups read from the input file to be certain the script is reading the `user_password.txt` file correctly:
```jsx
    # Debug: Log the username and groups read from the file
    log_message "Read line: username='$username', groups='$groups'"
```
This is useful for troubleshooting.

**Iteration 2: Check if usernames or groups are empty**
```jsx
    if [[ -z "$username" || -z "$groups" ]]; then
        log_message "Error: Username or groups missing in line: $username"
        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.
- **`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**
```jsx
    # 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
```
- **`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:
```jsx
        # Create the user with the personal group
        useradd -m -g "$username" "$username"
        log_message "Created user: $username"
```
- **`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. 
```jsx
        # Generate a random password and set it for the user
        password=$(generate_password)
        echo "$username:$password" | chpasswd
        log_message "Set password for user: $username"
```
- **`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**
```jsx
        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
```
- **`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:
```jsx
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 8: Store the username and password securely**
```jsx
        # 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
```
- **`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:
```jsx
done < "$1"
```
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](https://github.com/FavourDaniel/user-management-in-linux).

## Run the Script
To execute the Bash script without calling Bash in your terminal, make the script executable: 
```jsx
chmod +x create_users.sh
```

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

```jsx
./create_users.sh user_passwords.txt
```
## 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:
```jsx
id <username>
```

To verify that the user password file was successfully created in the /var/secure/ directory, run:
```jsx
cat /var/secure/user_passwords.txt
```

To verify group existence, run:
```jsx
getent group <username>
```

To verify the log file as created and all actions are logged correctly without any errors or unexpected behaviors, run:
```jsx
cat /var/log/user_management.log
```

- To verify that each user has a personal group with the same name as their username, run:
```jsx
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](https://hng.tech/internship). If you are looking to hire talented Developers and Designers from the internship, visit [HNG hire](https://hng.tech/hire).