跳转至

Harbor

Harbor 是一个CNCF基金会托管的开源的可信的云原生docker registry项目,可以用于存储、签名、扫描镜像内容,Harbor 通过添加一些常用的功能如安全性、身份权限管理等来扩展 docker registry 项目,此外还支持在 registry 之间复制镜像,还提供更加高级的安全功能,如用户管理、访问控制和活动审计等,在新版本中还添加了Helm仓库托管的支持。

Harbor最核心的功能就是给 docker registry 添加上一层权限保护的功能,要实现这个功能,就需要我们在使用 docker login、pull、push 等命令的时候进行拦截,先进行一些权限相关的校验,再进行操作,其实这一系列的操作 docker registry v2 就已经为我们提供了支持,v2 集成了一个安全认证的功能,将安全认证暴露给外部服务,让外部服务去实现。

Harbor 认证原理

上面我们说了 docker registry v2 将安全认证暴露给了外部服务使用,那么是怎样暴露的呢?我们在命令行中输入 docker login https://registry.qikqiak.com 为例来为大家说明下认证流程:

  1. docker client 接收到用户输入的 docker login 命令,将命令转化为调用 engine api 的 RegistryLogin 方法
  2. 在 RegistryLogin 方法中通过 http 盗用 registry 服务中的 auth 方法
  3. 因为我们这里使用的是 v2 版本的服务,所以会调用 loginV2 方法,在 loginV2 方法中会进行 /v2/ 接口调用,该接口会对请求进行认证
  4. 此时的请求中并没有包含 token 信息,认证会失败,返回 401 错误,同时会在 header 中返回去哪里请求认证的服务器地址
  5. registry client 端收到上面的返回结果后,便会去返回的认证服务器那里进行认证请求,向认证服务器发送的请求的 header 中包含有加密的用户名和密码
  6. 认证服务器从 header 中获取到加密的用户名和密码,这个时候就可以结合实际的认证系统进行认证了,比如从数据库中查询用户认证信息或者对接 ldap 服务进行认证校验
  7. 认证成功后,会返回一个 token 信息,client 端会拿着返回的 token 再次向 registry 服务发送请求,这次需要带上得到的 token,请求验证成功,返回状态码就是200了
  8. docker client 端接收到返回的200状态码,说明操作成功,在控制台上打印Login Succeeded的信息 至此,整个登录过程完成,整个过程可以用下面的流程图来说明:

docker login

要完成上面的登录认证过程有两个关键点需要注意:怎样让 registry 服务知道服务认证地址?我们自己提供的认证服务生成的 token 为什么 registry 就能够识别?

对于第一个问题,比较好解决,registry 服务本身就提供了一个配置文件,可以在启动 registry 服务的配置文件中指定上认证服务地址即可,其中有如下这样的一段配置信息:

......
auth:
  token:
    realm: token-realm
    service: token-service
    issuer: registry-token-issuer
    rootcertbundle: /root/certs/bundle
......

其中 realm 就可以用来指定一个认证服务的地址,下面我们可以看到 Harbor 中该配置的内容

关于 registry 的配置,可以参考官方文档:https://docs.docker.com/registry/configuration/

第二个问题,就是 registry 怎么能够识别我们返回的 token 文件?如果按照 registry 的要求生成一个 token,是不是 registry 就可以识别了?所以我们需要在我们的认证服务器中按照 registry 的要求生成 token,而不是随便乱生成。那么要怎么生成呢?我们可以在 docker registry 的源码中可以看到 token 是通过JWT(JSON Web Token)来实现的,所以我们按照要求生成一个 JWT 的 token 就可以了。

对 golang 熟悉的同学可以去 clone 下 Harbor 的代码查看下,Harbor 采用 beego 这个 web 开发框架,源码阅读起来不是特别困难。我们可以很容易的看到 Harbor 中关于上面我们讲解的认证服务部分的实现方法。

安装

$ git clone https://github.com/goharbor/harbor-helm
$ cd harbor-helm && git checkout v1.2.3

首先创建存储 PV/PVC:(volume.yaml)

