Linux Server Administration
Create users and groups, set permissions that actually hold, and manage services and packages on a server you did not build.
- Time
- 45 min
- Level
- Beginner
- Objectives
- 4 objectives
- Cost
- Free
Where this fits in the platform
Already built
—
This lab adds
- A Linux server you can navigate, inspect and administer
Before you start
You will need
- Any Linux (Ubuntu 22.04+ or RHEL 9)
- sudo access
You do not need these already — the lab environment below provides them.
You will be able to
- Create users and groups with correct ownership and permissions
- Read and set permissions without reaching for chmod 777
- Manage packages and services, and make them survive a reboot
- Find what is consuming disk on a server that is full
Cost — Free
— runs on any Linux machine, a VM, or a container
Nothing to pay in the browser. Open the terminal runs this against a simulated cloud — the same API calls and the same commands, with no account and no bill. The figure above applies only if you build it in your own.
The scenario#
You have been handed SSH access to a server somebody else built. A colleague needs to deploy to it, the application directory is owned by root, and the disk is at 91%. Nobody documented any of it.
This is the most common first task in the job, and none of it is exotic — it is users, permissions, services and disk.
Hands-on environment
Run this lab in a real terminal, free and in your browser. The environment is temporary and yours alone — break it as much as you like.
Open the terminalOpens in Killercoda, in a new tab — keep this page open for the steps.
Run it on your own machine
Run this lab on your own machine. One command starts the environment, with everything the lab needs already installed:
Run these against the node1 container rather than the controller — it is the machine with systemd. From the controller: `ssh node1`, or prefix a command with `ansible node1 -b -m shell -a "…"`.
You will need:
- bash
git clone https://github.com/EgyKode/EgyKode-lab.git
cd EgyKode-lab
./egykode start
./egykode shell
ssh node1You need Docker and Git installed. Everything else runs inside the environment. The first start downloads it and takes a few minutes; later starts are seconds.
Not sure what you already have? Run: npm run doctor — it checks and changes nothing.
Anything you tick here is your own record. EgyKode cannot see inside that terminal, so the success criteria stay self-assessed even when the environment checks your work for you.
A user for the job, not for the person
Step 1 of 4
The work#
What you are proving: You can create a service account whose access outlives the person who set it up.
Marking this settles success criterion 1.
Why
Create a group first, then a user in it. The group is what makes access
survivable when a second person needs it — a colleague joins by being added to
deployers, and nothing about the files has to change.
sudo groupadd --system deployers
sudo useradd --create-home --gid deployers --shell /bin/bash deploy
sudo passwd -l deploy # no password login; SSH keys only
id deployWhat you should see
id deploy prints the user with deployers as its primary group — something
like uid=998(deploy) gid=998(deployers) groups=998(deployers). The numbers
will differ; the group name is the part that matters.
--gid deployers puts the user in the group at creation. passwd -l locks the
password so the account cannot be used for interactive password login — an SSH
key is the only way in.
In production
A service account belongs to the job, not to a person. When the engineer who created it leaves, nothing about the deployment changes — which is exactly what does not happen when deployments run under someone's personal login.
What you are proving: You can set group ownership that new files inherit, rather than re-fixing permissions after every deploy.
Marking this settles success criterion 2.
Why
Ownership decides who can write today. The setgid bit decides who can write to the files created tomorrow — which is the part that breaks weeks later.
sudo mkdir -p /opt/app
sudo chown -R root:deployers /opt/app
sudo chmod -R 2775 /opt/appThe leading 2 is the setgid bit, and it is the part most guides omit. Without
it, a file created in /opt/app belongs to whichever user made it, and the
next deployer cannot overwrite it. With it, everything created inside inherits
the deployers group.
Prove it rather than assume it:
sudo -u deploy touch /opt/app/test.txt
ls -l /opt/app/test.txt # group must be "deployers"What you should see
ls -ld /opt/app shows drwxrwsr-x — note the s where the group execute bit
would be. The new file's group is deployers, not deploy.
Incident
sudo -u deploy touch gives Permission denied
/opt/app is not group-writable. Check ls -ld /opt/app — you want
drwxrwsr-x with the s, not drwxr-xr-x.
Incident
A new file has the wrong group
The setgid bit is missing. chmod g+s /opt/app and create the file again;
existing files keep their old group.
In production
chmod 777 makes both of these symptoms disappear, which is why it is so
common and why it keeps being wrong. It grants every user on the machine write
access to your application directory to solve a problem that group ownership
solves precisely. The same reasoning returns later as container users and IAM
policies: grant the narrowest thing that works.
What you are proving: You can tell a service that is running from one that will still be running after the machine restarts.
Marking this settles success criterion 3.
Why
enable and start are different things, and nothing in the output of a
working service tells you which one you did.
sudo apt-get update && sudo apt-get install -y nginx # or dnf on RHEL
systemctl status nginx
sudo systemctl enable --now nginx
systemctl is-enabled nginx # must print "enabled"What you should see
systemctl is-enabled nginx prints enabled. systemctl status nginx shows
Active: active (running) and, on the Loaded: line, enabled — both facts
are there, and only one of them survives a reboot.
--now does both. A service that is started but not enabled works perfectly
until the machine reboots at 3am and never comes back — and that failure looks
like a mystery unless you know to check this.
Incident
The service is running but gone after reboot
It was started, never enabled. systemctl is-enabled <service> tells you
which.
What you are proving: You can go from 'the disk is full' to the specific directory responsible, with the command that told you.
Marking this settles success criterion 4.
Why
A full disk breaks things that look unrelated — Docker cannot pull, Kubernetes evicts Pods, the database refuses writes — and every error message points somewhere else. Finding the cause is a fixed procedure, not a guess.
df -h # which filesystem is full?
sudo du -sh /var/* 2>/dev/null | sort -h | tail -5
sudo journalctl --disk-usageWork top-down: df names the filesystem, du narrows it to a directory. The
usual culprits are /var/log and unpruned container images. If the journal is
the problem:
sudo journalctl --vacuum-time=7dWhat you should see
df -h shows one filesystem near 100%, and the last line of the du output
names the directory holding most of it. You should be able to say both out
loud: which filesystem, and which directory inside it.
Incident
df says the disk is full but du finds nothing
A deleted file is still held open by a process. sudo lsof +L1 lists them;
restarting the holder releases the space.
In production
This is the difference between a filesystem and a directory tree. du walks
names; df asks the filesystem. A file whose last name has been removed is
gone from du and still occupying blocks, because the process holding it open
keeps the inode alive. That gap is the whole explanation.
Success criteria
0 of 4
The concept behind it
Next up
Lab 1 of 59 on the project path