作者:来自 Elastic Adrian Chen, Vu Pham
使用 Elastic Workflows、Synthetics 和 Osquery 自动化 TLS 证书监控:检测即将过期的证书、进行轮换,并在无需人工干预的情况下完成验证。
证书过期是唯一一种生产环境故障类别,它在签发时就已经 “写明了自己的结束日期”。这里没有异常需要检测,没有模型需要训练,也没有未知因素。然而,过期证书仍然会每个季度导致支付网关、内部 API 以及可观测性栈宕机,因为 “知道证书会过期” 和 “实际采取行动” 之间的鸿沟,仍然依赖表格、日历提醒,以及刚好有人注意到。
本文将填补这一鸿沟:通过 Elastic Synthetics、Osquery、ES|QL 和 Elastic Workflows(在 9.4 中正式可用),构建一个闭环 TLS 证书监控流水线 —— 可以检测即将过期的证书、定位所有受影响主机、完成轮换,并进行验证,全程无需人工干预。
该设计背后的推理(信任缺口、修复鸿沟,以及贯穿整个流程的 sense → think → act → verify 循环)在《自动化可靠性:自愈企业的架构》中有详细说明。建议先阅读那一篇来理解 “为什么”。证书是这一模式的理想首个场景,因为信号明确、修复动作清晰,而且自动化本身的影响范围足够小,可以在建立信任后再扩展到更复杂、更噪声的领域。
完整源码(synthetics journey、workflow YAML 示例、ES|QL 告警查询示例、ingest pipeline,以及一个短期证书本地实验环境)都在 aiops-synthetics-lab 仓库中。本文讲的是模式,仓库提供完整实现。
将构建映射到闭环
闭环的每个阶段都对应一个具体的 Elastic 组件,以及伴随仓库中的一个对应工件。下表是快速参考;随后的图展示了数据如何在各阶段之间流动。
| 阶段 | 组件 | 在仓库中的位置 |
|---|---|---|
| Sense | Synthetics 浏览器 journeys + Osquery 证书 pack | journeys/,helpers/tls.ts,docs/ingest-pipeline-synthetics-browser.json |
| Think | 针对 synthetics-* 的 ES | QL 告警规则(按 CN + tags 去重) |
| Act | 智能证书轮换升级与修复 workflow | docs/elastic-samples/workflows/01-...yaml |
| Verify | Synthetics 重新读取已轮换证书链;canary curl_certificate 验证 fleet | docs/elastic-samples/workflows/02-...yaml,local-lab/ |
发现:选择合适的两种方法
用于发现证书的方法有四种,但如下表所示,其中两种方法可以最快实现价值回报。
| 方法 | 机制 | 结论 |
|---|---|---|
| 已知端点 | Playwright 浏览器测试验证服务端证书链 | ✅ 验证客户端实际看到的内容,包括中间证书 |
| OS 探测 | Osquery 查询本地主机 keystore | ✅ 可发现后端服务上的证书,而这些服务可能从未被 synthetics 访问过 |
| CA 日志轮询 | 监控证书颁发机构(CA)签发日志 | ✗ 噪声较大 —— 证书频繁签发但从未真正部署 |
| 网络发现 | 子网扫描器提取 TLS 握手信息 | ✗ 触发 IDS,影响传统 OT 环境,与安全团队存在摩擦 |
端点 synthetics 提供客户端视角。Osquery 提供主机视角。一个负载均衡器可以对外提供有效的公共证书,但下游的内部主机可能仍在向其对等节点提供已过期的证书 —— 从外部完全不可见,但在 keystore 中完全可见。你需要两者结合。
图2:Synthetics 验证客户端看到的内容;Osquery 枚举主机持有的内容。两者都映射到 ECS 的 tls.server.x509.*,因此 workflow 可以在无需转换的情况下基于 common_name 进行关联。
左侧(黄色)展示基于 agent 的监控:Elastic Agent 在每台主机上运行,并使用 Osquery certificates pack 读取本地操作系统的 keystore——Linux 上的 /etc/ssl,以及 Windows 上的 CurrentUser 和 LocalMachine 存储。这是主机视角:无论服务是否暴露公网端点,主机实际持有的证书都能被看到。
右侧(蓝色)展示无 agent 监控:Elastic Synthetics 对可访问的 HTTPS 端点执行浏览器旅程,并像客户端一样捕获完整证书链。这是客户端视角。
两条路径都会将输出规范化为 ECS 的 tls.server.x509.* 字段,因此 workflow 可以直接基于 common_name 进行关联,无需任何格式转换。
架构概览
该架构由四个水平层组成,每一层对应闭环中的一个阶段。
信号采集(顶部)展示三种数据源:Synthetics 是主要的告警驱动源;Osquery 被 workflow 用于主机发现;canary curl_certificate 用于按需验证影响范围。
告警层在 synthetics-* 上运行 ES|QL 规则,当证书进入过期窗口时触发。
智能升级与修复层即 workflow 本身:首先进行预检严重性映射,然后通过 Osquery 进行主机发现,接着根据不同情况分支处理 —— 7 天内过期的证书自动轮换,7–90 天窗口创建 Jira 工单,任何需要人工介入的情况则通过 PagerDuty 通知。
影响验证层按需运行 canary workflow,在任何轮换操作前后测量影响范围(blast radius)。
图3:四个层级,一个闭环。Synthetics 触发告警,Osquery 映射目标主机,而 canary 验证用于衡量影响范围。
前置条件
| 要求 | 说明 |
|---|---|
| Elastic Stack 9.3+ | ES |
| 通过 Fleet 部署 Elastic Agent | 主机需启用 Osquery Manager 集成 |
| Elastic Synthetics | 需要一个 Synthetics 项目以及私有或托管 location |
| 网络出站能力 | 集群需可访问 Ansible Tower、Jira、Slack、PagerDuty 等连接器 |
| Node.js 18 LTS+ | 用于本地运行 Synthetics 项目 |
感知(Sense):信号采集
克隆配套仓库即可获取本文中使用的完整工件集合:Synthetics 浏览器 journeys、Osquery pack 配置、Elastic Workflow YAML 定义、ES|QL 告警查询、ingest pipeline 定义,以及用于在短期证书环境中测试的本地实验环境。
执行 npm ci 会安装在本地运行浏览器 journeys 所需的 Node.js 依赖。
`
1. git clone https://github.com/adrianchen-es/aiops-synthetics-lab.git
2. cd aiops-synthetics-lab
3. npm ci
`AI写代码
CSV 驱动的端点
端点通过一个 CSV 文件进行管理,因此新增主机不需要任何代码修改。只需编辑 journeys/tls-browser/tls-target-hosts.csv:
`
1. host,criticality,assertionText,assertionSelector
2. elastic.co,critical,Elastic,h1
3. internal-api.mycompany.com,high,,
4. payment-gateway.internal,critical,,
`AI写代码
criticality 会变成一个 journey 标签(criticality:critical),并随之进入告警 payload,用于驱动升级与路由策略 —— 基础分诊无需 CMDB 查询。
npm run generate:tls-targets 会从 CSV 重建 TypeScript 主机模块,并在 npm test 和 npm run push 之前自动执行。
该仓库还在 journeys/tls/、journeys/demos/ 和 journeys/kibana/ 下提供了额外分组,分别用于非浏览器 TLS 检查、badssl 风格演示,以及多步骤 Kibana 登录检查。npm run push:tls 及类似命令用于按文件夹范围进行部署。
浏览器 TLS journey
每个主机都会运行自己的 journey。证书检查依赖 Node 内置的 tls 模块,其中 cert.fingerprint256 在 TLS 握手过程中由 OpenSSL 预计算生成,因此不会产生额外开销。
``
1. import { journey, step, expect } from '@elastic/synthetics';
2. import { TLS_TARGET_HOSTS } from '../../helpers/tlsTargetHosts.tls-browser.generated';
3. import { fetchCertInfo, checkCertTrusted, logCertInfo } from '../../helpers/tls';
5. for (const { host, criticality, assertionText, assertionSelector } of TLS_TARGET_HOSTS) {
6. journey(
7. { name: `TLS Browser Check - ${host}`, tags: criticality ? [`criticality:${criticality}`] : [] },
8. ({ page }) => {
9. step(`TLS Validation for ${host}:443`, async () => {
10. // Route-stub: gives Synthetics UI the actual hostname instead of about:blank.
11. await page.route('**/*', route => route.fulfill({ status: 200, body: 'TLS check context' }));
12. await page.goto(`https://${host}`, { waitUntil: 'commit' });
14. const cachedCert = await fetchCertInfo(host, 443);
15. const trusted = await checkCertTrusted(host, 443);
16. logCertInfo(host, 443, cachedCert); // emits TLS_CERT stdout line for ingest pipeline
18. expect(cachedCert.sha256).toMatch(/^([0-9A-F]{2}:){31}[0-9A-F]{2}$/);
19. expect(cachedCert.validTo.getTime(), `Certificate expired: ${host}`).toBeGreaterThan(Date.now());
20. expect(trusted, `CA untrusted: ${host}`).toBe(true);
21. });
23. step(`Navigate to ${host} and verify page content`, async () => {
24. await page.context().route('**', route => route.continue());
25. const response = await page.goto(`https://${host}`, { waitUntil: 'domcontentloaded', timeout: 30_000 });
26. expect(response).not.toBeNull();
27. if (assertionText && assertionSelector) {
28. await expect(page.locator(assertionSelector)).toContainText(assertionText);
29. }
30. });
31. }
32. );
33. }
``AI写代码
成功与失败的运行都会将 tls.server.x509.* 写入 synthetics-* 索引中,因此告警规则会同时读取两者。
ingest pipeline(不要跳过)
logCertInfo() 会为每次检查输出一条结构化的 TLS_CERT stdout 日志。如果没有 ingest pipeline,这些数据会被写入 synthetics.payload.message,而即使 journeys 正常运行,Kibana 的 TLS Summary 卡片也会保持空白。大多数证书监控教程都会跳过这一步,但我第一次就被这个问题坑过。
`
1. curl -X PUT -H "Content-Type: application/json" \
2. -u "elastic:${ELASTIC_PASSWORD}" \
3. "https://<YOUR_CLUSTER_HOST>:9243/_ingest/pipeline/synthetics-browser%40custom" \
4. --data-binary @docs/ingest-pipeline-synthetics-browser.json
`AI写代码
流水线会将嵌入的 JSON 解析为 tls.server.x509 和 tls.server.hash,并移除指纹中的冒号(colon),以匹配 UI 期望的格式。
通过 CI/CD 部署监控
`
1. export KIBANA_URL="https://your-deployment.kb.us-east-1.aws.elastic-cloud.com"
2. export SYNTHETICS_API_KEY="<your-kibana-api-key>"
3. npm run push
`AI写代码
新增一个服务只需要在 PR 中增加一行 CSV,而不是在 Kibana 表单中手动填写;这种“监控即代码”的理念与《通过 GitOps 进行 Logstash 管道管理与配置》中覆盖的模式一致。
该仓库的 GitHub Actions workflow 会在每个 PR 上运行 tsc --noEmit、test:dry 和 test:unit(这些步骤都不需要网络访问),因此可以在合并前就捕获 journey 的问题。
Osquery 主机证书清单
浏览器 synthetics 只能看到绑定在活跃 Web 监听端口上的证书。后端数据库、内部 gRPC 服务以及消息队列 broker 都不会出现在任何 Playwright 测试中。Osquery pack 会每天查询每台主机的证书存储,并将结果写入 logs-osquery_manager.result*,其 ECS 映射如下:
`
1. "ecs_mapping": {
2. "tls.server.x509.subject.common_name": { "field": "common_name" },
3. "tls.server.x509.not_before": { "field": "not_valid_before" },
4. "tls.server.x509.not_after": { "field": "not_valid_after" },
5. "tls.server.hash.sha1": { "field": "sha1" },
6. "file.path": { "field": "path" }
7. }
`AI写代码
如果没有 ECS 映射,证书字段会落在专有的 osquery.* 列中。有了 ECS 映射之后,tls.server.x509.subject.common_name 可以直接与 Synthetics 告警 payload 进行匹配,而这正是 workflow 中主机发现步骤所依赖的内容。
CN 匹配是必要条件,但不足够。现代证书通常包含多个 SAN 和通配符主题。主机发现查询基于 common_name 进行 join,因为告警 payload 最清晰地暴露了这个字段;在大量使用通配符(.svc.cluster.local、.internal)的环境中,需要将 join 扩展到同时匹配 tls.server.x509.alternative_names,并过滤掉由中心化续签的共享通配符证书——否则,一个基于 Let's Encrypt 的通配符续签可能会在同一天将 workflow 扩散到集群中的所有主机。
思考:基于 Synthetics 数据进行告警
告警规则运行在 synthetics-* 上,而不是 Osquery 上。Synthetics 会捕获客户端实际看到的证书状态,包括主机 keystore 无法检测到的中间证书链问题。Osquery 则在 workflow 内部被调用,用于确定需要采取行动的主机范围。
创建一个 Elasticsearch 查询规则,使用以下 ES|QL(完整文件见 docs/elastic-samples/alerts/01-certificate-approaching-30d-expiring.md —— 注意该仓库文件使用的是 30 天窗口以实现更严格告警;下面的 90 天版本是我个人偏好的默认配置):
`
1. FROM synthetics-*,*:synthetics-*
2. | WHERE tls.server.x509.not_before IS NOT NULL
3. AND tls.server.x509.not_after IS NOT NULL
4. AND tls.server.x509.subject.common_name IS NOT NULL
5. | STATS
6. record_count = COUNT(*),
7. tls.server.x509.not_after = MAX(tls.server.x509.not_after),
8. tls.server.x509.not_before = MIN(tls.server.x509.not_before),
9. monitor.name = VALUES(monitor.name),
10. @timestamp = MAX(@timestamp)
11. BY tls.server.x509.subject.common_name, tags
12. | WHERE NOW() + 90d > tls.server.x509.not_after
13. OR tls.server.x509.not_before > NOW()
14. | EVAL days_until_expiry = DATE_DIFF("days",
15. tls.server.x509.not_before,
16. tls.server.x509.not_after)
17. | KEEP @timestamp, tls.server.x509.subject.common_name,
18. tls.server.x509.not_before, tls.server.x509.not_after,
19. monitor.name, tags, days_until_expiry
20. | SORT @timestamp
21. | LIMIT 1000
`AI写代码
按 common_name 和 tags 进行 STATS BY 聚合会将数据去重为“每个证书 + 每个 criticality 标签一行”,因此规则只会针对每个即将过期的证书触发一次,而不会在每个 location 的 monitor 执行时重复触发。通过 MAX(not_after) 和 MIN(not_before) 可以在所有观测到的文档中确定唯一的有效期窗口。
两个值得注意的调优点:
- ACME 证书(90 天有效期)会在每个检查周期中一直显示 days_until_expiry 接近 90。应单独创建一个更严格阈值(<14 天)的规则,并使用不同的升级路径。
- 对于跨集群索引模式(:synthetics-),在依赖 tags 进行路由之前,需要确认各个集群中的 tags 字段填充一致性。
执行:智能证书轮换升级与修复
完整 workflow 位于 docs/elastic-samples/workflows/01-smart-certificate-rotation-escalation-remediation.yaml。将其复制到 Kibana → Stack Management → Workflows 中,替换 connector IDs,即可运行下图所示的流程。接下来内容是仅通过浏览 YAML 很难直观理解的关键设计模式。
图4:每个叶节点都有一个明确的契约——自动轮换、创建工单、软通知或紧急告警(page)。workflow 不会静默丢弃任何证书。下图展示了智能升级与修复的运行方式。其下方解释了每个阶段的设计逻辑。
图5:相同逻辑只是从决策轴转移到了时间轴。
| 阶段 | 行为 | 价值点 |
|---|---|---|
| 1. 预检(Pre-flight) | 捕获严重性 + business-hours 标记 | 在 9.3 中 console step 作为表达式计算器;在 9.4 中 data.set 是可命名、可测试的等价方式 |
| 2. 主机发现 | 在 logs-osquery_manager.* 上运行 ES | QL,匹配告警中的 CN |
| 3. 防护(Guard) | 无 Osquery 主机匹配则邮件通知并中止 | 证书在链路中可见但不在 keystore 中 = 实际问题 |
| 4. 分支(Branch) | < 7 天 → 自动轮换;否则 → Jira 工单 | 单一阈值,无模糊逻辑,无歧义 |
| 5. 轮换(Rotate) | 新鲜度检查 → Ansible webhook + Slack | 执行前 circuit breaker,防止误操作扩散 |
| 6. 升级(Escalate) | 轮换失败时软通知邮件或 PagerDuty | 严重性 + 业务时间共同决定通知路径 |
关键模式
设置 workflow 变量。预检步骤会计算并存储一些值,供后续所有分支使用。在 9.3 中,唯一的方法是 “滥用” console 步骤 —— 通过 Liquid 模板在 message 中渲染字符串,然后通过 steps..output 访问。在 9.4 中,data.set 将其变成一个显式、可命名、可测试的操作,并提供类型化输出字段。
9.3 —— 使用 console 作为表达式计算器(兼容 9.3 和 9.4;不推荐用于新 workflow):
`
1. - name: business_hours_check
2. type: console
3. with:
4. message: |-
5. {%- assign hour = "now" | date: "%H", "Australia/Sydney" | plus: 0 %}
6. {%- if hour >= 8 and hour < 17 %}true{%- else %}false{%- endif %}
`AI写代码
9.4 —— data.set(新 workflow 推荐方式):
`
1. - name: business_hours_check
2. type: data.set
3. with:
4. is_business_hours: |-
5. {%- assign hour = "now" | date: "%H", "Australia/Sydney" | plus: 0 -%}
6. {%- if hour >= 8 and hour < 17 %}true{%- else %}false{%- endif -%}
`AI写代码
data.set 的输出可以通过 steps.business_hours_check.output.is_business_hours 访问,这是一个命名字段,而不是原始字符串。同样的升级方式也适用于完整 workflow YAML 中的 severity_mapping 和 pagerduty_criticality_mapping 步骤。你可以根据团队需要调整时区。
跨源主机发现。告警在 Synthetics 上触发;该查询向 Osquery 查询哪些主机持有即将过期的 CN:
``
1. - name: find_affected_hosts
2. type: elasticsearch.esql.query
3. with:
4. query: |
5. FROM logs-osquery_manager.result-*
6. | WHERE tls.server.x509.subject.common_name == "{{ event.alerts[0]['tls.server.x509.subject.common_name'][0] }}"
7. AND `file.path` != "LocalMachine\\Certificate Enrollment Requests"
8. | EVAL days_until_expiry = DATE_DIFF("days", tls.server.x509.not_before, tls.server.x509.not_after)
9. | KEEP host.hostname, tls.server.x509.subject.common_name,
10. tls.server.x509.not_before, tls.server.x509.not_after,
11. file.path, days_until_expiry
12. format: json
``AI写代码
输出列在下方按位置访问:[0] host.hostname,[5] days_until_expiry。Windows Certificate Enrollment Requests 排除项会过滤掉正在等待续签的请求,这些请求在续签过程中会在 keystore 中产生误报匹配。
断路器(circuit breaker)。不要对当前无法自我观测的主机执行操作:
`
1. - name: check_host_freshness
2. type: elasticsearch.esql.query
3. with:
4. query: |
5. FROM metrics-system.cpu-*
6. | WHERE host.name == "{{ steps.find_affected_hosts.output.values[0][0] }}"
7. | STATS latest_metric = MAX(@timestamp)
8. | EVAL latency_sec = (TO_LONG(NOW()) - TO_LONG(latest_metric)) / 1000
9. | LIMIT 1
11. - name: remediation_routing_logic
12. type: if
13. condition: 'steps.check_host_freshness.output.values[1] <= 300'
14. steps:
15. - name: trigger_ansible_rotation
16. type: http
17. with:
18. url: "{{consts.ansible_webhook}}"
19. method: POST
20. body: { extra_vars: { target_host: "{{ steps.find_affected_hosts.output.values[0][0] }}" } }
21. headers: { Authorization: "Bearer {{consts.ansible_token}}" }
`AI写代码
这是“信任缺口(trust deficit)断路器”的具体化版本。我最初写这个 workflow 时没有加入新鲜度检查,我们曾经愉快地把 Ansible 任务发给那些其实已经“离开现场”的主机。Tower 返回 200,因为 job 已经排队,workflow 也宣告成功,但证书实际上根本没有完成轮换。两行 YAML 加上一份本不该存在的复盘。
Ansible Tower 只是一个示例 —— type: http 可以适用于任何支持 webhook 的执行器:Rundeck、AWX、Salt、GitHub Actions 的 workflow_dispatch,或者一个简单封装 kubectl rollout restart 的内部服务。这个模式的关键是:在执行动作之前必须进行健康检查。
consts: 中的 ansible_token 字面值只是为了增强文本可读性。在生产环境中,应通过 Workflows 的 secret 管理机制注入凭据(或从引用的 connector 中加载),确保它不会出现在版本控制的 YAML 中。
当断路器触发时,会进行基于严重性的升级处理。低严重性且在非工作时间的情况只发送非紧急邮件;其他情况则触发 PagerDuty,并将严重性映射为 PagerDuty 的语义模型。与上面相同的 9.3 / 9.4 模式:
9.3:
`
1. - name: pagerduty_criticality_mapping
2. type: console
3. with:
4. message: |-
5. {%- assign sev = steps.severity_mapping.output -%}
6. {%- case sev -%}
7. {%- when "critical" -%}critical
8. {%- when "high" -%}error
9. {%- when "medium" -%}warning
10. {%- else -%}info
11. {%- endcase -%}
`AI写代码
9.4:
`
1. - name: pagerduty_criticality_mapping
2. type: data.set
3. with:
4. severity: |-
5. {%- assign sev = steps.severity_mapping.output.severity -%}
6. {%- case sev -%}
7. {%- when "critical" -%}critical
8. {%- when "high" -%}error
9. {%- when "medium" -%}warning
10. {%- else -%}info
11. {%- endcase -%}
`AI写代码
映射步骤必须在 hard_notification 引用其输出之前运行;在重构时要保持这个顺序。
在 9.4 中,data.set 的输出通过字段名引用(steps.severity_mapping.output.severity);在 9.3 中,console 步骤则在 steps.severity_mapping.output 下暴露为原始字符串。
验证:闭合循环(Verify)
一个只执行动作然后 “离开” 的自愈系统并不是真正的自愈系统:它只是 “希望它成功”。Verify 阶段才是区分闭环系统与一次性脚本的关键,也是大多数 “自动修复” 实现悄悄失败的地方。
对于证书场景,验证有两个天然锚点。下一次 Synthetics 采样间隔(轮换后 1–10 分钟)会重新读取证书链——新的 not_after 和 fingerprint 会进入 synthetics-*,而告警规则也会因此停止匹配该 CN。没有告警,才是正确结果。
为了获得更深层的可信度,canary curl_certificate workflow 会确认整个 fleet(而不仅仅是公网端点)已经更新证书。在轮换成功路径的末尾添加如下逻辑块:
`
1. - name: wait_for_rotation
2. type: wait
3. with:
4. duration: 120s
6. - name: verify_rotation
7. type: elasticsearch.esql.query
8. with:
9. query: |
10. FROM synthetics-*
11. | WHERE tls.server.x509.subject.common_name
12. == "{{ event.alerts[0]['tls.server.x509.subject.common_name'][0] }}"
13. AND @timestamp > NOW() - 5 minutes
14. | STATS new_not_after = MAX(tls.server.x509.not_after),
15. new_fingerprint = VALUES(tls.server.hash.sha256)
16. | EVAL days_remaining = DATE_DIFF("days", NOW(), new_not_after)
18. - name: rotation_outcome
19. type: if
20. condition: "${{ steps.verify_rotation.output.values[0][2] > 30 }}"
21. steps:
22. - name: notify_verified
23. type: slack
24. connector-id: "slack-sre-channel-uuid"
25. with:
26. message: "✅ Rotation verified. {{ steps.verify_rotation.output.values[0][2] }} days remaining."
27. else:
28. - name: notify_verification_failed
29. type: pagerduty
30. connector-id: "<your-pagerduty-connector-uuid>"
31. with:
32. eventAction: "trigger"
33. severity: "error"
34. summary: "Rotation triggered but Synthetics still shows old cert. Investigate Ansible job and DNS/cache."
`AI写代码
验证查询读取与告警规则相同的数据源,因此两者定义不会发生漂移。检查本身是数值型的(days_remaining > 30),没有任何歧义空间。
如果验证失败,其升级级别会比原始告警更高:一个本应即将过期的证书在“自动修复失败”后仍保持错误状态,比最初未处理的情况更危险,因为自动化已经吞掉了原本会提醒人的信号。
120 秒的等待时间是保守设置。对于由 sidecar 进行 ACME 续签的证书来说,下一个监控周期(通常不到 1 分钟)已经足够。对于由 Ansible 驱动、涉及反向代理 reload 的 PKI 轮换,我的经验是两分钟比较合适:既足够避免追逐旧缓存,又不会让值班人员在误判成功的情况下安心入睡。
为了在投入生产前完整演练整个闭环,仓库中的 local-lab/ 目录提供了一个 Docker Compose 环境,用于生成一个 1 天有效期的自签名证书,并通过 Nginx 或 Apache 提供服务。更新证书、重启容器,然后观察 Synthetics 在一个监控周期内捕获新的 fingerprint——与真实轮换结构完全一致,但没有生产风险。
如果跳过 Verify,你构建的只是一个“伪装成闭环的开环系统”。当第一次静默失败的轮换在凌晨 4 点演变成告警时,信任缺口就会重新出现。
影响验证:canary workflow
这个组合 workflow 处理的是可预测的情况。但有些问题超出了可预测范围:例如 Synthetics monitor 出现 TLS handshake timeout,但证书链并不明显;或者你在执行轮换前想确认“blast radius(影响范围)”。
在这些情况下,可以通过 Osquery 的 curl_certificate live query 从客户端视角获取 TLS 链信息,这个视角来自实际与目标通信的机器。一个反向代理可能对外持有有效的公网证书,但下游内部主机却仍然使用过期的 CA 签发证书,从而导致微服务调用失败——这种情况从 endpoint polling 无法看到,但在内部 canary 主机上是完全可见的。
仓库中提供了两个 workflow 文件:02-canary-certificate-impact-check.yaml(9.3 兼容)以及 02-canary-certificate-impact-check-9.4.yaml(9.4+ 默认 GA)。两者都可以通过两种方式调用:
-
在 Kibana 中手动执行,输入 target_host 和 target_port
-
作为 Agent Builder tool 自动注册,由 SRE Certificate Agent 在 triage 过程中调用(注册模式与《如何使用 Agent Builder 排查 Kubernetes Pod 重启与 OOMKilled 事件》中一致)
YAML 中最关键的部分是:发起 curl_certificate live query(agent_all: true),轮询 live-query API 直到状态为 completed,然后用一次 ES|QL 聚合结果。这个 polling 步骤正是 9.3 与 9.4 差异最明显的地方。
9.3 —— foreach + 故意的失败绕过方案:
`
1. consts:
2. items: [1] # single-element list; foreach runs once per retry attempt
4. steps:
5. - name: while_not_workaround_loop
6. type: foreach
7. foreach: "${{consts.items}}"
8. steps:
9. - name: osquery_check_query_completion
10. type: kibana.request
11. with:
12. method: GET
13. path: /api/osquery/live_queries/{{ steps.osquery_check_host.output.data.action_id }}
14. - name: wait_10s
15. type: wait
16. with:
17. duration: 10s
18. - name: retry
19. type: http
20. if: "${{ steps.osquery_check_query_completion.output.data.status != 'completed' }}"
21. with:
22. url: "https://httpbin.org/status/404" # deliberate 404 triggers on-failure
23. on-failure:
24. retry:
25. max-attempts: 9
`AI写代码
9.4 —— 使用 data.set 的原生 while 循环:
`2. - name: poll_osquery_completion
3. type: while
4. condition: "${{ steps.osquery_poll_state.output.query_status != 'completed' }}"
5. max-iterations:
6. limit: 10
7. on-limit: fail
8. steps:
9. - name: wait_10s
10. type: wait
11. with:
12. duration: 10s
13. - name: check_query_completion
14. type: kibana.request
15. with:
16. method: GET
17. path: /api/osquery/live_queries/{{ steps.osquery_check_host.output.data.action_id }}
18. - name: osquery_poll_state
19. type: data.set
20. with:
21. query_status: "{{ steps.check_query_completion.output.data.status }}"`AI写代码
查看 9.3 → 9.4 迁移指南可获取完整变更列表。ES|QL 聚合步骤在两个版本中是完全一致的:
`
1. FROM logs-osquery_manager.result*
2. | WHERE action_id == "{{ steps.osquery_check_host.output.data.queries[0].action_id }}"
3. | WHERE NOW() > tls.server.x509.not_after
4. | STATS expired_canary_hosts = COUNT_DISTINCT(agent.name)
5. BY certificate_validity_duration = DATE_DIFF("day",
6. tls.server.x509.not_before,
7. tls.server.x509.not_after)
`AI写代码
expired_canary_hosts 是已确认证书过期的 agent 数量。certificate_validity_duration > 90 用于区分长生命周期 PKI 与短生命周期 ACME 证书,workflow 会据此进行响应路由——你可以将 YAML 中的 console 占位符替换为 Slack、PagerDuty 或后续修复步骤(remediation step),按需配置。
注册为 agent 工具
将该 workflow 注册为一个 tool,使任何 Agent Builder agent 都可以在 triage 过程中调用它:
`
1. POST kbn:/api/agent_builder/tools
2. {
3. "id": "o11y.canary_certificate_check",
4. "type": "workflow",
5. "description": "Queries curl_certificate from all fleet agents to confirm real TLS impact for a given hostname and port. Returns count of affected canary hosts and certificate type (PKI vs ACME).",
6. "tags": ["observability", "certificates"],
7. "configuration": {
8. "workflow_id": "<your-osquery-curl-workflow-id>",
9. "wait_for_completion": true
10. }
11. }
`AI写代码
打包为技能(9.4+)
Elastic 9.4 引入了 skills,这是比单个 tool 更高层级的抽象。tool 是单一的离散操作(例如运行查询、调用 webhook),而 skill 会将指令、工具以及参考上下文打包成一个可复用能力集合。agent 会按需加载这些能力,而不是一直将其放在 system prompt 中。
对于证书 triage(排查),skill 可以将 canary 影响检查工具与领域特定指令组合在一起,例如如何解释 PKI 与 ACME 的结果、何时升级告警 vs 何时转向 DNS 处理,以及应触发哪些后续 workflow。agent 可以在对话上下文指向 TLS 问题时自动选择该 skill,或者用户通过 slash 命令(/cert-triage)显式调用。
skills 在 Agent Builder 的 skill library 中进行管理,并可供 workspace 中的所有 agent 使用:一份定义,可被 SRE agent、on-call agent 以及未来任何需要证书能力的 agent 共享。可通过 GET /api/agent_builder/skills 获取可用 skills。
加载该 skill 后,一个典型的 agent 驱动 triage 流程如下:
`
1. SRE: "The Synthetics monitor for blueprint-portal is failing. Investigate."
3. Agent: [Reviews synthetics logs — TLS handshake error on blueprint-portal.internal]
4. Agent: [Invokes o11y.canary_certificate_check for blueprint-portal.internal:443]
6. Canary result: expired intermediate cert confirmed on 3 of 12 agents — PKI class (365-day cert)
8. Agent: "The Synthetic failure is caused by an expired intermediate CA on the downstream
9. gateway. Three canary hosts confirm. The leaf cert is valid — this is a CA renewal gap on
10. the internal chain, not a service certificate expiry."
12. SRE: "Execute the proxy cache flush workflow."
13. Agent: [Invokes proxy flush workflow via MCP]
14. Agent: "Flush confirmed. Monitor should recover within one check interval."
`AI写代码
当 canary 检查结果正常时,agent 会将流程转向 DNS、路由或上游服务健康检查,而不需要 SRE 手动去查询任何内容。
运行层面的考虑
工作流本身的可观测性。
当人们看到这个模式时,我最常被问到的问题是同一个:“你怎么知道这个 workflow 还在按预期工作?”
Workflows 在 Kibana 中的运行历史(run history)会展示每一次执行、状态以及每一步的输出。而最直观地证明这个闭环正在被正确执行的信号,来自 alerts 索引本身:
`
1. // Certificates that fired the alert more than once in the last 14 days —
2. // strong signal that rotation is failing silently and the loop is open
3. FROM .alerts-observability.metrics.alerts-default
4. | WHERE @timestamp > NOW() - 14 days
5. AND kibana.alert.rule.name == "Certificate Expiry"
6. | STATS fires = COUNT(*) BY tls.server.x509.subject.common_name
7. | WHERE fires > 1
8. | SORT fires DESC
`AI写代码
把这个查询固定在一个显眼的位置。一个 CN 在索引中出现多次,意味着 workflow 正在执行,但动作没有真正生效;这正是该架构要暴露的失败模式。配套的 meta-alert 查询已经提交在 docs/elastic-samples/alerts/02-certificate-workflow-verification.md。
后端主机的 criticality 上下文。CSV 中的 tags 字段只覆盖具有 active Synthetic 监控的端点。对于通过 Osquery 发现的、没有监控覆盖的后端服务证书,应在 remediate_or_notify 之前增加一个 LOOKUP JOIN enterprise_cmdb ON host.name 的步骤。有两种模式可以保持 CMDB 索引更新:
-
pull(通过 Logstash 同步 CMDB 导出)
-
push(当 Osquery 发现不在 CMDB 中的主机时,workflow 通过 ServiceNow webhook 回写——这本身就是一个值得调查的真实发现)
在生产环境前进行端到端演练。仓库的 local-lab/ 目录提供一个 1 天有效期的自签名证书环境,可通过 Nginx(8443 端口)或 Apache(8444 端口)提供服务,并包含 renew-tls-certs.sh 脚本。在将 workflow 指向关键服务之前,建议先在该环境中运行整个闭环一到两天进行验证。
成功的样子
| 指标 | 健康状态 |
|---|---|
| CN 在 14 天内的重复触发次数 | 接近 0 —— 每次告警都会在一个周期内被处理并验证 |
| Workflow 成功率 | 占绝对主导;失败运行主要是 host-stale 升级,而不是系统 bug |
| 自动轮换占比(auto-rotation share) | 随着团队对 ≤ 7 天窗口的信任提升而上升 |
| 手动 Jira 工单数量 | 下降,因为更多证书类别从“需要工单”升级为“自动轮换” |
| 凌晨 4 点证书告警 | 接近 0。如果没有下降,说明闭环没有真正收敛。先从 repeat-fires 查询开始检查 |
单个最有价值的指标是最后一个。一个成功的自愈系统,是那种连续几个月都不会被人注意到的系统。
仓库结构
图6:CSV 输入,监控输出。示例 workflow YAML、告警 ES|QL、ingest pipeline,以及短生命周期证书的本地实验环境全部被放在同一位置,并进行统一版本控制。
下一步
-
克隆仓库,用你的端点和 criticality 标签编辑 journeys/tls-browser/tls-target-hosts.csv
-
只需执行一次安装 ingest pipeline:
PUT _ingest/pipeline/synthetics-browser@custom -
使用 npm run test:demos 在 revoked.badssl.com 上运行测试,确认 Sense 和 ingest pipeline 已正确连接
-
可选:将 journey 指向 local-lab/ Nginx(https://127.0.0.1:8443),用 1 天证书环境演练完整闭环
-
执行 npm run push 推送 monitors
-
通过 Kibana API 部署 Osquery pack
-
将 docs/elastic-samples/alerts/01-…md 中的 ES|QL 粘贴到 Kibana ES|QL rule
-
将 docs/elastic-samples/workflows/01-…yaml 导入 Kibana Workflow,并配置 connector IDs。在生产启用自动轮换前,务必先加入 Verify block
-
可选:导入 docs/elastic-samples/workflows/02-…yaml,并注册为 Agent Builder tool
可在 Elastic Cloud Serverless 或 Elastic Cloud 上尝试运行
常见问题
为什么 TLS 证书在生产中仍然会过期,明明过期时间是已知的?
过期时间是已知的,但真正的问题在于“执行链路”不可靠——检测到期、触发动作、完成轮换之间往往依赖表格、日历提醒或人工记忆。自动化通过 Elastic Synthetics 和 Osquery 持续感知状态,再由 Elastic Workflows 完成闭环执行,从而消除这个缺口。
用 Elastic Synthetics 做证书监控和简单 cron/告警有什么区别?
Synthetics 会以客户端视角捕获完整证书链,包括中间 CA 问题,这是主机侧检查无法看到的。同时它将结果映射到 tls.server.x509.* ECS 字段,使告警和修复 workflow 使用同一数据源,无需转换层,也不会出现 schema drift。
怎么确认自动轮换真的成功,而不是只是“排队执行了任务”?
轮换后 workflow 会等待 120 秒,再通过 ES|QL 查询 synthetics-*,检查新的 not_after 是否大于 30 天。如果仍然是旧证书,则升级为比原始告警更高的级别,因为“静默失败的轮换”比“过期提醒”更危险。
如果 Ansible 对已下线的主机执行轮换会发生什么?
workflow 在触发 Ansible 前会进行 circuit breaker 检查(metrics-system.cpu-*)。如果主机在过去 300 秒没有 telemetry,则跳过执行并升级告警。否则 Tower 可能返回 200(只是 job queue),但证书实际上不会更新。
什么时候用 Osquery curl_certificate canary workflow,而不是主轮换流程?
主流程处理可预测的证书过期问题;canary workflow 用于不确定故障(如 TLS handshake timeout)。它会对 fleet 执行 live query,帮助区分“公网端点问题”和“内部链路问题”。
这个方案必须每个证书都配 Synthetics 吗?
不需要。Osquery 会扫描所有主机的证书存储,包括数据库、gRPC、消息队列等没有 HTTP endpoint 的服务。Synthetics 覆盖客户端视角,Osquery 覆盖主机视角,两者都写入 tls.server.x509.*,因此 workflow 可以直接 join。
用 common name join 有什么限制?
在大量 wildcard 场景(*.internal 等)下,单一 Let’s Encrypt wildcard 可能触发整集群误匹配。需要扩展到 tls.server.x509.alternative_names,并过滤中心化 wildcard。
需要哪个 Elastic 版本?
-
Elastic Workflows:9.4 GA(9.3 为技术预览)
-
ES|QL LOOKUP JOIN:9.1+
-
推荐直接使用 9.4,因为支持 data.set 和 native while loop
tool 和 skill 有什么区别?
tool 是单一动作(查询、webhook、执行 workflow),skill 是将工具 + 指令 + 上下文打包成可复用能力包,agent 会按需加载。对于证书 triage,skill 能让 agent 自动理解 PKI/ACME 差异并选择正确升级路径。
相关阅读
- Automated Reliability: The Architecture of Self-Healing Enterprises — 该构建的灵感来源。重点讲解“为什么要做”:包括 remediation gap(修复断层)、trust deficit(信任缺口),以及该实现所落地的 sense → think → act → verify 闭环模型。
- How to Troubleshoot Kubernetes Pod Restarts & OOMKilled Events with Agent Builder — 一个高度相关的 Agent Builder 排障模式示例;这里的 SRE Certificate Agent 采用了相同的注册与调用机制。
- Agentic CI/CD: Kubernetes Deployment Gates with Elastic MCP Server — 将“workflow-as-tool(工作流即工具)”模式扩展到部署流水线,用于在发布阶段基于条件(例如证书健康状态)进行自动门控。
- Elastic Ramen: A CLI harness for SRE investigation and remediation — 将 Agent Builder 的对话能力、skills 与 tools 引入 CLI,使工程师可以在同一个终端线程中完成“调查 → 分析 → 修复”的完整流程。