0084. Admin-gated support bundle, etcd snapshot and packet capture downloads

Status: Accepted · Date: 2026-10-27 · Design section: UI backend, Trust, Secrets Decided: three read-only downloads from a node's Talos API are served by the UI backend (DiagnosticsService, server-streaming byte chunks over the existing Connect transport): DownloadSupportBundle (one node, or every approved node of a cluster), DownloadEtcdSnapshot (a control plane node) and CapturePackets (one interface, 30 s by default and 5 min at most, 100 MiB at most, optional validated BPF filter); each needs the Kubernetes RBAC verb download on the subresource talosmachines/support-bundle, talosmachines/etcd-snapshot or talosmachines/packet-capture, checked with a SelfSubjectAccessReview as the caller, and granted to nobody by default, including the ichor-ui ServiceAccount; the chart grants it to named groups or, if the operator insists, to the ServiceAccount; every download records a Warning Event DiagnosticsDownload on the TalosMachine before any byte is read and is refused if that Event cannot be recorded; the backend mints a Talos client certificate per call with the least role the call needs (os:reader, os:etcd:backup, os:operator); nothing is stored; the UI adds an Actions menu on the node page, the CLI adds ichor node support-bundle, ichor node pcap and ichor etcd snapshot

Context

ADR 0082 listed three actions the Talos SDK enables that only read from a node: a support bundle (item 3), an etcd snapshot (item 5) and a packet capture (item 9). It proposed them as TalosOperation kinds, because the rule is that the controller is the only writer to Talos nodes. These three write nothing to a node, and an operation would have to put the result somewhere: a Secret or a CR would hold secrets at rest (the snapshot holds every Kubernetes Secret), which the design forbids. So they are served on demand by the UI backend, to the caller, and kept nowhere. Writes still go only through CRs and the controller.

