Skip to content

Hosting Bare Git Repositories over SSH on Linux

Hosting Bare Git Repositories over SSH on Linux

Sometimes I want a private place to push Git repositories, but I do not need a web interface, issue tracker, pull requests, runners, or another database to maintain. A Linux server, a directory with reliable storage, and SSH are enough.

The result is deliberately simple:

text
developer workstation
        |
        | Git over SSH
        v
dedicated git account + git-shell
        |
        v
/srv/git/project.git

The server stores bare repositories. There is no checked-out working tree on the server; clients clone, fetch, and push over SSH. Git already includes the server-side commands this requires, and git-shell restricts the service account to those Git operations instead of providing a general-purpose shell.

This is a good fit for a homelab, NAS, or small trusted team. It is not a replacement for GitHub, GitLab, Forgejo, or Gitea when you need code review, fine-grained permissions, audit features, or a web UI.

What You Need

  • a Linux server with persistent storage
  • OpenSSH server
  • Git
  • an administrative account with sudo
  • an SSH key on each client that needs access

The examples below use:

  • git.example.com as the server
  • admin as the administrative SSH account
  • git as the restricted service account
  • /srv/git as the repository root
  • project.git as an example repository

Replace those values to match your environment.

1. Install Git and OpenSSH

On Debian or Ubuntu:

bash
sudo apt update
sudo apt install git openssh-server

Equivalent packages are available on other Linux distributions. Confirm that Git provides git-shell:

bash
command -v git-shell

It is commonly installed at /usr/bin/git-shell. Add that exact path to /etc/shells if it is not already listed:

bash
git_shell="$(command -v git-shell)"
grep -Fxq "$git_shell" /etc/shells || printf '%s\n' "$git_shell" | sudo tee -a /etc/shells

2. Create a Restricted Git Account

Create one service account for Git traffic:

bash
sudo useradd --create-home --shell "$(command -v git-shell)" git
sudo passwd --lock git

If the account already exists, set its shell explicitly:

bash
sudo usermod --shell "$(command -v git-shell)" git

Using git-shell matters. Git clients can still invoke the server-side commands used by clone, fetch, and push, but an ordinary interactive SSH login is rejected.

3. Create the Repository Directory

Create a location for bare repositories and give the service account ownership:

bash
sudo install -d -o git -g git -m 0750 /srv/git

Only the git account needs to write here in this simple model. Administrators should create and maintain repositories with sudo -u git; doing so avoids ownership drift.

If several local administrators need direct group access, use a dedicated group and a setgid directory instead. For example, mode 2770 causes new entries to inherit the directory's group. Do not make the repository tree world-writable.

4. Authorize Client SSH Keys

Prepare the service account's SSH directory:

bash
sudo install -d -o git -g git -m 0700 /home/git/.ssh
sudo touch /home/git/.ssh/authorized_keys
sudo chown git:git /home/git/.ssh/authorized_keys
sudo chmod 0600 /home/git/.ssh/authorized_keys

Open the file as an administrator:

bash
sudoedit /home/git/.ssh/authorized_keys

Add one public key per line. A modern Ed25519 entry looks like this:

text
restrict ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... workstation-name

The optional OpenSSH restrict key option disables forwarding, PTY allocation, and several other SSH features that a Git-only key does not need. git-shell remains the main control that limits commands to Git operations.

Never copy a private key to the server. Only the contents of the client's .pub file belong in authorized_keys.

5. Create a Bare Repository

The restricted git account cannot open a shell and should not be able to create arbitrary repositories remotely. Create each repository through the administrative account:

bash
ssh -t admin@git.example.com \
  'sudo -u git git init --bare --initial-branch=main /srv/git/project.git'

The --bare option makes the repository itself the Git database. This is the correct layout for a shared remote because nobody edits a working tree on the server.

On the server, the resulting directory should belong to the service account:

bash
sudo stat -c '%U:%G %a %n' /srv/git/project.git
sudo -u git git -C /srv/git/project.git rev-parse --is-bare-repository