apiVersion: v1
kind: PersistentVolume
metadata:
  name: harbor-registry
  labels:
    app: harbor-registry
spec:
  capacity:
    storage: 5Gi
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  nfs:
    server: 10.151.30.11
    path: /data/k8s/harbor/registry
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: harbor-registry
  namespace: kube-ops
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
  selector:
    matchLabels:
      app: harbor-registry
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: harbor-chartmuseum
  labels:
    app: harbor-chartmuseum
spec:
  capacity:
    storage: 5Gi
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  nfs:
    server: 10.151.30.11
    path: /data/k8s/harbor/chartmuseum
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: harbor-chartmuseum
  namespace: kube-ops
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
  selector:
    matchLabels:
      app: harbor-chartmuseum
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: harbor-jobservice
  labels:
    app: harbor-jobservice
spec:
  capacity:
    storage: 1Gi
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  nfs:
    server: 10.151.30.11
    path: /data/k8s/harbor/jobservice
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: harbor-jobservice
  namespace: kube-ops
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
  selector:
    matchLabels:
      app: harbor-jobservice
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: harbor-database
  labels:
    app: harbor-database
spec:
  capacity:
    storage: 1Gi
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  nfs:
    server: 10.151.30.11
    path: /data/k8s/harbor/database
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: harbor-database
  namespace: kube-ops
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
  selector:
    matchLabels:
      app: harbor-database
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: harbor-redis
  labels:
    app: harbor-redis
spec:
  capacity:
    storage: 1Gi
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  nfs:
    server: 10.151.30.11
    path: /data/k8s/harbor/redis
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: harbor-redis
  namespace: kube-ops
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
  selector:
    matchLabels:
      app: harbor-redis

我们这里为了简单使用的是 NFS 来做共享存储,要注意在使用之前,我们需要提前在 nfs server 上手动创建 path 路径,各个节点上也必须安装 nfs,否则不能挂载,直接创建上面的存储相关的资源对象:

$ kubectl apply -f volume.yaml
$ kubectl get pv
NAME                 CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                         STORAGECLASS   REASON   AGE
harbor-chartmuseum   5Gi        RWO            Retain           Bound    kube-ops/harbor-chartmuseum                           20m
harbor-database      1Gi        RWO            Retain           Bound    kube-ops/harbor-database                              20m
harbor-jobservice    1Gi        RWO            Retain           Bound    kube-ops/harbor-jobservice                            20m
harbor-redis         1Gi        RWO            Retain           Bound    kube-ops/harbor-redis                                 20m
harbor-registry      5Gi        RWO            Retain           Bound    kube-ops/harbor-registry                              20m
$ kubectl get pvc -n kube-ops
NAME                 STATUS   VOLUME               CAPACITY   ACCESS MODES   STORAGECLASS   AGE
harbor-chartmuseum   Bound    harbor-chartmuseum   5Gi        RWO                           20m
harbor-database      Bound    harbor-database      1Gi        RWO                           20m
harbor-jobservice    Bound    harbor-jobservice    1Gi        RWO                           20m
harbor-redis         Bound    harbor-redis         1Gi        RWO                           20m
harbor-registry      Bound    harbor-registry      5Gi        RWO                           20m

然后指定自己的 values:(qikqiak-values.yaml)

expose:
  type: ingress
  tls:
    enabled: true
  ingress:
    hosts:
      core: core.harbor.domain
      notary: notary.harbor.domain
externalURL: https://core.harbor.domain
persistence:
  enabled: true
  resourcePolicy: "keep"
  persistentVolumeClaim:
    registry:
      existingClaim: "harbor-registry"
      storageClass: "-"
    chartmuseum:
      existingClaim: "harbor-chartmuseum"
      storageClass: "-"
    jobservice:
      existingClaim: "harbor-jobservice"
      storageClass: "-"
    database:
      existingClaim: "harbor-database"
      storageClass: "-"
    redis:
      existingClaim: "harbor-redis"
      storageClass: "-"

