+++ title = "SSH Access to Git Repository in Docker" date = 2022-01-23T15:06:47+09:00 tags = ["sysadmin"] +++
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.
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.
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:
git user on the host~git/.ssh to /data/git/.ssh in the Gogs container/app/gogs/gogs (make sure it's owned by root and
is chmod-ed 0755)