惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Threatpost
The Hacker News
The Hacker News
AWS News Blog
AWS News Blog
Spread Privacy
Spread Privacy
T
Tenable Blog
C
CERT Recently Published Vulnerability Notes
Cisco Talos Blog
Cisco Talos Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
S
Securelist
P
Privacy & Cybersecurity Law Blog
Know Your Adversary
Know Your Adversary
T
The Exploit Database - CXSecurity.com
Latest news
Latest news
D
Darknet – Hacking Tools, Hacker News & Cyber Security
I
Intezer
F
Fortinet All Blogs
Engineering at Meta
Engineering at Meta
Simon Willison's Weblog
Simon Willison's Weblog
The Register - Security
The Register - Security
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
L
Lohrmann on Cybersecurity
C
Cyber Attacks, Cyber Crime and Cyber Security
Microsoft Azure Blog
Microsoft Azure Blog
P
Proofpoint News Feed
H
Help Net Security
T
Threat Research - Cisco Blogs
D
DataBreaches.Net
S
Schneier on Security
Cyberwarzone
Cyberwarzone
Google DeepMind News
Google DeepMind News
P
Privacy International News Feed
S
Secure Thoughts
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Recorded Future
Recorded Future
C
Cybersecurity and Infrastructure Security Agency CISA
MyScale Blog
MyScale Blog
M
MIT News - Artificial intelligence
Stack Overflow Blog
Stack Overflow Blog
IT之家
IT之家
人人都是产品经理
人人都是产品经理
NISL@THU
NISL@THU
博客园 - Franky
T
Tor Project blog
G
GRAHAM CLULEY
博客园 - 【当耐特】
Jina AI
Jina AI
Security Archives - TechRepublic
Security Archives - TechRepublic
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
A
About on SuperTechFans
Hacker News - Newest:
Hacker News - Newest: "LLM"

东东's Blog

Memos: MacOS 下编译安装 Aseprite 脚本 日本关西系列|Day 2 京都的半日闲逛 美国Apple官网购买礼品卡订阅 ChatGPT Plus 北京・中关村森林公园(2026) 日本关西系列|Day 1 抵达临空城与大阪首日 Memos: 查询 Apple ID 注册时间 Memos: Ghostty 开箱即用配置 代码考古:用 gitcharts 挖掘 Git 仓库的演变轨迹 烹饪日记:香煎罗非鱼 Memos: 新时代程序员的顶级焦虑 Memos: 博客新增图文布局和轮播图效果支持 Memos: 最适合空气炸锅烤着吃的红薯品类 Memos: 记录「95分」好吃的‘小帅香菇面’ 妻子爷爷的‘朝鲜军功奖章’ Memos: 博客切换为 Shiki 代码高亮方案 Memos: 博客新增划线、重点及荧光笔效果支持 记录博客字体分包与字体子集化 Memos: 邻座吃饭的一家三口 人在囧途之哈囧 哈尔滨・乡村的冬季 查看香烟生产日期 哈尔滨・东北虎林园 Memos: 来自日本的 ndjp 提供免费的三级子域名 Memos: 刚听说 autojump, 真的好用 Memos: 体验 OpenCode + Superpowers + GPT 5.2 开发需求 Memos: 杰我睿爆雷 GoReleaser 自动发布 Go 镜像到 DockerHub & GitHub Release 初识 Volta & Corepack 前端版本管理工具 部署 Beszel 把 “小鸡们” 归拢起来 Memos: Claude Code in Action 中文版教程 2025 年度回顾 Memos: 体验 tanaos-text-anonymizer-v1 NER 模型 Memos: 查询 Google 账号注册时间 Memos: 关于 Z30 在室内摄像被手机降维打击这点儿事儿 阅读《我与地坛》 Memos: Ghostty + Neovim + LazyVim Memos: 找到 Cursor 运行巨慢的一个原因 Memos: 京东家政 哈尔滨灵活就业人员医保退休待遇申领条件 记地暖不热的维修过程 Memos: 赛博菩萨 Cloudflare 又挂了 AnyTLS 软件的配置与使用 阅读《在巴东》 Memos: Web Archive 暂时离线 忆时光:十五年前我的家(动迁前夕) macOS 系统部署 Valkey 集群模式 阅读《一个名叫欧维的男人决定去死》 Memos: Cursor 服务故障部分功能不可用 阅读《丰乳肥臀》 爱人回家送奶奶 Memos: AWS 美东可用区 P0 故障(us-east-1) 2025 北京社保下限上调|个体户缴费随之上涨 铁锅重生记 不锈钢盆与放心水源改造计划 阅读《不被大风吹倒》 基于 Supabase 构建示例应用(中篇):实现 Vue 前端页面 基于 Supabase 构建示例应用(上篇):数据库与接口 阅读《三体》之地球往事 Oracle Free 实例重装系统 非京籍个体户缴纳社保(补充):“无有效的汇总预处理信息” 解决办法 阅读《芯片简史》 阅读《简约至上:交互式设计四策略(第2版)》 阅读《审判》 阅读《统计数字会撒谎》 达达秒送骑士 日本关西系列|在动物园前站找到海南本线 观影《长安的荔枝》 Memos: 记录两个在线工具 地球 Online:外卖骑手体验报告 杜师傅夜话:附身与归途 日本关西系列|使用投放硬币的行李寄存箱 日本关西系列|将多余零钱充值到西瓜卡 日本关西系列|网上购买大阪往返白滨高速巴士 乌鲁木齐・赛里木湖 “778 老哥” 摄影摘选(转载) 使用 Restic 来备份重要数据 Backing Up Important Data with Restic Sauvegarder des données importantes avec Restic Resticで重要なデータをバックアップする Cursor 开发 Obsidian 插件记录 非京籍个体户缴纳社保(十):新增并缴纳个人所得税-工资薪金 非京籍个体户缴纳社保(九):公积金开户增员与缴费 非京籍个体户缴纳社保(八):税务申报与工商年报 非京籍个体户缴纳社保(七):社保费用申报与缴纳 非京籍个体户缴纳社保(六):医保公共服务平台 - 增员确认 非京籍个体户缴纳社保(五):北京电子税务局 - 税务报道 非京籍个体户缴纳社保(四):北京市社会保险网上服务平台 - 增员与社保卡领取 非京籍个体户缴纳社保(三):社会保险网上服务平台 - 单位信息登记 非京籍个体户缴纳社保(二):北京 e 窗通平台提交申请 非京籍个体户缴纳社保(一):概览与先期准备 养老保险零基础入门指南(速通版) 了解北京门诊看病工会“二次报销”互助金 注册 US.KG 免费域名(dpdns.org) 再思 JWT 的使用场景和算法选择 黑龙江・木兰县属小村落的星空(2024) Nginx 启用 HTTPS/3 优化网站的 SSL Labs 总体评级为 A+(禁用旧协议 & 启用 HSTS) 了解 OCSP Stapling 证书吊销验证机制 山东・烟台中秋两三日(2024) 分享改造后的博客发布流程和访问链路 边缘网络:白嫖 Cloudflare R2 博客图床(DNS 国内外分流)
白嫖 Cloudflare R2 + Worker 搭建私有镜像仓库
2024-10-10 · via 东东's Blog

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

参考