Table of Contents
- Introduction
- Module 1: Managing Users and Groups
- Module 2: Working with Object Permissions
- Module 3: Working with Object Ownership
- Module 4: Managing System Processes
- Module 5: Managing System Log Operations
- Summary
- Command-Line Quick Reference
Introduction
When you are responsible for a complex system running hundreds or even thousands of processes simultaneously, controlling access to that system is one of the most important tasks you have as an administrator. Ensuring that key resources are protected from unauthorized activity is central to almost every other administrative responsibility.
This material introduces the tools needed to both control system access and keep daily operations effective and efficient. It covers four closely related areas:
- The role played by users and groups in defining who can authenticate to a system.
- How access to file system objects is defined through permission profiles and ownership settings.
- How system processes are identified, monitored, and controlled.
- How everything happening on a system can be tracked through system logs.
Forcing anyone to sign in to a predefined user account before being granted access achieves two important goals:
- It ensures that everyone provides authentication credentials proving who they are and that they have a right to access what they are requesting.
- It allows administrators to accurately attribute system events to a specific user. If an important resource is unexpectedly modified or removed, the responsible account can be identified — not necessarily to assign blame, but because the account may have been hijacked by an attacker. Knowing which account was used in an intrusion gives defenders a head start on containment, with the first step usually being to at least temporarily disable that account.
mindmap
root((System Management<br/>and File Security))
Users and Groups
User accounts
Group membership
Templates via /etc/skel
Permissions and Ownership
Symbolic and octal notation
chmod
chown and chgrp
SUID, SGID, sticky bit
Processes
ps and top
Process lifecycles
Job control
Logging
rsyslog
journald
Remote log forwarding
Module 1: Managing Users and Groups
Understanding Linux User Accounts
The demonstrations in this module were carried out on a Red Hat Enterprise Linux 10 machine — an important detail because many of the tools behave a little differently on Debian/Ubuntu-family systems, a distinction covered later in this module.
To add a new user account on Red Hat, Fedora, or any distribution in the Red Hat family, the traditional tool is useradd. Creating a user is a privileged operation, so it must be run with sudo.
sudo useradd -m -s /bin/dash user1
-mtells Linux to also create a home directory for the new user.-sdefines a non-default shell interpreter (in this example,dash) — most of the time this flag is unnecessary and the system default shell is used.user1is the name of the account being created.
Immediately after creation, the account exists but has no password and cannot be used to log in. The passwd command, followed by the account name, sets (or resets) that password:
sudo passwd user1
It is not considered good practice for an administrator to actually know the passwords assigned to individual user accounts. The password set this way should be treated as a temporary value that the user is expected to change the first time they log in.
Listing the contents of /home after this shows one subdirectory for the administrator’s own account and a second one for the newly created user1:
cd /home
ls -l
Attempting to enter the user1 directory while logged in as a different, non-root account is denied — the directory belongs exclusively to user1, and only sudo (or the user themselves) can access it.
Working with Linux Groups
Linux is a multi-user environment, and it is common for more than one user to need access to a single set of resources. Consider a web server powering a multi-tiered e-commerce site: a team of developers needs access to the application code, a team of database managers needs access to the database engine, and a marketing team needs to monitor site access logs. Granting everyone full access to the entire stack is unsafe, but managing access on a strict one-to-one basis quickly becomes unmanageable. Groups solve this problem — a developers group, for example, can be associated with the directories that hold the application code.
To add an existing user to a group:
sudo usermod -aG developers user1
usermod— “mod” stands for modify.-aappends the user to a new group without removing them from any existing group memberships.-G(uppercase) identifies the group to add the user to.- The final argument is the user account being modified.
Running this command against a group that does not yet exist will fail. New groups are created with groupadd (behavior differs slightly on Ubuntu, covered in the next section):
sudo groupadd developers
sudo usermod -aG developers user1
To confirm the change:
groups user1
Because every user is, by default, assigned a private group that shares their username, user1 will belong to both the user1 group and developers.
The id command prints a user’s numeric identifiers:
id user1
In this walkthrough, user1’s user ID is 1001, the user1 group’s ID is also 1001, and the developers group’s ID is 1002 — the next group created would be expected to receive ID 1003. The starting point for ID assignment can be adjusted through the /etc/login.defs configuration file, though this is rarely necessary.
Every user account is represented by an entry in /etc/passwd:
less /etc/passwd
Each line records the account name, its user ID, its home directory, and its default shell interpreter. Groups are represented the same way in /etc/group:
less /etc/group
To actually work as a given account, you can either log in directly with its credentials or, from an existing shell, switch identities with su:
su - user1
The shell prompt changes to visually indicate the active account. Running pwd shows the current location; typing cd with no target argument is a shortcut that always returns you to your current user’s home directory. Typing exit drops out of the current session and returns to the previous one — typing it a second time closes that session too.
flowchart TD
A[sudo useradd -m user] --> B[sudo passwd user]
B --> C[groupadd group-name]
C --> D["sudo usermod -aG group-name user"]
D --> E["groups user / id user"]
E --> F["/etc/passwd and /etc/group updated"]
F --> G["su - user (switch identity)"]
Working with Users and Groups on Debian and Ubuntu Systems
Ubuntu and other Debian-family distributions generally include the same useradd and groupadd commands as Red Hat, but administrators on those systems tend to prefer the more user-friendly adduser and addgroup wrappers when they are available.
To reach an Ubuntu machine (in this case an Ubuntu container running on the same network) from a Red Hat host:
ssh ubuntu@<ubuntu-container-ip>
Because passwordless SSH access was already configured, no password prompt appears. On login, Ubuntu shows a system status summary with basic health information. As on Red Hat, a home directory using the account name exists under /home — here, /home/ubuntu.
Creating a new user interactively with adduser:
sudo adduser user2
Unlike useradd, the -m flag is unnecessary — adduser creates the home directory by default, reports the assigned user and group IDs, and references the /etc/skel directory (covered in the next section). The command also prompts interactively for a new password (rather than requiring a separate passwd invocation) and offers optional fields for extra account information (full name, room number, phone numbers, and so on), which can simply be skipped by pressing Enter. A final confirmation prompt (uppercase Y indicating the default) completes the process.
You may notice a legacy users group referenced during this process. Some systems still create it for backward-compatibility reasons, but it rarely comes up in day-to-day administration.
Ubuntu’s equivalent of groupadd is addgroup, though groupadd remains available and commonly used:
sudo groupadd developers
sudo usermod -aG developers user2
sudo usermod -aG developers ubuntu
There is no practical limit to how many users can belong to a group, or how many groups a single user can belong to. Running groups user2 confirms the memberships (including the legacy users group), and id ubuntu shows the full set for a user with many memberships.
Switching to the new account and attempting a privileged edit demonstrates an important default restriction:
su - user2
sudo nano /etc/passwd
By default, user2 is not permitted to use sudo — a sensible security default, since best practice limits every account’s authority to only what it needs. When elevated authority genuinely is required, it can be granted by adding the account to the sudo group:
cat /etc/group | grep sudo
sudo usermod -aG sudo user2
cat /etc/group | grep sudo
After this change, switching to user2 and attempting sudo nano /etc/passwd again succeeds, with no warning about insufficient permissions to save changes.
| Task | Red Hat / Fedora | Ubuntu / Debian |
|---|---|---|
| Create user | useradd -m -s <shell> <user> | adduser <user> (interactive, home dir + password prompt included) |
| Set/change password | passwd <user> | passwd <user> (still available; adduser prompts automatically) |
| Create group | groupadd <group> | groupadd <group> or addgroup <group> |
| Add user to group | usermod -aG <group> <user> | usermod -aG <group> <user> |
| Grant admin rights | Add to wheel group (varies) | usermod -aG sudo <user> |
Creating User Templates with /etc/skel
Every user created on a Linux system typically receives a home directory of the same name at the root level of /home. That directory is a place to store personal files, but it can also be pre-configured so new users start with a helpful working environment. Those pre-configurations come from /etc/skel:
ls -la /etc/skel
A hidden file is simply a regular file whose name begins with a dot; it will not show up in a normal directory listing, which is mainly a convenience so day-to-day work files are easier to find. Hidden files still carry real value, though. Two commonly seen ones are:
.bashrc— environment settings used by a user’s non-login shell sessions (the kind of session you get when launching a terminal window from a graphical desktop)..profile— provides a similar environment, but for login shell sessions.
Listing a newly created user’s home directory (ignoring files such as .lesshst and .sudo_as_admin_successful, which appear only after the account has been used for a while) shows that .bash_logout, .bashrc, and .profile are identical to what exists in /etc/skel. This is not a coincidence: skel is short for skeleton — the directory where an administrator places any resources intended to be automatically copied into the home directory of every newly created user. These three files exist by default on nearly all Linux machines, and administrators are free to edit them or add anything else users are expected to need.
To demonstrate this, a couple of simple changes were made to /etc/skel/.bashrc before creating a new user:
sudo nano /etc/skel/.bashrc
# Aliases
alias update='sudo apt update && sudo apt upgrade -y'
alias logs='cd /var/log'
An alias is a text string that invokes a command — a shortcut for commands that are long, complex, or used often. Here, typing update refreshes the local apt package index and applies any available updates, while typing logs changes directory straight to /var/log.
After saving the file, a new user created from that point forward automatically inherits the customized .bashrc:
sudo adduser user3
su - user3
logs # changes directory to /var/log, confirming the alias works
update # prompts for a sudo password; fails because user3 isn't in the sudo group, but the alias itself fires correctly
Both aliases work as expected. Typing cd with no argument returns to the user’s own home directory, where less .bashrc confirms the lines added to the /etc/skel template are present.
flowchart LR
A["/etc/skel (template files:<br/>.bashrc, .profile, .bash_logout)"] -->|"copied automatically on account creation"| B["New user's home directory"]
C["Admin edits /etc/skel/.bashrc<br/>(adds aliases, environment settings)"] --> A
B --> D["All future new users<br/>inherit the customization"]
Module 2: Working with Object Permissions
Understanding Permission Notation Systems
Security is never a single control you configure once and forget. It has to be built in layers: physical locks on server room doors and effective network firewalls sit at the outer perimeter, while at the file system level, Linux permissions create the balance between legitimate accessibility and authorization restrictions.
Every file and directory on a Linux system carries a standard set of permission metadata. Listing a directory with hidden entries included and the long format shows this clearly:
ls -al mystuff/
drwxrwxr-x 2 ubuntu ubuntu 4096 ... .
drwxr-xr-x 5 ubuntu ubuntu 4096 ... ..
-rw-rw-r-- 1 ubuntu ubuntu 0 ... file1
-rw-r--r-- 1 root root 0 ... file2
- The first two hidden entries (
.and..) represent the current directory and the one immediately above it. - The two owner/group columns show the specific user and group account associated with each object.
file1belongs to theubuntuuser/group;file2belongs torootbecause it was created withsudo. - The 10-character string on the far left is the object’s permission profile in symbolic notation.
Permissions are assigned to three principals:
| Position | Principal | Meaning |
|---|---|---|
| Characters 1–3 | Owner / user | Permissions given to the object’s owner |
| Characters 4–6 | Group | Permissions given to all members of the object’s associated group |
| Characters 7–9 | Others | Permissions given to every other user on the system |
The leading character indicates the object type: d for a directory, or a dash (-) for a regular file. Within each three-character set:
r— the right to read the contents of a file or directory.w— the right to write/edit the object (a dash means this right is absent).x— the right to execute the object. For a script to be runnable, its permission profile must includex.
Beyond the r/w/x symbols, command syntax also uses u (user/owner), g (group), and o (others) to refer to the three principals — used extensively with chmod’s symbolic mode.
A parallel system, octal notation, represents permissions with numbers from 0–7:
| Permission | Symbol | Octal value |
|---|---|---|
| Read | r | 4 |
| Write | w | 2 |
| Execute | x | 1 |
flowchart TB
subgraph Symbolic["Symbolic notation: rwxrwxrwx"]
U["Owner: rwx"]
G["Group: rwx"]
O["Others: rwx"]
end
subgraph Octal["Octal equivalent: 777"]
U2["4+2+1 = 7"]
G2["4+2+1 = 7"]
O2["4+2+1 = 7"]
end
A file that permits full access to everyone (owner, group, and others) is rwxrwxrwx in symbolic notation, or 777 in octal — the owner’s 4 (read) + 2 (write) + 1 (execute) sums to 7, and the group and others each receive the same total. If instead the group should have read and write but not execute, and others should have only read, the symbolic form is rwxrw-r--, which is 764 in octal: the owner keeps 7, the group loses execute (7 − 1 = 6), and others get only read (4).
Editing Object Permissions with chmod
A common real-world scenario: multiple users need access to important data files that currently exist inside one user’s home directory. Ideally such files would live somewhere equally accessible to everyone, but that is not always possible — this walkthrough demonstrates opening up access to a file in place, while illustrating some of the subtler behavior of Linux permissions along the way.
Starting point: a data subdirectory inside the ubuntu user’s home directory, containing a spreadsheet file stats.csv.
ls -al | grep data
ls -al data/
Both the data directory and the stats.csv file end their permission strings with three dashes — others have no access at all. Switching to a different account (user2) and attempting to cd into data, or to read the file directly from outside the directory, both fail.
To fix this, permissions need to change at three levels: the file itself, its parent directory (data), and that directory’s parent (the home directory) — because in the context of a directory, the execute permission controls whether a user can travel through it to reach whatever exists within.
# Home directory: allow others to execute (traverse) but not read or write
chmod 771 /home/ubuntu
# data directory: allow others to read and execute (traverse), but not write
chmod 755 data
# stats.csv: allow others to read only
chmod 644 stats.csv
By default, a home directory is created with 750 (owner: full rights 7; group: read + execute 5; others: none 0). The only necessary change for the home directory here is to add 1 for others, granting execute/traversal without opening up read or write access.
Testing as user2 after the changes:
su - user2
cd ~ubuntu/data
ls -al
cat stats.csv
Both actions, which previously failed, now succeed — confirming that others can traverse into the directory and read the file’s contents, but nothing more.
Balancing Accessibility with Security
Permissions can also be edited using symbolic notation, which — unlike octal — is not declarative; it explicitly adds to or subtracts from the existing permission set rather than replacing it outright.
# Add write permission for others
chmod o+w stats.csv
# Remove execute permission for the owner
chmod u-x stats.csv
u,g,oidentify the target principal (user/owner, group, others).+adds a permission;-removes one.r,w,xidentify which permission is being added or removed.
After chmod o+w stats.csv, listing the file shows others now have both read and write, but not execute — exactly as expected. After chmod u-x stats.csv, the owner’s x becomes a dash.
For resources that multiple users need to access, it is generally better practice to place them in a shared, non-user-specific location in the file system hierarchy — a root-level directory such as /var is a popular choice, with permissions fine-tuned to reflect the exact access needs of the team (a pattern explored further in the ownership module).
flowchart LR
A["Target file/directory<br/>needs access opened up"] --> B{"Which principals<br/>need access?"}
B -->|Owner only| C["No change needed"]
B -->|Group| D["chmod g+rwx or octal group digit"]
B -->|Others| E["chmod o+rx or octal others digit"]
E --> F{"Is this a directory<br/>several levels deep?"}
F -->|Yes| G["Grant execute (traverse)<br/>on every parent directory too"]
F -->|No| H["Done"]
Module 3: Working with Object Ownership
Managing File and Directory Ownership
Fine-tuning an object’s user and group ownership profile is a major part of building secure, usable Linux systems. This walkthrough simulates a shared development environment.
mkdir ~/code
chmod 773 ~/code
773 gives the owner full rights (7), the group full rights (7), and others write + execute (2 + 1 = 3), without read.
sudo touch code/config.ini code/README
sudo mkdir code/src
Because these were created with sudo, they belong to root. Listing the directory as a non-root user without sudo initially returns nothing, since read permission was never assigned to others — running the same listing with sudo confirms the files exist.
To transfer ownership of a single file to a regular user account:
sudo chown user2 code/config.ini
ls -l code/
The group owner is unchanged (still root); only the file owner changes to user2. Testing as user2 (in a second shell) confirms full, unprivileged read/write access to config.ini via a text editor, with no warnings.
To transfer ownership of an entire directory tree — not just one file — the -R (recursive) flag applies the change to the directory itself, its contents, and any subdirectories:
sudo chown -R user3 code/
The group owner for everything under code/ remains root, but the object owner is now user3 throughout. Switching to user3 confirms the ability to create a new file inside code/src without needing sudo.
sequenceDiagram
participant Admin as Admin (sudo)
participant FS as File System
participant U2 as user2
participant U3 as user3
Admin->>FS: sudo touch code/config.ini (owned by root)
Admin->>FS: sudo chown user2 code/config.ini
U2->>FS: edit config.ini (succeeds, no sudo needed)
Admin->>FS: sudo chown -R user3 code/
U3->>FS: touch code/src/newfile (succeeds, no sudo needed)
Managing Group Ownership for Files and Directories
Combining groups with object ownership enables precise, collaborative access-control configurations. Consider a shared website directory used by an entire development team:
sudo mkdir -p /var/www/site
cd /var/www/site
touch index.html # fails - the logged-in user has no rights here
sudo touch index.html # succeeds, but now owned by root
A file created this way, owned by root, cannot be edited by other team members — not useful for collaboration. The fix is to assign group ownership of the shared directory using chgrp:
sudo chgrp developers /var/www/site
The owner remains root, but the group becomes developers — any user who is a member of that group should now be able to work with everything inside. Two further refinements make this genuinely collaborative:
# Give the group full access, others none
sudo chmod 770 /var/www/site
# Set the "set-group-ID" bit so new files inherit the directory's group
sudo chmod g+s /var/www/site
The set-group-ID (SGID) bit ensures that all new files and subdirectories created inside /var/www/site inherit the group ownership of the parent directory, rather than the primary group of whichever user created them. Practically, this means every team member can work with files created by any other team member.
# As a member of "developers" (no sudo needed):
touch /var/www/site/new-page.html
ls -l /var/www/site/
The new file belongs to the creating user, but to the shared developers group — exactly the collaborative behavior needed.
Group ownership can also resolve access problems for individual files. Suppose a logviewers group is created for staff who need read access to an application’s logs:
sudo groupadd logviewers
sudo usermod -aG logviewers user2
sudo usermod -aG logviewers user3
sudo touch /var/log/application.log
sudo chown root:logviewers /var/log/application.log
sudo chmod 640 /var/log/application.log
640 gives the owner (root) full read/write, members of logviewers read-only, and no access at all to everyone else. Combining Linux user accounts, groups, and object permissions this way makes it possible to build extremely precise access-control profiles.
| Command | Purpose |
|---|---|
chown user file | Change the owner of a file/directory |
chown user:group file | Change owner and group in one step |
chown -R user dir/ | Change owner recursively through a directory tree |
chgrp group file | Change only the group owner |
chmod g+s dir/ | Set the SGID bit so new items inherit the directory’s group |
Configuring Special Access: SUID, SGID, and the Sticky Bit
Three special access-control tools go beyond the standard rwx model:
- set-user-ID (SUID) — lets a normal user run a privileged command without needing explicit root access.
- set-group-ID (SGID) — already introduced above; ensures group ownership consistency for files created in shared directories.
- Sticky bit — prevents group members who have access to a shared directory from deleting files created by other users.
SUID is best demonstrated through passwd itself, since it is the most well-known real-world use of the bit. passwd writes a hashed password into the highly restricted /etc/shadow file — yet ordinary, non-root users are able to run it successfully:
which passwd
ls -l $(which passwd)
-rwsr-xr-x 1 root root ... /usr/bin/passwd
Two details matter here: the x in the “others” position means any user can execute the binary, and the s in the owner’s position means the SUID bit is set. SUID temporarily grants anyone executing the file the full permissions of the file’s owner — root in this case — which is how a normal user’s passwd invocation is able to write to /etc/shadow.
This does not mean SUID hands over full root control: a user executing a SUID binary can only perform the operations the binary itself was programmed to allow. As long as the program was written correctly, the rest of the system stays protected.
A custom script illustrates the mechanism (and its limitations) directly:
#!/bin/bash
# demo-suid.sh
echo "Current user: $(whoami)"
echo "Applying update..."
echo "Secret data written by $(whoami)" >> /root/secret.txt
chmod +x demo-suid.sh
sudo chown root demo-suid.sh
sudo chmod u+s demo-suid.sh
ls -l demo-suid.sh
Even with the SUID bit correctly applied (visible as the s character in place of x in the owner’s permission set), running the script as a normal user is refused. The SUID bit was really designed for compiled binaries; for security reasons, many modern systems block it from taking effect on shell scripts. passwd itself remains the reliable, working demonstration of the mechanism.
SGID on a directory (already used in the previous section) is applied via octal notation by adding 2 to the front of the mode:
sudo mkdir /srv/projects
sudo chown :developers /srv/projects
sudo chmod 2775 /srv/projects
ls -ld /srv/projects
The permission string shows an s in what is normally the group’s execute position, and the group owner is developers. Files created inside by different members of the group (user2, then ubuntu) each keep their individual owner, but all share the developers group — enabling collaboration without any single user losing control over their own files.
Sticky bit, invoked symbolically with t, complements this by protecting individual ownership within a shared, group-writable directory:
sudo chmod +t /srv/projects
ls -ld /srv/projects
The t appears at the end of the “others” section of the permission string. With the sticky bit applied, a file created by one user inside /srv/projects cannot be deleted by a different user — even one who is also a member of developers — while the file’s actual owner retains full ability to delete it.
touch /srv/projects/owned-by-ubuntu.txt # created as the ubuntu user
su - user2
rm /srv/projects/owned-by-ubuntu.txt # denied - sticky bit protects it
exit
rm /srv/projects/owned-by-ubuntu.txt # succeeds - the file's actual owner can delete it
| Special bit | Symbolic form | Octal prefix | Applies to | Effect |
|---|---|---|---|---|
| SUID (set-user-ID) | u+s (shows as s in owner’s x slot) | 4xxx | Files (executables) | Execution runs with the file owner’s privileges |
| SGID (set-group-ID) | g+s (shows as s in group’s x slot) | 2xxx | Files and directories | New files in the directory inherit its group owner |
| Sticky bit | +t (shows as t in others’ x slot) | 1xxx | Directories | Only a file’s owner (or root) may delete it, even in a group-writable directory |
flowchart TD
A["Special access bit needed"] --> B{"What problem<br/>are you solving?"}
B -->|"Non-root users need to run<br/>a privileged operation"| C["SUID: chmod u+s binary"]
B -->|"Shared directory should keep<br/>consistent group ownership"| D["SGID: chmod g+s directory"]
B -->|"Shared directory should protect<br/>files from deletion by others"| E["Sticky bit: chmod +t directory"]
Removing User Accounts Safely
Eventually team members leave — sometimes amicably, sometimes under circumstances that raise concern about deliberate damage on the way out. Either way, quickly and correctly disabling that account’s access limits the organization’s exposure. A safe, ordered removal process:
1. Stop any active processes owned by the departing account.
ps -u user3
sudo kill 1404
ps -u user3 lists processes owned by the account; kill followed by the process ID stops the identified process (a top session running under user3 in this example).
2. Temporarily lock the account to block future logins while preserving its state:
sudo cat /etc/shadow | grep user3 # inspect the existing password hash
sudo usermod -L user3 # lock the account
sudo cat /etc/shadow | grep user3 # an "!" now prefixes the password hash
The change is reversible — removing the ! (or unlocking with usermod -U user3) reactivates the account, since the underlying password hash is still intact.
3. Back up the user’s home directory before anything is deleted:
mkdir -p ~/backups
sudo tar -czf ~/backups/user3-backup.tar.gz /home/user3
-ccreate,-zcompress with gzip,-fspecifies the archive filename.- Confirm the archive exists and, ideally, extract it to verify its contents are readable — never assume an important backup works until you have tested it.
4. Remove the account from any groups it belongs to.
groups user3
sudo gpasswd -d user3 logviewers
sudo gpasswd -d user3 users
groups user3 # only the user's own private group (user3) should remain
5. Fully remove the account, which is more involved if the account is still logged in elsewhere:
ps aux | grep user3
sudo pkill -u user3
sudo userdel -r user3
If userdel fails because a lingering process refuses to terminate, escalate:
sudo kill <pid> # try a normal termination first
sudo pkill -9 -u user3 # force-kill any remaining processes
sudo userdel -r user3 # -r also removes the user's home directory
A message about a missing mail spool during userdel is generally harmless in this context. Listing /home afterward confirms the account’s directory is gone.
flowchart TD
A["Decision to remove a user account"] --> B["1. Stop active processes:<br/>ps -u user, sudo kill pid"]
B --> C["2. Lock the account:<br/>sudo usermod -L user"]
C --> D["3. Back up home directory:<br/>sudo tar -czf backup.tar.gz /home/user"]
D --> E["4. Remove group memberships:<br/>sudo gpasswd -d user group"]
E --> F["5. Fully delete the account:<br/>sudo userdel -r user"]
F --> G{"userdel fails?"}
G -->|"Yes - lingering process"| H["sudo pkill -9 -u user, then retry userdel"]
G -->|No| I["Account and home directory removed"]
Module 4: Managing System Processes
Monitoring Running Processes with ps
Full process and performance monitoring could fill entire courses on its own; the focus here is narrower — identifying individual processes that might be misbehaving (consuming excessive memory or storage, for example) and that may need to be forcefully shut down.
The first stop is ps. With no arguments, ps prints only the processes running in the current shell — always including the shell itself (bash) and the ps command just executed.
ps
To see processes system-wide:
ps aux
a— all processes, wherever they are running.u— additional columns, including the user who launched each process and its resource usage.x— include background processes with no controlling terminal.
Output is ordered by process ID by default — lower IDs were launched earlier in the current session. CPU and memory figures are shown as a percentage of system totals.
To make the output more manageable:
ps aux --sort=-%cpu | head -10 # top 10 processes by CPU usage, descending
ps aux --sort=-%mem | head -10 # top 10 processes by memory usage, descending
ps -o pid,user,%cpu,%mem,cmd # only the columns that matter
A realistic scenario for observing load in near real time combines ps with watch:
watch 'ps aux --sort=-%cpu | head'
From a second shell, generating artificial CPU load:
stress --cpu 4
After a short delay, four stress worker processes appear at the top of the watch output, each consuming a very large share of a single CPU core — a clear sign that demand for the CPU is backlogged. Ctrl+C in the stress shell ends the load.
A similar demonstration uses the yes command, which prints the character y endlessly until stopped:
yes > /dev/null &
Output is discarded to /dev/null so it causes no harm, but the process still consumes real CPU cycles. The command runs in the background (note the reported process ID), and after a few seconds it, too, appears at the top of the watch output. To stop it:
kill <pid>
Because the process is owned by the current user, no sudo is required.
Monitoring Running Processes with top
Where the ps + watch combination has no built-in filtering or interactivity, top provides a similarly comprehensive but fully interactive view:
top
The top five lines report general system state, including uptime and load averages for the past 1, 5, and 15 minutes — if those numbers climb above the number of CPU cores available, the system is overloaded. Following that are CPU and memory health metrics, then a live table of system and user-owned processes ordered (by default) with the highest resource consumers near the top.
Interactive controls within top:
| Key | Effect |
|---|---|
M (uppercase) | Sort by memory usage |
P (uppercase) | Sort by CPU usage |
u (lowercase) | Prompt for a username and filter to that user’s processes |
1 | Break out per-core CPU usage individually |
k | Kill a process by ID from within the top interface |
q | Quit top |
Running stress while watching top demonstrates this clearly — pressing 1 reveals each CPU core individually becoming active as load increases.
Two additional tools round out a fast system-health check:
free -h # human-readable memory report
df -h # human-readable disk-space report
free reports total, used, free, and available memory — applications that run slowly or fail to complete are frequently victims of memory starvation, making free an early troubleshooting stop. Low free memory combined with high swap usage is a signal to shut down memory-hungry processes or add more RAM. df reports free disk space across mounted partitions; running out of storage is another very common source of problems.
flowchart LR
A["Suspect a resource problem"] --> B{"Which resource?"}
B -->|CPU / process-specific| C["ps aux --sort=-%cpu<br/>or top (P to sort by CPU)"]
B -->|Memory| D["free -h<br/>or top (M to sort by memory)"]
B -->|Disk space| E["df -h"]
C --> F["Identify offending PID"]
D --> F
F --> G["kill / top's k command"]
Controlling Process Lifecycles
Once a misbehaving process is identified, the next step is actually terminating it — whether it is unresponsive or has no graphical interface at all.
Filtering processes belonging to a multi-component service (Apache, in this example) reveals a parent/child structure:
ps aux | grep apache2
ps auxf | grep apache2 # -f shows parent/child relationships as a tree
Processes started by root and shown as children of that original process mean there is a good chance killing the parent process ID takes the children down with it — though this is not guaranteed.
pstree shows the entire process hierarchy in a nested, tree-style view, similar in spirit to the tree command for files and directories:
pstree
pstree -p # include process IDs at every level
Under systemd, apache2 shows its own child processes and threads, each with a distinct thread ID — a reminder that shutting down a complex, multi-process program cleanly can require more care than a single kill.
sudo kill -- -<pid> # the leading dash asks kill to also target child processes
ps auxf | grep apache2
A leftover htcacheclean process (an auto-scheduled script that manages the size of Apache’s disk cache, only indirectly connected to the web server) may remain after this. It can be terminated the regular way:
kill <pid>
If the process refuses to die, it is time to understand kill signals:
kill -l # lists all 64 available kill signals
| Signal | Number | Effect |
|---|---|---|
SIGQUIT | 3 | Equivalent to pressing Ctrl+C to interrupt a process |
SIGTERM | 15 | Requests a graceful shutdown — the default signal kill sends with no options |
SIGKILL | 9 | Forces immediate termination with no chance to shut down gracefully |
sudo kill -9 <pid> # force-terminate a stubborn process
To bring Apache back after experimenting:
sudo systemctl start apache2
For multiple instances of the same program running under different user accounts, killing each process ID individually is impractical. killall terminates every instance of a named program system-wide:
sudo killall stress
flowchart TD
A["Process needs to be stopped"] --> B{"Is it part of a<br/>multi-process service?"}
B -->|Yes| C["ps auxf or pstree -p<br/>to see parent/child relationships"]
B -->|No| D["kill pid (sends SIGTERM by default)"]
C --> D
D --> E{"Process still running?"}
E -->|Yes| F["kill -9 pid (SIGKILL, forced)"]
E -->|No| G["Done"]
F --> H{"Multiple instances across<br/>several accounts?"}
H -->|Yes| I["sudo killall program-name"]
H -->|No| G
Moving Processes to the Background
A common workflow: monitoring logs on the command line, needing to quickly run a different command, then returning to the monitoring session — without shutting it down and restarting it.
tail -f /var/log/syslog
Without -f, tail simply prints the most recent 10 lines and exits. With -f, new log events are appended live as they are written.
Pressing Ctrl+Z pauses the command and returns control of the prompt, assigning the paused command a job number (job 1 in this example):
ps # shows tail still present, along with its PID
jobs # shows the sleeping job
Stacking a second paused command illustrates job control further:
sleep 1000
# Ctrl+Z pauses it as job 2
jobs
To resume a paused job in the background, freeing the shell for other work:
bg %2 # resumes job 2 (the sleep command) in the background
jobs # tail is still stopped; sleep is now running
To bring a job back into the foreground:
fg %1 # resumes job 1 (tail) in the foreground
Even though tail was not actively displaying anything while stopped, it resumes exactly where it left off, picking up new log entries that arrived in the meantime (for example, the hourly cron scheduler’s routine scan for scheduled scripts).
Pressing Ctrl+C (rather than Ctrl+Z) terminates the foregrounded command outright:
# Ctrl+C shuts down tail for good
jobs # only "sleep" remains, now in the running state
kill %2 # terminate the sleep job by job number (its PID would also work)
jobs # sleep no longer appears in the list
sequenceDiagram
participant Shell
participant Tail as tail -f (job 1)
participant Sleep as sleep 1000 (job 2)
Shell->>Tail: tail -f /var/log/syslog
Shell->>Tail: Ctrl+Z (pause, job 1)
Shell->>Sleep: sleep 1000
Shell->>Sleep: Ctrl+Z (pause, job 2)
Shell->>Sleep: bg %2 (resume in background)
Shell->>Tail: fg %1 (resume in foreground)
Shell->>Tail: Ctrl+C (terminate)
Shell->>Sleep: kill %2 (terminate)
Module 5: Managing System Log Operations
Working with rsyslog Logging
No matter how small or routine an event is, if it happened on a Linux machine there is almost certainly a record of it somewhere. Sooner or later a system crash or a malicious intrusion will require tracking down every scrap of available evidence, so knowing where log messages live — and how to filter them effectively — is essential.
Most Linux distributions ship with two logging systems operating side by side: rsyslog and the newer journald. For the most part, both work with the same underlying log messages; they simply provide different tooling for accessing them.
rsyslog is an open-source project; while not every distribution includes it by default, it remains widely available and installable anywhere it is needed. journald, tightly integrated with the systemd ecosystem, is growing in popularity, but rsyslog remains firmly in use across the Linux world.
The logger command manually submits a message into the rsyslog system — effectively simulating what an application or system process does automatically:
logger -p user.info "Test informational message"
logger -p auth.err "Test authentication error message"
-p specifies a facility.priority pair. Facilities identify the general category of message (user, auth, cron, kern, and others); priorities range from the lowest, debug, up to the highest, emerg. Only logging destinations configured to react to a given priority (or a lower/equal severity, depending on configuration) will process it — emerg-level messages are read by essentially any configuration.
man logger # full list of available facilities and priority levels
tail /var/log/syslog # most logs land here (on Red Hat, historically /var/log/messages)
tail /var/log/auth.log # authentication-facility messages are routed here instead
The user.info test message appears in syslog; the auth.err message does not — because messages targeting the auth facility are configured to be routed straight to auth.log.
rsyslog configuration files live in /etc/rsyslog.d/, and are conventionally numbered so rsyslog knows the order in which to execute them:
sudo nano /etc/rsyslog.d/01-custom.conf
# 01-custom.conf
auth.* /var/log/auth.log
cron.info /var/log/cron.log
kern.* /var/log/kern.log
- The first directive sends every
auth-facility message, at any priority (*), toauth.log— this typically already exists in the default configuration. - The second sends
cron-facility messages ofinfopriority or higher tocron.log. - The third sends kernel-related messages of any priority to
kern.log.
sudo systemctl restart rsyslog
Before this change, no cron.log or kern.log existed under /var/log. Generating a matching test message and re-listing the directory confirms the new files are created on demand:
logger -p cron.info "Test cron message"
ls /var/log/
cat /var/log/cron.log
The new log entry contains a timestamp, host name, username, and the message text.
Extending the configuration further illustrates how to route messages by both facility and priority, and how to suppress duplicate delivery:
# Additions to 01-custom.conf
*.emerg /var/log/emergency.log
user.debug /var/log/debug.log
user.debug stop
The stop directive prevents user.debug-level messages from also being written to syslog or any other file further down the configuration — useful when high log volume risks overwhelming logging systems.
sudo systemctl restart rsyslog
logger -p user.debug "Test debug message that should not reach syslog"
cat /var/log/debug.log # the message appears here
grep "Test debug" /var/log/syslog # no match - the stop directive worked as intended
logger -p 0.info "Test emergency-level message" # facility any (*), priority emerg
cat /var/log/emergency.log
These same techniques — building targeted rsyslog rules and using logger to validate them — carry over directly into how real applications and scripts should be designed to produce clean, well-organized log output.
flowchart LR
A["logger -p facility.priority 'message'"] --> B{"rsyslog.d config files<br/>(processed in numeric order)"}
B -->|"auth.*"| C["/var/log/auth.log"]
B -->|"cron.info+"| D["/var/log/cron.log"]
B -->|"kern.*"| E["/var/log/kern.log"]
B -->|"*.emerg"| F["/var/log/emergency.log"]
B -->|"user.debug"| G["/var/log/debug.log"]
G -->|"stop directive"| H["Suppressed from syslog"]
Importing Remote Logs with rsyslog
One of rsyslog’s most valuable capabilities is building surprisingly capable centralized logging infrastructure — useful whenever regulatory, security, or operational efficiency requirements call for maintaining logs on a dedicated remote server.
In this walkthrough, two Ubuntu containers share a network with no firewall restricting traffic between them: a machine named demos acts as the logging server, and a machine named base is the client whose logs will be forwarded.
On the server (demos), a new configuration file is created that will execute before any other custom rules (hence the 00 prefix):
sudo nano /etc/rsyslog.d/00-forward.conf
module(load="imtcp")
input(type="imtcp" port="514")
# Template for organizing remote logs
template(name="RemoteHost" type="list") {
constant(value="/var/log/remote/")
property(name="hostname")
constant(value="/")
property(name="programname")
constant(value=".log")
}
# Route remote logs to separate file by hostname
:hostname, !isequal, "localhost" ?RemoteHost
# Stop processing to prevent local processing of remote logs
:hostname, !isequal, "localhost" stop
module(load="imtcp")andinput(type="imtcp" port="514")tell rsyslog to listen on network port514(the rsyslog default) using TCP — slightly slower than UDP, but more likely to succeed in delivering messages reliably.- The
RemoteHosttemplate organizes incoming logs into/var/log/remote/<hostname>/<programname>.log. - The final two lines route any message whose originating host is not
localhostinto that template, thenstopfurther local processing — ensuring logs arriving from remote clients are not also treated as though they originated locally.
sudo systemctl restart rsyslog
ss -tlnp | grep 514 # confirm rsyslog is listening on TCP port 514
On the client (base), a much simpler configuration forwards everything to the server:
sudo nano /etc/rsyslog.d/00-forward.conf
*.* @@<server-ip-address>:514
*.*— every facility, at every priority level.@@— use the TCP protocol for transmission (a single@would mean UDP).<server-ip-address>— obtained on the server withip addr.:514— the network port the server is listening on.
ip addr # run on the server to find its IP address
sudo systemctl restart rsyslog # run on the client after saving the config
Generating a test message on the client and then checking the server confirms the pipeline works almost instantly:
logger "Test remote log message"
# on the server:
cd /var/log/remote
ls
ls base/
cat base/ubuntu.log
Within seconds of the configuration going live, a base subdirectory appears, containing separate log files per program/user (for example, an ubuntu.log capturing anything generated by the ubuntu user on the remote client, complete with timestamp and metadata) — along with logs generated by the rsyslog service itself.
When experimenting with this setup, remember to disable or remove the forwarding configuration files on both the client and the server afterward, to avoid unintentionally overwhelming the system with unnecessary log traffic.
sequenceDiagram
participant Client as base (client)
participant Server as demos (server)
Note over Server: 00-forward.conf loads imtcp<br/>module, listens on TCP 514
Server->>Server: systemctl restart rsyslog
Note over Client: 00-forward.conf: *.* @@server-ip:514
Client->>Client: systemctl restart rsyslog
Client->>Server: logger "Test remote log message" (forwarded via TCP 514)
Server->>Server: writes to /var/log/remote/base/ubuntu.log
Understanding journald Logging
journald manages logs from deep inside the systemd ecosystem. Rather than the plain-text files that form the backbone of rsyslog, journald stores logs in indexed binary files, accessible only through journalctl commands. For the most part, both logging systems see the same underlying events — journald simply represents a different administrative approach.
journalctl
This drops into a pager showing logs in chronological order — potentially hundreds of pages long. Within the pager:
| Key | Effect |
|---|---|
/ then a search term | Search forward (case-sensitive) |
n | Jump to the next match |
End | Jump to the very end of the log |
q | Quit back to the shell |
journalctl -r # reverse chronological order - most recent entries first
Line numbering with -r starts at 1, but the content shown is still the most recent entries — useful when the events of interest are more likely to be recent. Long lines that extend past the screen edge remain accessible with the arrow keys.
Because journald logs can grow into the tens or even hundreds of thousands of entries on a busy server, monitoring disk usage matters:
journalctl --disk-usage
journald behavior is controlled through /etc/systemd/journald.conf:
less /etc/systemd/journald.conf
Any changes made to that file, or to files added under /etc/systemd/journald.conf.d/, take effect once the service is restarted — and can always be reversed by deleting the customization, at which point journald regenerates the file with its defaults.
| Setting | Purpose |
|---|---|
Storage=auto (default) | Logs are stored persistently only if /var/log/journal already exists and is writable; otherwise logs are discarded at shutdown |
Storage=persistent | Creates /var/log/journal if needed and reliably preserves logs across reboots |
Compress=yes | Compresses older journal files (slightly slower access, smaller footprint) |
SystemMaxUse= | Sets an upper limit on total space journal files may consume; oldest files are deleted once the limit is reached |
SystemMaxFileSize= | Rotates individual journal files before they exceed the configured size |
Some installations, by default, do not preserve journald logs between reboots (Storage=auto with no persistent journal directory present) — switching to Storage=persistent addresses that.
sudo systemctl restart systemd-journald
sudo systemctl status systemd-journald
The service name is systemd-journald — not journalctl and not journald on their own.
ls /var/log/journal/
ls /var/log/journal/<machine-id>/
Files in this directory use the .journal extension, named beginning with either system or user-<uid> (identifying the owner of the events they contain). These binary files are not meaningfully readable by any tool other than journalctl.
flowchart TB
A["systemd-journald"] --> B["Binary, indexed<br/>.journal files"]
B --> C{"Storage= setting"}
C -->|auto, no /var/log/journal| D["Logs discarded at shutdown"]
C -->|persistent| E["Logs preserved in<br/>/var/log/journal across reboots"]
B --> F["Compress=yes<br/>(older files compressed)"]
B --> G["SystemMaxUse / SystemMaxFileSize<br/>(space limits, rotation)"]
Filtering journald Logs
While journalctl shows every available entry by default, its real strength — and its biggest advantage over plain-text rsyslog files — lies in fast, structured filtering.
journalctl -b -1 # logs from the boot session immediately before the current one
journalctl -b -2 # two sessions back, and so on, as far back as logs are retained
journalctl -b -1 -r # combine with reverse order
When troubleshooting an unexpected crash, the most relevant events are usually near the very end of a session — jumping to the end and working backward for errors is a good starting strategy.
journalctl -f # like tail -f: print the most recent 10 messages, then wait for new events (Ctrl+C to quit)
Filtering to a specific system unit narrows results dramatically:
journalctl -u ssh # events from the SSH service only
journalctl -u apache2 # the .service suffix is optional
Combining a boot-session filter with a priority filter:
journalctl -b -1 -p err # entries from the previous boot, at priority 3 (err) or more severe
Time-based filtering accepts plain English:
journalctl -u apache2 --since "2 hours ago"
journalctl -u apache2 --since "12 hours ago"
| Flag | Purpose |
|---|---|
-b [N] | Filter by boot session (-1 = previous boot, -2 = two boots back, etc.) |
-r | Reverse chronological order (most recent first) |
-f | Follow mode — print recent entries, then wait for new ones |
-u <unit> | Filter to a specific systemd unit/service |
-p <level> | Filter by priority level (numeric or name, e.g. err) |
--since / --until | Filter by natural-language or explicit time ranges |
--disk-usage | Report total space consumed by journal files |
flowchart TD
A["journalctl"] --> B{"What are you<br/>looking for?"}
B -->|"A specific service"| C["journalctl -u service-name"]
B -->|"A specific time window"| D["journalctl --since '...' --until '...'"]
B -->|"A previous boot session"| E["journalctl -b -1"]
B -->|"Only errors or worse"| F["journalctl -p err"]
B -->|"Live tail of new events"| G["journalctl -f"]
C --> H["Combine filters as needed"]
D --> H
E --> H
F --> H
Summary
Effective Linux system management rests on four interlocking pillars:
- Users and groups establish who can authenticate to a system and provide the building blocks —
useradd/adduser,usermod,groupadd/addgroup, and/etc/skeltemplates — for organizing people into manageable, purpose-driven collections rather than managing access one account at a time. - Permissions and ownership define what an authenticated user, or a member of a group, is allowed to do with any given file or directory. Symbolic (
rwx) and octal (0–7) notation express the same underlying model;chmod,chown, andchgrpare the tools for shaping it; and the special SUID, SGID, and sticky bits solve access problems that ordinary permission bits cannot. - Process management —
ps,top,pstree,kill,killall, and job control withbg/fg/jobs— gives administrators the visibility and control needed to identify and resolve resource contention or runaway processes before they threaten system stability. - System logging, through both the mature, plain-text rsyslog ecosystem and the newer, binary, tightly
systemd-integrated journald, ensures that every event — routine or malicious — leaves a trail that can be filtered, correlated, and acted on, whether locally or centralized across an entire fleet of servers.
Together, these four areas form the practical foundation of day-to-day Linux administration: controlling who can get in, precisely defining what they can do once they are in, keeping a watchful eye on what is actually running, and maintaining a reliable record of everything that happens along the way.
Command-Line Quick Reference
Users and groups
sudo useradd -m -s /bin/bash <user> # create a user (Red Hat/Fedora style)
sudo adduser <user> # create a user, interactive (Ubuntu/Debian style)
sudo passwd <user> # set/change a password
sudo usermod -aG <group> <user> # add a user to a supplementary group
sudo groupadd <group> # create a new group (or addgroup on Ubuntu)
groups <user> # list a user's group memberships
id <user> # show numeric UID/GID information
su - <user> # switch to another user's shell
Permissions and ownership
ls -al # view permissions, owner, and group
chmod 755 file # set permissions using octal notation
chmod u+x,g-w,o=r file # set permissions using symbolic notation
chown user file # change owner
chown user:group file # change owner and group together
chown -R user dir/ # change owner recursively
chgrp group file # change only the group owner
chmod u+s file # set the SUID bit
chmod g+s dir/ # set the SGID bit
chmod +t dir/ # set the sticky bit
Account removal
ps -u <user> # list a user's active processes
sudo usermod -L <user> # lock an account
sudo tar -czf backup.tar.gz /home/<user> # back up a home directory
sudo gpasswd -d <user> <group> # remove a user from a group
sudo userdel -r <user> # delete the account and its home directory
Process management
ps aux # list all system processes
ps aux --sort=-%cpu | head # top CPU consumers
top # interactive process monitor
pstree -p # process hierarchy with PIDs
free -h # memory usage, human-readable
df -h # disk usage, human-readable
kill <pid> # request graceful termination (SIGTERM)
kill -9 <pid> # force termination (SIGKILL)
sudo killall <name> # kill every instance of a named program
jobs # list background/stopped jobs
bg %<n> / fg %<n> # resume a job in background/foreground
Logging: rsyslog
logger -p <facility>.<priority> "message" # manually submit a log message
tail -f /var/log/syslog # follow the main system log
sudo systemctl restart rsyslog # apply configuration changes
Logging: journald
journalctl # view all logs
journalctl -r # reverse chronological order
journalctl -u <unit> # filter by systemd unit
journalctl -b -1 # logs from the previous boot
journalctl -p err # filter by priority level
journalctl --since "2 hours ago" # filter by time range
journalctl --disk-usage # check space used by journal files
sudo systemctl restart systemd-journald # apply journald.conf changes
Search Terms
system · management · security · systems · cybersecurity · networking · managing · processes · groups · object · ownership · user · accounts · journald · linux · logging · logs · monitoring · permissions · rsyslog · running · users