Harbor 介绍
Harbor 是由 VMware 开源的一款云原生制品仓库,Harbor 的核心功能是存储和管理 Artifact。Harbor 允许用户用命令行工具对容器镜像及其他 Artifact 进行推送和拉取,并提供了图形管理界面帮助用户查看和管理这些 Artifact。在 Harbor 2.0 版本中,除容器镜像外,Harbor 对符合 OCI 规范的 Helm Chart、CNAB、OPA Bundle 等都提供了更多的支持。
Harbor 整体架构如上图所示是 Harbor 2.0 的架构图,从上到下可分为代理层、功能层和数据层。
- 代理层:代理层实质上是一个 Nginx 反向代理,负责接收不同类型的客户端请求,包括浏览器、用户脚本、Docker 等,并根据请求类型和 URI 转发给不同的后端服务进行处理。
- 功能层:
- Portal:是一个基于 Argular 的前端应用,提供 Harbor 用户访问的界面。
- Core:是 Harbor 中的核心组件,封装了 Harbor 绝大部分的业务逻辑。
- JobService:异步任务组件,负责 Harbor 中很多比较耗时的功能,比如 Artifact 复制、扫描、垃圾回收等。
- Docker Distribution:Harbor 通过 Distribution 实现 Artifact 的读写和存取等功能。
- RegistryCtl:Docker Distribution 的控制组件。
- Notary(可选):基于 TUF 提供镜像签名管理的功能。
- 扫描工具(可选):镜像的漏洞检测工具。
- ChartMuseum(可选):提供 API 管理非 OCI 规范的 Helm Chart,随着兼容 OCI 规范的 Helm Chart 在社区上被更广泛地接受,Helm Chart 能以 Artifact 的形式在 Harbor 中存储和管理,不再依赖 ChartMuseum,因此 Harbor 可能会在后续版本中移除对 ChartMuseum 的支持。
- 数据层:
- Redis:主要作为缓存服务存储一些生命周期较短的数据,同时对于 JobService 还提供了类似队列的功能。
- PostgreSQL:存储 Harbor 的应用数据,比如项目信息、用户与项目的关系、管理策略、配置信息、Artifact 的元数据等等。
- Artifact 存储:存储 Artifact 本身的内容,也就是每次推送镜像、Helm Chart 或其他 Artifact 时,数据最终存储的地方。默认情况下,Harbor 会把 Artifact 写入本地文件系统中。用户也可以修改配置,将 Artifact 存储在外部存储中,例如亚马逊的对象存储 S3、谷歌云存储 GCS、阿里云的对象存储 OSS 等等。
Docker Compose 部署 Harbor
前提要求
硬件要求:
./install.sh
查看 Harbor 组件运行状况:
> docker-compose ps Name Command State Ports ------------------------------------------------------------------------------------------------------------ harbor-core /harbor/entrypoint.sh Up (healthy) harbor-db /docker-entrypoint.sh 96 13 Up (healthy) harbor-jobservice /harbor/entrypoint.sh Up (healthy) harbor-log /bin/sh -c /usr/local/bin/ ... Up (healthy) 127.0.0.1:1514->10514/tcp harbor-portal nginx -g daemon off; Up (healthy) nginx nginx -g daemon off; Up (healthy) 0.0.0.0:8888->8080/tcp,:::8888->8080/tcp redis redis-server /etc/redis.conf Up (healthy) registry /home/harbor/entrypoint.sh Up (healthy) registryctl /home/harbor/start.sh Up (healthy)
登录页面
浏览器输入 http://11.8.36.21:8888 访问 Harbor 页面,用户名和密码为 harbor.yml 配置文件中默认设置的 admin,Harbor12345。
推送镜像
从公网拉取一个 nginx:1.19 版本的镜像:
> docker pull nginx:1.19 1.19: Pulling from library/nginx 69692152171a: Already exists 49f7d34d62c1: Pull complete 5f97dc5d71ab: Pull complete cfcd0711b93a: Pull complete be6172d7651b: Pull complete de9813870342: Pull complete Digest: sha256:df13abe416e37eb3db4722840dd479b00ba193ac6606e7902331dcea50f4f1f2 Status: Downloaded newer image for nginx:1.19
编辑 /etc/docker/daemon.json,设置允许访问的 HTTP 仓库地址。
{ "insecure-registries":["11.8.36.21:8888"] }
修改镜像 tag:
docker tag nginx:1.19 11.8.36.21:8888/library/nginx:1.19
登录 Harbor:
> docker login 11.8.36.21:8888 Username: admin Password: Login Succeeded
推送镜像到 Harbor:
> docker push 11.8.36.21:8888/library/nginx:1.19 The push refers to a repository [11.8.36.21:8888/library/nginx] f0f30197ccf9: Pushed eeb14ff930d4: Pushed c9732df61184: Pushed 4b8db2d7f35a: Pushed 431f409d4c5a: Pushed 02c055ef67f5: Pushed 1.19: digest: sha256:eba373a0620f68ffdc3f217041ad25ef084475b8feb35b992574cd83698e9e3c size: 1570
查看推送的镜像:
HTTPS 配置(可选)
在生产环境中建议配置 HTTPS,可以使用由受信任的第三方 CA 签名的证书,也可以使用自签名证书。如果想要启用 Content Trust with Notary 来正确签名所有图像,则必须使用 HTTPS。
创建目录
首先创建目录存放生成的证书。
mkdir /root/cert cd /root/cert/
生成 CA 证书
本次实验中我们使用自签名证书。生产环境中应使用受信任的第三方 CA 签名的证书。
生成 CA 证书私钥
openssl genrsa -out ca.key 4096
生成 CA 证书
-subj 表示证书的组织。CN 后面的值改成 harbor 的 IP 地址或者域名。
openssl req -x509 -new -nodes -sha512 -days 3650 \ -subj "/C=CN/ST=Beijing/L=Beijing/O=example/OU=Personal/CN=11.8.36.21" \ -key ca.key \ -out ca.crt
生成 Server 证书
生成 Harbor 使用的证书和私钥。
生成 Server 私钥
openssl genrsa -out server.key 4096
生成 Server 证书签名请求(CSR)
生成 Harbor 的证书签名请求,使用上面生成的 CA 证书来给 Server 签发证书。
openssl req -sha512 -new \ -subj "/C=CN/ST=Beijing/L=Beijing/O=example/OU=Personal/CN=11.8.36.21" \ -key server.key \ -out server.csr
生成 x509 v3 扩展文件
通过 docker 或者 ctr 等工具拉取 HTTPS 的镜像时,要求 HTTPS 的证书包含 SAN 扩展。
SAN(Subject Alternative Name) 是 SSL 标准 x509 中定义的一个扩展。使用了 SAN 字段的 SSL 证书,可以扩展此证书支持的域名,使得一个证书可以支持多个不同域名的解析。例如下图中 Google 的这张证书的主题备用名称(SAN)中列了一大串的域名,因此这张证书能够被多个域名所使用。对于 Google 这种域名数量较多的公司来说,使用这种类型的证书能够极大的简化网站证书的管理。使用以下命令生成 x509 v3 扩展文件:
cat > v3.ext <<-EOF authorityKeyIdentifier=keyid,issuer basicConstraints=CA:FALSE keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment extendedKeyUsage = serverAuth subjectAltName = IP:11.8.36.21 EOF
如果是域名访问通过下面方式生成 x509 v3 扩展文件:
cat > v3.ext <<-EOF authorityKeyIdentifier=keyid,issuer basicConstraints=CA:FALSE keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment extendedKeyUsage = serverAuth subjectAltName = @alt_names [alt_names] DNS.1=yourdomain.harbor.com EO
使用 CA 证书签发 Server 证书
openssl x509 -req -sha512 -days 3650 \ -extfile v3.ext \ -CA ca.crt -CAkey ca.key -CAcreateserial \ -in server.csr \ -out server.crt
查看当前目录生成的文件:
root@ydt-net-portainer:/root/cert #ll total 32 -rw-r--r-- 1 root root 2025 Aug 6 20:44 ca.crt -rw-r--r-- 1 root root 3243 Aug 6 20:40 ca.key -rw-r--r-- 1 root root 17 Aug 6 21:03 ca.srl -rw-r--r-- 1 root root 2045 Aug 6 21:03 server.crt -rw-r--r-- 1 root root 1704 Aug 6 20:47 server.csr -rw-r--r-- 1 root root 3247 Aug 6 20:45 server.key -rw-r--r-- 1 root root 202 Aug 6 21:00 v3.ext
为 Harbor 和 Docker 配置证书
将 server 证书和密钥复制到 Harbor 主机上的 /data/cert 目录中
mkdir -p /data/cert cp server.crt /data/cert/ cp server.key /data/cert/
转换 server.crt 为 server.cert
Docker 守护程序会认为 .crt 文件是 CA 证书,因此需要将 server 证书转换为 server.cert 文件。其实改下后缀就可以了,证书里面的内容是一样的。
openssl x509 -inform PEM -in server.crt -out server.cert
将 server 证书,密钥和 CA 证书复制到 Harbor 主机上的 Docker 证书目录中
需要提前创建好 Docker 证书目录,如果使用 443 端口监听 HTTPS 请求,则目录为 IP/域名 即可,如果使用非 443 端口,则目录为 IP/域名:端口。
mkdir -p /etc/docker/certs.d/11.8.36.21:8443 cp server.cert /etc/docker/certs.d/11.8.36.21:8443 cp server.key /etc/docker/certs.d/11.8.36.21:8443 cp ca.crt /etc/docker/certs.d/11.8.36.21:8443
查看 Docker 证书目录文件:
root@ydt-net-portainer:/root/cert #ll /etc/docker/certs.d/11.8.36.21:8443/ total 12 -rw-r--r-- 1 root root 2025 Aug 6 21:15 ca.crt -rw-r--r-- 1 root root 2045 Aug 6 21:15 server.cert -rw-r--r-- 1 root root 3247 Aug 6 21:15 server.key
重启 Docker Engine
systemctl restart docker
重新部署 Harbor
修改 harbor.yml 配置文件,添加 HTTPS 相关配置,指定 HTTPS 的端口号和证书路径:
使用 prepare 脚本生成 HTTPS 配置
使用 prepare 脚本为反向代理 Nginx 容器生成 HTTPS 配置。
./prepare
删除原有 Harbor 容器
Harbor 原有的数据文件默认是挂载在宿主机的 /data 目录下,因此删除 Harbor 容器并不会丢失数据。
docker-compose down -v
重新启动 Harbor
docker-compose up -d
登录 HTTPS 页面Docker 拉取和推送镜像
Docker 想要拉取或者推送 HTTPS 镜像仓库的镜像,需要在 Docker 证书目录中配置证书,这里的 Docker 客户端是另一台机器,首先在这台机器上创建目录。
mkdir /etc/docker/certs.d/11.8.36.21:8443
然后从 Harbor 主机拷贝证书文件到 Docker 客户端上,需要 server 的证书和密钥以及 CA 证书。
scp /root/cert/server.key root@11.8.36.95:/etc/docker/certs.d/11.8.36.21:8443 scp /root/cert/server.cert root@11.8.36.95:/etc/docker/certs.d/11.8.36.21:8443 scp /root/cert/ca.crt root@11.8.36.95:/etc/docker/certs.d/11.8.36.21:8443
拉取镜像:
root@ydt-net-nginx-cisp:/root #docker pull 11.8.36.21:8443/library/nginx:1.19 Trying to pull repository 11.8.36.21:8443/library/nginx ... 1.19: Pulling from 11.8.36.21:8443/library/nginx Digest: sha256:eba373a0620f68ffdc3f217041ad25ef084475b8feb35b992574cd83698e9e3c Status: Downloaded newer image for 11.8.36.21:8443/library/nginx:1.19
推送镜像:
#登录 Harbor 镜像仓库 root@ydt-net-nginx-cisp:/root #docker login https://11.8.36.21:8443 Username: admin Password: Login Succeeded #给镜像打 tag,换个名字 root@ydt-net-nginx-cisp:/root #docker tag 11.8.36.21:8443/library/nginx:1.19 11.8.36.21:8443/library/nginx-2:1.19 #推送镜像 root@ydt-net-nginx-cisp:/root #docker push 11.8.36.21:8443/library/nginx-2:1.19 The push refers to a repository [11.8.36.21:8443/library/nginx-2] f0f30197ccf9: Pushed eeb14ff930d4: Pushed c9732df61184: Pushed 4b8db2d7f35a: Pushed 431f409d4c5a: Pushed 02c055ef67f5: Pushed 1.19: digest: sha256:eba373a0620f68ffdc3f217041ad25ef084475b8feb35b992574cd83698e9e3c size: 1570
Containerd 配置镜像仓库
Kubernetes 最早将在 1.23 版本弃用 Docker 作为容器运行时,并在博客中强调可以使用如 Containerd 等 CRI 运行时来代替 Docker。对于 Containerd 来说,不能像 Docker 一样使用 docker login 登录到镜像仓库,需要修改其配置文件来进行认证。
方式一 跳过证书验证
编辑 /etc/containerd/config.toml 文件,添加以下配置。注意这里有个天坑:registry.mirrors 后面跟的才是 Harbor 主机的地址,一定要写对,反倒是 endpoint 中的内容可以随便写。
[plugins."io.containerd.grpc.v1.cri".registry] [plugins."io.containerd.grpc.v1.cri".registry.mirrors] [plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"] endpoint = ["https://registry-1.docker.io"] [plugins."io.containerd.grpc.v1.cri".registry.mirrors."11.8.75.154:11111"] #一定要写对 endpoint = ["https://11.8.75.154:11111"] #其实可以随便写 [plugins."io.containerd.grpc.v1.cri".registry.configs] [plugins."io.containerd.grpc.v1.cri".registry.configs."11.8.75.154:11111".tls] insecure_skip_verify = true #跳过证书验证 [plugins."io.containerd.grpc.v1.cri".registry.configs."11.8.75.154:11111".auth] username = "admin" password = "Harbor12345"
方式二 配置证书
如果想要安全些,可以把 CA 证书拷贝到 containerd 的机器上,然后修改 /etc/containerd/config.toml,指定 CA 证书。
[plugins."io.containerd.grpc.v1.cri".registry] [plugins."io.containerd.grpc.v1.cri".registry.mirrors] [plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"] endpoint = ["https://registry-1.docker.io"] [plugins."io.containerd.grpc.v1.cri".registry.mirrors."11.8.75.154:11111"] endpoint = ["https://11.8.75.154:11111"] [plugins."io.containerd.grpc.v1.cri".registry.configs] [plugins."io.containerd.grpc.v1.cri".registry.configs."11.8.75.154:11111".tls] ca_file = "/etc/ssl/certs/ca.crt" #指定CA证书 [plugins."io.containerd.grpc.v1.cri".registry.configs."11.8.75.154:11111".auth] username = "admin" password = "Harbor12345"
配置文件后重启 Containerd:
systemctl restart containerd
当 Kubernetes 需要拉取 Harbor 的镜像时,会自动根据 Containerd 的配置认证 Harbor 镜像仓库。
Kubernetes 部署 Harbor
上面介绍的通过 Docker Compose 方式部署 Harbor 的方式通常仅在单机测试环境下使用,在生产环境中用户可能需要在 Kubernetes 集群中部署 Harbor。Harbor 提供了 Helm Chart 来帮助用户在 Kubernetes 上快速部署 Harbor。
添加 Harbor Helm Chart 仓库,并将 Helm Chart 下载到本地。
helm repo add harbor https://helm.goharbor.io helm pull harbor/harbor --untar
编辑 harbar/values.yaml 文件:
- 修改服务类型为 nodePort,这样 Kubernetes 集群外部的机器就可以通过 Node:IP 来访问 Harbor。
- 设置 HTTPS 证书的域名。
helm install harbor -n cloudnative-lab harbor --create-namespace
查看部署的 Harbor 容器。
❯ kubectl get pod -n cloudnative-lab | grep harbor harbor-chartmuseum-685fccc58d-n6bs7 1/1 Running 0 5m46s harbor-core-58db6ff9b9-zfk2z 1/1 Running 1 5m46s harbor-database-0 1/1 Running 0 5m46s harbor-jobservice-6867cc6bfb-cpvrq 1/1 Running 0 5m46s harbor-nginx-7949594686-f4cxz 1/1 Running 0 5m46s harbor-notary-server-6845f46559-975g4 1/1 Running 2 5m46s harbor-notary-signer-6bcf747cc7-9k62c 1/1 Running 2 5m46s harbor-portal-c55c48545-twnfn 1/1 Running 0 5m46s harbor-redis-0 1/1 Running 0 5m46s harbor-registry-7df77757c4-k4kqz 2/2 Running 0 5m46s harbor-trivy-0 1/1 Running 1 5m46s
查看 Harbor 相关的 service,注意名为 harbor 的 service,这是 Harbor 反向代理 Nginx 的容器对应的 service,通过 NodePort 的方式暴露到集群外,下面在 Kubernetes 集群外我们都是通过这个 service 来访问 Harbor。
❯ kubectl get svc -n cloudnative-lab | grep harbor harbor NodePort 24.3.218.77 <none> 80:30002/TCP,443:30003/TCP,4443:30004/TCP 6m7s harbor-chartmuseum ClusterIP 24.3.89.193 <none> 80/TCP 6m8s harbor-core ClusterIP 24.3.166.42 <none> 80/TCP 6m8s harbor-database ClusterIP 24.3.68.131 <none> 5432/TCP 6m8s harbor-jobservice ClusterIP 24.3.96.160 <none> 80/TCP 6m8s harbor-notary-server ClusterIP 24.3.15.36 <none> 4443/TCP 6m7s harbor-notary-signer ClusterIP 24.3.150.117 <none> 7899/TCP 6m7s harbor-portal ClusterIP 24.3.183.66 <none> 80/TCP 6m7s harbor-redis ClusterIP 24.3.254.28 <none> 6379/TCP 6m7s harbor-registry ClusterIP 24.3.167.212 <none> 5000/TCP,8080/TCP 6m7s harbor-trivy ClusterIP 24.3.110.121 <none> 8080/TCP 6m7s
客户端添加 hosts 记录,编辑 /etc/hosts 添加,11.8.38.43 是其中一个 Kubernetes 节点的 IP 地址,myharbor.com 是我们前面部署 Harbor 时指定的域名。
11.8.38.43 myharbor.com
通过 Kubernetes 部署 Harbor 默认会生成自签名证书并启动 HTTPS 加密。浏览器输入 https://myharbor.com:30003 访问 Harbor 用户界面。
使用 Harbor 作为 Helm Chart 制品仓库
Harbor 不仅可以作为镜像仓库,还可以管理符合 OCI 规范的 Helm Chart、CNAB、OPA Bundle 等 Artifact。
WebUI 上传 Helm Charts
首先新建一个项目名为 helm-repo。在大规模集群环境下,如果所有 Docker 主机都从一个镜像仓库中拉取镜像,那么此镜像仓库很可能会成为镜像分发的瓶颈,影响镜像分发的速度。可以通过搭建多个镜像仓库并配合使用远程复制功能,解决这一问题。如下图所示,图中的镜像仓库分为两级:主仓库和子仓库。在主仓库和子仓库之间配置了远程复制策略。当一个应用镜像被推送到主仓库后,根据所配置的复制策略,镜像可以立即被分发到其他子镜像仓库。集群中的 Docker 主机就可以就近在其中任意一个子仓库中拉取所需的镜像,减轻主仓库的压力。
首先新建一个目标仓库,这里的目标仓库选择前面用 Docker Compose 部署的 Harbor。
镜像扫描
代码和软件通常具有缺陷,作为应用和其所依赖的软件包和操作系统的打包形式,容器镜像自然也不例外。恶意的攻击者会利用其中的一些缺陷非法入侵系统,破坏系统的运行或者窃取私密信息,这些缺陷就是我们熟知的漏洞。缺陷一旦被认定为漏洞,就可以通过 MITRE 公司注册为 CVE(Common Vulnerabilities and Exposures,公开披露的计算机安全漏洞列表)。
Harbor 支持 Trivy 和 Clair 作为镜像扫描器,通过 Helm 方式部署的 Harbor 默认安装了 Trivy。
可以在项目中选择指定的 Artifact 进行扫描,也可以在审查服务中进行全局漏洞扫描。
全局漏洞扫描:
镜像签名
TUF 是一种安全软件分发规范,具有由非对称密钥表示的具有层次结构的角色,并且运用这些非对称密钥签名的元数据来建立信任。开源项目 Notary 基于 TUF 实现,提供了完整的工具链来更好地支持内容信任流程。
通过 Helm 方式部署 Harbor 默认安装了 Notary。在 Harbor 内核服务中实现了签名管理器,可通过 Notary 服务器实现 Artifact 数字签名的管理。内容信任确保客户端或者容器运行时拉取的 Artifact 内容真实可靠,从而更好地提高系统的安全性。
新建一个项目 sign 存放该实验的镜像。
#30004 是 Notary 服务对外暴露的端口 ~/.docker/tls/myharbor.com:30004 #30003 是 Portal 服务对外暴露的端口 /etc/docker/certs.d/myharbor.com:30003
在本地给镜像打 tag 并推送镜像到 Harbor。在上传成功后会继续内容信任的签名步骤。如果根密钥还未创建,则系统会要求输入强密码以创建根密钥,之后在启用内容信任的条件下推送镜像都需要该密码。同时,系统还会要求输入强密码以创建正在推送的镜像仓库的目标密钥。
> docker tag 11.8.36.21:8443/library/nginx:1.19 myharbor.com:30003/sign/nginx-sign:1.19 > docker push myharbor.com:30003/sign/nginx-sign:1.19 The push refers to a repository [myharbor.com:30003/sign/nginx-sign] f0f30197ccf9: Layer already exists eeb14ff930d4: Layer already exists c9732df61184: Layer already exists 4b8db2d7f35a: Layer already exists 431f409d4c5a: Layer already exists 02c055ef67f5: Layer already exists 1.19: digest: sha256:eba373a0620f68ffdc3f217041ad25ef084475b8feb35b992574cd83698e9e3c size: 1570 Signing and pushing trust metadata #创建根密钥 You are about to create a new root signing key passphrase. This passphrase will be used to protect the most sensitive key in your signing system. Please choose a long, complex passphrase and be careful to keep the password and the key file itself secure and backed up. It is highly recommended that you use a password manager to generate the passphrase and keep it safe. There will be no way to recover this key. You can find the key in your config directory. Enter passphrase for new root key with ID 00eeb53: Repeat passphrase for new root key with ID 00eeb53: #创建正在推送的镜像仓库的目标密钥 Enter passphrase for new repository key with ID 45f6c55 (myharbor.com:30003/sign/nginx-sign): Repeat passphrase for new repository key with ID 45f6c55 (myharbor.com:30003/sign/nginx-sign): Finished initializing "myharbor.com:30003/sign/nginx-sign" Successfully signed "myharbor.com:30003/sign/nginx-sign":1.19
生成的密钥都会以 ~/.docker/trust/private/.key
路径存放,对应的 TUF 元数据文件被存放在 ~/.docker/trust/tuf//<镜像仓库路径>/metadata
目录下。
#tree ~/.docker/trust/ /root/.docker/trust/ ├── private │ ├── root_keys │ │ └── 00eeb53b454983f95c12718d1dcfdbc1e600253c20eab1ca8ee5743dac9f0fa0.key │ └── tuf_keys │ └── myharbor.com:30003 │ └── sign │ └── nginx-sign │ └── 45f6c55ea9846cf0ba552915e0599b4e7f45c742f6418c5f1116b61f2650ca48.key └── tuf └── myharbor.com:30003 └── sign └── nginx-sign ├── changelist └── metadata ├── root.json
签名成功后,登录 Harbor 管理界面,可以在镜像的 tag 列表中查看该镜像处于已签名的状态。
于是想到有可能是时间的问题,可能是 Docker 客户端的时间比证书生效的时间还早。查看了系统时间果然发现时间比标准时间慢了 6 分钟。设置 NTP 同步时间以后就可以正常登录了。
#date Mon Aug 9 20:54:43 CST 2021 root@ydt-net-nginx-cisp:/etc/docker/certs.d/myharbor.com:30003 # #ntpdate ntp3.aliyun.com 9 Aug 21:00:47 ntpdate[96996]: step time server 203.107.6.88 offset 355.206298 sec root@ydt-net-nginx-cisp:/etc/docker/certs.d/ #date Mon Aug 9 21:00:51 CST 2021 #docker login https://myharbor.com:30003 Username: admin Password: Login Succeeded
参考资料
- Harbor 权威指南
- https://blog.csdn.net/weixin_34387468/article/details/91855502
- https://zhuanlan.zhihu.com/p/336866221
- https://fuckcloudnative.io/posts/install-harbor-on-kubernetes/
- https://goharbor.io/docs/1.10/install-config/configure-https/
- https://www.bladewan.com/2020/02/22/harbor_notary/