Security

SSH Hardening: Auditing and Limiting Shell Access for Service Accounts

Audit every Linux account that has a login shell, move service accounts to nologin, and restrict SSH with AllowGroups and Match blocks so only people who need a session get one.

AdvancedLinux server6 min read

To restrict shell access for service accounts, you first list every account on the VPS that can start an interactive session, then give each one the smallest shell it can work with. A backup user, a web app user or a monitoring agent rarely needs a login prompt — and every account that has one is another password or key an attacker can try.

This guide audits accounts on Ubuntu 22.04/24.04, Debian 12 and AlmaLinux/Rocky 8–9, moves service accounts to nologin, and limits SSH itself with AllowGroups and Match blocks.

Before you start

  • Root or sudo access on your Linux VPS.
  • A second SSH session already open and tested. Keep it open until you have finished every change.
  • A note of which accounts run services: check systemd units, cron jobs and web app pools before you touch anything.
  • Your VPS provider's browser console as a fallback if SSH locks you out.
  • A backup of the SSH config:
Bash
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak
sudo ls /etc/ssh/sshd_config.d/

Warning: Changing a login shell or an SSH access rule can lock out a working account. Never close your current session until you have verified the new one works.

Audit which accounts have a login shell

Start by listing every account whose shell is not nologin or false. This is your attack surface.

Bash
getent passwd | awk -F: '$7 !~ /(nologin|false)$/ {printf "%-20s %-6s %s\n", $1, $3, $7}'

Human accounts usually have a UID of 1000 or higher, service accounts sit below it:

Bash
awk -F: '$3 >= 1000 && $3 < 65534 {print $1, $7}' /etc/passwd

Now check what each account actually does. A service account that owns running processes is in use; one that owns nothing may be leftover from an old install.

Bash
ps -u svc-backup -o pid,etime,cmd
id svc-backup
sudo -l -U svc-backup

Review recent logins so you know which accounts are genuinely interactive:

Bash
last -a | head -20
sudo journalctl -u ssh --since "7 days ago" | grep -i "Accepted"

Tip: Write the results into a short table in your notes: account, shell, purpose, needs login (yes/no). That list drives every change below.

Choose the right shell for each account

Three options cover almost every case:

  • /usr/sbin/nologin on Debian and Ubuntu, /sbin/nologin on AlmaLinux and Rocky. The account can still own files and run processes, but an interactive login is refused with "This account is currently not available."
  • /bin/false does the same job and returns immediately. Use it when a script checks the exit code.
  • /bin/rbash (restricted bash) for accounts that need a limited interactive session, for example a deploy user who should only run a handful of commands.

Check which of these your system already knows about:

Bash
cat /etc/shells

chsh refuses shells that are not listed in /etc/shells, so add nologin there if it is missing.

Remove login shells from service accounts

Change the shell for each account that does not need a prompt:

Bash
sudo usermod -s /usr/sbin/nologin svc-backup
sudo usermod -s /usr/sbin/nologin svc-monitor
getent passwd svc-backup

On AlmaLinux and Rocky use /sbin/nologin instead.

Warning: Do not change the shell of an account that a unit starts with su - user -c "..." or sudo -u user -i. Those need a real shell. systemd units that set User= in the unit file do not, because systemd starts the binary directly.

Verify nothing broke:

Bash
systemctl status your-service --no-pager
sudo journalctl -u your-service --since "5 min ago"

Limit SSH access with AllowGroups and Match

Shell changes stop interactive logins, but SSH still accepts the account's key before the shell is checked. Close that gap by naming who may connect at all.

Create a group for people who need SSH, add yourself, then restrict access in a drop-in file. Ubuntu 22.04+, Debian 12 and RHEL 8–9 all include /etc/ssh/sshd_config.d/*.conf.

Bash
sudo groupadd -f ssh-users
sudo usermod -aG ssh-users deploy
# /etc/ssh/sshd_config.d/10-access.conf
AllowGroups ssh-users
PermitRootLogin no
DenyUsers svc-backup svc-monitor

AllowGroups is a whitelist: once it is set, only members of those groups may authenticate. DenyUsers is a belt-and-braces block for service accounts that must never open a session.

Test the syntax before reloading, and keep your existing session open:

Bash
sudo sshd -t
sudo systemctl reload ssh

On AlmaLinux and Rocky the unit is sshd, so use sudo systemctl reload sshd.

Restrict what a service account can do over SSH

Some automation must connect over SSH — for example a push-based backup. Pin it to one command in the account's authorized_keys:

command="/usr/local/bin/backup-only.sh",no-port-forwarding,no-agent-forwarding,no-pty,no-X11-forwarding ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... backup@example.com

The command= option ignores whatever the client asks to run and executes your script instead. no-pty removes the interactive terminal, and the forwarding options stop the key being reused as a tunnel.

If several accounts share the same rule, use a Match block instead. Match blocks must sit at the end of the config file, after every global directive:

Match User svc-deploy
    ForceCommand /usr/local/bin/deploy-only.sh
    PermitTTY no
    AllowTcpForwarding no
    X11Forwarding no

Check it works

Confirm the running configuration matches what you wrote:

Bash
sudo sshd -T | grep -Ei 'allowgroups|denyusers|permitrootlogin'

Then try to log in as a service account from another machine. You should be refused:

Bash
ssh svc-backup@203.0.113.10

Expect svc-backup@203.0.113.10: Permission denied or "This account is currently not available." Both mean the restriction is working. Your own account should still connect normally:

Bash
ssh deploy@203.0.113.10

Finally, check the server log for the attempt:

Bash
sudo journalctl -u ssh --since "10 min ago" | grep -i svc-backup

Troubleshooting

  • "This account is currently not available." on a legitimate login — the account now has nologin. Restore it with sudo usermod -s /bin/bash deploy, then re-check whether it really needs a shell.
  • Nobody can connect after adding AllowGroups — the group name is wrong or your user is not a member. Open your VPS provider's console, run id deploy and sudo sshd -T | grep -i allowgroups, fix the group, then sudo sshd -t && sudo systemctl reload ssh.
  • A service fails to start after the shell change — the unit uses su - or a login shell. Either set the shell back or change the unit to start the binary directly with User= and ExecStart=.
  • sshd -t reports an error and reload fails — usually a Match block placed above global directives, or a typo in a drop-in file. Fix the file, run sudo sshd -t again, and only then reload.
  • SFTP stops working for an account — SFTP needs a shell or Subsystem sftp internal-sftp. Add ForceCommand internal-sftp inside the account's Match block. See secure file transfer with SFTP over SSH keys.

Next steps

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.