我们使用我们自己手动创建的 PVC 来指定存储,不过还需要注意的是需要指定 storageClass: "-",否则会使用默认的 StorageClass,然后直接使用 Helm3 进行安装:

$ helm install harbor --namespace kube-ops ./harbor-helm -f .qikqiak-values.yaml
NAME: harbor
LAST DEPLOYED: Mon Dec 16 11:06:54 2019
NAMESPACE: kube-ops
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
Please wait for several minutes for Harbor deployment to complete.
Then you should be able to visit the Harbor portal at https://core.harbor.domain.
For more details, please visit https://github.com/goharbor/harbor.

稍等一会儿正常来说就可以安装成功了:

$ kubectl get pods -n kube-ops                                 
NAME                                           READY   STATUS    RESTARTS   AGE
harbor-harbor-chartmuseum-699d9764b7-5nvlf     1/1     Running   0          14m
harbor-harbor-clair-c9757f6cb-kjrms            1/1     Running   6          14m
harbor-harbor-core-d997f76d-bdqww              1/1     Running   0          3m1s
harbor-harbor-database-0                       1/1     Running   0          14m
harbor-harbor-jobservice-549cd6b67b-5jsgn      0/1     Running   3          14m
harbor-harbor-notary-server-7969fbdf87-tkn89   1/1     Running   0          113s
harbor-harbor-notary-signer-7fbd575976-twvwx   1/1     Running   0          113s
harbor-harbor-portal-7dddbf759c-zdmkl          1/1     Running   0          14m
harbor-harbor-redis-0                          1/1     Running   0          14m
harbor-harbor-registry-6bbb6b955f-xgdmp        2/2     Running   0          14m

另外在安装过程还遇到一个问题是 redis 的权限问题,查看 redis 的 Pod 会遇到如下的错误信息:

$ kubectl logs -f harbor-harbor-redis-0 -n kube-ops
1:M 16 Dec 03:56:12.147 # Background saving error
1:M 16 Dec 03:56:18.066 * 1 changes in 900 seconds. Saving...
1:M 16 Dec 03:56:18.067 * Background saving started by pid 161
161:C 16 Dec 03:56:18.070 # Failed opening the RDB file dump.rdb (in server root dir /var/lib/redis) for saving: Permission denied
......

这主要是因为我们这里运行的 redis 这个 Pod 中指定了一个 fsGroup

...
securityContext:
  fsGroup: 999
...

而我们这里将数据持久化到了 NFS 上面,而持久化的目录 /data/k8s/harbor/redis 的 ownership 是 root:root,所以会权限错误,我们可以手动更改下 NFS 上面目录的 owenrship 即可:

$ chown 999:999 -R /data/k8s/harbor/redis

由于我们指定的是 ingress 的模式,所以我们可以通过在本地 /etc/hosts 里面加上对应的域名解析:

$ kubectl get ingress -n kube-ops
NAME                    HOSTS                                     ADDRESS   PORTS     AGE
harbor-harbor-ingress   core.harbor.domain,notary.harbor.domain             80, 443   14m

然后我们就可以通过 core.harbor.domain 去访问 Harbor 的 Dashboard 页面了,由于我们配置了自动跳转,所以会自动跳转到 https:

harbor login

然后可以使用 admin:Harbor12345 进行登录:

harbor dashboard

我们可以看到有很多功能,默认情况下会有一个名叫 library 的项目,改项目默认是公开访问权限的,进入项目可以看到里面还有 Helm Chart 包的管理,可以手动在这里上传,也可以对改项目里面的镜像进行一些配置,比如是否开启自动扫描镜像功能:

harbor project settings

推送镜像

然后我们来测试下使用 docker cli 来进行 pull/push 镜像,由于上面我们安装的时候通过 Ingress 来暴露的 Harbor 的服务,而且强制使用了 https,所以如果我们要在终端中使用我们这里的私有仓库的话,就需要配置上相应的证书:

$ docker login core.harbor.domain
Username: admin
Password:
Error response from daemon: Get https://core.harbor.domain/v2/: x509: certificate signed by unknown authority

