22 mins
4309 words
Loading views
一次到位:用脚本批量创建 Cloudflare 全权限 API 令牌(Git Bash 完整代码 + 五连坑排错实录)

Cloudflare 控制台里手动勾选几百个权限项太费劲?本文完整复盘用 API 一次性创建全权限令牌的过程:引导令牌准备、账户级/区域级/用户级三大作用域、每个下拉项自动取最高权限、policy 数量自动分块,并逐一排查 6007 编码、400 条数上限、1001 子令牌限制、9109 权限不足、6111 请求头格式五个报错,附最终版 Git Bash 脚本与错误码速查表。

在 Cloudflare 控制台创建自定义令牌时,账户、区域、用户三组权限下拉项加起来有 400 多个,逐个勾选到手酸,还容易漏勾。其实令牌创建页面背后就是一个 API——本文记录我把它完全脚本化的过程:从 Python 版改成 Git Bash 版,接连踩中 6007、400、1001、9109、6111 五个坑,最终一个脚本跑出包含 232 个权限项的「全功能」令牌。完整代码与排错思路都在文中,可直接抄走复用。

📑 目录h2


一、思路:控制台点到手酸,不如直接调 APIh2

需求很朴素:创建一个拥有所有编辑权限的功能令牌,不要遗漏任何一个下拉项。在控制台里意味着要对着几百个下拉框逐个点选,一次误触就得重来。而「创建令牌」页面本身就是在调 Cloudflare API,所以整件事可以翻译成四步:

  1. 拉权限组 —— 调 GET /user/tokens/permission_groups,一次拿到全部 413 个权限组
  2. 按作用域过滤 + 取最高权限 —— 账户级、区域级、用户级分开处理;同一下拉项里,有「编辑/Write」就取它,只有「读取/Read」就取读取(比如审计日志、分析这类天生只读的项),保证一项不漏
  3. 组装 policy —— 每条 policy 绑定一个资源范围和一批权限组,超上限就自动分块
  4. 一次性提交 —— POST /user/tokens 创建令牌

最终一次跑通的成绩单:232 个权限项、9 条 policy,覆盖账户级 + 区域级的全部可授予项,令牌值当场返回(只显示一次)。

二、动手前先搞懂三个概念h2

2.1 引导令牌:API 令牌不能凭空创建h3

想用 API 创建令牌,手里必须先有一个具备令牌管理权限的「引导令牌」,最小配置只需一行权限:

第一列第二列第三列
帐户API 令牌编辑

如果创建的令牌还要包含用户级权限,引导令牌再加一行「用户 → API 令牌 → 编辑」。引导令牌本身权限不小,用完建议立即删除。

2.2 三大作用域:policy 的地基h3

Cloudflare 的权限组按资源作用域分成三类,它们在请求体里的 resources 资源键完全不同,不能合并进同一条 policy:

作用域权限组过滤关键字resources 资源键实测权限项数量
账户级com.cloudflare.api.account{"com.cloudflare.api.account.*":"*"}181 项
区域级com.cloudflare.api.account.zone{"com.cloudflare.api.account.zone.*":"*"}51 项
用户级com.cloudflare.api.user{"com.cloudflare.api.user.<用户ID>":"*"}零星几项

另一个硬约束:单个令牌的 policy 总条数上限是 10 条(第五节坑二会讲到怎么撞上、怎么绕开)。所以脚本的分块策略是「按作用域各自成条、每条尽量装满、总数不超 10」。

2.3 账户 ID、用户 ID、成员 ID 是三个不同的东西h3

这是本次踩坑里最迷惑人的一环,先一次说清:

名称在哪看标识什么
账户 ID(Account ID)控制台页面 URL 里一个 Cloudflare 账户(组织),一个用户可以属于多个账户
用户 ID(User ID)右上角头像 → 我的个人资料,页面底部你这个登录人;com.cloudflare.api.user.<ID> 只认它
成员 ID(Member ID)账户成员列表成员关系记录,本文用不到

资源键里的用户 ID 千万不能填成账户 ID,否则键值不匹配会直接报错。另外,想通过 GET /user 接口自动取用户 ID,引导令牌还得有「用户 → 用户详细信息 → 读取」权限——没有的话接口返回 9109,脚本只能跳过用户级(坑四详解)。

三、最终版脚本(Git Bash)h2

3.1 依赖h3

  • Git Bash(自带 curl)
  • jq:Git Bash 默认没有,安装任选其一:
