Home / Stories / Cross-domain context
Story · Investigation Roadmap

Sixteen alerts, one switch. A failing playbook, one missing probe.

Incidents do not respect team boundaries. The cause of a Kubernetes alert can be a network uplink; the cause of a failed Ansible run can be a Vault pod three layers down that nobody noticed was sealed. This story shows where the assistant is heading: joining what it knows across inventory, network, CI, cluster and cloud, so you get one root-cause finding instead of a pile of symptoms. Most of it is designed, not yet built, and each step says which.

Starts fromA burst of alerts, or a failing CI job, that looks like many unrelated problems.
Looks atNetBox inventory (sites, devices, switch ports, VLANs, owners), Zabbix host data, alert labels, CI job logs, the Git repos that declare each dependency, live cluster state.
You getOne root-cause issue in place of N alert issues, or a draft MR on the repo that owns the cause, citing the symptoms elsewhere as evidence.
01 · Scenario A: the burst

Sixteen alerts in four minutes, across four teams.

orders-api is timing out, payments reports its database unreachable, a node exporter goes quiet, a queue backs up. Each alert, read on its own, points at a different service and a different owner.

Today each one gets its own investigation and, if it warrants one, its own issue. Sixteen honest findings about the wrong thing.

02 · What exists today In development

A known-cascade table for Kubernetes alerts.

A deterministic co-firing correlator already knows that KubeletDown explains the pod alerts on that node, and that a scraper going dark makes its targets look down. It elects a root and annotates the others as dependents, confined to one tenant so an outage elsewhere is never mis-labelled as "fix the root".

It ships switched off and only annotates; it does not yet collapse N issues into one. And it cannot see what is outside the cluster: a switch, an uplink, a VLAN.

03 · Ask the inventory Roadmap

NetBox knows which switch port every host hangs off.

Each alert's instance or node label is joined, in-process, to a NetBox record: address to host, host to interface, interface to switch port and VLAN. Fourteen of the sixteen alerts land on hosts behind the same access switch uplink.

The join is strict. A key that matches two hosts abstains rather than guessing, and "not in NetBox" is only said when the read was complete.

04 · A second witness Roadmap

Zabbix saw the port go down first.

For VM and bare-metal estates monitored by Zabbix, its problems become one more alert source and its host inventory (OS, hardware, resources) sits beside NetBox as observed context.

Here Zabbix raised an interface-down problem on the switch uplink about ninety seconds before the first Kubernetes alert. Two independent sources, one cause.

05 · Masked at the source Roadmap

The model learns what a host is, never which host.

Inventory is core-infrastructure detail. Addresses, MAC addresses, serials, VLAN ids, racks and site names are used for the join and then dropped. What reaches an LLM is a closed-schema card: role, platform family, status, update cadence, whether an owner is declared and an opaque team handle.

Topology is expressed as relations between opaque handles, not as invented addresses. Any field not on the allowlist is dropped by default, and a canary test proves no raw value survives.

06 · One finding Roadmap

One root-cause issue instead of sixteen.

The dependents are folded into a single issue that names the shared uplink, lists every alert it explains, and states what could not be joined. The two alerts that did not fit the pattern keep their own investigation.

07 · Scenario B: the playbook

An Ansible run fails fetching a secret. Every hour.

The scheduled Ansible apply mints its SSH certificate from Vault before any play runs. Since last night it fails at that step: HTTP 503, Vault is sealed. The playbook repo is fine. The problem is somewhere else.

08 · Follow the declared edge Roadmap

From the CI log to the repo that owns the cause.

The failure signature (vault … 503 … sealed) is classified deterministically. The CI script itself names the Vault address and auth mount, so the dependency is declared, not guessed. The cluster map resolves that Vault to its HelmRelease in the cluster repo.

In the cluster, the Vault pods are Running, 0/1 ready, zero restarts. The one auto-unseal attempt at startup failed against Google Cloud KMS, and nothing ever tried again.

09 · The missing check In development

Auto-unseal, and no liveness probe on seal status.

A rule in the availability pack looks for exactly this shape: a Vault HelmRelease with KMS auto-unseal and no liveness probe. Readiness only takes a sealed pod out of the Service; a liveness probe on /v1/sys/health restarts it, and the restart retries the unseal.

