SSH Access to Git Repository in Docker

| More posts about

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 using the 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-send-pack which 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 typically git@github.com: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 not.

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 command. And verify-user 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 Gitea work.

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 ~git/.ssh/authorized_keys. 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 ~git/.ssh folder 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 looks like /app/gogs/gogs serv key-1. So we can just make /app/gogs/gogs available on the host and forward the command to the docker container.

Most instructions 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:

  1. Have a git user on the host
  2. Bind mount ~git/.ssh to /data/git/.ssh in the Gogs container
  3. Add the shim script to /app/gogs/gogs (make sure it’s owned by root and is chmod-ed 0755)
  4. Add the listed sudoers rules