+++ 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
)