我们计划使用Harbor来做镜像管理。相对原生的docker registry,Harbor有一些优势,例如可以集成用户认证、提供项目的概念、项目间可以隔离、镜像复制等功能。

Harbor的认证一般可以采用DB或者LDAP的方式,在安装Harbor时指定 auth_modedb_authldap_auth 。以LDAP为例,安装后,用户可以使用LDAP账户/密码,登录到Harbor的web页面,也可以 使用LDAP账户/密码, docker login 到Harbor,Harbor会去LDAP服务器那里去bind做用户认证。

但在将Harbor集成到kubernetes时,会遇到一个问题。

我们希望在容器平台上可以获取到用户自己的项目、镜像,Harbor提供了这些API,但调用API时,只支持了basic auth和session两种方式,而容器平台、kubernetes都是OIDC token的方式。

所以,用户可以调用容器平台来获取自己的项目(使用用户自己的token),但容器平台无法使用该token来调用Harbor的API,因为Harbor并不能识别这个token。

一个办法是让用户在容器平台上保存自己的用户名、密码,容器平台请求Harbor API时,使用这个账户密码。但在企业应用中,通常账户密码都是ERP账户,其绑定了很多权限,用户无法接受密码泄露的风险。

有什么别的方法吗?

有的。实际上,从架构上来说,从容器平台调用Harbor API,和,从容器平台调用k8s API,本质上是一致的,所以只要Hack Harbor的源码,让它能支持OIDC token认证,就可以了。

用户从web登录Harbor、用户docker login,仍然是走之前Harbor的LDAP认证(注意Harbor会自己在DB中存储一份不包含密码的用户信息);容器平台调用Harbor API时,则使用新的OIDC token。

先来看看Harbor目前支持的认证(v1.5.1):

src/ui/filter/security.go

  • secret: 内置secret,例如 ui、jobserver 调用 adminserver时,使用 -H “Authorization: Harbor-Secret xxx”。secret在adminserver创建时生成,可以去adminserver容器里看环境变量。
  • basic auth:输入用户名、密码(docker login时使用?)。harbor针对basic auth,有3种认证方式:DB、LDAP、UAA。
  • session:会话,web访问harbor时使用。
  • token:Admiral时使用。Admiral是Vmware的一个容器管理平台。用户可以在Admiral上配置使用Harbor作为Reigistry;用户使用Harbor API时,携带token到harbor;harbor会再去Admiral这里去验证token合法性,验证方式就是拿token调用Admiral的endpoint后检查返回值。

其实,OIDC token的认证方式,和Admiral token是比较类似的:用户登录外部认证平台后获取token;携带该token调用Harbor API;Harbor调用外部认证平台鉴别该token是否合法。

因此,只要给 reqCtxModifiers 增加一种认证方式就可以了。注意,不要加在 unauthorizedReqCtxModifier后面,这个是switch 的 default分支。

harbor auth

// Init ReqCtxMofiers list
func Init() {
        // standalone
        reqCtxModifiers = []ReqCtxModifier{
                &secretReqCtxModifier{config.SecretStore},
                &basicAuthReqCtxModifier{},
                &sessionReqCtxModifier{},
                &oidcTokenReqCtxModifier{},
                &unauthorizedReqCtxModifier{}}

那么Harbor是怎么支持多种用户认证方式呢?其实就是轮询,只要有一个能通过就行。

// SecurityFilter authenticates the request and passes a security context
// and a project manager with it which can be used to do some authN & authZ
func SecurityFilter(ctx *beegoctx.Context) {
	if ctx == nil {
		return
	}

	req := ctx.Request
	if req == nil {
		return
	}

	// add security context and project manager to request context
	for _, modifier := range reqCtxModifiers {
		if modifier.Modify(ctx) {
			break
		}
	}
}

简要写一下 oidcTokenReqCtxModifier 的逻辑。

func (t *oidcTokenReqCtxModifier) Modify(ctx *beegoctx.Context) bool {
        token := getBearerToken(ctx.Request)
        if len(token) == 0 {
                return false
        }

        idToken, err := config.Verifier.Verify(ctx.Request.Context(), token)
        if err != nil {
                return false
        }

        var claims Claims
        err = idToken.Claims(&claims)
        if err != nil {
                log.Info("token claim failed, ", err)
                return false
        }

        user := &models.User{
                Username: claims.Name,
        }

        pm := config.GlobalProjectMgr
        secureCtx := local.NewSecurityContext(user, pm)
        setSecurCtxAndPM(ctx.Request, secureCtx, pm)

        return true

简单来说,就是从req中拿到Token,再去找 OIDC Provider(在我们的案例中是dex)Verify该token的合法性,并获取用户的一些基本信息,如用户名、email等。

pm(ProjectManager) 的作用是什么呢?走读一下代码会发现,它主要负责为一些api调用提供数据,例如查询用户的Project。

为什么这里直接使用了 GlobalProjectMgr 呢?实际上,我们这里只是 增加 做了OIDC的认证,用户仍然还是可以通过之前的LDAP方式登录到Harbor上去 docker login/push/pull的,也就是说,用户、项目、镜像等信息,仍然是存储在Harbor的数据库中的,因此,Project Manager直接使用 GlobalProjectMgr 即可,它在查询信息时,只需要从 secureCtx 中获取用户名就行了。