The rule fires on its own, outage or not. The attribution in step 8 is what lets the MR cite the playbook failures as evidence.

orders-api p99 > 2s
shop · 14:02
PostgresUnreachable
payments · 14:02
NodeExporterDown ×3
platform · 14:03
QueueDepthHigh
shop · 14:03
KubeletDown ×2
platform · 14:03
BackupJobLate
platform · 14:04
TargetDown ×5
monitoring · 14:04
CertExpirySoon
shop · 14:05
16 alerts · 4 teams · 16 separate investigations
co-firing correlation · annotate mode · one tenant
  • KubeletDown elected root · node-7
  • pod alerts on node-7 dependent of KubeletDown
  • TargetDown ×5 blind pipeline artefact
  • PostgresUnreachable no known edge
  • orders-api p99 no known edge
  • NodeExporterDown ×3 no known edge
the cascade table stops at the cluster boundary: no switch, no uplink
inventory join · in-process only
host-a1 · node-7
host-a2 · node-8
host-a3 · db primary
host-a4 · queue
+ 10 more alerts
2 alerts · no match
switch-h3 · uplink 1access switch · VLAN handle v-2
14 of 16 alerts behind it
host ← interface ← switch port ← VLAN · ambiguous keys abstain
timeline · two independent sources
  • 14:00:31 Zabbix · interface down · switch-h3 uplink 1
  • 14:02:04 Alertmanager · orders-api p99
  • 14:02:10 Alertmanager · PostgresUnreachable
  • 14:03:00 Alertmanager · KubeletDown ×2
  • 14:04:12 Alertmanager · TargetDown ×5
Zabbix problem · Interface down
first signal · 93 s before the first Kubernetes alert
raw record · never leaves the process
name: db-07.dc1.example
primary_ip: 192.0.2.47
interface: eth0 · aa:bb:cc:…
switch_port: sw-dc1-r4 Gi1/0/17
vlan: 112
serial: XK4…
owner: (a person)
comments: free text
what the model sees
host: h-5c1e
role: database
platform: vm
status: active
update_cadence: monthly
owner_declared: true
owner_team: team-91a0
address_scope: private
behind: sw-3f02 (uplink u-1)
unmapped field → dropped · canary in every leaf → zero canaries out
16 alerts4 teams
Inventory join14 on one uplink
1 issue+ 2 kept separate
Issue · Access switch uplink down explains 14 alerts
category:reliabilitysource:alertrisk:high
CI · ansible apply · scheduled · job log
$ ci/mint-ssh-cert
  requesting fleet SSH certificate from Vault
  vault PUT auth/fleet/login: HTTP 503 ['Vault is sealed']
✕ job failed before ansible-playbook started

scheduled applies, newest first
✕ ✕ ✕ ✕ ✕ ✕ ✕ ✕ ✕ ✕ ✕ ✕ … 12 in a row
reconciler run-health: RED · 12 consecutive scheduled failures
Playbook repo · CIfailure signature: vault 503 sealed
Declared edgeCI script names the Vault address + auth mount
Cluster repo · Vault HelmReleaseresolved through the cluster map
Google Cloud KMSauto-unseal, tried once at startup
live: vault-0/1/2 · Running · 0/1 ready · 0 restarts · sealed
infrastructure/vault/helmrelease.yaml
spec:
  chart: { spec: { chart: vault } }
  values:
    server:
      ha:
        enabled: true
        config: |
          seal "gcpckms" {
            key_ring = "…"  crypto_key = "vault-unseal"
          }
      # no livenessProbe
  • KMS auto-unseal in the active config fires
  • Shamir / manual unseal never fires: a restart would re-seal it

The merge request you would get.

Illustrative, and on the roadmap as a whole: the probe values and the reasoning are the ones the rule is being built with; the cross-repo evidence section is the planned attribution step.

Draft

Enable the sealed-restart liveness probe on auto-unseal Vault HelmRelease vault/vault

from gitops-ai/vault-autounseal-liveness into main · 1 file · +7 −0
category:reliabilitysource:availability_rulesrisk:mediumxrev::LGTM
OverviewCross-repo evidenceValidationChanges
Summary

This Vault unseals itself through Google Cloud KMS. Vault attempts auto-unseal once, at startup. If that attempt fails, the pod stays sealed and Running indefinitely: readiness only removes it from the Service, and nothing restarts it.

