sshd_config Match Blocks Per User and Group
Split sshd policy with Match User, Group, and Address so git, sftp, humans, and CI automation stop sharing one global sshd_config on the VPS.
Netbay Cloud Team
Netbay Engineering
On this page
A single sshd_config that tries to serve humans, a git account, an SFTP dropbox, and a CI key will end up too open for the bot and too tight for the human. Match blocks split the daemon's policy by user, group, address, and local port without running a second sshd. Directives inside a Match apply only when the connection matches. Directives above the first Match remain global. That order is the entire mental model, and it is the part the man page buries.
This is not a hardening starter pack. PasswordAuthentication should already be off. Match is how you stop treating every account as the same admin.
Global defaults, then Match
Put the boring defaults in a drop-in that loads first. PermitRootLogin no, PasswordAuthentication no, PubkeyAuthentication yes, AllowAgentForwarding no, GatewayPorts no. Then add a numbered file that contains only Match stanzas. Match must be last in the effective config because a later global directive would not apply inside the match, and a Match in the middle of globals is how people lose track of what is in force.
sudo tee /etc/ssh/sshd_config.d/70-match.conf > /dev/null <<'EOF'
Match User git
AllowTcpForwarding no
DisableForwarding yes
PermitTTY no
ForceCommand /usr/bin/git-shell
AllowAgentForwarding no
Match Group sftponly
ChrootDirectory /home/%u
ForceCommand internal-sftp
AllowTcpForwarding no
PermitTTY no
X11Forwarding no
Match Group sudo
AllowTcpForwarding yes
PermitTTY yes
Match Address 10.0.0.8/32
AllowUsers deploy ci
EOF
sudo sshd -tIndentation is conventional, not required. Keywords inside Match are a subset: you cannot Match and then change Port, for example. If sshd -t complains that a directive is not allowed in a Match, it is telling the truth. Move that keyword to the global section.
ChrootDirectory for SFTP requires the chroot path to be owned by root and not group-writable. The user's writable directory sits inside that. Getting this wrong produces a silent disconnect after auth, which looks like a key problem and is not.
User, Group, Address, LocalPort
Match User git is exact. Match User deploy,ci is a list. Match Group sudo follows Unix groups at authentication time, so usermod -aG is how you grant the human policy. Match Address 203.0.113.10/32 is the bastion's public IP, or the private IP if the connection arrives on the RFC1918 path. Match LocalPort 2222 is how a break-glass listener on a second port gets a different policy, which the last post in this series will use.
Criteria can combine: Match User ci Address 10.0.0.8/32. All listed criteria must hold. There is no OR inside one Match; you write two Match blocks instead.
ForceCommand overrides anything the client asked to run, including git-system and scp. For the git account that is the feature. For a human account it is a lockout. AuthorizedKeysCommand and authorized_keys options like command= interact with ForceCommand; the sshd option wins. Keep forced commands in one place so you can grep them.
Test the effective config without guessing
sshd -T prints the effective configuration for a dummy connection. Add -C to supply user, host, addr, and lport so Match stanzas evaluate.
sudo sshd -T -C user=git,host=localhost,addr=10.0.0.8 | grep -Ei "forcecommand|allowtcp|chroot|permitty|allowusers"
sudo sshd -T -C user=deploy,host=localhost,addr=10.0.0.8 | grep -Ei "forcecommand|allowtcp|chroot|permitty"
sudo systemctl reload sshRead the output. If ForceCommand is still none for git, the Match did not fire — usually a typo in the username or a drop-in that never loaded. Ubuntu 24.04 reads /etc/ssh/sshd_config.d/*.conf in lexical order. 70-match.conf after 50-hardening.conf is the right shape.
Keep a second SSH session open while you reload. Match Address that forgets your current IP is a classic lockout. If you use a jump host, the app node sees the bastion address, not your laptop. Match Address on the app node should list the bastion, not your home ISP.
What not to express with Match
Do not use Match to re-enable PasswordAuthentication for one user. That user will be brute-forced. Do not use Match User root as a way to keep root login; leave PermitRootLogin no. Do not put secrets in Match. Do not rely on Match Host with a DNS name if the client IP is what you actually know — Host is the name the client sent, which is not authentication.
A short takeaway: globals first, Match last, one stanza per role, sshd -T -C before reload. Humans get a tty, git gets git-shell, sftp gets a chroot, CI gets an address allowlist. Apply it on a Netbay Ubuntu 24.04 VPS in Lucknow — up in under 60 seconds at netbayhosts.in.
Keep reading
Follow along on a real VPS
Deploy Linux in under 60 seconds
These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.
Deploy an instance