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://firstname.lastname@example.org:10022/username/project.git # ugh! ugly
I want them to be like:
email@example.com:username/project.git # ah! pretty
If you don’t want to read the whole thing, here are the steps:
- Create a
gituser in your real system
- Give it uid 1000 and gid 1000; or create a modified
passwdfile, identical to that inside the Gogs image, but with the UID and GID of your real
gituser, and mount it over
/etc/passwdwhen 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.
gogs/git/.ssh. If you choose to map the volume
/datato some other place in your system, make sure its parent directory is owned by
git. 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
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
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.