全面搭建 Docker 私有镜像库,带认证服务器!
简介
网上有大量文章介绍如何搭建 Docker 私有镜像库,但多数都比较浅,属于一种临时方案,存在诸多的安全风险。 比如最常见的一种就是使用官方提供的 registry 容器来搭建:
$ docker run -d -p 5000:5000 \
-v /opt/data/registry:/var/lib/registry registry
这样,就启动了一个 registry 服务器了,我们可以把镜像名标记为本地服务器名称之后就可以 push/pull 等操作了。 如下:
docker tag nginx:latest 127.0.0.1:5000/nginx:latest
docker push 127.0.0.1:5000/nginx:latest
docker image rm 127.0.0.1:5000/nginx:latest
最显然的问题就是使用本地作为镜像服务器,除非你为了好玩而不是真正的实际应用,在本机创建私有镜像库就是自己骗自己,电脑炸了什么都没了。
所以你会想到,我不用本机,我用局域网中其他主机来当服务器总可以吧,比如 NAS 机之类的。
虽然避免了单机关于故障问题,但这种使用 127.0.0.1:5000
IP 地址访问的方式会显得很傻逼,也不方便。
现代人还有谁去输 IP 地址来访问服务的?
有人会想到说在 /etc/hosts
中添加一个别名不就搞定了?
127.0.0.1 docker.register
但是,你有没有想过,这种 HTTP 方式访问一个镜像服务器是不符合我们云原生的安全规范的,明文的方式传递认证信息当然会存在安全隐患。
所以,为了使我们所有云相关的操作有安全保障,我们应该使用 HTTPS 加密连接。
一个最常见的方案是:
我们增加一个网关服务器(反向代理功能),把 registry 服务器当作是内网服务器,既然是内网,通常来说,用 HTTP 协议来访问问题不大。所有来自外部的访问全部使用 HTTPS 协议去请求网关服务器,这里的网关服务器我们可以使用 Nginx 来搭建,我们也常常把它称为反向代理服务器。
下面是 Nginx 的配置代码:
upstream registryserver {
server registry-server.io:5000;
}
map $upstream_http_docker_distribution_api_version $docker_distribution_api_version {
'' 'registry/2.0';
}
server {
listen 80;
server_name home-reg.io reg.router.slab;
access_log /var/log/nginx/home-reg.access.log;
error_log /var/log/nginx/home-reg.error.log;
location /{
if ($http_user_agent ~ "^(docker\/1\.(3|4|5(?!\.[0-9]-dev))|Go ).*$" ) {
return 404;
}
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://registryserver;
proxy_read_timeout 90;
add_header 'Docker-Distribution-Api-Version' $docker_distribution_api_version always;
}
}
server {
listen 443;
server_name home-reg.io reg.router.slab;
client_max_body_size 0; # Disabled to prevent 413's
ssl_certificate /etc/nginx/ssl/home-reg.crt;
ssl_certificate_key /etc/nginx/ssl/home-reg.key;
ssl_trusted_certificate /etc/nginx/ssl/ca.crt;
# required to avoid HTTP 411: see Issue #1486 (https://github.com/moby/moby/issues/1486)
chunked_transfer_encoding on;
access_log /var/log/nginx/home-reg.access.log;
error_log /var/log/nginx/home-reg.error.log;
location / {
if ($http_user_agent ~ "^(docker\/1\.(3|4|5(?!\.[0-9]-dev))|Go ).*$" ) {
return 404;
}
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://registryserver;
proxy_read_timeout 90;
add_header 'Docker-Distribution-Api-Version' $docker_distribution_api_version always;
}
}
到现在,我们解决了客户端访问网关服务器使用 HTTPS 连接的问题。但是,还有一个问题,如何向镜像仓库(docker registry)进行认证?
解决方法是: 搭建一个认证服务器(Authentication Server)!
Authentication Server
我们先简单了解一下 Docker 开源的这个 Registry 历史:
Registry 最早的版本叫 V1, 它不提供登陆 (Authentication)功能,所以,用户只能匿名 pull/push。或者只能通过外部的程序来进行 authentication。
Registry V2 虽然提供 token-based Authentication,但是没有提供生成 token 的 server。比如 basic auth 中,可以用 htpasswd 来生成密码。
而 token 则没有工具可以生成,registry 可以生成,但是前提是你以 basic auth 方式登陆后才可以,否则没办法在没登录的情况下直接生成 token 。
因此,registry v2 常见的就是还是使用 username + passwd 的方式登陆。这种方式不够安全,所以我们接下来探索更安全的 token-based authentication 方法。
Token-based Authentication
使用 Token-based Authentication 第一个特点就是更加安全,因为 token 具有 pull/push 权限,但没有修改密码的权限,token 泄漏也没有致命危险(比泄漏 username/password 要好)。
Registry 认证的原理都是差不多的,比如 basic auth,registry 通过保存了密码的文件 registry.htpasswd 进行认证;而 token-based auth,registry 则通过 auth server 提供的 certificate 进行认证(auth server 用 private key 对 token 签名,证书则公开给 registry,registry 就可以用证书来验证了)。
这里,我们选择 docker_auth 这个开源的认证服务器
docker_auth 实现了 docker spec 中的 token 协议
docker_auth 支持的登陆方法有:
Supported authentication methods(登陆方式):
- Static list of users
- Google Sign-In (incl. Google for Work / GApps for domain) (documented here)
- Github Sign-In
- Gitlab Sign-In
- LDAP bind (demo)
- MongoDB user collection
- MySQL/MariaDB, PostgreSQL, SQLite database table
- External program
Supported authorization methods(授权方式):
- Static ACL
- MongoDB-backed ACL
- MySQL/MariaDB, PostgreSQL, SQLite backed ACL
- External program
项目地址:https://github.com/cesanta/docker_auth
接下来,实战操作:
第一步:创建密钥和证书
docker_auth 需要使用 HTTPS 连接以确保通讯安全,因此我们需要使用 CA 为其签发一个 server.crt
。
docker_auth 还需要对 token 进行签名,我们还需要用 CA 再签发一个专门用于对 token 签名的 token-sign.crt
。同时,这个 token-sign.crt
也是需要放入 registry 的,因为它需要对 token 进行验证。
为了简便,我们可以把 server.crt
同时作为 token-sign.crt
用。
openssl req -new -nodes \
-keyout auth-server.key \
-out auth-server.csr \
-subj "/CN=auth-server"
openssl ca -days 391 \
-in auth-server.csr \
-out auth-server.crt \
-cert ca.crt \
-keyfile ca.key \
-create_serial \
-extensions x509_ext \
-extfile <(cat /etc/ssl/openssl.cnf - <<END
[ x509_ext ]
basicConstraints = CA:false
subjectKeyIdentifier = hash
keyUsage = digitalSignature, keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth
subjectAltName = DNS.1:auth-server.io,IP:10.10.0.3,IP:127.0.0.1
END
)
第二步:编写 docker_compose.yml
version: "3.9"
services:
registry:
image: registry:2
ports:
- "5000:5000"
- "443:443"
restart: always
environment:
- REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY=/var/lib/registry
- REGISTRY_HTTP_ADDR="0.0.0.0:443"
- REGISTRY_AUTH=token # 使用 token auth 的方式
- REGISTRY_AUTH_TOKEN_REALM=https://auth-server.io:5001/auth
- REGISTRY_AUTH_TOKEN_SERVICE="Docker registry"
- REGISTRY_AUTH_TOKEN_ISSUER="Auth Service"
- REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE=/certs/auth-server.crt
- REGISTRY_HTTP_TLS_CERTIFICATE=/certs/registry-server.crt
- REGISTRY_HTTP_TLS_KEY=/certs/registry-server.key
#- REGISTRY_HTTP_TLS_CLIENTCAS=' - /certs/domain.ca-bundle' # 如果需要 client HTTPS,请加上这行
- TZ=Asia/Shanghai
volumes:
- /opt/docker/registry/certs:/certs
- /opt/docker/registry/auth:/auth
- registry-vol:/var/lib/registry
#- $PWD/reg_config.yml:/etc/docker/registry/config.yml
dockerauth:
image: cesanta/docker_auth
ports:
- "5001:5001"
volumes:
- ./config.yml:/config/auth_config.yml:ro
- ./ssl:/ssl
- ./logs:/logs
#command: -alsologtostderr=true -log_dir=/logs /config/extAuth.yml
environment:
- TZ=Asia/Shanghai
restart: always
volumes:
registry-vol:
driver: local
driver_opts:
o: bind
type: none
device: /opt/docker/registry/data
启动后
第三步:登录 Registry
执行下列命令登陆:
docker login https://registry-server.io/v2
输入正确的用户名(配置在 docker_auth 的 config.yml
中)后,会出现如下提示:
Error response from daemon: login attempt to https://registry-server.io/v2/ failed with status: 401 Unauthorized
最后发现是 Issuer 写错了,registry server 和 auth server 配置文件中的 Issuer 必须相同。
登陆成功:
输入 curl 命令访问 auth server 看看:
curl --cacert /opt/docker/registry/certs/ca.crt --user admin:badmin https://auth-server.io:5001/auth | jq
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IkRRTTQ6TjJXVTozRkVKOjNLNUo6TDNVUzpIT0pQOkdZRUk6VVZRQTpaSTY1OkREQ1Q6N002Tjo3WVRZIn0.eyJpc3MiOiJBY21lIGF1dGggc2VydmVyIiwic3ViIjoiYWRtaW4iLCJhdWQiOiIiLCJleHAiOjE2NDAwMTA5NDgsIm5iZiI6MTY0MDAxMDAzOCwiaWF0IjoxNjQwMDEwMDQ4LCJqdGkiOiIzNzIzMzA0MjA1MTAzNDAwMDEwIiwiYWNjZXNzIjpbXX0.Vs19bXQIcwoF7qgxyEpMtmo9wHZtCRNIptz_rnWEzf8nIFdhPCDiC8AO94jNhpGxfpuz9XnksodMu7o1HsQyhhtR4_9m00MJInrk7fq-GBZV90C6S_W5tamHvwh1PM5Qzsg4LAvqKJb9xnXbOk3e-LYDsQglPkRsb_3lkTCbTO9rkb8itBuTNbY45XDbvdPpKa-mSLYt0AaXAKSHxoXOOq00JZU2VaECh2G4g5bvY7SaP30L5HB4MqNQU3m5Nnmtqo7V46Zwm_mU-2rzRdc1UtXEL7M0MwqhvNXE4M7dmNEZLdp8nErJI8fpsEzr9QZWvWFEsEGB8YkiW8uUb0SHDw",
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IkRRTTQ6TjJXVTozRkVKOjNLNUo6TDNVUzpIT0pQOkdZRUk6VVZRQTpaSTY1OkREQ1Q6N002Tjo3WVRZIn0.eyJpc3MiOiJBY21lIGF1dGggc2VydmVyIiwic3ViIjoiYWRtaW4iLCJhdWQiOiIiLCJleHAiOjE2NDAwMTA5NDgsIm5iZiI6MTY0MDAxMDAzOCwiaWF0IjoxNjQwMDEwMDQ4LCJqdGkiOiIzNzIzMzA0MjA1MTAzNDAwMDEwIiwiYWNjZXNzIjpbXX0.Vs19bXQIcwoF7qgxyEpMtmo9wHZtCRNIptz_rnWEzf8nIFdhPCDiC8AO94jNhpGxfpuz9XnksodMu7o1HsQyhhtR4_9m00MJInrk7fq-GBZV90C6S_W5tamHvwh1PM5Qzsg4LAvqKJb9xnXbOk3e-LYDsQglPkRsb_3lkTCbTO9rkb8itBuTNbY45XDbvdPpKa-mSLYt0AaXAKSHxoXOOq00JZU2VaECh2G4g5bvY7SaP30L5HB4MqNQU3m5Nnmtqo7V46Zwm_mU-2rzRdc1UtXEL7M0MwqhvNXE4M7dmNEZLdp8nErJI8fpsEzr9QZWvWFEsEGB8YkiW8uUb0SHDw"
}
可见,能得到 token。
总结:
前面我们测试过,Registry 使用 basic auth 的话,也会给用户颁发 token 来认证。
那为什么还要用一个额外的第三方 auth server(docker_auth)呢,使用 docker_auth 到底有什么优势?
- 使用 docker_auth 后,登陆 regsitry server 时使用的不再是 registry 中的 user 了,而是 docker_auth 中的 user。这样的好处有多个:
- 不会泄漏 registry 中的 user:password。
- 可以在 docker_auth 创建具有不同权限的 user,有的 user 只能 pull,有的能 pull 也能 push。
- 有 acl 支持不同权限操作
-
现在访问 registry server 的实际是由 auth server 签发的 token,有过期时间,而且 token 泄漏不会造成致命风险,因此是一种更加安全的访问方式。
-
docker_auth 支持许多第三方的 token 机构,比如 github, gitlab, google, 等等。
唯一遗憾的是:
使用 docker_auth 中的用户登陆 docker,对 docker 来说依然是匿名的,因此 pull limit 会消耗 anonymose 的额度。
我们可以用用下面的办法来验证,
TOKEN=(curl "https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimitpreview/test:pull" | jq -r .token)
curl --head -H "Authorization: Bearer $TOKEN" https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest
可以看到,匿名用户的额度确实减少了!
下面,我们测试一下用户权限:
使用 test 用户登陆,它没有 push 权限,我们来验证一下:
再使用 admin 登陆试试:
思考:
在上面 docer_auth 的配置中,我们把 user 和 password 都明文(base64)保存到了 config.yaml
,这样显然是不安全的。有没有办法通过外部程序来管理 user:password 呢?(其实,这也回到了我们怎么保存 docker credential 的问题)
答案是:有的!
docker_auth 提供了下面几种方式:
- OAuth
- Google Sign-in
- Github
- Gitlab
- LDAP bind
- Database
- MongoDB user collection
- MySQL/MariaDB, PostgreSQL, SQLite
- External program
配置样例:
# config.yaml for Registry
# auth:
# token:
# realm: "https://127.0.0.1:5001/auth"
# service: "Docker registry"
# issuer: "Acme auth server"
# rootcertbundle: "/path/to/server.pem"
server:
addr: ":5001"
certificate: "/ssl/auth-server.crt"
key: "/ssl/auth-server.key"
token:
issuer: "Auth Service" # Must match issuer in the Registry config.
expiration: 900
users:
# Password is specified as a BCrypt hash. Use `htpasswd -nB USERNAME` to generate.
"admin":
password: "$2y$05$LO.vzwpWC5LZGqThvEfznu8qhb5SGqvBSWY1J3yZ4AxtMRZ3kN5jC" # badmin
"test":
password: "$2y$05$WuwBasGDAgr.QCbGIjKJaep4dhxeai9gNZdmBnQXqpKly57oNutya" # 123
"": {} # Allow anonymous (no "docker login") access.
acl:
- match: {account: "admin"}
actions: ["*"]
comment: "Admin has full access to everything."
- match: {account: "test"}
actions: ["pull"]
comment: "User \"test\" can pull stuff."
# All logged in users can pull all images.
- match: {account: "/.+/"}
actions: ["pull"]
# Anonymous users can pull "hello-world".
- match: {account: "", name: "hello-world"}
actions: ["pull"]
# Access is denied by default.
我们可以把 auth 中的信息写在 docker-compose.yml
的环境变量中
需要注意的是:
Authentication 和 HTTPS Connection 是不同的概念,使用 HTTPS 和 mutial TLS 只是保证通信是加密的;但 Registry,还需要认证用户身份,也就是 Authentication。当然,Authentication 也可以用 HTTP 连接,但是非常不安全。
最常见的认证方式是 username:password 的方式,但是 Registry V2 可以采用 Bearer Token。
Registry V2 提供的 auth 方式有(在 config.yml
中配置):
auth:
silly:
realm: silly-realm
service: silly-service
token:
autoredirect: true
realm: token-realm
service: registry-server.io
issuer: registry-token-issuer # my.portus
rootcertbundle: /root/certs/bundle.crt
htpasswd:
realm: basic-realm
path: /path/to/htpasswd
docker_auth 提供了我们生成 bearer token 并使用 bearer token 进行认证的功能。
version: "3.9"
services:
registry:
image: registry:2
ports:
- "5000:5000"
- "443:443"
restart: always
environment:
- REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY=/data
- REGISTRY_AUTH=token
- REGISTRY_AUTH_TOKEN_REALM=https://example.docker.com:5001/auth
- REGISTRY_AUTH_TOKEN_SERVICE="Docker registry"
- REGISTRY_AUTH_TOKEN_ISSUER="Auth Service"
- REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE=/ssl/domain.crt
- REGISTRY_HTTP_TLS_CERTIFICATE=/ssl/domain.crt
- REGISTRY_HTTP_TLS_KEY=/ssl/domain.key
- REGISTRY_HTTP_TLS_CLIENTCAS=' - /certs/domain.ca-bundle'
volumes:
- ./ssl:/ssl
- ./data:/data
dockerauth:
image: cesanta/docker_auth
ports:
- "5001:5001"
volumes:
- ./:/config:ro
- ./ssl:/ssl
- ./extensions:/extensions
#command: -alsologtostderr=true -log_dir=/logs /config/extAuth.yml
restart: always
对比 htpasswd 的 auth 方式:
version: '3.9'
services:
registry:
image: registry:2
ports:
- "5000:5000"
- "443:443"
environment:
REGISTRY_AUTH: htpasswd
REGISTRY_AUTH_HTPASSWD_REALM: Registry
REGISTRY_AUTH_HTPASSWD_PATH: /auth/registry.password
REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /var/lib/registry
REGISTRY_HTTP_ADDR: 0.0.0.0:443
REGISTRY_HTTP_TLS_CERTIFICATE: /certs/home-registry.crt
REGISTRY_HTTP_TLS_KEY: /certs/home-registry.key
REGISTRY_HTTP_TLS_CLIENTCAS: ' - /certs/domain.ca-bundle'
volumes:
- /opt/docker/registry/certs:/certs
- /opt/docker/registry/auth:/auth
- registry-vol:/var/lib/registry
- $PWD/config.yml:/etc/docker/registry/config.yml
volume:
registry-vol:
driver: local
driver_opts:
o: bind
type: none
device: /opt/docker/registry/data
全文完!
如果你喜欢我的文章,欢迎关注我的微信公众号 deliverit。