< 返回博客

docker内外sshd共享22端口


最近想在服务器上搭建一个git服务,原因是平时写的一些小项目往github上推送不太合适,但是又想在各设备之间同步,网盘什么的还是太麻烦了,搭个git就很方便了。

其实很早之前都有这个想法了,但是一直没有动手,因为当时知道的开源的git服务项目只有gitlab,而gitlab对我来说太大了,服务器现在也接近饱和了,跑个gitlab我的杂七杂八的服务就得关掉了。直到前几天,有个学弟问我关于把gitea往外网转发的问题,我才发现gitea这个东西。试了一下还挺好用,今天晚上就搞了一下。

问题

搭建没啥好说的,docker-compose就行了,这也是官方推荐的搭建方法。(说实话,容器技术能让人感到运维也不是那么枯燥的事情,用起来有一种说不出的舒服)。
docker跑起来之后,有个令我很难受的地方:git自身有利用ssh克隆的方法,所以像gitea、gitlab、github之类的解决方案也都提供了ssh克隆的功能,gitea容器里也自己跑了一个sshd,问题在于,宿主机有自己的sshd,所以这个sshd不能监听在宿主机的22端口上,在外面克隆的时候就得用类似ssh://git@git.ykai.top:5001/xxx/yyy.git这样的url,虽然无伤大雅,而且gitea也会帮你自动生成,但还是感到不舒服。

所以我就想着,能不能有个办法,让容器内外的sshd共享22端口。想了想,感觉最容易实现的方式就是把让宿主机的sshd将一些特定的ssh登录请求转发给容器里面,这样宿主机就透明了。

首先我注意到,在gitea的文档中,有一部分就是介绍转发ssh请求的,SSH Container Passthrough,但是我一时搞不懂他是什么原理,也就没有动手。

然后我就去网上搜索ssh forwardingssh forwarding to another host之类的,不幸的是,搜出来都是用ssh做端口转发的。就在我快要不知道怎么继续的时候,我搜了个docker share port 22 with host,出来了一篇国外老兄写的文章。这篇文章介绍地稍微详细一些,我理解大概意思之后,一阵捣鼓,也算是弄出来了。

前置知识

ssh的authorized_keys文件

ssh验证过程中,有个~/.ssh/authorized_keys文件是用来保存已信任主机的公钥的,要连接的主机通过了验证就能登录上。这个文件的一般格式是这样的:

ssh-rsa 客户机的公钥 xxx@xxx

实际上,这个文件还可以在前面加上选项,以使得在登录过程中执行某个命令,比如:

command="ls" ssh-rsa 客户机的公钥 xxx@xxx

这样子的话,在这个客户机登录的时候就会执行ls命令了。同时,ssh会把客户端原本想要执行的命令放在SSH_ORIGINAL_COMMAND环境变量里面,将上面的ls改为env命令就能看到了。

想想这意味这什么。这个功能给我们提供了一种能力,让我们可以在sshd执行客户端命令之前截获这条命令,相当于一个beforeExecute的钩子。

我们可以用这个功能来实现一个简单的ssh登录转发功能。

先在服务器上用docker跑一个sshd,监听在5000端口上,可以先不设置密钥登录,简单的密码登录就可以了,用户名为inner。然后在宿主机上面新建一个用户,比如叫outer,在outer的家目录下建立.ssh/authorized_keys文件,向文件中写上你自己电脑的公钥,然后在公钥前面加上登陆到docker内的命令。最终的文件内容大概是这样:

command="ssh -p 5000 inner@localhost" ssh-rsa 你的公钥 你的主机名

这时候可以尝试在你的电脑上面执行ssh outer@服务器ip,会发现提示输入密码,这个密码就是登录进容器内sshd的密码。输入正确的密码,就可以登录到容器内部的sshd了。

至此,已经实现了一个简易的ssh登录转发的功能了。看上去,宿主机和容器内共享了同一个22端口。

但还没完,想一想,我们现在的思路是,本地机器连接宿主机,然后宿主机连接向docker内,这个过程中是有两个验证过程的,但我们最终想要达到的是宿主机透明。怎么做呢。其实也很简单,所谓的透明就是只需要进行一次验证,相当于我们要把两扇门改为一扇门,思路是这样的:让宿主机的outer用户使用容器内innerauthorized_keys文件(使得可以通过第二扇门的也可以通过第一扇门),然后让outer可以密钥登录进inner(让第二扇门敞开)。

