Users, sudo, and SSH Hardening¶
What You'll Learn¶
- How users, groups, and system accounts work, and when to use each
- How to grant least-privilege
sudoaccess with sudoers drop-ins - How to set up key-based SSH and a hardened
sshdconfiguration without locking yourself out - How to add a host firewall and basic brute-force protection
Users and Groups¶
# A human user with a home directory and bash
sudo useradd -m -s /bin/bash -c "Priya Shah" priya
sudo passwd priya # or skip passwords entirely and use SSH keys
# A service account: no home login, no shell
sudo useradd --system --no-create-home --shell /usr/sbin/nologin orders
# Groups
sudo groupadd devops
sudo usermod -aG devops priya # -a appends; without it you REPLACE all groups
id priya
getent group devops
# Lock and remove
sudo usermod -L -e 1 priya # lock the password and expire the account
sudo userdel -r priya # delete the user and home directory
| File | Contains |
|---|---|
/etc/passwd |
Accounts: UID, primary GID, home, shell |
/etc/shadow |
Password hashes and expiry — readable only by root |
/etc/group |
Groups and members |
Group membership changes take effect at the user's next login.
sudo: Least Privilege¶
Never edit /etc/sudoers directly. Use drop-in files, and always validate them.
# Full sudo for the ops team, password required
%devops ALL=(ALL:ALL) ALL
# The deploy user may restart one service and nothing else, without a password
deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart orders-api, /usr/bin/systemctl status orders-api
Avoid shell escapes in NOPASSWD rules
Granting NOPASSWD on editors, pagers, find, tar, or scripts the user can edit is effectively full root — each can spawn a shell. Allow exact commands with exact arguments.
Key-Based SSH¶
On your workstation:
ssh-keygen -t ed25519 -C "priya@laptop"
ssh-copy-id -i ~/.ssh/id_ed25519.pub priya@server.example.com
ssh priya@server.example.com
Permissions must be strict, or sshd ignores the keys:
A client config saves typing and centralizes bastion settings:
Host bastion
HostName bastion.example.com
User priya
IdentityFile ~/.ssh/id_ed25519
Host app-*
User priya
ProxyJump bastion
IdentityFile ~/.ssh/id_ed25519
Harden sshd¶
Use a drop-in so package upgrades don't overwrite your settings. Ubuntu 24.04 and recent RHEL read /etc/ssh/sshd_config.d/*.conf.
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
AllowGroups ssh-users
MaxAuthTries 3
LoginGraceTime 30
ClientAliveInterval 300
ClientAliveCountMax 2
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding no # set to yes on a bastion host that needs ProxyJump
Apply it without locking yourself out:
sudo groupadd -f ssh-users
sudo usermod -aG ssh-users priya # make sure YOUR user is in the allowed group
sudo sshd -t # validate syntax; no output means OK
sudo systemctl reload ssh # the service is "sshd" on RHEL-family systems
# Keep your current session open and test from a NEW terminal
ssh priya@server.example.com
Changing the SSH port on Ubuntu 24.04
Ubuntu uses socket activation for SSH by default, so the listening port comes from ssh.socket, not only sshd_config. Change it with sudo systemctl edit ssh.socket (clear and set ListenStream=), then daemon-reload and restart ssh.socket. A non-standard port reduces log noise; it isn't a security control.
Host Firewall¶
# Ubuntu: ufw
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow from 10.0.0.0/16 to any port 22 proto tcp # SSH from the VPC only
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose
# RHEL family: firewalld
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --permanent --add-rich-rule='rule family=ipv4 source address=10.0.0.0/16 service name=ssh accept'
sudo firewall-cmd --permanent --remove-service=ssh
sudo firewall-cmd --reload
In the cloud, security groups are the first layer; a host firewall is defense in depth when a security group is misconfigured.
Brute-Force Protection¶
[sshd]
enabled = true
backend = systemd
maxretry = 5
findtime = 10m
bantime = 1h
With password authentication disabled, brute-force attempts can't succeed — fail2ban mostly reduces log noise and load.
Audit Access¶
last -n 20 # recent logins
lastb -n 20 2>/dev/null # failed logins (needs root)
journalctl -u ssh --since today | grep -E 'Accepted|Failed'
sudo grep -h -v '^#' /etc/sudoers.d/* | grep -v '^$' # current sudo grants
awk -F: '$3 == 0 {print $1}' /etc/passwd # UID 0 accounts — should be only root
At scale, replace static authorized_keys files with short-lived SSH certificates or a session manager (for example AWS Systems Manager Session Manager), so access can be revoked centrally and every session is logged.
Common Mistakes¶
usermod -Gwithout-a, silently removing a user from every other group — includingsudo.- Disabling password authentication before confirming key login works, from a session you then close.
- Editing
/etc/sudoerswith a normal editor, introducing a syntax error that breakssudofor everyone. NOPASSWD: ALLfor automation accounts, orNOPASSWDon commands that can spawn a shell.- Sharing one SSH key or one account across a team, so access can't be revoked per person and logs show no individual.
- Leaving SSH open to
0.0.0.0/0because "keys are secure anyway".
Interview Questions¶
- How do you give a deploy user permission to restart a single service and nothing else?
- Walk through hardening SSH on a new server without locking yourself out.
- What's the difference between a system account and a regular user?
- Why is
NOPASSWDonvimorlessdangerous? - How would you manage SSH access for 200 servers and 50 engineers?
Next¶
Continue to Packages and Patching.