白嫖 Cloudflare R2 + Worker 搭建私有镜像仓库

Published: 2024-10-10

Tags: Cloudflare R2

本文总阅读量

前些天发现一个 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 Generatorec_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,很开心

参考