You're the new junior administrator at Heliotrope Defense Systems. The CTO walks over to your desk. "Three engineers start Monday. They need shared storage, SSH access, and to be set up properly — not the way the last admin did it. Have them ready by EOD Friday."
By the end of this lab you will have created a group, three users, set aged passwords, built a shared project directory with the right permissions, configured key-based SSH (with root login disabled), mounted a data volume, and — in the bonus round — fully off-boarded a team member who decided to leave on Tuesday.
Setup — before you start
You need a Linux system where you have root access and won't break anything important. Do not do this on your personal workstation or any production machine.
- Recommended: Ubuntu 22.04+ or Rocky Linux 9 in a VirtualBox / VMware / UTM VM, or a free-tier EC2 / Lightsail instance you can destroy when done.
- Also fine: a Docker container (
docker run -it --rm ubuntu:22.04 bash) for steps 1–5; SSH and mount work less cleanly inside a container, so use a real VM for steps 6–8. - You'll need: root or
sudoaccess, network connectivity (for the SSH key copy step), and basic familiarity with a text editor likenanoorvi. - Time budget: 60–90 minutes the first time through; ~30 minutes if you've already done it once.
The team is called engineering. Create the group first, before you create any users — that way you can add each new user to it at creation time instead of having to come back and modify them afterward.
groupadd adds a single line to /etc/group with the new group name, a placeholder for the (essentially unused) group password, an auto-assigned GID, and no members yet. The GID will be something like 1001 or 1002 depending on what's already there.
Confirm the group exists and is empty:
You should see the line with the group name, the placeholder x, the GID, and a trailing colon with no members after it. That trailing colon is normal — it means "no supplementary members yet."
The three engineers are alice, bob, and carol. Each gets a home directory, bash as a login shell, a sensible comment field, and membership in engineering from the start.
-m creates each user's home directory and copies in the skeleton files from /etc/skel. -s /bin/bash gives them an interactive shell. -c populates the GECOS comment field with their name and team. -G engineering adds the supplementary group.
By default, useradd also creates a user-private group with the same name as each user (alice's primary group is alice, bob's is bob, etc.), and engineering becomes a supplementary group for each.
-G during creation, which is fine because there are no existing memberships to wipe out. After the account exists, always use usermod -aG to append — never usermod -G on an existing user, or you'll silently wipe out everything they had.
Inspect each user's groups:
All three users appear in the engineering group line, and each has the group listed as supplementary.
Give each user a temporary password and force them to change it at first login. The policy: maximum 90 days, minimum 1 day, 14 days of warning before expiry.
chage -d 0 sets the "last password change" to epoch day zero, which the system interprets as "the password is expired." At next login (console, SSH, anywhere) the user will be required to change it before doing anything else. The other flags set the rolling policy after they pick a new one.
The "must be changed" lines confirm the next login will prompt for a new password.
The team needs a place to put shared work that all three of them can read and write. Put it at /srv/engineering, give it group ownership engineering, and set the setgid bit so files created inside automatically inherit the group.
mkdir -p creates the parent /srv/engineering if it doesn't exist, and the brace expansion makes three subdirectories in one call. chgrp -R engineering walks the whole tree, setting group ownership on every entry. The mode 2770 is the important part:
- 2 — setgid bit on directories; new files inherit the group
- 7 — rwx for owner
- 7 — rwx for group
- 0 — nothing for others (deliberately denying access to anyone not in the group)
Become alice, create a file in the shared directory, and confirm the group ownership inherited automatically:
The group on test.txt is engineering, not alice — that's the setgid bit doing its job. Bob and Carol will also be able to read and write the file because they're in the same group.
Three changes to /etc/ssh/sshd_config: only members of engineering can SSH at all, root login is disabled, and password authentication is off (keys only).
PermitRootLogin no — the root account cannot log in over SSH at all. Anyone needing root must log in as themselves first, then sudo. Every privileged action is now attributable to a real person.
PasswordAuthentication no — brute-force password guessing is no longer a path in. Only clients with a valid private key can authenticate.
AllowGroups engineering — only members of the engineering group can SSH. Anyone else gets denied immediately, even with valid credentials.
sshd -tAnd keep a second SSH session open during the restart, so if you locked yourself out, you still have a way back in to fix it.
Now alice needs a key on the server. From her workstation:
After this point, alice's next login uses her key (no password prompt), the server still forces her to change her expired password before doing anything else, and then she has normal access.
From a third machine that is not alice's workstation, try to SSH as alice (you should be rejected — no matching key), and try as root (you should be rejected immediately):
Both rejected. Now repeat from alice's workstation and she should land at a password-reset prompt.
The team's shared directory is going to fill up — they need their own dedicated 500 GB volume. Pretend you've attached a new disk to the VM (in VirtualBox: Settings → Storage → Add Hard Disk; on EC2: Attach Volume). The new device shows up as /dev/sdb. We'll format it, mount it at /srv/engineering, and make the mount persistent.
dd if=/dev/zero of=/var/data.img bs=1M count=512 && losetup -fP /var/data.img. The loop device will appear as /dev/loop0 — substitute that for /dev/sdb1 below.
blkid reports the UUID we'll use in /etc/fstab. Using the UUID instead of /dev/sdb1 means the mount survives the disk being re-enumerated as /dev/sdc after a reboot — UUIDs follow the filesystem, not the device path.
The mount options nosuid,nodev are defense in depth: even if a malicious file lands here somehow, it can't be a setuid-root binary and there can be no device nodes on this partition.
mount -a mounts everything in fstab that isn't already mounted — the same code path that runs at boot. If you got the fstab syntax wrong, now is when it tells you, instead of leaving you with an unbootable system.
The shared directory now lives on its own 500 GB volume, mounted with security-friendly options, and the mount will reappear on every reboot.
Bob has accepted a job elsewhere. His last day is today. You need to disable his account completely — not "lock the password and call it done," but the full close: no password login, no key login, no shell, no running session, no group membership, and the audit trail preserved for thirty days before deletion.
Each step closes a specific door:
- Step 1 blocks password authentication.
- Step 2 sets the account to expired — the system treats it as a closed account regardless of which credential is presented.
- Step 3 means even if a credential succeeds, the user lands on
/usr/sbin/nologin, which prints a polite message and exits. - Step 4 revokes group access so even if bob came back via a different identity, he doesn't get into the team directory.
- Step 5 blocks key-based SSH (which doesn't care about the password lock).
- Step 6 ends any sessions in progress.
- Step 7 stops scheduled tasks that would otherwise keep running as bob.
Password locked. Account expired in the distant past. Shell set to nologin. Removed from engineering. Bob's home directory is preserved on disk — you'll archive and delete after the 30-day window.
The team is bringing on a contractor named dave. He needs to drop files into a specific directory using SFTP and nothing else — no shell, no other paths, no SSH tunneling. This is what SSH's ChrootDirectory directive was made for.
The Match Group contractors block applies only to users in that group. Inside the match, ChrootDirectory uses %u as a placeholder for the user's name, so dave is jailed to /srv/contractors/dave. ForceCommand internal-sftp bypasses any shell entirely — the SSH server runs the SFTP subsystem and nothing else.
The strict ownership and permission requirements on the chroot path itself (root:root, not group-writable) are mandatory: sshd refuses to chroot into a directory anyone but root can modify. This is the one footgun of SSH chroot — if dave can write to /srv/contractors/dave, sshd rejects his connection with a confusing error.
From dave's workstation:
Dave sees a tiny filesystem with only his uploads directory in it. He cannot cd /etc — from his point of view /etc doesn't exist. SSH (interactive shell) is rejected because his shell is nologin and the SFTP-only configuration is enforced.
For each of the eight steps, capture and submit:
- The command(s) you actually ran (paste from your terminal — not copied from this page).
- The verification output showing the step worked.
- One sentence in your own words explaining what would have gone wrong if you'd skipped this step.
A clean lab writeup is a deliverable in its own right — the discipline of recording what you did is what separates the engineer who can repeat their work from the one who can't.