The second command should print true.

6. Connect a Local Repository

For an existing local repository, add the SSH URL and push its current branch:

bash
git remote add origin \
  ssh://git@git.example.com/srv/git/project.git

git push -u origin main

If the local branch is named master, push master instead. It is better to choose the initial branch deliberately than to assume every repository uses the same convention.

To clone the repository elsewhere:

bash
git clone ssh://git@git.example.com/srv/git/project.git

The shorter SCP-like syntax works too:

bash
git clone git@git.example.com:/srv/git/project.git

I prefer the explicit ssh:// form in documentation because the protocol and absolute path are unambiguous.

7. Verify the Setup

Check the configured remote:

bash
git remote -v
git ls-remote origin

You can also test the account restriction directly:

bash
ssh -T git@git.example.com

A message such as fatal: Interactive git shell is not enabled is expected. That failure is a successful security check: SSH authentication worked, but the account did not provide an interactive shell. Git fetches and pushes should still work.

Creating More Repositories

Repeat the administrative initialization command for each new repository:

bash
ssh -t admin@git.example.com \
  'sudo -u git git init --bare --initial-branch=main /srv/git/another-project.git'

Then add the new remote locally and push. Keeping repository creation behind the administrative account prevents the restricted service account from turning into a general remote shell.

Optional Push Protection

For repositories where rewriting shared history would be especially painful, reject non-fast-forward pushes and branch deletion on the server:

bash
sudo -u git git -C /srv/git/project.git config receive.denyNonFastForwards true
sudo -u git git -C /srv/git/project.git config receive.denyDeletes true

These settings are policy choices, not requirements. They also prevent intentional force-pushes and branch deletion, so enable them only when that tradeoff is desirable.

Back It Up

A bare repository contains committed history, branches, tags, and other Git refs, but it is still only one copy of the data. Include /srv/git in filesystem snapshots or a regular backup job, and test restoration occasionally.

Also remember what Git does not contain: uncommitted work, ignored files, external issue trackers, deployment secrets, and files stored outside the repository all need their own protection.

Troubleshooting

Permission denied (publickey)

Check that the correct public key is present in /home/git/.ssh/authorized_keys. The .ssh directory should normally be mode 0700, the file mode 0600, and both should be owned by git.

Use verbose SSH output from the client to see which keys are offered:

bash
ssh -vT git@git.example.com

fatal: Interactive git shell is not enabled

This is expected from a direct ssh git@... login. Use Git commands such as git clone, git fetch, git push, or git ls-remote instead.

does not appear to be a git repository

Confirm the remote path, the .git suffix, and that the repository was initialized with --bare:

bash
ssh admin@git.example.com \
  'sudo -u git git -C /srv/git/project.git rev-parse --is-bare-repository'

Pushes fail with a permissions error

Inspect ownership as an administrator:

bash
ssh admin@git.example.com \
  'sudo find /srv/git/project.git -maxdepth 2 -not -user git -ls'

An empty result is ideal. Create repositories and perform server-side maintenance as git so new files do not acquire the wrong owner.

Git reports “dubious ownership” during administration

This can happen when an administrator runs Git directly against a repository owned by the service account. Do not weaken Git's global ownership checks just to silence it. Run the command as the owner instead:

bash
sudo -u git git -C /srv/git/project.git fsck

Know the Limits

Every SSH key in this design maps to the same Unix account. By default, every authorized user has the same access to every repository that account can reach, and server logs identify the shared git account rather than a distinct Git user.

That is often acceptable for one person, a household, or a small trusted group. If you need per-user and per-repository permissions while keeping an SSH-first workflow, look at Gitolite. If you also want a browser, pull requests, issues, or account management, a forge such as Forgejo or Gitea is a better next step.

Closing

This setup is intentionally boring: SSH handles authentication, git-shell limits the service account, and bare repositories hold the Git data. There is little to run, patch, or troubleshoot, and every normal Git client already knows how to use it.

For a small private repository store, that simplicity is the feature.