Linux Security & SSH Hardening
Lock down SSH without locking yourself out, and know how to recover when you inevitably do.
- Time
- 45 min
- Level
- Beginner
- Objectives
- 4 objectives
- Cost
- Free
Where this fits in the platform
Already built
This lab adds
- Key-only SSH access, root login disabled
Before you start
You will need
- A Linux host you can reach another way (console, snapshot, or a second VM)
You do not need these already — the lab environment below provides them.
You will be able to
- Move from password login to key-only authentication safely
- Grant administrative access without handing out root
- Verify a change from a second session before trusting it
Cost — Free
— a VM, a container, or a spare machine.
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#
A server is reachable on port 22 with password authentication and a shared root login. It is being scanned within minutes of being created — that is not paranoia, it is what the auth log shows.
This lab closes it down. The order matters more than the settings: get it wrong and you lock yourself out of a machine you cannot physically reach.
Keep your current session open until the very end. Every step below is verified from a second connection. If something is wrong, the first session is the only way back in.
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`.
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.
An administrative user that is not root
Step 1 of 7
What you are proving: You can create an administrative account that records who did what, rather than sharing root
Marking this settles success criterion 1.
sudo adduser --gecos "" deploy
sudo usermod -aG sudo deploy # wheel on RHEL
sudo install -d -m 700 -o deploy -g deploy /home/deploy/.sshRoot should not be a login account. Every action becomes anonymous the moment
several people share one, and sudo records who did what.
What you are proving: You can install an SSH key with the permissions sshd silently requires
Marking this settles success criterion 1.
On your own machine:
ssh-keygen -t ed25519 -C "deploy@egykode" -f ~/.ssh/egykode
ssh-copy-id -i ~/.ssh/egykode.pub deploy@<host>Ed25519 over RSA: shorter, faster, and no key-size decision to get wrong.
If ssh-copy-id is unavailable, the permissions are the whole trick:
sudo -u deploy mkdir -p /home/deploy/.ssh
sudo -u deploy tee -a /home/deploy/.ssh/authorized_keys < key.pub
sudo chmod 700 /home/deploy/.ssh
sudo chmod 600 /home/deploy/.ssh/authorized_keysSSH silently ignores an authorized_keys it considers too permissive, and
logs nothing helpful. If key auth "does not work", check these two modes first.
What you are proving: You can verify a new login path works before removing the one you are currently using
Marking this settles success criterion 1.
# from a SECOND terminal, leaving the first connected
ssh -i ~/.ssh/egykode deploy@<host> 'whoami'
ssh -i ~/.ssh/egykode deploy@<host> 'sudo -v && echo "sudo ok"' # prompts; that is correctDo not proceed until the first succeeds. This single step is the difference between a hardening exercise and a support ticket.
sudo -n will fail here, and should. -n forbids prompting, sudo wants
to re-authenticate the human, and an account created with --disabled-password
has nothing to type. Unattended access needs an explicit rule naming the
commands it may run:
sudo tee /etc/sudoers.d/deploy <<'EOF'
deploy ALL=(root) NOPASSWD: /usr/bin/systemctl reload ssh, /usr/bin/systemctl status ssh
EOF
sudo chmod 440 /etc/sudoers.d/deploy
sudo visudo -c -f /etc/sudoers.d/deploy # before you trust itNOPASSWD: ALL is one word shorter and hands a root shell to anyone who ever
holds that key.
What you are proving: You can disable password and root login without locking yourself out
Marking this settles success criterion 2.
sudo tee /etc/ssh/sshd_config.d/99-hardening.conf <<'EOF'
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
AllowUsers deploy
EOF
sudo sshd -t # ALWAYS test the config first
sudo systemctl reload sshdsshd -t parses the configuration without applying it. A typo here plus a
restart is how a machine becomes unreachable — and reload keeps existing
sessions alive, where restart does not.
A drop-in under sshd_config.d/ rather than editing sshd_config means a
package upgrade cannot silently revert your changes.
What you are proving: You can prove each authentication route behaves as intended from a separate session
Marking this settles success criterion 3.
ssh -i ~/.ssh/egykode deploy@<host> 'echo ok' # must succeed
ssh -o PreferredAuthentications=password deploy@<host> # must be refused
ssh root@<host> # must be refusedOnly when all three behave should you close the original session.
What you are proving: You can enable a firewall in an order that does not disconnect you
Marking this settles success criterion 4.
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw enable
sudo ufw status verboseAdd the SSH rule before enabling. ufw enable on a remote machine with no
SSH rule disconnects you immediately, and it is the single most common way
people lock themselves out.
What you are proving: You can show how much real attack traffic a host on the internet receives
This step settles no success criterion on its own.
sudo journalctl -u ssh --since "1 hour ago" | grep -ci "failed password"Run this on any host that has been on the internet for a day. The number is usually in the thousands, and it is the argument for everything above.
Key auth fails with no useful error
Permissions. ~/.ssh must be 700 and authorized_keys 600, owned by the user. sudo journalctl -u ssh shows 'Authentication refused: bad ownership'.
Locked out after reloading sshd
Use the provider's console or serial access, or attach the disk to another instance. This is why step 3 exists.
ufw enable disconnected you
The allow rule must come first. Recover via console, then ufw allow 22/tcp.
sudo asks for a password in scripts
Expected, and correct. Use a dedicated automation user with a narrowly scoped NOPASSWD rule rather than disabling it globally.
Success criteria
0 of 4
The concept behind it
Next up
Lab 3 of 59 on the project path