把 Grafana 安全发布到公网:用 Caddy、Docker 与最小暴露面配置 HTTPS

Grafana 部署在服务器后,最直接的远程访问方式是把容器的 3000 端口映射到公网,例如 3000:3000。这种配置虽然简单,却把应用服务直接暴露给互联网:流量默认没有 TLS 保护,扫描器可以直接识别端口,登录入口也缺少统一的访问策略。即使 Grafana 自身启用了账号密码,公网链路、证书更新、域名跳转和安全响应头仍需要单独处理。

更稳妥的结构是只让反向代理接收公网流量,由它完成 HTTPS 和证书管理;Grafana 仅接入 Docker 内部网络,不发布宿主机端口:

text 复制代码
浏览器
  │ HTTPS :443
  ▼
Caddy 反向代理
  │ HTTP(Docker 私有网络)
  ▼
Grafana :3000

这套结构解决的是传输安全和暴露面问题,不等于让 Grafana 自动具备完整的互联网安全能力。管理员仍需维护强密码、账号权限、插件更新、日志审计和数据备份。对于只供少量固定人员使用的面板,VPN 或零信任访问网关通常比直接公网发布更容易收紧边界。

核心原理

TLS 终止与反向代理

浏览器与 Caddy 建立 TLS 连接,证书校验、HTTPS 重定向和加密都发生在这一层。Caddy 解密请求后,通过容器网络把请求转发给 Grafana。Grafana 无须自己读取证书私钥,也不必直接监听公网地址。

反向代理会传递原始主机名、协议和客户端地址等信息。Grafana 需要知道自己的外部地址,否则登录跳转、邮件链接或子路径资源可能指向内部的 http://grafana:3000。因此必须明确配置 domain 与 root_url。

最小暴露面

Docker Compose 中的 expose 只用于声明容器内部端口,不会像 ports 那样将端口发布到宿主机。Grafana 与 Caddy 加入同一个私有网络后,Caddy 可以通过服务名 grafana 访问 3000 端口,而公网只能连接 Caddy 发布的 80 和 443 端口。

80 端口通常用于 HTTP 到 HTTPS 跳转,也可能参与证书签发验证。若采用 DNS 验证或已有外部负载均衡器,端口要求会不同,需按实际证书方案调整。

自动证书的前提

要让 Caddy 自动申请公开可信的证书,至少需要满足以下条件:

  1. 域名的 A 或 AAAA 记录正确指向服务器公网地址。
  2. 公网能够访问服务器的 80 和 443 端口。
  3. 域名不是仅在局域网解析的内部名称。
  4. Caddy 的数据目录可以持久化,以保存证书和账户信息。

若服务器位于 NAT 后面,还需要在路由器或云负载均衡器上完成端口转发。只有内网地址且无法入站访问时,应改用 VPN、隧道或 DNS 验证方案,不能假设 HTTP 证书验证一定成功。

部署步骤

1. 准备域名与目录

假设使用 grafana.example.com,先在 DNS 控制台添加解析记录,再创建部署目录:

bash 复制代码
mkdir -p grafana-public
cd grafana-public

不要直接照搬示例域名。替换后可用以下命令检查解析结果:

bash 复制代码
dig +short grafana.example.com A
dig +short grafana.example.com AAAA

如果配置了 AAAA 记录,必须确保 IPv6 路由和防火墙同样可用;否则客户端可能优先连接不可达的 IPv6 地址。

2. 设置环境变量

在当前终端设置域名、镜像版本和初始管理员密码。版本号应选择经过团队验证的 Grafana 版本,本文不假定某个版本始终是最新版本:

bash 复制代码
export GRAFANA_DOMAIN='grafana.example.com'
export GRAFANA_VERSION='填写经过验证的版本号'
export GRAFANA_ADMIN_PASSWORD="$(openssl rand -base64 32)"

环境变量只对当前 shell 及其子进程有效。若通过 CI/CD 部署,应从秘密管理系统注入管理员密码,避免写入 Compose 文件、Git 仓库或命令历史。初始密码也不应作为长期共享凭据,首次登录后应建立实名账号并限制管理员数量。

3. 编写 Compose 配置

创建 compose.yaml:

yaml 复制代码
services:
  grafana:
    image: grafana/grafana:${GRAFANA_VERSION:?set GRAFANA_VERSION}
    restart: unless-stopped
    environment:
      GF_SERVER_DOMAIN: ${GRAFANA_DOMAIN:?set GRAFANA_DOMAIN}
      GF_SERVER_ROOT_URL: https://${GRAFANA_DOMAIN}/
      GF_SERVER_ENFORCE_DOMAIN: "true"
      GF_SECURITY_ADMIN_USER: admin
      GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:?set GRAFANA_ADMIN_PASSWORD}
      GF_SECURITY_COOKIE_SECURE: "true"
      GF_SECURITY_COOKIE_SAMESITE: strict
      GF_USERS_ALLOW_SIGN_UP: "false"
      GF_AUTH_ANONYMOUS_ENABLED: "false"
    volumes:
      - grafana-data:/var/lib/grafana
    expose:
      - "3000"
    networks:
      - monitoring

  caddy:
    image: caddy:2
    restart: unless-stopped
    depends_on:
      - grafana
    environment:
      GRAFANA_DOMAIN: ${GRAFANA_DOMAIN:?set GRAFANA_DOMAIN}
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy-data:/data
      - caddy-config:/config
    networks:
      - monitoring

networks:
  monitoring:
    driver: bridge

volumes:
  grafana-data:
  caddy-data:
  caddy-config:

这里没有为 Grafana 配置 ports,因此 服务器IP:3000 不应成为访问入口。443/udp 用于支持 HTTP/3;若网络策略不允许 UDP 443,可删除这一映射,常规 HTTPS 仍可通过 TCP 443 工作。

4. 配置 Caddy

创建 Caddyfile:

caddyfile 复制代码
{$GRAFANA_DOMAIN} {
    encode zstd gzip

    header {
        X-Content-Type-Options nosniff
        Referrer-Policy strict-origin-when-cross-origin
        -Server
    }

    reverse_proxy grafana:3000
}

不建议在不了解面板嵌入需求时直接添加严格的 Content-Security-Policy 或 X-Frame-Options。这类响应头可能影响 Grafana 插件、外部认证或 iframe 嵌入,应在测试环境根据实际功能逐项收紧。

先检查 Compose 展开结果和 Caddy 配置:

bash 复制代码
docker compose config
docker compose run --rm caddy caddy validate --config /etc/caddy/Caddyfile

docker compose config 的输出可能包含展开后的管理员密码,不要把完整输出上传到工单或公开日志。

5. 启动并检查状态

bash 复制代码
docker compose up -d
docker compose ps
docker compose logs --tail=100 caddy
docker compose logs --tail=100 grafana

然后验证 HTTP 跳转、证书和 Grafana 健康接口:

bash 复制代码
curl -I "http://${GRAFANA_DOMAIN}"
curl -I "https://${GRAFANA_DOMAIN}/login"
curl -fsS "https://${GRAFANA_DOMAIN}/api/health"
openssl s_client -connect "${GRAFANA_DOMAIN}:443" \
  -servername "${GRAFANA_DOMAIN}" </dev/null

不要把固定状态码写进自动化判断而忽略跳转差异。更可靠的验收项是:HTTP 最终转到 HTTPS;证书主机名匹配且链路验证通过;登录页资源可以加载;健康接口可访问;外部访问无法连接 3000 端口。

6. 收紧主机防火墙

云安全组和主机防火墙应只开放实际需要的入口。下面是使用 UFW 的示例,执行前必须确认 SSH 端口,避免将自己锁在服务器外:

bash 复制代码
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw enable
sudo ufw status verbose

如果 SSH 不是 22 端口,应替换为真实端口。数据库、Grafana 3000 端口以及 Docker 管理接口都不应面向公网开放。

上线后的安全与运维

公网登录入口会持续遭遇扫描和口令尝试。至少应完成以下工作:

  • 禁止匿名访问与自助注册,按团队职责分配 Viewer、Editor 和 Admin 权限。
  • 数据源账号采用只读或任务所需的最小权限,不要把数据库超级用户交给 Grafana。
  • 定期更新 Grafana、Caddy 和插件;更新前阅读对应版本说明并备份数据。
  • 监控反向代理访问日志、Grafana 登录事件和异常状态码。
  • 将管理入口限制到 VPN、固定出口 IP 或统一身份认证系统,条件允许时启用多因素认证。
  • 对 /var/lib/grafana 对应的数据卷做备份,并实际演练恢复。