这是因为我们没有提供证书文件,我们将使用到的 ca.crt 文件复制到 /etc/docker/certs.d/core.harbor.domain 目录下面,如果该目录不存在,则创建它。ca.crt 这个证书文件我们可以通过 Ingress 中使用的 Secret 资源对象来提供:

$ kubectl get secret harbor-harbor-ingress -n kube-ops -o yaml
apiVersion: v1
data:
  ca.crt: <ca.crt>
  tls.crt: <tls.crt>
  tls.key: <tls.key>
kind: Secret
metadata:
  creationTimestamp: "2019-12-16T03:29:59Z"
  labels:
    app: harbor
    chart: harbor
    heritage: Helm
    release: harbor
  name: harbor-harbor-ingress
  namespace: kube-ops
  resourceVersion: "11786346"
  selfLink: /api/v1/namespaces/kube-ops/secrets/harbor-harbor-ingress
  uid: aaf69ffc-aaa1-439b-98ae-e8a2c13d7ea4
type: kubernetes.io/tls

其中 data 区域中 ca.crt 对应的值就是我们需要证书,不过需要注意还需要做一个 base64 的解码,这样证书配置上以后就可以正常访问了。

不过由于上面的方法较为繁琐,所以一般情况下面我们在使用 docker cli 的时候是在 docker 启动参数后面添加一个 --insecure-registry 参数来忽略证书的校验的,在 docker 启动配置文件 /usr/lib/systemd/system/docker.service 中修改ExecStart的启动参数:

ExecStart=/usr/bin/dockerd --insecure-registry core.harbor.domain

或者在 Docker Daemon 的配置文件中添加:

$ cat /etc/docker/daemon.json
{
  "insecure-registries" : [
    "core.harbor.domain"
  ],
  "registry-mirrors" : [
    "https://ot2k4d59.mirror.aliyuncs.com/"
  ]
}

然后保存重启 docker,再使用 docker cli 就没有任何问题了:

$ docker login core.harbor.domain
Username: admin
Password:
Login Succeeded

比如我们本地现在有一个名为 busybox 的镜像,现在我们想要将该镜像推送到我们的私有仓库中去,应该怎样操作呢?首先我们需要给该镜像重新打一个 core.harbor.domain 的前缀,然后推送的时候就可以识别到推送到哪个镜像仓库:

$ docker tag busybox core.harbor.domain/library/busybox
$ docker push core.harbor.domain/library/busybox
The push refers to repository [core.harbor.domain/library/busybox]
adab5d09ba79: Pushed 
latest: digest: sha256:4415a904b1aca178c2450fd54928ab362825e863c0ad5452fd020e92f7a6a47e size: 527

推送完成后,我们同样可以在 Portal 页面上看到这个镜像的信息: harbor project demo

镜像 push 成功,同样可以测试下 pull:

$ docker rmi core.harbor.domain/library/busybox
Untagged: core.harbor.domain/library/busybox:latest
Untagged: core.harbor.domain/library/busybox@sha256:4415a904b1aca178c2450fd54928ab362825e863c0ad5452fd020e92f7a6a47e
$ docker rmi busybox
Untagged: busybox:latest
Untagged: busybox@sha256:061ca9704a714ee3e8b80523ec720c64f6209ad3f97c0ff7cb9ec7d19f15149f
$ docker pull core.harbor.domain/library/busybox:latest
latest: Pulling from library/busybox
Digest: sha256:4415a904b1aca178c2450fd54928ab362825e863c0ad5452fd020e92f7a6a47e
Status: Downloaded newer image for core.harbor.domain/library/busybox:latest

$ docker images |grep busybox
docker images |grep busybox
core.harbor.domain/library/busybox                        latest              d8233ab899d4        10 months ago       1.2MB

到这里证明上面我们的私有 docker 仓库搭建成功了,大家可以尝试去创建一个私有的项目,然后创建一个新的用户,使用这个用户来进行 pull/push 镜像,Harbor 还具有其他的一些功能,比如镜像复制,大家可以自行测试,感受下 Harbor 和官方自带的 registry 仓库的差别。