Set Up a Git-Only SSH Account with git-shell
You will finish with an SSH account that can serve Git repositories through git-shell, but cannot open an ordinary shell. The workflow uses the Git 2.43.0 manpage and the installed /usr/bin/git-shell. It creates one test repository and a small account configuration; it does not change an existing service account unless you deliberately substitute its name.
The route
Jump straight to the step you need, or tick off Done means at the end.
Allow about 20 minutes. You need root or equivalent privileges to create the account, change its login shell and place a repository in its home directory. You also need an SSH key on the client. The commands below use the placeholder account gitreader and host git.example.test. Replace both before running them.
Checkpoint
Stop after each section and run its verification command. Do not apply the account changes to a production Git service until you have confirmed which repositories it must serve and how you will recover its previous login shell.
1. Confirm the installed command
First check the binary and package version. These are ordinary, read-only commands:
$ command -v git-shell
/usr/bin/git-shell
$ git --version
git version 2.43.0
$ dpkg-query -W -f='${Package} ${Version}\n' git-man
git-man 1:2.43.0-1ubuntu7.3
Your package revision may differ. The relevant behaviour is the Git version shown by git --version. The shell accepts Git's server-side commands after -c: git-upload-pack for fetches, git-receive-pack for pushes and git-upload-archive for remote archives. An ordinary SSH session is not an allowed command.
2. Create a dedicated account
This is the first state-changing step and requires elevated privileges. Use a dedicated account rather than changing the login shell of a personal user. The --system and --home options are provided by Debian and Ubuntu's useradd; check your distribution's account-management command if they are unavailable:
# useradd --system --create-home --home-dir /srv/gitreader --shell /usr/bin/git-shell --user-group gitreader
# install -d -o gitreader -g gitreader -m 0750 /srv/gitreader/repositories
# getent passwd gitreader
gitreader:x:...:...::/srv/gitreader:/usr/bin/git-shell
Do not reuse an existing account name in this command. If the account already exists, use getent passwd gitreader and inspect its home directory before changing anything. To undo a newly created account, first preserve or remove any repositories you placed there, then run userdel gitreader as root. That deletion is irreversible for files owned only by the account, so do not use it as a cleanup shortcut on an existing service.
3. Install a repository the account may serve
A Git SSH URL normally names a bare repository, so create one under the account's repository directory. This example creates an empty repository without changing any working tree:
$ sudo -u gitreader git init --bare /srv/gitreader/repositories/example.git
Initialized empty Git repository in /srv/gitreader/repositories/example.git/
$ sudo -u gitreader git -C /srv/gitreader/repositories/example.git rev-parse --is-bare-repository
true
If sudo -u is not available, run the same commands as root with the explicit account, or create the repository elsewhere and set its ownership with care. Do not make the repository writable by every SSH user. The account needs access to the repository objects and refs; the parent directory must also allow it to be traversed.
Checkpoint
The repository path in the next steps is /srv/gitreader/repositories/example.git. Git's SSH transport sends that path to git-shell; it is not a local shell path expansion and must match the repository you installed.
4. Give the client key access
SSH key authorisation is separate from git-shell. Create the account's authorized_keys file as root, then make it readable only by the account:
# install -d -o gitreader -g gitreader -m 0700 /srv/gitreader/.ssh
# install -o gitreader -g gitreader -m 0600 /dev/null /srv/gitreader/.ssh/authorized_keys
# sh -c 'cat /path/to/client-key.pub >> /srv/gitreader/.ssh/authorized_keys'
# chown gitreader:gitreader /srv/gitreader/.ssh/authorized_keys
The final command in this example changes state and requires elevated privileges. Review the file before testing. Do not paste a private key into it, and do not append an untrusted public key to a production account. If a key is compromised, remove that public-key line and reload nothing: sshd reads authorized_keys for the next connection.
5. Verify Git transport access
From the client, ask Git to list the empty repository's references:
$ git ls-remote ssh://[email protected]/srv/gitreader/repositories/example.git
An empty repository normally produces no reference lines and exits successfully. Check that explicitly:
$ git ls-remote ssh://[email protected]/srv/gitreader/repositories/example.git
$ printf 'exit status: %s\n' "$?"
exit status: 0
If you see a host-key prompt, verify the host fingerprint through your normal administrator before accepting it. A public Git server may use a different SSH configuration or a forced command in authorized_keys; do not remove those controls merely to make this test pass.
6. Prove that an interactive shell is refused
Run an SSH connection with no remote command. With git-shell as the login shell, it should not provide a normal prompt. On Git 2.43.0 the command exits non-zero and prints a refusal such as:
$ ssh [email protected]
fatal: Interactive git shell is not enabled.
Connection to git.example.test closed.
$ printf 'exit status: %s\n' "$?"
exit status: 128
The exact wording and SSH connection line can vary. The useful result is that no shell prompt appears and the connection closes. Do not create ~/git-shell-commands casually: according to the manpage, that directory enables custom commands and also permits interactive use. If it exists, a command named no-interactive-login can abort interactive login, but every executable placed there becomes part of your account's security boundary.
7. Test a real clone and push
Use a temporary working copy on the client. This changes only the temporary directory and the test repository:
$ tmpdir=$(mktemp -d)
$ git clone ssh://[email protected]/srv/gitreader/repositories/example.git "$tmpdir/example"
Cloning into '.../example'...
warning: You appear to have cloned an empty repository.
$ cd "$tmpdir/example"
$ git config user.name 'Test User'
$ git config user.email '[email protected]'
$ printf '%s\n' 'git-shell test' > README.md
$ git add README.md && git commit -m 'Add test file'
$ git push origin HEAD
To git.example.test:/srv/gitreader/repositories/example.git
* [new branch] HEAD -> main
The branch name may be master or another configured default. The important checks are that the commit succeeds and the push is handled by git-receive-pack, not by a general shell. Remove the temporary working copy when you have verified the result:
$ cd /
$ rm -rf "$tmpdir"
This removal is irreversible, so confirm that $tmpdir points only to the temporary directory you just created. It does not remove the server repository.
Common failure points
Git reports a repository error: check the SSH URL and the account's permission to traverse every parent directory. The path must identify a bare repository and must be the path visible to the server account.
SSH says permission denied: check the public-key line, ownership and modes of .ssh and authorized_keys, then check the SSH daemon logs. Changing the login shell will not repair key authentication.
An interactive prompt appears: stop using the account until you confirm its actual shell with getent passwd gitreader. Inspect ~/git-shell-commands too. A custom command directory changes the default security posture.
You need administrative commands: keep them out of the Git account unless you have reviewed their argument handling, ownership and authorisation. The manpage describes custom commands as executable files in git-shell-commands; it does not turn them into a general-purpose access-control system.
Done means
- The dedicated account's shell is the installed
/usr/bin/git-shell. - The intended client key authenticates without exposing a private key.
git ls-remoteand a real clone and push work for the intended repository.- An SSH connection without a command closes instead of opening a shell.
- The account has no unreviewed
git-shell-commandsdirectory. - You recorded how to restore the previous shell or remove only the test account and repository.