前些天发现一个 Cloudflare Worker,可以搭配其 R2 存储创建私有镜像仓库,本篇记录部署过程和使用方法
部署服务
克隆 Worker 代码
本地电脑执行,安装依赖
$ git clone https://github.com/cloudflare/serverless-registry.git
$ cd serverless-registry
# brew install pnpm
$ pnpm install
安装完成依赖后,通过模版创建自己的 Wrangler 配置文件
$ cp wrangler.toml.example wrangler.toml
Cloudflare Wrangler 是一个命令行工具,用于管理和部署 Cloudflare Workers
修改 R2 存储桶名称(需要是不存在的桶名称)以及用于认证的用户名密码
## Production
[env.production]
r2_buckets = [
{ binding = "REGISTRY", bucket_name = "r2-image-registry-prod" }
]
[env.production.vars]
USERNAME = "<your-username>"
PASSWORD = "<your-password>"
安装 Wrangler CLI
$ npm install -g wrangler
$ wrangler --version
$ wrangler login
它会调用浏览器弹出网页进行授权,点击同意
创建 R2 存储桶
$ npx wrangler --env production r2 bucket create r2-image-registry-prod
部署 Worker
$ npx wrangler deploy --env production
部署成功后会输出镜像仓库地址:https://r2-registry-production.kissbug8720.workers.dev
在 Cloudflare 控制台「Workers & Pages」页面可以看到刚创建的 Worker
更新用户名/密码可以执行以下命令
$ npx wrangler secret put USERNAME --env production
$ npx wrangler secret put PASSWORD --env production
推送/拉取镜像进行测试
注意以下命令中的参数替换为你自己的地址和用户名
# 设置环境变量
$ export USERNAME=dong
$ export REGISTRY_URL=r2-registry-production.kissbug8720.workers.dev
# 登录/推送
$ docker login --username $USERNAME $REGISTRY_URL
$ docker pull alpine:latest
$ docker tag alpine:latest $REGISTRY_URL/alpine:latest
$ docker push $REGISTRY_URL/alpine:latest
在 R2 控制台上可以看到镜像已存储在 R2 桶
# 拉取测试
$ docker rmi alpine:latest $REGISTRY_URL/alpine:latest
$ docker pull $REGISTRY_URL/alpine:latest
拉取成功
测试用户权限
可以通过 Curl 指定用户名密码的方式查询镜像列表来验证用户权限
$ curl -X GET -u dong:mypasswd https://r2-registry-production.kissbug8720.workers.dev/v2/_catalog
重新部署
如果修改了 wrangler.toml 配置文件(例如:添加启用 Logs 的配置)
# wrangler.toml (wrangler v3.78.6^)
[observability]
enabled = true
可以重新执行命令部署
$ npx wrangler deploy --env production
启用日志后调试代码看日志很方便
使用 JWT 认证方式
这里官方仓库没有仔细介绍,看代码才捋顺通
需要注意
JWT 认证方式和用户名密码在当前 worker 实现上是二选一的,启用 JWT 后用户名密码认证将失效
生成并配置公钥
生成公钥密码对,详参:《再思 JWT 的使用场景和算法选择》,可直接运行以下命令
# 私钥
$ openssl ecparam -name prime256v1 -genkey -noout -out ec_private.pem
# 公钥
$ openssl ec -in ec_private.pem -pubout -out ec_public.pem
接下来使用在线工具:JWK Generator 将 ec_public.pem 的内容转换为 JWK 格式的 Key
粘贴到左侧,其它参数不用选择,点击 “Generate”
生成的结果在下方的 “PEM Generation Results”
将 JSON 格式的 Key 复制
{
"kty": "EC",
"kid": "5910d7df-3a81-4a08-acd0-5dcd58486ed2",
"crv": "P-256",
"x": "hBBteH7wFAkCjoEmBMrKM9_5XtxdJLGMXMDL3QaT7nY",
"y": "lSFOx774nFGYnV_vluA-Elp5Lv64uszu8pLzH7nOJU0"
}
借助在线工具:https://www.base64encode.org/ 将 JSON Key 使用 Base64 Encode 编码,这就得到了 JWK
ewogICJrdHkiOiAiRUMiLAogICJraWQiOiAiNTkxMGQ3ZGYtM2E4MS00YTA4LWFjZDAtNWRjZDU4NDg2ZWQyIiwKICAiY3J2IjogIlAtMjU2IiwKICAieCI6ICJoQkJ0ZUg3d0ZBa0Nqb0VtQk1yS005XzVYdHhkSkxHTVhNREwzUWFUN25ZIiwKICAieSI6ICJsU0ZPeDc3NG5GR1luVl92bHVBLUVscDVMdjY0dXN6dThwTHpIN25PSlUwIgp9
这个 Encoded JWK 就是 JWT_REGISTRY_TOKENS_PUBLIC_KEY
环境变量所需的内容
执行以下命令将 Key 应用设置到 Cloudflare
$ npx wrangler secret put JWT_REGISTRY_TOKENS_PUBLIC_KEY --env production <<EOF
ewogICJrdHkiOiAiRUMiLAogICJraWQiOiAiNTkxMGQ3ZGYtM2E4MS00YTA4LWFjZDAtNWRjZDU4NDg2ZWQyIiwKICAiY3J2IjogIlAtMjU2IiwKICAieCI6ICJoQkJ0ZUg3d0ZBa0Nqb0VtQk1yS005XzVYdHhkSkxHTVhNREwzUWFUN25ZIiwKICAieSI6ICJsU0ZPeDc3NG5GR1luVl92bHVBLUVscDVMdjY0dXN6dThwTHpIN25PSlUwIgp9
EOF
执行成功
生成用于认证的 JWT Token
Python3 代码示例(摘自:《再思 JWT 的使用场景和算法选择》),此处不多做介绍
import time
import jwt
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
# 定义常量
EC_PRIVATE_KEY_PATH = "./ec_private.pem"
EC_PUBLIC_KEY_PATH = "./ec_public.pem"
# 加载私钥
def load_private_key(private_key_path):
with open(private_key_path, "rb") as key_file:
private_key = serialization.load_pem_private_key(
key_file.read(),
password=None,
backend=default_backend()
)
return private_key
# 加载公钥
def load_public_key(public_key_path):
with open(public_key_path, "rb") as key_file:
public_key = serialization.load_pem_public_key(
key_file.read(),
backend=default_backend()
)
return public_key
# 生成 JWT 令牌
def generate_jwt_token(private_key, algorithm):
payload = {
"iat": int(time.time()), # 使用秒级时间戳
"username": "v0",
"capabilities": ["pull", "push"],
# "aud": "r2-registry-production.kissbug8720.workers.dev"
"iat": int(time.time()) # 使用秒级时间戳
}
return jwt.encode(payload, private_key, algorithm=algorithm)
# 验证 JWT
def verify_jwt_token(token, public_key, algorithm):
try:
decoded = jwt.decode(token, public_key, algorithms=[algorithm])
return decoded
except jwt.InvalidTokenError:
return None
# 主函数
def __main__():
algorithms = [
("ES256", EC_PRIVATE_KEY_PATH, EC_PUBLIC_KEY_PATH),
]
for algorithm, private_key_path, public_key_path in algorithms:
# 加载私钥和公钥
private_key = load_private_key(private_key_path)
public_key = load_public_key(public_key_path)
# 生成 JWT
jwt_token = generate_jwt_token(private_key, algorithm)
print(f"生成的 {algorithm} JWT:", jwt_token)
# 验证 JWT
decoded_payload = verify_jwt_token(jwt_token, public_key, algorithm)
if decoded_payload:
print(f"{algorithm} JWT 验证成功:", decoded_payload)
else:
print(f"{algorithm} JWT 验证失败")
print("---")
if __name__ == "__main__":
__main__()
输出
这里的 JWT 即使用 ec_private.pem 签名后的认证所需 Token,可以使用 Curl 请求 Registry 获取镜像列表进行权限验证(执行前注意替换 Token)
$ curl -X GET -u dong:<your-jwt> https://r2-registry-production.kissbug8720.workers.dev/v2/_catalog
输出
{"repositories":["alpine"]}
简单在说下 JWT 在此处的流程,我们将 JWK 格式的公钥提交到 Cloudflare 后,推送的代码从环境变量 JWT_REGISTRY_TOKENS_PUBLIC_KEY
可以获取到 Key,然后我们通过 Python 脚本借助私钥生成了 JWT,JWT 的参数中允许 PUSH 和 PULL 能力,签名后的 JWT 相当于密码,需要私密保存,请求 Cloudflare 时,Worder 代码中判断有 JWK 环境变量,就使用 JWT 的认证方式,使用签名验证这个 Token 是不是我们通过私钥签发的,校验没问题后读取出权限,进而允许 PUSH 和 PULL,完成认证过程
本例中的 Worker 代码是从 Github 下载下来的基础功能代码,遇到问题以代码为准,调试代码可以借助 Cloudflare Worker 中的 Logs 查看日志去定位解决
如需停用 JWT 认证,在 Cloudflare 控制台删除 JWT_REGISTRY_TOKENS_PUBLIC_KEY
环境变量即可
$ curl -X GET -u dong:mypasswd https://r2-registry-production.kissbug8720.workers.dev/v2/_catalog
配置镜像回源(未调试通过)
没有测试通过,暂也记录下过程,给后续尝试的人一个参考
从日志看登录到 DockerHub 成功,但后续的请求报错
设置 REGISTRY_TOKEN
这个是 DockerHub 的用户密码
$ npx wrangler secret put REGISTRY_TOKEN --env production
编辑配置文件,添加 REGISTRIES_JSON 配置,注意替换为你的 DockerHub 用户名,配置中的变量 REGISTRY_TOKEN 不要修改,代码回自动从刚设置的 REGISTRY_TOKEN 环境变量中获取
[env.production.vars]
REGISTRIES_JSON = "[{ \"registry\": \"https://registry.hub.docker.com\", \"password_env\": \"REGISTRY_TOKEN\", \"username\": \"kissbug8720\" }]"
DockerHub 的用户名密码可以使用以下命令登录进行验证确认
$ docker login https://registry.hub.docker.com
执行命令重新部署
$ npx wrangler deploy --env production
拉取镜像进行测试
$ export REGISTRY_URL=r2-registry-production.kissbug8720.workers.dev
$ docker rmi nginx:latest $REGISTRY_URL/nginx:latest
$ docker pull $REGISTRY_URL/nginx:latest
到这一步,也翻了会儿源码,暂时也就先研究到这里,兴许是个 BUG... 以后真用到再说
小小收获
测试 fallback 功能时发现 README.md 中的配置示例有误, 这不白给的 BUG! 感觉 PR 在向我招手🙋
赶紧提交了一个 PR(#63),一天后被合并,也许是近些年第一次提交 PR 同时也被 Merged,很开心