Prometheus Remote Write 凭证管理方案¶
问题背景¶
TFRSManager 部署 VictoriaMetrics 接收来自多个 K8s 集群的 Prometheus remote_write 数据。 当前设计存在三个缺陷:
- Nginx 鉴权无 fail-closed 机制 — 若忘记配置 htpasswd 文件,VictoriaMetrics 暴露在公网
- Operator 侧凭证未定义 — CRD 无 remote_write 凭证字段,且原 spec 中凭证由 Operator 自行生成(单端无法完成双端鉴权)
- 磁盘明文存储密钥不安全 — htpasswd 文件与服务器登录权限同级,无法审计、无法轮换
整体架构¶
Infisical (密钥源,唯一真实来源)
/ | \
/ | \
TFRSManager 服务端 Admin API Operator (通过 CRD 注入)
(启动时拉取密钥 → (轮换触发) (读取 Secret → 注入 Prometheus)
动态生成 htpasswd
写入 tmpfs)
| |
Nginx (auth_basic Prometheus (remote_write)
引用 tmpfs 上的 htpasswd) basic_auth → Secret
| |
+---------- TLS (HTTPS) ----------------------+
metrics.turingfocus.cn/api/v1/write
核心原则:Infisical 是凭证的唯一真实来源(Single Source of Truth)
1. Nginx 侧加固方案¶
1.1 Fail-Closed 设计¶
目标:即使运维忘记配置 htpasswd 文件,VictoriaMetrics 也不会暴露。
# /etc/nginx/conf.d/metrics.turingfocus.cn.conf
# 上游:VictoriaMetrics 只监听 127.0.0.1,物理隔离
upstream victoriametrics {
server 127.0.0.1:8428;
}
server {
listen 443 ssl http2;
server_name metrics.turingfocus.cn;
ssl_certificate /etc/nginx/ssl/metrics.turingfocus.cn.pem;
ssl_certificate_key /etc/nginx/ssl/metrics.turingfocus.cn.key;
# === Fail-Closed 关键配置 ===
# htpasswd 文件不存在时,Nginx 启动就会报错并拒绝加载此 server block
# 效果:没有密码文件 = 服务不启动 = 无法访问
# 仅开放 remote_write 端点
location /api/v1/write {
auth_basic "VictoriaMetrics Remote Write";
auth_basic_user_file /run/secrets/vm-htpasswd; # tmpfs,非持久化磁盘
# 仅允许 POST(remote_write 只需要 POST)
limit_except POST {
deny all;
}
proxy_pass http://victoriametrics;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 10m;
}
# 拒绝所有其他路径(显式 deny,不依赖默认行为)
location / {
return 403;
}
}
server {
listen 80;
server_name metrics.turingfocus.cn;
return 301 https://$host$request_uri;
}
1.2 Fail-Closed 保障层次¶
| 层次 | 机制 | 效果 |
|---|---|---|
| L1 - Nginx 启动检查 | auth_basic_user_file 指向的文件不存在时 Nginx 拒绝启动 |
无密码文件 = 服务不可用 |
| L2 - VictoriaMetrics 绑定 | VictoriaMetrics 只监听 127.0.0.1:8428 |
即使 Nginx 出问题,外部也无法直连 |
| L3 - HTTP 方法限制 | limit_except POST { deny all; } |
防止 GET 读取数据 |
| L4 - 路径白名单 | 仅 /api/v1/write,其余 403 |
防止访问 VM 的查询/管理端点 |
1.3 htpasswd 生成脚本(由 deploy.sh 调用)¶
htpasswd 不再静态存放在磁盘上,而是服务启动时从 Infisical 动态拉取并写入 tmpfs:
#!/usr/bin/env bash
# scripts/generate-vm-htpasswd.sh
# 从 Infisical 拉取 remote_write 密码,生成 htpasswd 写入 tmpfs
# 由 deploy.sh 或 systemd ExecStartPre 调用
set -euo pipefail
HTPASSWD_DIR="/run/secrets"
HTPASSWD_FILE="${HTPASSWD_DIR}/vm-htpasswd"
INFISICAL_SECRET_PATH="/metrics"
INFISICAL_SECRET_NAME="REMOTE_WRITE_PASSWORD"
# 确保 tmpfs 目录存在(/run 本身就是 tmpfs)
mkdir -p "${HTPASSWD_DIR}"
# 从 Infisical CLI 拉取密码
PASSWORD=$(infisical secrets get "${INFISICAL_SECRET_NAME}" \
--path="${INFISICAL_SECRET_PATH}" \
--plain 2>/dev/null)
if [ -z "${PASSWORD}" ]; then
echo "[FATAL] 无法从 Infisical 获取 remote_write 密码,中止启动" >&2
exit 1
fi
# 生成 htpasswd(bcrypt 加密)
htpasswd -Bbn tfrs-prometheus "${PASSWORD}" > "${HTPASSWD_FILE}"
chmod 600 "${HTPASSWD_FILE}"
echo "[INFO] htpasswd 已生成到 ${HTPASSWD_FILE}"
2. Operator CRD 与 Helm Chart 改动¶
2.1 TFRClusterSpec 新增字段¶
remote_write 凭证属于集群级配置(同一集群内所有租户共享同一个 remote_write 端点),
因此放在 TFRClusterSpec 而非 TFRTenantSpec。
// tfrcluster_types.go
// TFRClusterSpec 新增 Metrics 字段
type TFRClusterSpec struct {
// ... 现有字段 ...
// Metrics 指标推送配置,用于将租户 Prometheus 数据推送到 TFRSManager VictoriaMetrics。
// Metrics configuration for pushing tenant Prometheus data to TFRSManager VictoriaMetrics.
// +kubebuilder:validation:Required
Metrics MetricsConfig `json:"metrics"`
}
// MetricsConfig 指标推送配置
// Metrics push configuration
type MetricsConfig struct {
// RemoteWrite Prometheus remote_write 配置
// Prometheus remote_write configuration
// +kubebuilder:validation:Required
RemoteWrite RemoteWriteConfig `json:"remoteWrite"`
}
// RemoteWriteConfig Prometheus remote_write 端点与认证配置
// Prometheus remote_write endpoint and authentication configuration
type RemoteWriteConfig struct {
// URL remote_write 端点地址
// remote_write endpoint URL
// 示例: "https://metrics.turingfocus.cn/api/v1/write"
// +kubebuilder:validation:Required
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:Pattern=`^https?://`
URL string `json:"url"`
// Username Basic Auth 用户名
// Basic Auth username
// +kubebuilder:validation:Required
// +kubebuilder:validation:MinLength=1
Username string `json:"username"`
// Password Basic Auth 密码(明文,Operator 将其写入 K8s Secret)
// Basic Auth password (plaintext, Operator writes it into K8s Secret)
// +kubebuilder:validation:Required
// +kubebuilder:validation:MinLength=1
Password string `json:"password"`
// ClusterID TFRSManager 数据库中的集群 ID,作为指标标签注入
// Cluster ID from TFRSManager database, injected as metric label
// +kubebuilder:validation:Required
// +kubebuilder:validation:MinLength=1
ClusterID string `json:"clusterID"`
}
关键决策:所有字段 Required,无默认值。 必须由部署人员显式注入,Operator 不自行生成任何凭证。
2.2 Operator Controller 改动¶
tfrtenant_controller.go 在创建 PrometheusOptions 时,从 TFRCluster 读取 metrics 配置:
// tfrtenant_controller.go - reconcile 中传递 remote_write 配置
prometheusOpts := tenant.PrometheusOptions{
TenantBaseOptions: baseOpts,
OtelCollectorEndpoint: otelEndpoint,
Resources: prometheusResources,
// 新增:从 TFRCluster 传入
RemoteWriteURL: cluster.Spec.Metrics.RemoteWrite.URL,
RemoteWriteUsername: cluster.Spec.Metrics.RemoteWrite.Username,
RemoteWritePassword: cluster.Spec.Metrics.RemoteWrite.Password,
ClusterID: cluster.Spec.Metrics.RemoteWrite.ClusterID,
}
PrometheusOptions 新增字段:
// options.go
type PrometheusOptions struct {
TenantBaseOptions
OtelCollectorEndpoint string
Resources *corev1.ResourceRequirements
// remote_write 配置(从 TFRCluster.spec.metrics 传入)
RemoteWriteURL string
RemoteWriteUsername string
RemoteWritePassword string
ClusterID string
}
prometheus.go 改动:
1. Prometheus ConfigMap 中添加 remote_write 段
2. 创建 tfrs-remote-write-credentials Secret(存放 username + password)
3. Prometheus Deployment 挂载该 Secret(通过环境变量或文件)
2.3 Helm Chart values.yaml (tfrcluster)¶
# chart/tfrcluster/values.yaml 新增
# 指标推送配置(必填)/ Metrics push configuration (required)
# 用于将租户 Prometheus 指标推送到 TFRSManager VictoriaMetrics
# For pushing tenant Prometheus metrics to TFRSManager VictoriaMetrics
metrics:
remoteWrite:
url: "" # 必填: "https://metrics.turingfocus.cn/api/v1/write"
username: "" # 必填: "tfrs-prometheus"
password: "" # 必填: 从 Infisical 获取后通过 --set 注入
clusterID: "" # 必填: TFRSManager 数据库中的集群 ID
2.4 Webhook 校验¶
为 TFRCluster 添加 Validating Webhook,在 CR 创建/更新时校验:
func (r *TFRCluster) ValidateCreate() (admission.Warnings, error) {
if r.Spec.Metrics.RemoteWrite.URL == "" {
return nil, fmt.Errorf("spec.metrics.remoteWrite.url 必须配置")
}
if r.Spec.Metrics.RemoteWrite.Password == "" {
return nil, fmt.Errorf("spec.metrics.remoteWrite.password 必须配置")
}
// ...
}
3. Infisical 集成与密钥轮换方案¶
3.1 Infisical 密钥布局¶
Infisical Project: tfrs
Environment: prod
Path: /metrics/
- REMOTE_WRITE_USERNAME = "tfrs-prometheus"
- REMOTE_WRITE_PASSWORD = "<随机生成的强密码>"
3.2 密钥流转全链路¶
Infisical (密钥存储)
│
├──① TFRSManager 服务器 deploy.sh
│ → infisical secrets get REMOTE_WRITE_PASSWORD --path=/metrics/
│ → htpasswd -Bbn ... > /run/secrets/vm-htpasswd (tmpfs)
│ → nginx -s reload
│
├──② K8s 集群部署时
│ → helm upgrade tfrcluster ... \
│ --set metrics.remoteWrite.password=$(infisical secrets get REMOTE_WRITE_PASSWORD --path=/metrics/ --plain)
│ → Operator 将密码写入各租户 namespace 的 K8s Secret
│ → Prometheus 引用 Secret 进行 remote_write
│
└──③ CI/CD Pipeline(CNB/GitHub Actions)
→ 使用 Infisical 集成自动注入环境变量
磁盘上不存储任何明文密钥。 htpasswd 写入 tmpfs(/run),重启即消失。K8s Secret 由 etcd 加密存储。
3.3 密钥轮换流程¶
触发条件¶
- 定期轮换(建议 90 天)
- 疑似泄露时立即轮换
- 人员变动时
轮换步骤(零停机)¶
Step 1: 在 Infisical 更新密码
↓
Step 2: TFRSManager 侧 — 重新运行 generate-vm-htpasswd.sh
→ 生成新 htpasswd(同时包含新旧密码,双条目过渡)
→ nginx -s reload(平滑重载,不中断连接)
↓
Step 3: Operator 侧 — 更新 TFRCluster CR
→ kubectl patch tfrcluster ... --type=merge \
-p '{"spec":{"metrics":{"remoteWrite":{"password":"NEW_PASSWORD"}}}}'
→ Operator reconcile → 更新所有租户 namespace 的 Secret
→ Prometheus 自动 reload(通过 --web.enable-lifecycle + ConfigMap 变更触发)
↓
Step 4: 验证所有集群 remote_write 正常后
→ 从 htpasswd 中移除旧密码条目
→ nginx -s reload
关键设计:双密码过渡期¶
htpasswd 支持多条目,在过渡期内同时保留新旧密码:
# /run/secrets/vm-htpasswd(过渡期)
tfrs-prometheus:$2y$05$NEW_HASH... # 新密码
tfrs-prometheus-old:$2y$05$OLD_HASH... # 旧密码(临时保留)
或更简洁 — 使用同一用户名的两个条目(htpasswd 会逐一匹配直到成功):
这确保了旧密码在所有集群更新完毕前仍然有效。
3.4 已部署资源的自动更新¶
当 TFRCluster CR 的 spec.metrics.remoteWrite.password 被 patch 后:
- Operator 自动 reconcile — TFRCluster spec 变化触发 reconcile
- Operator 遍历所有 TFRTenant — 更新每个 namespace 下的
tfrs-remote-write-credentialsSecret - Prometheus 感知变更 — 两种方式:
- 方式 A(推荐): Secret 挂载为文件,Prometheus 配置引用文件路径,kubelet 会自动同步 Secret 变更到 Pod 文件系统(默认 ~1min)
- 方式 B: Operator 在更新 Secret 后,调用 Prometheus
/-/reload端点触发配置热重载
# Prometheus remote_write 配置(引用 Secret 文件,自动感知轮换)
remote_write:
- url: "https://metrics.turingfocus.cn/api/v1/write"
basic_auth:
username_file: /etc/prometheus/remote-write-credentials/username
password_file: /etc/prometheus/remote-write-credentials/password
write_relabel_configs:
- source_labels: [__name__]
regex: 'llm_tokens|resource_utilization'
action: keep
使用 username_file / password_file(Prometheus 2.26+ 支持)而非内联值,
Prometheus 会在每次 remote_write 时重新读取文件,无需重启即可生效。
4. 实施清单¶
Phase 1: TFRSManager 侧(Nginx 加固 + Infisical 集成)¶
| 任务 | 文件/位置 | 说明 |
|---|---|---|
| 更新 Nginx 配置 | docs/nginx-victoriametrics.conf → 部署到服务器 |
加入 fail-closed、方法限制、路径白名单 |
| VictoriaMetrics 绑定 127.0.0.1 | docker-compose.prod.yml |
确认 8428 只绑定 127.0.0.1 |
| 创建 Infisical 密钥 | Infisical Web UI | /metrics/REMOTE_WRITE_PASSWORD |
| 编写 htpasswd 生成脚本 | scripts/generate-vm-htpasswd.sh |
从 Infisical 拉取,写入 tmpfs |
| 集成到 deploy.sh | scripts/deploy.sh |
启动前调用生成脚本 |
Phase 2: Operator 侧(CRD + Controller + Helm)¶
| 任务 | 文件 | 说明 |
|---|---|---|
| CRD 新增 MetricsConfig | api/infra/v1/tfrcluster_types.go |
Required 字段 |
| deepcopy 重新生成 | make generate |
|
| CRD manifest 重新生成 | make manifests |
|
| Webhook 校验 | api/infra/v1/tfrcluster_webhook.go |
校验必填字段 |
| PrometheusOptions 新增字段 | internal/controller/infra/tenant/options.go |
|
| Prometheus ConfigMap 加 remote_write | internal/controller/infra/tenant/prometheus.go |
|
| 创建 Secret + Volume 挂载 | internal/controller/infra/tenant/prometheus.go |
|
| Controller 传递参数 | internal/controller/infra/tfrtenant_controller.go |
从 TFRCluster 读取 |
| Helm values 新增 | chart/tfrcluster/values.yaml |
metrics.remoteWrite.* |
| Helm template 映射 | chart/tfrcluster/templates/tfrcluster.yaml |
|
| 单元测试 | internal/controller/infra/tenant/prometheus_test.go |
Phase 3: 密钥轮换自动化(可后续迭代)¶
| 任务 | 说明 |
|---|---|
| Admin API 密钥轮换接口 | POST /admin/metrics/rotate-credentials,自动执行双密码过渡 |
| Infisical Webhook | 密钥变更时自动通知 TFRSManager 刷新 htpasswd |
| 监控告警 | remote_write 失败率 > 0 时告警(基于 VM 自身 /metrics) |
5. 安全审计对照¶
| 风险 | 缓解措施 |
|---|---|
| 忘记配置密码 | Nginx 引用不存在的 htpasswd 文件拒绝启动(fail-closed) |
| 服务器被入侵读取密钥 | htpasswd 在 tmpfs(/run),不落盘;密钥源在 Infisical |
| CRD 未填密码就部署 | Webhook 校验 Required 字段,拒绝创建 |
| 密钥泄露 | Infisical 支持审计日志 + 版本历史;轮换流程支持零停机 |
| 中间人攻击 | remote_write 强制 HTTPS(URL 校验 ^https://) |
| 暴力破解 | htpasswd 使用 bcrypt($2y$);可配合 Nginx limit_req 限速 |
| 已部署资源密钥过期 | password_file 模式自动感知 Secret 变更,无需重启 Prometheus |