Terminal window
winget install jqlang.jq
# 或手动下载 jq-windows-amd64.exe,改名为 jq.exe,
# 放进 C:\Program Files\Git\usr\bin\,重开 Git Bash

装完执行 jq --version 确认可用。

3.2 完整脚本h3

保存为 create_token.sh。这一版吸收了后面五个坑的全部修复:请求体写文件再 --data-binary 发送(防编码)、policy 自动分块(防超限)、EXCLUDE_REGEX 排除不可授予项(防 1001)、用户 ID 直接写死(绕开 /user 权限依赖)、if 代替 &&(防 set -e 意外退出):

#!/usr/bin/env bash
# create_token.sh —— 在 Git Bash 中运行
set -euo pipefail
export LANG=C.UTF-8 LC_ALL=C.UTF-8
: "${CF_BOOT_TOKEN:?请先 export CF_BOOT_TOKEN=引导令牌}"
TOKEN_NAME="full-edit-token" # 建议英文,创建后可在控制台改名
INCLUDE_ACCOUNT=true # 包含账户级权限
INCLUDE_ZONE=true # 包含区域(Zone)级权限
INCLUDE_USER=true # 包含用户级权限
USER_ID="你的用户ID" # 用户 ID(不是账户 ID),控制台头像→我的个人资料可查
ALLOW_IPS='[]' # 例如 '["1.2.3.4/32"]',空数组为不限制
EXPIRES_ON="" # 例如 "2027-01-01T00:00:00Z",空为永不过期
MAX_POLICIES=10 # Cloudflare 单个令牌的 policy 上限
EXCLUDE_REGEX="API Tokens?" # 排除不可授予的权限组,如 "API Tokens?|Billing|Registrar",空为不排除
API="https://api.cloudflare.com/client/v4"
AUTH="Authorization: Bearer $CF_BOOT_TOKEN"
# 1. 拉取全部权限组
GROUPS_JSON=$(curl -s -H "$AUTH" "$API/user/tokens/permission_groups")
echo "$GROUPS_JSON" | jq -e '.success' >/dev/null \
|| { echo "获取权限组失败:"; echo "$GROUPS_JSON"; exit 1; }
echo "权限组总数: $(echo "$GROUPS_JSON" | jq '.result | length')"
# 2. 取某作用域下"每个下拉项的最高权限"的 id 列表
best_ids() {
local scope="$1"
echo "$GROUPS_JSON" | jq -c --arg scope "$scope" --arg ex "$EXCLUDE_REGEX" '
[ .result[]
| select(.scopes | index($scope))
| select(if $ex == "" then true else (.name | test($ex) | not) end)
| . as $g
| ($g.name | capture("^(?<base>.*?)\\s+(?<lvl>Read|Write|Edit)$")? // {base:$g.name, lvl:"Other"}) as $m
| {base:$m.base, id:$g.id,
rank:(if $m.lvl=="Write" or $m.lvl=="Edit" then 2 elif $m.lvl=="Read" then 1 else 3 end)}
]
| group_by(.base)
| map(max_by(.rank) | {id})
'
}
# 3. 收集各作用域的权限项
declare -a SCOPE_RES SCOPE_IDS
add_scope() { # $1=资源JSON $2=权限ID数组JSON
local n
n=$(echo "$2" | jq 'length')
if [ "$n" -eq 0 ]; then echo "跳过空作用域: $1"; return 0; fi
SCOPE_RES+=("$1"); SCOPE_IDS+=("$2")
}
if [ "$INCLUDE_ACCOUNT" = true ]; then
add_scope '{"com.cloudflare.api.account.*":"*"}' "$(best_ids com.cloudflare.api.account)"
fi
if [ "$INCLUDE_ZONE" = true ]; then
add_scope '{"com.cloudflare.api.account.zone.*":"*"}' "$(best_ids com.cloudflare.api.account.zone)"
fi
if [ "$INCLUDE_USER" = true ]; then
add_scope "{\"com.cloudflare.api.user.$USER_ID\":\"*\"}" "$(best_ids com.cloudflare.api.user)"
fi
# 4. 自动计算每条 policy 的容量,使总条数 <= MAX_POLICIES
N=${#SCOPE_RES[@]}
[ "$N" -eq 0 ] && { echo "没有可提交的权限作用域"; exit 1; }
for i in $(seq 0 $((N-1))); do
echo "作用域 $((i+1)): $(echo "${SCOPE_IDS[$i]}" | jq 'length') 个权限项 ${SCOPE_RES[$i]}"
done
if [ "$N" -gt "$MAX_POLICIES" ]; then
echo "作用域数量已超过 policy 上限,请拆分令牌"; exit 1
fi
CHUNK=1
while :; do
need=0
for i in $(seq 0 $((N-1))); do
c=$(echo "${SCOPE_IDS[$i]}" | jq 'length')
need=$(( need + (c + CHUNK - 1) / CHUNK ))
done
[ "$need" -le "$MAX_POLICIES" ] && break
CHUNK=$((CHUNK+1))
[ "$CHUNK" -gt 1000 ] && { echo "无法满足上限"; exit 1; }
done
echo "自动选定每条 policy 容量: $CHUNK(预计 $need 条 policy)"
# 5. 生成 policies
POLICIES='[]'
for i in $(seq 0 $((N-1))); do
P=$(jq -cn --argjson res "${SCOPE_RES[$i]}" --argjson ids "${SCOPE_IDS[$i]}" --argjson n "$CHUNK" '
[ range(0; ($ids|length); $n) as $k
| {effect:"allow", resources:$res, permission_groups:$ids[$k:$k+$n]} ]')
POLICIES=$(jq -cn --argjson a "$POLICIES" --argjson b "$P" '$a + $b')
done
echo "将提交 $(echo "$POLICIES" | jq 'length') 条 policy,共 $(echo "$POLICIES" | jq '[.[].permission_groups|length]|add') 个权限项"
# 6. 组装请求体(写入文件,避免 shell/编码问题)
jq -n --arg name "$TOKEN_NAME" --argjson p "$POLICIES" \
--argjson ips "$ALLOW_IPS" --arg exp "$EXPIRES_ON" '
{name:$name, policies:$p}
+ (if ($ips|length)>0 then {condition:{"request.ip":{in:$ips}}} else {} end)
+ (if $exp != "" then {expires_on:$exp} else {} end)' > body.json
jq -e . body.json >/dev/null && echo "body.json 合法,大小: $(wc -c < body.json) 字节"
# 7. 创建令牌
RESP=$(curl -s -X POST "$API/user/tokens" \
-H "$AUTH" -H "Content-Type: application/json; charset=utf-8" \
--data-binary @body.json)
if [ "$(echo "$RESP" | jq -r '.success')" = "true" ]; then
echo "创建成功,请立即保存(仅显示一次):"
echo "$RESP" | jq -r '.result.value'
rm -f body.json
else
echo "创建失败:"
echo "$RESP" | jq '.errors'
echo "请求体保留在 body.json 中以便排查"
echo "提示:若报错与用户级权限有关,把 INCLUDE_USER 改为 false 后重试"
exit 1
fi

3.3 脚本里的四个设计点h3

  1. 每个下拉项只取最高权限:jq 里用正则把权限组名剥掉 Read / Write / Edit 后缀,按基础名分组后取档位最高的那个——Write/Edit 记 2 分、Read 记 1 分、无后缀的项记 3 分原样保留。这样「DNS 编辑」不会和「DNS 读取」重复占坑,天生只读的「审计日志」也能保住它的读取权限。
  2. 排除机制:EXCLUDE_REGEX 默认排除 API Tokens,原因见坑三;以后遇到其他不可授予的组(账单、注册机构之类),往里追加即可。
  3. 自动分块:从容量 1 开始递增试算,找到「总 policy 条数 ≤ 10」的最小容量,分块最均匀,不会出现某条过肥。
  4. 失败时保留 body.json:报错时请求体原地保留,直接打开看就是最真实的排查现场;成功才删。

四、使用步骤h2

第 1 步:建引导令牌。 在控制台「创建令牌 → 创建自定义令牌」里勾好权限后复制令牌值:

第一列第二列第三列
帐户API 令牌编辑
用户(可选)API 令牌编辑

第 2 步:填用户 ID。 控制台右上角头像 → 我的个人资料,页面底部抄下用户 ID,替换脚本里的 USER_ID。不需要用户级权限就改成 INCLUDE_USER=false。

第 3 步:运行。

Terminal window
export CF_BOOT_TOKEN='cfut_这里粘贴真实引导令牌'
bash create_token.sh

第 4 步:保存令牌。 成功时的输出长这样(令牌值只显示这一次,立即收进密码管理器):

权限组总数: 413
作用域 1: 181 个权限项 {"com.cloudflare.api.account.*":"*"}
作用域 2: 51 个权限项 {"com.cloudflare.api.account.zone.*":"*"}
自动选定每条 policy 容量: 26(预计 9 条 policy)
将提交 9 条 policy,共 232 个权限项
body.json 合法,大小: 18871 字节
创建成功,请立即保存(仅显示一次):
cfut_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

五、踩坑实录:五个报错逐一击破h2

坑一:6007 Malformed JSON——Git Bash 的编码暗坑h3

第一次运行,权限组拉取正常(413 个)、policy 也生成了 12 条,提交却报:

[
{
"code": 6007,
"message": "Malformed JSON in request body"
}
]

原因:-d "$BODY" 把含中文的 JSON 按系统代码页(GBK)编码发出,服务器按 UTF-8 解析,直接判非法。

解决三件套:

  1. 请求体先用 jq 写入文件(jq 输出必然是 UTF-8);
  2. 发送改用 --data-binary @body.json,原样传字节,不做任何转码;
  3. Content-Type 头补上 charset=utf-8;令牌名干脆用英文(full-edit-token),创建后可在控制台改名,一劳永逸。

顺手加一道 jq -e . body.json 自检,提交前就能发现 JSON 是否合法。修完这关,报错从「服务器看不懂」变成了「服务器看懂了但不答应」——这是好事。

坑二:400——policy 条数超上限h3

编码修好,新的报错更有信息量:

policies must have a length lower than or equal to 10

原因:每条 policy 装 25 个权限组,233 个权限项被切成 11 条,超过 Cloudflare 单令牌 10 条 policy 的硬上限。而且账户级和区域级资源键不同、不能合并,各类作用域至少各占 1 条,余量比想象中紧。

解决:放弃固定分块大小,从 1 开始递增试算,找到满足「总条数 ≤ 10」的最小容量。233 项最终按每条 26 个分成 9 条,全部塞进上限以内。如果未来权限组更多、单条也报「权限组过多」,就把账户级和区域级拆成两个令牌分别创建。

坑三:1001——子令牌不允许管理令牌h3

{
"code": 1001,
"message": "sub-token is not allowed to have permissions to manage other tokens"
}

原因:Cloudflare 的安全设计——通过 API 创建的子令牌,不允许再携带「API 令牌」类的管理权限,防止令牌自我繁殖。账户级的 API Tokens、用户级的对应项都在此列,这是硬限制,绕不过去。

解决:EXCLUDE_REGEX="API Tokens?" 一条正则同时匹配单复数,把它们从提交列表里剔除。其他不可授予的组报错时把名称追加进去即可,例如 "API Tokens?|Billing|Registrar"。可以先用这条命令确认要排除哪些:

Terminal window
curl -s -H "Authorization: Bearer $CF_BOOT_TOKEN" \
https://api.cloudflare.com/client/v4/user/tokens/permission_groups \
| jq -r '.result[] | select(.name|test("token";"i")) | .name'

真需要令牌管理权限只有两条路:控制台手动勾选,或者 Global API Key 配合 X-Auth-Email / X-Auth-Key 请求头调用接口——后者权限太大,不建议长期使用。排除后权限项从 233 降到 232,脚本自动重新分块,一次通过。

坑四:9109——读不到用户 IDh3

{
"code": 9109,
"message": "Unauthorized to access requested resource"
}

9109 的含义是「令牌有效,但对这个资源没有权限」,我在两个场景撞见它:

场景 A:脚本调 GET /user 自动取用户 ID。该接口要求引导令牌具备「用户 → 用户详细信息 → 读取」,引导令牌没勾,于是只能跳过用户级。两个办法:

  • 推荐:控制台头像 → 我的个人资料,手动抄用户 ID 写进脚本的 USER_ID(本文最终版已内置);
  • 或者干脆 INCLUDE_USER=false——用户级只有「用户详细信息、通知」等零星几项,管理域名和账户资源完全够用。

场景 B:脚本第一步拉权限组就报 9109。这通常说明把刚创建的「全功能」令牌当成引导令牌用了——它恰恰被我们排除了 API 令牌权限,自然调不动令牌接口。换回有「帐户 → API 令牌 → 编辑」权限的引导令牌即可。

排查时先跑令牌验证,能快速分辨「令牌死了」还是「权限不够」:

Terminal window
curl -s -H "Authorization: Bearer $CF_BOOT_TOKEN" \
https://api.cloudflare.com/client/v4/user/tokens/verify | jq .

返回 "status": "active" 说明令牌本身没问题,剩下的就是权限范围的事。

坑五:6111——请求头格式无效h3

{
"code": 6003,
"message": "Invalid request headers",
"error_chain": [
{
"code": 6111,
"message": "Invalid format for Authorization header"
}
]
}

原因:6111 发生在鉴权之前,说明 Authorization 头后面的值根本不像个令牌。三种经典翻车:

  1. 把示例里的占位文字原样 export 了进去(真的会发生);
  2. 新开了终端窗口——export 只在当前窗口有效,脚本退出后变量就没了;
  3. 复制令牌时混进了换行或空格。

排查两连:

Terminal window
echo "长度: ${#CF_BOOT_TOKEN}"
echo "开头: ${CF_BOOT_TOKEN:0:5}"

正常长度约 53 个字符、开头是 cfut_。怀疑有隐藏字符就清一遍再验证:

Terminal window
CF_BOOT_TOKEN=$(echo "$CF_BOOT_TOKEN" | tr -d '\r\n ')
curl -s -H "Authorization: Bearer $CF_BOOT_TOKEN" \
https://api.cloudflare.com/client/v4/user/tokens/verify | jq .

六、错误码速查表h2

把五连坑浓缩成一张表,下次对着报错直接查:

错误码含义常见原因解决办法
6007请求体非法 JSONGit Bash 下含中文的 JSON 按 GBK 发出写入 UTF-8 文件 + --data-binary,Content-Type 加 charset=utf-8,令牌名用英文
400请求校验失败policy 超过 10 条上限自动分块(本文脚本已内置);仍超限就拆成两个令牌
1001子令牌不允许管理令牌权限组里含 API Tokens 类EXCLUDE_REGEX 排除;真需要只能控制台手动建
9109无权访问该资源令牌缺对应接口权限,或用错了令牌补引导令牌权限 / 手动填 USER_ID / 换回引导令牌
6111Authorization 头格式无效占位符没替换、新终端没重新 export、混入空白字符echo ${#CF_BOOT_TOKEN} 检查,tr -d 清理后重新 export
6003请求头无效(6111 的外层)同 6111同 6111
1000令牌无效令牌已被删除或复制不完整重新创建引导令牌

七、安全须知h2

全权限令牌 ≈ 把账户钥匙交出去。

  • 这枚令牌覆盖账单、域名、DNS 等几乎所有编辑权限,一旦泄露等于账户被完全控制。创建时强烈建议同时设置 ALLOW_IPS(来源 IP 白名单)和 EXPIRES_ON(过期时间),别贪「永不过期」的省事。
  • 令牌值只显示一次,收到立即进密码管理器;不要写进代码、不要提交进仓库、不要贴进任何对话或截图——所有在对话、截图、日志里出现过的令牌都应视为已泄露,立刻到控制台删除或滚动更换。
  • 引导令牌按最小权限创建(只勾 API 令牌编辑那一行),用完即删,不要留着「下次再用」。
  • 「子令牌不能管理令牌」是 Cloudflare 刻意的安全设计,别想着绕过;真有该需求就控制台手动建,或短期用 Global API Key 并及时轮换。

写在最后h2

回顾整条流水线:引导令牌 → 拉全量权限组 → 每个下拉项取最高权限 → 按作用域自动分块 → 请求体写文件防编码坑 → 一次性提交。从「一个一个勾到手酸」到「一条命令 30 秒出全功能令牌」,中间隔着的不是多高深的技术,而是把页面操作翻译成 API 的耐心,和五个报错一个个啃下来的排错记录。

我在实际跑通的这一轮里,用户级因为引导令牌权限不足被跳过,最终 232 项全是账户级 + 区域级——管理域名和账户资源完全够用;真需要用户级,按 2.3 节抄好用户 ID 填进去重跑就行。

以上脚本与排错过程均来自本机真实操作记录。如果你在别的错误码上卡住过,欢迎在评论区补充,我会陆续更新进速查表。


Author: 夜猫子Ai手记
Post: 一次到位:用脚本批量创建 Cloudflare 全权限 API 令牌(Git Bash 完整代码 + 五连坑排错实录)

Comments