所以我们首先使用mount --bind /path/to/inner/.ssh /home/outer/.ssh达成第一点,然后为outer使用ssh-keygen,并将公钥加入到authorized_keys中,达成第二点。

现在,如果容器内有个进程向innerauthorized_keys文件中加入了一项(需要带有command),从容器外访问outer就可以直接登录进inner了。

需要注意的几点:

  1. docker容器内外共享uid和gid,sshd会检查文件的uid,所以需要保证outerinner的uid是一样的,我使用的镜像是有uid设置的功能的;
  2. .ssh目录和.ssh/authorized_keys的权限要注意,都不允许其他用户(包括组内用户)有可写权限,所以一般权限给744和644就行(我给的700和600);
  3. 那位老兄的文章中使用的软链接实现共享.ssh目录,但我试了下,没成功,主要还是权限问题,sshd检测权限的机制还不是很清楚,所以我就使用了简单易行的mount --bind

git通过ssh来拉取仓库的原理

git有个git-upload-pack命令,这个命令可以将一个仓库的数据发送到stdout中。在使用git clone git@git.ykai.top:xxx/yyy.git的时候,git就会通过ssh到git@git.ykai.top来执行git-upload-pack xxx/yyy.git,这样仓库的数据就会通过ssh发送到客户端了。在这个过程中,ssh充当了一个建立连接的角色。git push差不多也是同样的原理。(思路是这样,至于具体的通讯协议暂时不用了解。)

其实,有了这个功能,我们已经可以实现一个简易的git服务了。

假设我的服务器是ykai.top,用户名是kkk,同时用户kkk的家目录下有个目录为xxx/yyy.git的git仓库,那么我就可以直接使用git clone kkk@ykai.top:xxx/yyy.git来克隆这个仓库了。

但这么原始的一个服务肯定不满足我们的要求,我们还需要一整套的解决方案,好在gitlab、gitea之类的项目已经做好了,还提供给了我们一个易用的web端和丰富的功能。

gitea的工作方式

其实git从gitea、gitlab上通过ssh克隆一个项目时候,大概就是将上面两个功能结合起来。

我们先来看看gitea容器内/data/gitea/git/.ssh/authorized_keys里面的内容(这个文件是容器内的ssh验证文件):

# gitea public key
command="/app/gitea/gitea --config='/data/gitea/conf/app.ini' serv key-1",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa 用户的公钥 tag

用户每在gitea的web端增加一个公钥,这个文件就会增加类似这样的一项。这样,某个用户使用ssh登陆的时候,就会执行/app/gitea/gitea --config='/data/gitea/conf/app.ini' serv key-1,这个是gitea的一个命令,这个命令可以通过key-n来得知是哪个用户发起的请求,同时,还可以通过SSH_ORIGINAL_COMMAND环境变量来得知用户需要进行哪些操作,比如git-upload-pack,然后gitea就可以进行自己的逻辑了,比如判断用户是否有权限对该仓库进行操作、记录用户拉取记录等等。

让gitea和宿主机共享22端口

听起来跟上面说的ssh登陆转发差不多。启动gitea的容器,ssh开在宿主机的5001端口,然后在宿主机上建立一个git用户,如发炮制,把请求转发进容器。

操作过程中就会发现有问题了,现在authorized_keys中的command不是我们能控制的,是gitea写死到里面的。

这个情况的处理办法也算是个奇技淫巧。因为现在容器内外共享验证文件,所以从外面登录进宿主机的时候执行的也是/app/gitea/gitea --config='/data/gitea/conf/app.ini' serv key-1,那我们可以在宿主机上也建立一个/app/gitea/gitea的脚本,在这个脚本里面完成我们需要的操作就好了。

在宿主机上新建脚本/app/gitea/gitea,内容为:

#! /bin/sh

ssh -p 5001 git@127.0.0.1 \
    "SSH_ORIGINAL_COMMAND=$(printf "%q" "$SSH_ORIGINAL_COMMAND")" "$0" "$@"

加上执行权限,这样就又能够把登录转发到容器内部了。

这个脚本也很简单,没啥说的,要注意的是中间的那个printf,其中的%q是为了转义变量中的特殊字符,这个东西搞了我好长时间。还有后面的$0和$@,是为了把命令重新传递给容器内的gitea。

好了,大功告成,再也不用忍受奇怪的url了。


参考