镜像仓库管理:与Portus不得不说的那些事

背景:

  目前在做一个云计算相关的项目,其中有这样一个需求:每个平台用户都有自己的docker镜像仓库(docker registry),用户可以对自己的镜像仓库的push/pull权限进行管理,也就是默认情况下用户只能操作自己的仓库,另外其他指定的用户可以对自己的仓库进行操作。(其实在这平台中只有一个镜像仓库,不同用户的仓库是通过namespace来隔离的)

  docker registry是通过access token进行权限验证的,而token的颁发依托于一个第三方的Authorization Service。Portus其实就是扮演了这个第三方的Authorization Service的角色啦,于是决定用Portus来实现这个需求。

  所以部署一个Portus让用户来管理自己的镜像仓库就行呗?答案恐怕是否定的。Portus毕竟是别人做的一款软件,总不能让用户直接去操作Portus的界面吧。当然最重要的问题在于:Portus对用户的操作权限进行管理是基于portus自己的数据库,而平台有自己的用户中心(User Certer),用户在docker login镜像仓库时应能够使用平台的用户名和密码进行登陆。所以此番与Portus周旋的核心矛盾就是——Portus与自家的用户中心进行集成!

解决过程:

坑1:Portus的部署

  于是就先找到了Portus项目的源码(https://github.com/SUSE/Portus),见到项目源码后我眼前一黑:这TM居然是个ruby语言项目,而我此前对ruby语言可以说是一无所知(值得一提的是项目组其他人也都没搞过ruby开发,领导机智地让我来替大伙儿先把这个浑水给淌了。。。)。说回部署,官网上(http://port.us.org/)说得很清楚,只要执行一行命令就可以了:

$ docker-compose up

  执行完之后,得,慢得一批,有好多依赖要下载,居然还要下载go语言的依赖包。在这里如果部署的机器没有FQ的话会出错,因为go的依赖默认是从go的官网上下载,而go的官网被墙了。。。解决方法就是用mod把依赖的下载源改一下就可以了。

  build了很久之后终于build完了,可以看到portus创建了好几个服务,有portus自身的还有数据库的同时还启动了一个registry,默认情况下portus只能控制他自个儿启动的这个registry,如果要连接其他的registry需要另行配置。部署完成后去浏览器访问3000端口,首先要创建一个管理员用户,然后再配置一个registry地址,配置完之后发现——

  用不了。。。

  于是猜想八成是有配置配的不对吧。于是开始仔细审阅docker-compose.yml文件,发现了一个非常可疑的${MACHINE_FQDN},有多处环境变量都跟这个值有关,进入到容器里查看了一下这个值是172.17.0.1。问题找到了,这个值应该是registry服务所在的主机地址。而这个配置需要在.env里修改。

  总算部署成功啦。

坑2:Portus与LDAP和OAuth2的集成

  既然目的是用第三方的账号进行登陆,那么第一选择当然是看一下Portus有没有现成的能够基于第三方账号进行登陆的方式。果然有~,Portus已经实现了基于LDAP和OAuth2.0的第三方授权的登陆方式。LDAP的话基本上只要按照官方文档提供的说明修改配置文件就可以了(按照官网说明改了配置之后发现登陆不了。。。问题出在哪儿暂时还不清楚,不排除是Portus自身的bug的可能,所以这里先不深究了)。OAuth2.0的话有现成的使用谷歌,Github和Gitlab账号进行登陆的实现,修改配置文件就可以,但如果要实现其他的网站授权登陆的话还需要自己开发一个Strategies插件,不难实现,只需要少量的代码(可以参考lib/omni_auth/strategies目录下的bitbucket.rb文件,另外实现定制的Oauth2授权方式的具体步骤可以参考config/initializer/devise/oauth.rb文件中的代码注释)。配置成功后重新部署可以看到登陆页面的下方多了点儿东西~

  选择授权的网站会跳转到第三方的授权页面,点击同意登陆之后会再一次返回Portus登陆界面,需要你确认一下用户名还要设置一个display name,设置完成后就进入Portus啦。那我现在可以用平台账号docker login了嘛?答案是否定的,Oauth2.0协议只是允许客户端(client)获取用户的某些信息,如用户名邮箱电话之类的,但它是不允许客户端存储用户的密码的,也就是说Portus并不知道用户的密码。到这里有点儿晕了。。。登陆Portus不是目的呀,在Portus上一顿操作但是不能docker login不是白搭么。

  研究一番后发现事情是这样子的:不管是基于Oauth2还是LDAP,用户在第一次登陆成功后,Portus都会在自己的数据库里保存一份用户的信息(用户名,密码,邮箱),而之后不管是登陆Portus还是docker registry,Portus都会根据数据库中的用户信息对用户的身份进行验证。也就是说,看似是基于第三方授权的登陆,但实际上跟Portus默认的创建一个新用户的方式在本质上是一样的。这样就会造成一个问题,如果我在Portus的用户设置里把我的邮箱给改了(通过查看Portus的接口说明可以看到,邮箱也是一个用户记录的唯一标识),当我再一次通过第三方授权登陆时,Portus又会创建一个新的用户。这样的话,明明两次授权登陆的人都是“我”,而Portus里却有两个不同的User。另外,Portus里的创建的User的用户名密码和授权网站的用户信息里的用户名密码是不一样的。Portus通过Oauth2创建的用户会有一个随机的密码(参考app/models/user.rb的代码),用户第一次登陆成功后可以在用户设置页面选择创建一个application token,然后用这个application token作为密码去docker login。那么能否通过修改源代码让Portus在创建本地的User时就把用户的平台账号的密码写到Portus的数据库里呢?显然是不行的,不过多解释啦,最基本的信息安全问题。

  归根到底,基于Oauth2来实现登陆的所有的问题其实都是由于用户的信息在不同的数据库保存了两份儿而导致的数据一致性的问题。况且,即使通过一系列同步手段做到了数据的一致性,用Oauth2来实现登陆还需要用户在Portus页面进行操作,而按照需求,Portus这个东西应该是对用户透明的,用户只需要在命令行docker login就行了,不需要在Portus页面做任何操作。

  所以到这里可以确定,基于Oauth2.0好LDAP的授权登陆方式被pass掉了。。。

坑3:调试源码

  好吧让我们回到最初的起点,暂时把Portus放到一边,我们先仔细研究一下docker registry。docker login一个registry的过程是什么样的呢?开头说了,是在客户端和docker registry中间引入了一个Authorization Service,三者形成了一个三角关系。client在对registry进行操作时会先从Authorization Service获取一个token,然后把这个token出示给registry,如果这个token正确,用户的这一次操作就被允许了,如果这个token不正确,那么这一次操作就会被认为没有被授权。那么这个token究竟是个什么玩意儿?其实就是一串被加密的字符串,这个字符串里记录了用户的权限信息,registry拿到token后进行解密,将token里记录的用户权限与用户当前执行的操作进行一下对比。举个简单的例子,用户执行docker push my.registry.cn/john/my-image时,如果registry发现Authorization Service那边分发给客户端的token里记录的当前用户对my.registry.cn仓库中的john命名空间下的镜像只有pull的权限没有push的权限,那么registry就认为此次操作是没有经过授权的。docker registry这种基于token的权限验证方式详细可参考:https://docs.docker.com/registry/spec/auth/token/

  现在这么一想,这个docker login的问题其实很弱智:docker registry其实把用户权限的管理全权托付给力Authorization Service(Portus),他说你有权限你就有,不管你的用户信息是不是存在数据库里。之前我们一直在研究Portus的实现原理,发现Portus的各种对用户权限的管理都是基于数据库的,然后陷入了解决数据一致性这个问题的死胡同里。其实现在我们完全可以把Portus查数据库的实现部分替换掉,换成我们自己想要的方式,例如调用用户中心的某些查询接口。

  所以我们要做的其实很简单,找到Portus接收docker客户端请求并返回token这块儿的代码,把他改成我们要的实现就行了。

  终于有了明确的方向,现在就开始debugging吧~话说这玩意儿应该怎么调试啊。。。ruby和ruby on rails之前都没接触过,而这个项目的部署还依赖于docker-compose,我一时半会儿也没找到好的调试办法,就只能改代码-打log-部署-查看log这样来了。前面说了,Portus部署一次需要很久,那是因为第一次部署需要下载很多依赖,这些依赖下载完会作为一个docker分层缓存在本地,所以只要第一次部署时把build出来的镜像记一个特定的tag并把这个镜像一直保存在本地,这样本地就相当于已经有了Portus部署时需要的依赖了,部署就会快很多。这里还有一个小坑,ruby项目里的Gemfile和Gemfile.lock这个两个文件一定要保持同步,如果往Gemfile里加了其他的依赖,一定要把Gemfile.lock同步更新后再执行docker-compose,否则之前缓存的docker分层会失效!

  查看portus的主进程日志可以看到,日志主要记录了接口被访问的路径和大量的数据库的查询语句(再一次印证了Portus对权限的控制是基于数据库的)。我们在docker客户端执行docker login操作时可以看到/v2/token这个路径下的接口被访问了,还可以看到有些参数信息(acount, service, scope等)也被打印出来,这些参数其实就将决定用户的权限。知道了被访问的接口路径,我们不难找到,app/controllers/api/v2/token_controller.rb就是我们要重点调试的部分啦!虽然ruby代码阅读起来比较吃力,但其实这一部分的逻辑很简单:先验证用户输入的用户名和密码(用户每执行一次docker login后相应的登陆信息会保存到本地,之后的push/pull操作就不需要在输入用户名密码了),验证一下用户是不是存在以及密码是不是正确(这一部分是查数据库,把他干掉替换成我们的实现),接下来会用接收到的参数创建一个auth_scope对象,这个对象存储了用户当前要执行的操作action,然后拿着这个与数据库进行比对,看看这个action所需的权限有米有,没有权限的action会被delete,最终只剩下有权限的action(这一部分也是查数据库的,把他干掉,替换成我们的)。然后拿着这个auth_scoper去创建一个token返回给客户端,over!

  就这么简单。。。

  走通了这一步,后面权限控制部分的开发就很简单啦。

  (写的急,没贴源码,以后有时间的话再贴源码吧)

猜你喜欢

转载自www.cnblogs.com/yangduan1991/p/10466664.html