The calls were checked in Talos v1.14.3 (pkg/machinery and the node's RBAC table in internal/app/machined/pkg/system/services/machined.go).

Decision

API. DiagnosticsService in proto/ichor/v1alpha1/diagnostics.proto:

  • GetDiagnosticsPermissions(machine) returns three booleans, so the UI shows only what the user may start. The downloads ask again.
  • DownloadSupportBundle(machine | cluster), DownloadEtcdSnapshot(machine) and CapturePackets(machine, interface, bpf_filter, duration_seconds, max_bytes) each return a stream of DownloadChunk{filename, content_type, data}; filename and content_type are on the first chunk. A Connect stream rather than a separate HTTP download endpoint, because the browser cannot attach the OIDC bearer token to a plain link, and the stream goes through the same interceptors (authentication, metrics) as every other call.

Authorisation. The verb is download, the resource talosmachines, the subresource one of support-bundle, etcd-snapshot, packet-capture, the object name the machine's. A subresource rather than three pseudo-resources, because RBAC treats talosmachines/etcd-snapshot as a resource of its own: a role with * on talosmachines (the talosmachine_*_role.yaml helpers) does not include it, and the three can be granted separately. The pattern is ADR 0064's override: a SelfSubjectAccessReview made as the user, so Kubernetes decides. Differences from override:

  • With OIDC on, the review is made as the impersonated user.
  • With OIDC off, the review is made as the backend's own ServiceAccount. It holds nothing by default, so a default install refuses every download. It is not refused by a special rule; it is simply not granted, and a chart value can grant it (below).
  • In the CLI's in-process fallback the review is made as whoever the kubeconfig is.
  • A cluster support bundle checks every machine in it; a denial on any machine refuses the whole download.

Chart. ichor-diagnostics-role (a ClusterRole with the three subresources and the verb) is a helper role like ichor-override-role (rbac.helpers.enabled), and is also rendered when a grant is asked for. ichorUI.diagnostics.groups (a list of group names, with the OIDC groups prefix applied) binds it to those groups; ichorUI.diagnostics.serviceAccount: true binds it to ichor-ui's ServiceAccount, which lets anyone who can reach ichor-ui download secrets and is documented as such. Both are empty by default. ichor-ui-role is unchanged; rbac_enforced_test.go runs GetDiagnosticsPermissions and the authorisation check as the generated role and requires every download to be denied, and dist/chart/chart_test.go pins the defaults.

Audit. Before any byte is read, after the authorisation and request validation succeed, the backend creates a Warning Event with reason DiagnosticsDownload on the TalosMachine: who (the impersonated user, or the actor name), which download, when, and for a capture the interface, duration and whether a filter is set. It never carries content. If the Event cannot be created the download is refused (Internal), so an unaudited download of an etcd snapshot cannot happen. The Event is created with the backend's own client, as ADR 0065 does; the ichor-ui role already has events create. A refused request (bad input, no permission) leaves no Event.

Talos identity. ADR 0018 said the UI backend holds a read-only Talos identity. These calls need more on the node: EtcdSnapshot is open to os:admin, os:operator and os:etcd:backup, and PacketCapture to os:admin and os:operator only. The backend therefore mints a client certificate per download from the secrets bundle with the least role that call accepts: os:reader for the bundle, os:etcd:backup for the snapshot (a role made for exactly this), os:operator for the capture. The certificate lives for the call. The backend had the secrets bundle already (it reads it for every Talos connection), so this adds no new secret at rest, but it does mean a compromised backend could mint those certificates for any call, not only these three. The code path has no method that changes a node (DownloadTalosClient has three methods), and architecture.md's Trust section says so. Kubernetes RBAC on the verb is the control that decides who gets them.

Caps, enforced on the server whatever the caller asks.

  • Packet capture: duration_seconds 1 to 300, default 30; max_bytes 1 to 100 MiB, default 100 MiB; the capture stops at the first limit reached and the file is complete up to that point (a classic pcap, so a cut mid-packet at the byte limit is tolerated by readers). The interface name is checked against a short pattern. The BPF filter is accepted in tcpdump -dd format, the form Talos takes, and validated: every line must parse, at most 4096 instructions, and the program must pass golang.org/x/net/bpf's checks (jumps inside the program, ending in a return). Compiling a tcpdump expression such as tcp port 80 needs libpcap, which the pure-Go static binaries do not link; that is left out, and the UI and CLI help show how to produce the -dd text.
  • Support bundle: 256 MiB for the archive, 10 minutes, each log file 8 MiB at most (the tail), 5000 lines per service, 2000 processes, 2000 resources per type.
  • Etcd snapshot: 8 GiB (etcd's own recommended maximum) and 30 minutes.
  • At most two downloads run at once in one backend (ResourceExhausted beyond that), and a backend that starts draining cancels them.
  • When a limit stops a download after bytes were sent, the stream ends with an error (ResourceExhausted or DeadlineExceeded), so the client knows the file is incomplete; the CLI writes to a temporary file and renames it only on success, and the browser saves nothing on an error.

What the support bundle contains. Per node, under <machine name>/: version.json, dmesg.log (kernel ring buffer; the Command line: line has credential-shaped arguments masked by ADR 0073's rule), services.yaml (id, state, health) with logs/<service>.log (last 5000 lines of every Talos service), processes.json (pid, ppid, state, threads, CPU, memory, the executable's base name; never command lines, arguments or cgroup labels, as ADR 0082), mounts.json, memory.json, containers.json (ids, pods, names, images with credentials removed, status, pid; no environment), and resources/<namespace>/<type>.yaml, every COSI resource type that Talos does not mark sensitive, except the whole secrets and config namespaces and the KernelCmdlines and MachineConfigs types. collection-errors.txt lists what could not be read (an error text is cut at 300 bytes). A README.txt at the root says what is and is not in the file. talosctl support also collects the logs and manifests of kube-system pods through a kubeconfig fetched with os:admin; that is not done, because the Kubernetes API is reachable by other means and the kubeconfig is a credential. The archive is a zip, not encrypted (talosctl support encrypts to the Sidero Labs recipients by default, which an operator of Ichor does not want).

What is still sensitive. Service logs and the resource dumps are free text. Hostnames, IP addresses, MAC addresses, serials, extension configuration and anything a program wrote to a log (a token in a URL, say) remain. The etcd snapshot contains every Kubernetes Secret, unencrypted unless etcd encryption at rest is enabled in the cluster, and the packet capture records whatever is on the wire. Each file is therefore shown behind an "this file may contain secrets" confirmation in the UI, and the CLI writes it with mode 0600.

Delivery. UI: the stream is read into memory and saved with an object URL, refused above 1 GiB (the CLI has no such limit; the dialog and the error say so). The bearer token travels in a header and the request is a Connect POST with a JSON or protobuf content type, so a cross-site page cannot start a download (no ambient cookie, and a simple form cannot send that content type); with OIDC off there is no identity, and the ServiceAccount holds no grant by default. CLI: ichor node support-bundle NODE | --cluster NAME, ichor etcd snapshot --node NODE, ichor node pcap NODE --interface eth0 --duration 30s [--filter TEXT|@FILE] [--max-size BYTES]; each takes -o FILE (overriding the global -o yaml|json for these commands), writes mode 0600 and prints a reminder that the file may hold secrets. They go through ichor-ui like the other commands. If ichor-ui requires OIDC, the CLI falls back to the in-process path (internal/cli's localClients) with the user's kubeconfig, which then also needs to read the secrets bundle and reach the node's Talos port from the user's machine; if ichor-ui is on its own ServiceAccount and refuses, the error says to run again with --local.

UI. A node page "Actions" dropdown (preactcn dropdown-menu) with "Download support bundle", "Download etcd snapshot" (control planes only) and "Capture packets…" (a dialog with the interface chosen from the node's own links, a duration and a filter). Each ends in a preactcn alert-dialog that says the file may contain secrets and that the download is recorded. The menu is not rendered at all when GetDiagnosticsPermissions says the user may do none of them, and an action the user may not do is omitted. Results show as sonner toasts. The toast helper and Toaster mounted in app.tsx are this change's own: no shared one was on main when it was written.

Consequences

  • A default install can download nothing. An operator who wants it binds ichor-diagnostics-role to a group (OIDC on) or sets ichorUI.diagnostics.serviceAccount (OIDC off, and then any client of ichor-ui can).
  • The UI backend's Talos trust is wider than ADR 0018's: per call, it can act as os:operator for a packet capture. os:operator can also reboot a node, so the one call site that uses it must stay the packet capture.
  • The etcd snapshot goes through ichor-ui's memory-free io.Copy to the client; the backend holds at most a 64 KiB chunk at a time except for the support bundle (one file at a time, 8 MiB).
  • Not verified on hardware: nothing here has run against a real Talos node. The Talos side is the vendored client (client.EtcdSnapshot, PacketCapture, Dmesg, Logs, COSI List) and the roles are read from the node's RBAC table; the fake client in the tests stands in for the node. A Talos version that changes that table makes a download fail visibly with the node's refusal.
  • Whether os:operator or os:admin is needed for PacketCapture on the Talos versions Ichor supports besides 1.14 is not checked.

Amendment: GetDiagnosticsPermissions is replaced by GetMyPermissions(machine?, cluster?), one call that answers these three flags and ADR 0093's credentials flag with concurrent SelfSubjectAccessReviews; the pages fetch it once and pass the flags down.

Search the docs

Search pages and headings