仅备份 Compose 文件无法恢复仪表盘、用户、告警和本地配置。使用外部数据库保存 Grafana 元数据时,还要按照数据库自身的一致性要求备份,不能只复制容器目录。

可以先停止 Grafana 再归档本地数据卷,以减少写入中的不一致风险。具体卷名由 Compose 项目名决定,应通过 docker volume ls 确认,不能机械套用示例名称。对可用性要求较高的环境,应采用与存储后端匹配的在线备份方案。

常见问题

证书申请失败

先检查 DNS 是否解析到当前服务器,再确认 80、443 端口可以从公网访问。若日志显示连接到了错误地址,常见原因是旧 A 记录、不可达的 AAAA 记录、NAT 未转发或云安全组未放行。频繁重试可能触发证书机构的速率限制,修正网络问题后再重启服务。

页面能打开,但登录后反复跳转

重点检查 GF_SERVER_ROOT_URL 是否为真实的 HTTPS 外部地址,浏览器是否仍保留旧域名 Cookie,以及上游是否还有另一层代理改写协议。如果 TLS 在更外层的负载均衡器终止,还需确保代理链正确传递原始协议,且不要让多个代理互相执行冲突的重定向。

面板中的 WebSocket 或实时功能异常

Caddy 的 reverse_proxy 默认能够处理 WebSocket 升级,一般不需要手写 Upgrade 头。若前面还有 CDN、WAF 或负载均衡器,应逐层确认它们是否允许长连接和协议升级。排查时同时查看浏览器开发者工具与各层代理日志。

访问域名得到 502

502 通常表示 Caddy 无法连接上游。检查 Grafana 容器是否运行、两个服务是否加入同一网络,以及上游名称是否仍为 grafana:3000:

bash 复制代码
docker compose ps
docker compose logs --tail=200 grafana
docker compose exec caddy wget -qO- http://grafana:3000/api/health

如果最后一条命令使用的镜像环境不包含 wget,可改用同网络中的临时诊断容器,不应据此判断 Grafana 一定故障。

能否把 Grafana 放在 /grafana/ 子路径

可以,但要同时设置 GF_SERVER_ROOT_URL=https://域名/grafana/ 和 GF_SERVER_SERVE_FROM_SUB_PATH=true,并在反向代理中正确处理路径。子路径部署还可能影响 OAuth 回调、插件资源和外部链接,配置完成后需要覆盖登录、静态资源、分享链接及告警链接等场景。没有共享单域名的硬性需求时,独立子域名通常更简单。

总结

将 Grafana 开放到公网的关键不是增加一个端口映射,而是建立明确的边界:Caddy 负责公网 HTTPS 和证书生命周期,Grafana 留在容器私有网络,并通过正确的外部地址配置生成链接和 Cookie。部署完成后还要验证 DNS、跳转、证书、健康接口和 3000 端口的不可达性。

这套方案降低了直接暴露应用端口的风险,但最终安全性仍取决于账号体系、最小权限、网络限制、更新节奏和备份恢复能力。对于内部监控系统,应优先判断它是否真的需要面向整个互联网;能够通过 VPN 或受控身份入口访问时,进一步缩小可访问范围通常更合适。

相关推荐
gnip1 小时前
GetX 在 Flutter 中的使用:两种视图管理方式解析
前端·flutter
IT_陈寒1 小时前
Vue这个响应式陷阱我竟然踩了3次
前端·人工智能·后端
计算机魔术师1 小时前
AI智能体未经授权闯入政府网站,首例背后是什么?
前端
风骏时光牛马1 小时前
AI Bug快速定位与根因分析
前端
IT_陈寒1 小时前
Redis雪崩把我坑惨了,三招教你躲过去
前端·人工智能·后端
ikoala1 小时前
同样叫 Harness,DeepSeek Harness 和 Pi 根本不在同一层
前端·后端·ai编程
kyriewen1 小时前
我打回了 AI 写的 PR:新立 3 条规矩,第 1 条就有争议
前端·程序员·ai编程
默_笙1 小时前
🚗 把小说装进数据库了:我的第一个 RAG,和它的五个硬伤
前端·javascript
淸湫1 小时前
uni-app 微信小程序计算顶部区域(自定义导航栏、跨端适配)
前端