Share port 22 between Gogs inside Docker & the local system
Intro
Have you used Gogs? It’s great. Gogs is a Git service, much like GitHub and GitLab, but written in Go. It’s a immensely lighter than GitLab and it’s not lacking at all in features.
For many reasons, you may be using Docker to run Gogs. In my case, it’s because it hasn’t yet been added to Debian’s archives. In your case, maybe it’s because you need certain version, or because your first and last names start with a D and you must use Docker. You know who you are ;)
One problem I have with using Gogs in Docker right now is that I’m forced to choose between handling my real port 22 to Gogs’ own SSH server, or set it up to listen for SSH connections on a different port. I don’t like either choice.
With the first choice, I lose access to my real system through SSH, at least on the standard port. With the second choice I get ugly URLs for my repos. I don’t want them to look like this:
git://git@myserver.local:10022/username/project.git # ugh! ugly
I want them to be like:
git@myserver.local:username/project.git # ah! pretty
TLDR
If you don’t want to read the whole thing, here are the steps:
- Create a
git
user in your real system - Give it uid 1000 and gid 1000; or create a modified
passwd
file, identical to that inside the Gogs image, but with the UID and GID of your realgit
user, and mount it over/etc/passwd
when running the container. - Run the container with
-v ~git/gogs:/data -p 127.0.0.1:10022:22 -p 3000:3000
. The loopback IP is for increased security. - Symlink
~/.ssh
togogs/git/.ssh
. If you choose to map the volume/data
to some other place in your system, make sure its parent directory is owned bygit
. Otherwise SSH won’t accept the keys. - Generate an ssh key pair for your real git user.
- Run the follwing as root in the real system. This is where most of the magic happens ;)
- Sit back, relax and git clone, push and pull all day long ;)
How does it work
Now, let’s break down the solution. On a normal run of Gogs with Docker, you would do something like this:
And in the Gogs first-time run installation page, you adjusted both the HTTP Port and SSH Port settings to point to the mapped ports of your container. The second setting is exactly what I don’t want to do.
I want to be able to do:
as well as:
For that to work, it’ll have to be the SSH server of my real system that replies on both cases. There’s no magic way to automatically get the user git
to connect to one port and every other user to connect to port 22.
We must have a real git user
So, we must have a git
user at the real system, and when that user connects through SSH, we must take whatever she was trying to run and run it as the git
user inside the Gogs container.
Also, if we want Gogs’ user and SSH keys management to work, we must connect the ~/.ssh/authorized_keys
of the real git
user and the Gogs git
user.
You may have noticed that you can create many users inside Gogs, and all those users will SSH to the Gogs server with the same git
account. Gogs’ accomplishes this with several parameters before each line in ~/.ssh/authorized_keys
. They look like this:
Every time a Gogs user adds a SSH key, Gogs adds a line like that to the authorized_keys
file. And the command
option at the beginning of the line forces /app/gogs/gogs...
to be run when that key is used to log in. The second parameter to that command (key-1, key-2, etc.) is what Gogs uses to distinguish one user from the other to, for instance, keep Alice from changing Bob’s repos. For SSH, it’s always git
who is logging in.
The real and virtual git users will share authorized_keys
We must have this file serve as the authorized_keys
of the real git
user as well. Otherwise, the SSH keys that the Gogs users add through the web app would not work for the real system.
This is easy to do, we’ll just symlink the real user’s ~/.ssh
directory to the one that will be inside the container. For simplicity, I’ll create a gogs
directory under /home/git
and use that as the /data
volume on the container.
If, for any reason, you want to put the directory for /data
somewhere else, the parent of that place must be owned by git
. Otherwise, SSH will refuse to use any key it finds there.
The UID of the git
must be the same inside and outside the container. The reason is that when the gogs/gogs
image starts, it chown’s everything under the /data
volume to the git
user, including the .ssh
directory. And the real system SSH server will complain if that directory doesn’t belong to the real git
user.
To solve that we can either have the real git
user have UID 1000, which is what is used inside the container. Or something more flexible, we can prepare a passwd
file based on the one inside the image, but with the UID and GID of the git
user changed to those of the real one. Then we would mount that passwd file using -v /my/crafted/passwd:/etc/passwd
when running the container.
That forced command doesn’t exist
The next problem is that when a user tries to SSH as git
to the real system, the authorized_keys
file will force the command /app/gogs/gogs...
to be run on the real system, and that doesn’t exist.
We’ll create it then. In /app/gogs/gogs
we put this:
When you run ssh user@host command
(as git tools do), the given command is run as the given user on host
. If –as is the case with Gogs–, that user’s authorized_keys
have a forced command, that gets run instead, but the original command that the user was trying to run is saved in the environment variable SSH_ORIGINAL_COMMAND. That’s how Gogs knows what git was trying to do.
What we do here is run ssh again, now to log in as git
to the container. That’s the -p 10022
part. We disable strict host key checking, to avoid the host authenticity question. And we’ll run the same /app/gogs/gogs serv key...
that got us here inside the container. That’s what $0 $@
does. To finish we must send SSH_ORIGINAL_COMMAND with its current value into the container. That part I’m sure you already found ;)
To be able to run that second ssh, the real git
user must have a key in the authorized keys file of the virtual git
user –which is also its own–. So, we just need to make sure to generate a key pair with ssh-keygen
for it, and our fake /app/gogs/gogs
script will make sure to add it to the authorized_keys file so the ssh into the container succeeds.
The reason we do this every time is because Gogs deletes any non-gogs keys when recreating the authorized_keys file (for instance, when keys are removed in the UI).
Now go try your Gogs. At this point, it should all work transparently.
May, 2017 update
Thanks @pjeby for poiting out an escaping problem in the ssh command above, and for his notes about SELinux. Check his comments below.
Thanks @peterhalverson for pointing out that Gogs deletes non-gogs keys from the authorized_keys file when SSH Keys are removed in the web UI.