With the likes of Gogs and Gitea, self-hosting a personal git service has become quite common. It’s also not unlikely that the software is run via docker but this brings a problem with regards to SSH access.
Ideally, the git service container just exposes port 22 but this would conflict
with the host’s own SSH service. One solution would be to just use different
ports for the git SSH and host SSH and that’s perfectly fine. But we can also
just have the host SSH service forward the request to the git service itself
command option in
authorized_keys. And as we’ll find out later,
the git service itself is using this functionality.
How git over SSH works
When you do a
git push, what actually happens is it runs
git-receive-pack <directory> on the remote using SSH. The actual
communication then just simply happens via stdin/stdout. Conversely, doing a
git pull just runs
git-fetch-pack and the somewhat confusingly named
git-upload-pack on the remote.
So far so good, but did you notice that when cloning via SSH, the remote is
firstname.lastname@example.org:org/repo? If everyone SSHs in as the
git user, how
does the git service know which user is which? And how does it prevent users
from accessing each other’s repositories?
One typically thinks of
authorized_keys as just a list of allowed SSH keys but
it can do much more than that. Of particular
interest is the
command directive which gets run instead of the user supplied
command. The original command is passed in as an environment variable
SSH_ORIGINAL_COMMAND which can be used to check if we allow it to be run or
So with an
authorized_keys file like so:
command="verify-user user-1" ssh-rsa ... command="verify-user user-1" ssh-ed25519 ... command="verify-user user-2" ssh-rsa ... command="verify-user user-3" ssh-rsa ...
Each key is tied to a particular user by virtue of
can check the
SSH_ORIGINAL_COMMAND if the particular user is allowed access to
the particular repository. You can also implement additional restrictions like
pull-only or push-only permissions with this setup. This is how both Gogs and
Forwarding git SSH to Docker
When running Gogs on the host, it’s typically run as the
git user and when an
SSH key is added or removed, it simply rewrites
Thus it just works with the host SSH service without problems. When running
inside docker, one thing we can do is bind mount the host’s
into the docker container so that the host can authorize the SSH connections.
The problem lies with the
command which only exists inside the docker
container itself. So any user trying to connect can authenticate successfully
but will get a
command not found error. For the Gogs docker image, the command
/app/gogs/gogs serv key-1. So we can just make
available on the host and forward the command to the docker container.
I’ve seen with regards to this involves using
ssh to connect to the internal
docker SSH service but this just seems overly complicated to me. If you
remember, at it’s core git really only communicates over stdin/stdout and SSH is
just a means to get that.
If all we need is for our shim
/app/gogs/gogs to be able to run a command
inside the docker container with stdin/stdout attached, then we can actually
just do that with
docker exec. So it can be something like this:
#!/usr/bin/env bash # Requires the following in sudoers # git ALL=(ALL) NOPASSWD: /app/gogs/gogs # Defaults:git env_keep=SSH_ORIGINAL_COMMAND GOGS_CONTAINER=git-gogs-1 if [[ $EUID -ne 0 ]]; then exec sudo "$0" "$@" fi if [ "$1" != "serv" ]; then exit 1 fi exec docker exec -i -u git -e "SSH_ORIGINAL_COMMAND=$SSH_ORIGINAL_COMMAND" "$GOGS_CONTAINER" /app/gogs/gogs "$@"
So for git SSH access to Gogs running in docker, the necessary steps here are:
- Have a
gituser on the host
- Bind mount
/data/git/.sshin the Gogs container
- Add the shim script to
/app/gogs/gogs(make sure it’s owned by root and is chmod-ed
- Add the listed sudoers rules