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

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

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

推送镜像¶
然后我们来测试下使用 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 页面上看到这个镜像的信息:

镜像 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 仓库的差别。