feat(infra/domains): 域名池 + CDN 前置 + 签名端点分发 (tsk_NU9JuUweHWMt)
- domains.md: 四组域名隔离登记 + 冷备池 ≥5 + 启用流程(不含身份信息) - cdn/terraform: Cloudflare 配置即代码(WAF/bot/速率限制/代理DNS/回源鉴权注入)+ 30min 重放 Runbook - server/internal/originauth: 回源鉴权中间件,非 CDN 网段或鉴权头不符一律 403,支持双值轮换 - tools/endpoint-signer: 离线 Ed25519 签名 CLI(端点 + 公告文档,单调版本防回滚,key_id 双公钥轮换) - tools/publish-mirrors: ≥3 镜像发布 + hash 一致性校验 + 故障转移取回 - CLIENT-CONTRACT.md: schema/验签/防回滚/合并/兜底链/channel 客户端契约 - 出站独立出口要求写入部署文档;私钥/token/身份信息一律不入库 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
# CDN 前置(Cloudflare)配置即代码
|
||||
|
||||
把一个域名在 30 分钟内接到 Cloudflare 之后:WAF + bot 管理 + 速率限制全开、DNS 代理回源、回源鉴权 header 注入。对应 doc/05 §2 与 §5「CDN 账号风险」Runbook。
|
||||
|
||||
## 资源清单
|
||||
|
||||
| 文件 | 资源 | 作用 |
|
||||
|---|---|---|
|
||||
| `versions.tf` | provider | 锁 `cloudflare ~> 4.40`;token 走 `CLOUDFLARE_API_TOKEN` 环境变量 |
|
||||
| `settings.tf` | `cloudflare_zone_settings_override` | TLS1.3 / SSL strict / HSTS / 高安全级 |
|
||||
| `dns.tf` | `cloudflare_record` | 代理(橙云)A 记录,隐藏源站 IP |
|
||||
| `waf.tf` | `cloudflare_ruleset` ×2 + `cloudflare_bot_management` | Managed+OWASP WAF、`/v1/*` 速率限制、Bot 管理 |
|
||||
| `origin.tf` | `cloudflare_ruleset`(late_transform) | 回源注入 `X-Origin-Auth` |
|
||||
| `outputs.tf` | — | 代理后的主机名清单 |
|
||||
|
||||
## 绝不入库的东西
|
||||
|
||||
- **API token**:`export CLOUDFLARE_API_TOKEN=...`(本机由 rbw 取;CI 由 Secret 注入)。
|
||||
- **回源鉴权值**:`export TF_VAR_origin_auth_value=...`。
|
||||
- `terraform.tfvars`、`*.tfstate`、`.terraform/`:均 gitignore。
|
||||
|
||||
## 30 分钟重放流程(CDN 账号风险应急)
|
||||
|
||||
> 目标:旧 CDN 账号被封 / 要求实名时,在新账号 + 新(或同)域名上 30 分钟内重建全部防护并切流量。
|
||||
|
||||
```bash
|
||||
# 0. 准备(离线已存):新账号 API token、zone_id、account_id、origin_ip、回源鉴权值
|
||||
export CLOUDFLARE_API_TOKEN=... # 新账号 token(rbw get)
|
||||
export TF_VAR_origin_auth_value=... # 当前回源鉴权值
|
||||
|
||||
cd infra/domains/cdn/terraform
|
||||
cp terraform.tfvars.example terraform.tfvars # 填 zone_id/account_id/origin_ip/api_hostnames
|
||||
|
||||
# 1. 初始化 + 计划 + 应用(首次约 3–8 分钟)
|
||||
terraform init
|
||||
terraform plan -out tf.plan
|
||||
terraform apply tf.plan
|
||||
|
||||
# 2. 在新账号把域名 NS 指过去(注册商处改 NS),等待生效
|
||||
# 用多镜像签名端点更新把新 API 域名/镜像下发给客户端(见 ../../tools)
|
||||
|
||||
# 3. 验证回源鉴权(应 200)与直连源站(应 403,见 server/internal/originauth)
|
||||
```
|
||||
|
||||
把每次重放的**实测耗时**记到本目录 `REPLAY-LOG.md`(验收要求 ≤30min)。
|
||||
|
||||
## 注意
|
||||
|
||||
- `cloudflare_bot_management` 需要账号套餐支持(Pro+/Bot Management),免费账号把 `enable_bot_management=false`,改用面板 Bot Fight Mode。
|
||||
- WAF managed ruleset 的 `id` 为 Cloudflare 公开稳定 ID(Managed / OWASP)。
|
||||
- 本目录无法在本仓库 CI 内 `apply`(需真实账号);提交前至少 `terraform fmt -check && terraform validate`。
|
||||
@@ -0,0 +1,15 @@
|
||||
# Proxied DNS records. proxied = true keeps the origin IP hidden behind the CDN
|
||||
# (doc/05 §2 源站 IP 隐藏). The origin must NEVER have had an unproxied A record
|
||||
# (historic DNS is the most common leak path).
|
||||
resource "cloudflare_record" "api" {
|
||||
for_each = toset(var.api_hostnames)
|
||||
|
||||
zone_id = var.zone_id
|
||||
name = each.value
|
||||
type = "A"
|
||||
content = var.origin_ip
|
||||
proxied = true
|
||||
ttl = 1 # 1 = automatic (required when proxied)
|
||||
|
||||
comment = "pangolin api/distribution endpoint (managed by terraform)"
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
# Inject the origin-auth header on every request the CDN forwards to the origin
|
||||
# (doc/05 §2 回源鉴权). The Go originauth middleware rejects any request lacking
|
||||
# this header or coming from a non-CDN source IP.
|
||||
#
|
||||
# The value is sensitive and supplied via TF_VAR_origin_auth_value; it is never
|
||||
# stored in the repo. During quarterly rotation the origin accepts both the old
|
||||
# and new value (Config.Previous), so apply the new value here first, then retire
|
||||
# the old one on the origin after propagation.
|
||||
resource "cloudflare_ruleset" "origin_auth" {
|
||||
zone_id = var.zone_id
|
||||
name = "pangolin-origin-auth"
|
||||
kind = "zone"
|
||||
phase = "http_request_late_transform"
|
||||
|
||||
rules {
|
||||
action = "rewrite"
|
||||
description = "Inject origin-auth header on origin requests"
|
||||
enabled = true
|
||||
expression = "true"
|
||||
|
||||
action_parameters {
|
||||
headers {
|
||||
name = var.origin_auth_header
|
||||
operation = "set"
|
||||
value = var.origin_auth_value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
output "proxied_hostnames" {
|
||||
description = "Hostnames now proxied through Cloudflare."
|
||||
value = [for r in cloudflare_record.api : r.name]
|
||||
}
|
||||
|
||||
output "origin_auth_header" {
|
||||
description = "Header name the origin must enforce (value is secret, not exported)."
|
||||
value = var.origin_auth_header
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
# Zone-wide security/TLS baseline (doc/05 §2 常规 Web 安全基线).
|
||||
resource "cloudflare_zone_settings_override" "this" {
|
||||
zone_id = var.zone_id
|
||||
|
||||
settings {
|
||||
ssl = "strict" # full (strict): verify origin cert
|
||||
min_tls_version = "1.2"
|
||||
tls_1_3 = "on"
|
||||
always_use_https = "on"
|
||||
automatic_https_rewrites = "on"
|
||||
opportunistic_encryption = "on"
|
||||
brotli = "on"
|
||||
security_level = "high"
|
||||
challenge_ttl = 1800
|
||||
browser_check = "on"
|
||||
|
||||
# HSTS (doc/05 §2 全站 HSTS).
|
||||
security_header {
|
||||
enabled = true
|
||||
max_age = 31536000
|
||||
include_subdomains = true
|
||||
preload = true
|
||||
nosniff = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
# Copy to terraform.tfvars and fill in. terraform.tfvars is gitignored.
|
||||
# NEVER put the API token or origin_auth_value in a committed file.
|
||||
#
|
||||
# export CLOUDFLARE_API_TOKEN=... # provider auth
|
||||
# export TF_VAR_origin_auth_value=... # secret origin-auth header value
|
||||
|
||||
zone_id = "__CF_ZONE_ID__"
|
||||
account_id = "__CF_ACCOUNT_ID__"
|
||||
origin_ip = "__ORIGIN_IP__"
|
||||
|
||||
api_hostnames = [
|
||||
"api.example.com",
|
||||
"sub.example.com",
|
||||
]
|
||||
|
||||
origin_auth_header = "X-Origin-Auth"
|
||||
|
||||
rate_limit_requests_per_minute = 120
|
||||
rate_limit_mitigation_seconds = 600
|
||||
enable_bot_management = true
|
||||
|
||||
# origin_auth_value is intentionally omitted here — set TF_VAR_origin_auth_value.
|
||||
@@ -0,0 +1,50 @@
|
||||
variable "zone_id" {
|
||||
description = "Cloudflare zone ID for the domain being onboarded."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "account_id" {
|
||||
description = "Cloudflare account ID (used by account-scoped resources)."
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "origin_ip" {
|
||||
description = "Origin server IP the CDN proxies to. Only the CDN ever talks to it; clients never see it (doc/05 §2)."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "api_hostnames" {
|
||||
description = "Proxied hostnames served by this zone (API pool / subscription / website mirror)."
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
variable "origin_auth_header" {
|
||||
description = "Header name the CDN injects on origin requests; the Go originauth middleware checks it."
|
||||
type = string
|
||||
default = "X-Origin-Auth"
|
||||
}
|
||||
|
||||
variable "origin_auth_value" {
|
||||
description = "Secret origin-auth value. Provide via TF_VAR_origin_auth_value env var — NEVER commit it. Rotated quarterly (doc/06 §6)."
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "rate_limit_requests_per_minute" {
|
||||
description = "Per-IP request budget for /v1/* before mitigation."
|
||||
type = number
|
||||
default = 120
|
||||
}
|
||||
|
||||
variable "rate_limit_mitigation_seconds" {
|
||||
description = "How long an offending IP stays blocked."
|
||||
type = number
|
||||
default = 600
|
||||
}
|
||||
|
||||
variable "enable_bot_management" {
|
||||
description = "Enable the cloudflare_bot_management resource (requires Pro+/Bot Management on the plan)."
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
terraform {
|
||||
required_version = ">= 1.5"
|
||||
|
||||
required_providers {
|
||||
cloudflare = {
|
||||
source = "cloudflare/cloudflare"
|
||||
version = "~> 4.40"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# The API token is read from the CLOUDFLARE_API_TOKEN environment variable.
|
||||
# NEVER hardcode the token here or in any *.tfvars file (doc/06 §2 红线).
|
||||
provider "cloudflare" {}
|
||||
@@ -0,0 +1,67 @@
|
||||
# WAF managed rulesets (doc/05 §2 WAF 全开).
|
||||
resource "cloudflare_ruleset" "waf_managed" {
|
||||
zone_id = var.zone_id
|
||||
name = "pangolin-waf-managed"
|
||||
kind = "zone"
|
||||
phase = "http_request_firewall_managed"
|
||||
|
||||
rules {
|
||||
action = "execute"
|
||||
description = "Cloudflare Managed Ruleset"
|
||||
enabled = true
|
||||
expression = "true"
|
||||
|
||||
action_parameters {
|
||||
# Cloudflare Managed Ruleset (stable well-known ID).
|
||||
id = "efb7b8c949ac4650a09736fc376e9aee"
|
||||
}
|
||||
}
|
||||
|
||||
rules {
|
||||
action = "execute"
|
||||
description = "Cloudflare OWASP Core Ruleset"
|
||||
enabled = true
|
||||
expression = "true"
|
||||
|
||||
action_parameters {
|
||||
# OWASP Core Ruleset (stable well-known ID).
|
||||
id = "4814384a9e5d4991b9815dcfc25d2f1f"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Rate limiting (doc/05 §2 速率限制全开). Per-IP budget on the API surface.
|
||||
resource "cloudflare_ruleset" "rate_limit" {
|
||||
zone_id = var.zone_id
|
||||
name = "pangolin-rate-limit"
|
||||
kind = "zone"
|
||||
phase = "http_ratelimit"
|
||||
|
||||
rules {
|
||||
action = "block"
|
||||
description = "Per-IP rate limit on /v1/*"
|
||||
enabled = true
|
||||
expression = "(starts_with(http.request.uri.path, \"/v1/\"))"
|
||||
|
||||
ratelimit {
|
||||
characteristics = ["ip.src", "cf.colo.id"]
|
||||
period = 60
|
||||
requests_per_period = var.rate_limit_requests_per_minute
|
||||
mitigation_timeout = var.rate_limit_mitigation_seconds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Bot management (doc/05 §2 bot 管理全开). Requires Bot Management / Super Bot
|
||||
# Fight Mode on the plan; toggle with enable_bot_management.
|
||||
resource "cloudflare_bot_management" "this" {
|
||||
count = var.enable_bot_management ? 1 : 0
|
||||
zone_id = var.zone_id
|
||||
|
||||
enable_js = true
|
||||
sbfm_definitely_automated = "block"
|
||||
sbfm_likely_automated = "managed_challenge"
|
||||
sbfm_verified_bots = "allow"
|
||||
sbfm_static_resource_protection = false
|
||||
optimize_wordpress = false
|
||||
}
|
||||
Reference in New Issue
Block a user