This MR enables the chart's liveness probe on /v1/sys/health?standbyok=true. A sealed pod fails the probe, is restarted, and the restart retries the unseal.

Why these values
  • initialDelaySeconds: 60, then failureThreshold: 6 × periodSeconds: 10 with a 3 s timeout: a KMS blip of up to a minute does not cause a restart loop.
  • standbyok=true keeps healthy standbys at 200. It does not mask 474 (a standby that lost its active node), so that case restarts too.
When not to merge

If this Vault is uninitialised (501) or runs as an Enterprise DR / performance standby (472 / 473), this probe would restart-loop it. Check that neither applies before merging.

Symptom observed in another repository

The scheduled Ansible apply in the playbook repository has failed 12 times in a row at the certificate step with HTTP 503 ['Vault is sealed']. The CI script declares this Vault's address and auth mount, so the failures are attributed here, to the repository that owns the Vault.

EvidenceSource
12 consecutive failed scheduled applies, same signatureplaybook repo · CI job logs
Vault address + auth mount named in the CI scriptplaybook repo · declared edge
3 pods Running, 0/1 ready, 0 restarts, sealedcluster · read-only
KMS auto-unseal, no liveness probecluster repo · this HelmRelease
Note for the playbook repository

A comment on the latest failed run links this MR. No change is proposed there: the playbook is correct, it was the first thing to notice.

Checked before this MR was opened
  • The seal stanza is in the active config block (HA), not a commented-out or unused one.
  • No valuesFrom or post-renderer could override the probe.
  • The seal type is KMS-backed, so a restart retries the unseal instead of re-sealing a hand-unsealed pod.
After merge, expect
  • Vault pods roll one at a time; each comes back unsealed and Ready.
  • vault status shows Sealed: false on all replicas.
  • The next scheduled Ansible apply passes the certificate step.
Rollback

Revert this MR. The probe is the only change.

     server:
       ha:
         enabled: true
         config: |
           …
+      livenessProbe:
+        enabled: true
+        path: "/v1/sys/health?standbyok=true"
+        initialDelaySeconds: 60
+        periodSeconds: 10
+        failureThreshold: 6
+        timeoutSeconds: 3
R
independent reviewer second opinion, no pipeline context

Confirmed the seal stanza is live config and KMS-backed, and that the probe path tolerates standbys. The restart-on-sealed behaviour is the intended fix. LGTM.

What keeps it honest.

Joining domains multiplies what can go wrong. These are the design rules, not afterthoughts.

Masked at the sourceInventory passes through a closed-schema projection before it can reach an LLM. Unknown fields are dropped by default, join keys never appear on the card, and people's names are dropped entirely. Identifier masking with pseudonymous handles; no claim that the data cannot be linked back.
No guessing on joinsA key that matches two records abstains. An address and a hostname are only merged when one record carries both. "Absent" requires a complete read of a declared scope.
Declared edges onlyA CI failure is attributed to another repo only when the dependency is declared in the repo itself. An address injected from the environment makes the answer UNKNOWN, never a guess.
The wrong-direction proposal is pinnedThe probe rule never fires on a Vault with Shamir or manual unseal: there a liveness restart would re-seal a pod someone unsealed by hand.
Stale inventory is visibleEach finding that leans on NetBox reports how old the record is. A stale NetBox produces visible doubt, not silent false drift.
Deterministic firstThe join, the failure signatures and the attribution are rules, not model calls. The model sees a masked card when it helps, after a measured A/B shows it does.

Where this stands.

Live
  • Per-alert investigation with cross-cluster and ownership gates
  • Identifier masking on the review lane's LLM egress
  • Read-only cluster state for every investigation
In development
  • Co-firing correlation for known Kubernetes alert cascades (annotate mode, off by default)
  • Vault auto-unseal liveness rule in the availability pack
  • Scheduled CI run-health for an Ansible repo (report-only)
Roadmap
  • Read-only NetBox client and closed-schema host cards
  • Inventory join for alerts: shadow first, then inject after an A/B
  • Zabbix as an alert source and host-inventory source
  • CI-symptom to cross-repo root-cause attribution
  • NetBox-versus-git drift as draft MRs
← Previous storyAnsible CI/CD