跳转至

Prometheus Remote Write 凭证管理方案

问题背景

TFRSManager 部署 VictoriaMetrics 接收来自多个 K8s 集群的 Prometheus remote_write 数据。 当前设计存在三个缺陷:

  1. Nginx 鉴权无 fail-closed 机制 — 若忘记配置 htpasswd 文件,VictoriaMetrics 暴露在公网
  2. Operator 侧凭证未定义 — CRD 无 remote_write 凭证字段,且原 spec 中凭证由 Operator 自行生成(单端无法完成双端鉴权)
  3. 磁盘明文存储密钥不安全 — 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 会逐一匹配直到成功):

tfrs-prometheus:$2y$05$NEW_HASH...
tfrs-prometheus:$2y$05$OLD_HASH...

这确保了旧密码在所有集群更新完毕前仍然有效。

3.4 已部署资源的自动更新

当 TFRCluster CR 的 spec.metrics.remoteWrite.password 被 patch 后:

  1. Operator 自动 reconcile — TFRCluster spec 变化触发 reconcile
  2. Operator 遍历所有 TFRTenant — 更新每个 namespace 下的 tfrs-remote-write-credentials Secret
  3. Prometheus 感知变更 — 两种方式:
  4. 方式 A(推荐): Secret 挂载为文件,Prometheus 配置引用文件路径,kubelet 会自动同步 Secret 变更到 Pod 文件系统(默认 ~1min)
  5. 方式 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