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:
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.
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:
awk -F: '$3 >= 1000 && $3 < 65534 {print $1, $7}' /etc/passwdNow 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.
ps -u svc-backup -o pid,etime,cmd
id svc-backup
sudo -l -U svc-backupReview recent logins so you know which accounts are genuinely interactive:
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/nologinon Debian and Ubuntu,/sbin/nologinon 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/falsedoes 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:
cat /etc/shellschsh 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:
sudo usermod -s /usr/sbin/nologin svc-backup
sudo usermod -s /usr/sbin/nologin svc-monitor
getent passwd svc-backupOn 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:
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.
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-monitorAllowGroups 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:
sudo sshd -t
sudo systemctl reload sshOn 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.comThe 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 noCheck it works
Confirm the running configuration matches what you wrote:
sudo sshd -T | grep -Ei 'allowgroups|denyusers|permitrootlogin'Then try to log in as a service account from another machine. You should be refused:
ssh svc-backup@203.0.113.10Expect 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:
ssh deploy@203.0.113.10Finally, check the server log for the attempt:
sudo journalctl -u ssh --since "10 min ago" | grep -i svc-backupTroubleshooting
- "This account is currently not available." on a legitimate login — the account now has
nologin. Restore it withsudo 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 deployandsudo sshd -T | grep -i allowgroups, fix the group, thensudo 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 withUser=andExecStart=. sshd -treports an error and reload fails — usually aMatchblock placed above global directives, or a typo in a drop-in file. Fix the file, runsudo sshd -tagain, and only then reload.- SFTP stops working for an account — SFTP needs a shell or
Subsystem sftp internal-sftp. AddForceCommand internal-sftpinside the account'sMatchblock. See secure file transfer with SFTP over SSH keys.
Next steps
- Enforce key rotation policies for automated security so automation keys do not live forever.
- Disable root login and password authentication over SSH if you have not already.
- Add Fail2ban to block repeated failed logins from the same address.
