使用 Elastic Workflows、Synthetics 和 Osquery 进行 TLS 证书监控:消除手动续期

0 阅读24分钟

作者:来自 Elastic Adrian ChenVu Pham

使用 Elastic Workflows、Synthetics 和 Osquery 自动化 TLS 证书监控:检测即将过期的证书、进行轮换,并在无需人工干预的情况下完成验证。

证书过期是唯一一种生产环境故障类别,它在签发时就已经 “写明了自己的结束日期”。这里没有异常需要检测,没有模型需要训练,也没有未知因素。然而,过期证书仍然会每个季度导致支付网关、内部 API 以及可观测性栈宕机,因为 “知道证书会过期” 和 “实际采取行动” 之间的鸿沟,仍然依赖表格、日历提醒,以及刚好有人注意到。

本文将填补这一鸿沟:通过 Elastic Synthetics、Osquery、ES|QLElastic Workflows(在 9.4 中正式可用),构建一个闭环 TLS 证书监控流水线 —— 可以检测即将过期的证书、定位所有受影响主机、完成轮换,并进行验证,全程无需人工干预。

该设计背后的推理(信任缺口、修复鸿沟,以及贯穿整个流程的 sensethinkactverify 循环)在《自动化可靠性:自愈企业的架构》中有详细说明。建议先阅读那一篇来理解 “为什么”。证书是这一模式的理想首个场景,因为信号明确、修复动作清晰,而且自动化本身的影响范围足够小,可以在建立信任后再扩展到更复杂、更噪声的领域。

完整源码(synthetics journey、workflow YAML 示例、ES|QL 告警查询示例、ingest pipeline,以及一个短期证书本地实验环境)都在 aiops-synthetics-lab 仓库中。本文讲的是模式,仓库提供完整实现。

将构建映射到闭环

闭环的每个阶段都对应一个具体的 Elastic 组件,以及伴随仓库中的一个对应工件。下表是快速参考;随后的图展示了数据如何在各阶段之间流动。

阶段组件在仓库中的位置
SenseSynthetics 浏览器 journeys + Osquery 证书 packjourneys/,helpers/tls.ts,docs/ingest-pipeline-synthetics-browser.json
Think针对 synthetics-* 的 ESQL 告警规则(按 CN + tags 去重)
Act智能证书轮换升级与修复 workflowdocs/elastic-samples/workflows/01-...yaml
VerifySynthetics 重新读取已轮换证书链;canary curl_certificate 验证 fleetdocs/elastic-samples/workflows/02-...yaml,local-lab/
![](https://i-blog.csdnimg.cn/direct/ebce7c090be249ffa06744742d00560d.png) 图1:每个阶段都映射到一个特定的 Elastic 原语,并且每个阶段只依赖前一个阶段提供的干净数据。没有带外协调,没有共享的 “心智状态”。

Sense → Think → Act → Verify — closed-loop certificate self-healing

发现:选择合适的两种方法

用于发现证书的方法有四种,但如下表所示,其中两种方法可以最快实现价值回报。

方法机制结论
已知端点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写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

成功与失败的运行都会将 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写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

按 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.* 上运行 ESQL,匹配告警中的 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写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

输出列在下方按位置访问:[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写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

这是“信任缺口(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写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

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写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

映射步骤必须在 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写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

验证查询读取与告警规则相同的数据源,因此两者定义不会发生漂移。检查本身是数值型的(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)。两者都可以通过两种方式调用:

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写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

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写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

查看 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写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

打包为技能(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写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

当 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,以及短生命周期证书的本地实验环境全部被放在同一位置,并进行统一版本控制。

完整参考:github.com/adrianchen-…

下一步

  • 克隆仓库,用你的端点和 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 ServerlessElastic 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 差异并选择正确升级路径。

相关阅读

原文:www.elastic.co/observabili…