A support bundle is the archive a tool builds when something breaks: logs, configuration, resource dumps and process lists, zipped up so a maintainer can see what you see. It's also an export of your cluster's state that people attach to public GitHub issues, Slack threads and forum posts. Talos Linux's advisory CVE-2026-77564 is a small, clear example of what that can carry. Bundles from talosctl support included the credentials for private container registries.
It's rated low, and Talos has fixed it. This post is for platform teams that generate or receive support bundles from any tool, and for maintainers who build them. It covers how the Talos leak happened, why "redact the sensitive types" fails open, what a resource-level fix still can't see, and a script that scans a bundle before you share it.
The short answer
Before Talos 1.13.3 and 1.12.9, talosctl support bundles contained private registry credentials in the registry config resource, including the base64 auth value that decodes to a username and password. The bundle writer redacted a resource's contents only if its type was marked sensitive, and the registry config type wasn't. The fix releases redact registry credentials from the machine config, a follow-up commit marks seven more resource types sensitive, and Talos 1.14.0 encrypts bundles by default so only the intended recipients can read them. Upgrade, rotate any registry credential that appeared in a bundle you shared, and scan bundles from any tool before posting them anywhere.
How the leak happened
The bundle is assembled by Sidero Labs' go-talos-support library. For each resource type, it writes every resource's metadata and spec to a YAML file. The redaction rule is one condition: if the resource definition is marked Sensitive, the spec is replaced with <REDACTED>; otherwise it's written as is.
The registry configuration resource (RegistriesConfigs.cri.talos.dev) holds mirrors, TLS settings and auth entries with username, password, auth and identityToken fields. It wasn't marked sensitive, so its full spec went into resources/registryconfigs.cri.talos.dev.yaml. The advisory notes the same data also appeared in the generated /etc file specs.
| CVE | CVE-2026-77564 (GHSA-7cxh-94cf-mfp6) |
| Severity | Low, CVSS 3.1 score 3.3 |
| Fixed (per the advisory) | Talos 1.13.3 (26 May) and 1.12.9 (19 June) |
| Repository advisory | 24 August |
| GitHub advisory database | 15 October |
| Reported by | Thanzex |
The reporter's impact section is the useful part. On its own the bug is minor, but anyone who uploaded a bundle to a public place may have published registry credentials. If that registry is reachable from the internet and the credentials can push, the consequence stops being minor: someone could overwrite tags your clusters pull, which is a supply chain compromise.
The fix, and why the old rule failed open
Two changes went in. A May pull request redacts more secrets from the machine config: registry credentials, mutual TLS client keys, WireGuard private keys, two cloud VIP API tokens and static disk encryption passphrases. It also adds a test that walks every registered config document type and flags string fields whose YAML names look like secrets, so a new field can't slip through unnoticed. A June commit, included in 1.12.9, marks seven more resource types sensitive, including the registry config, the generated /etc files, and several Kubernetes component configs.
Then Talos 1.14.0 (3 September) changed the default. talosctl support now encrypts the bundle with age to the members of the siderolabs GitHub organization. You can add recipients, drop the defaults or turn encryption off. A bundle posted publicly by mistake is now unreadable to everyone else.
The design lesson is in the original rule. "Redact types marked sensitive" means every new resource type is exported in full until someone remembers to mark it. That's a deny-list, and deny-lists fail open as a codebase grows. The safer shapes are the ones Talos moved toward: a test that fails when a secret-looking field appears unredacted, and encryption that protects the whole bundle when a field is missed.
What a resource-level redactor can't see
Even a perfect resource redactor only covers the tool's own structured data. A Talos bundle also includes a process list with full command lines, logs from Talos services, and logs from kube-system pods. Those come from whatever is running on the node. A component that takes a key as a flag, or logs an Authorization header, puts that secret in the bundle, and no list of sensitive types will catch it.
The same is true of support bundles in general. They gather logs, environment details and configs because that's what makes them useful. Treat any bundle as containing secrets until you've checked.
Scan a bundle before you share it
This script reads a .zip, .tar, .tar.gz or a directory, including archives nested inside it, without extracting anything to disk. It looks for private keys, AWS key IDs, GitHub tokens, JWTs, bearer tokens, kubeconfig keys and secret-looking key-value pairs, and prints masked values only. For a registry auth value, it says whether it decodes to a username and password, without printing the password. It uses only the Python standard library:
#!/usr/bin/env python3
"""Scan a support or diagnostic bundle for credentials before you share it.
bundle_scan.py BUNDLE [BUNDLE ...] .zip, .tar, .tar.gz/.tgz, or a directory
Reads nested archives in memory, never extracts to disk, and prints masked
values only. Exit 1 if anything is found. Standard library only.
"""
import base64
import io
import pathlib
import re
import sys
import tarfile
import zipfile
RULES = [
("private key", re.compile(r"-----BEGIN (?:[A-Z]+ )?PRIVATE KEY-----")),
("AWS access key id", re.compile(r"\b(?:AKIA|ASIA)[0-9A-Z]{16}\b")),
("GitHub token", re.compile(r"\bgh[pousr]_[A-Za-z0-9]{36,}\b")),
("JWT", re.compile(r"\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}")),
("bearer token", re.compile(r"(?i)authorization:\s*bearer\s+([A-Za-z0-9._~+/=-]{16,})")),
("kubeconfig key", re.compile(r"\b(?:client-key-data|token):\s*([A-Za-z0-9+/=._-]{16,})")),
# key: value pairs whose key looks secret (YAML, JSON, TOML, env files)
("secret-looking field", re.compile(
r"""(?ix)["']?\b(password|passwd|secret|token|identitytoken|apikey|api_key|auth|
privatekey|private_key|passphrase|clientsecret)\b["']?\s*[:=]\s*["']?([^\s"',}]{6,})""")),
]
PLACEHOLDERS = re.compile(r"(?i)^(<redacted>|redacted|\*+|x{6,}|null|none|true|false|\$\{[^}]+\}|changeme)$")
TEXT_LIMIT = 20 * 1024 * 1024
def mask(value):
return value[:4] + "..." + f"({len(value)} chars)" if len(value) > 8 else "***"
def docker_auth_note(value):
"""A registry 'auth' value is base64(user:password); say so without printing it."""
try:
raw = base64.b64decode(value + "=" * (-len(value) % 4), validate=True).decode()
except Exception: # noqa: BLE001
return ""
return f" [decodes to user '{raw.split(':', 1)[0]}' + password]" if ":" in raw else ""
def scan_text(name, data, hits):
if b"\0" in data[:4096]:
return
for n, line in enumerate(data.decode("utf-8", "replace").splitlines(), 1):
for label, rx in RULES:
for m in rx.finditer(line):
value = m.group(m.lastindex) if m.lastindex else m.group(0)
if PLACEHOLDERS.match(value.strip()):
continue
extra = docker_auth_note(value) if label == "secret-looking field" and m.group(1).lower() == "auth" else ""
hits.append(f"{name}:{n}: {label}: {mask(value)}{extra}")
break
def walk(name, data, hits, depth=0):
if depth > 4:
return
buf = io.BytesIO(data)
if zipfile.is_zipfile(buf):
with zipfile.ZipFile(buf) as z:
for info in z.infolist():
if not info.is_dir() and info.file_size < TEXT_LIMIT:
walk(f"{name}!{info.filename}", z.read(info), hits, depth + 1)
return
try:
with tarfile.open(fileobj=io.BytesIO(data)) as t:
for m in t.getmembers():
if m.isfile() and m.size < TEXT_LIMIT:
walk(f"{name}!{m.name}", t.extractfile(m).read(), hits, depth + 1)
return
except tarfile.TarError:
pass
if data[:24].startswith(b"age-encryption.org/v1"):
hits.append(f"{name}: age-encrypted, cannot scan (only the recipients can read it)")
return
scan_text(name, data, hits)
def main(paths):
found = 0
for p in map(pathlib.Path, paths):
hits = []
files = sorted(f for f in p.rglob("*") if f.is_file()) if p.is_dir() else [p]
for f in files:
walk(str(f), f.read_bytes(), hits)
print(f"{p}: {len(hits)} finding(s)")
for h in hits:
print(" " + h)
found += sum("cannot scan" not in h for h in hits)
return 1 if found else 0
if __name__ == "__main__":
if len(sys.argv) < 2:
sys.exit(__doc__)
sys.exit(main(sys.argv[1:]))
We built three synthetic bundles shaped like a Talos bundle, with fake credentials (the AWS key is the example key from AWS's own documentation): one with the pre-fix registry resource, one with the spec redacted, and one file with an age header standing in for an encrypted bundle:
"""Synthetic support bundles shaped like a Talos bundle, with fake credentials."""
import base64, io, zipfile
AUTH = base64.b64encode(b"ci-bot:EXAMPLE-registry-pass").decode()
REGISTRY_SPEC = f"""metadata:
namespace: cri
type: RegistriesConfigs.cri.talos.dev
id: registries
spec:
mirrors:
registry.example.internal:
endpoints:
- endpoint: https://registry.example.internal
auths:
registry.example.internal:
username: ci-bot
password: EXAMPLE-registry-pass
auth: {AUTH}
"""
REDACTED_SPEC = """metadata:
namespace: cri
type: RegistriesConfigs.cri.talos.dev
id: registries
spec: <REDACTED>
"""
COMMON = { # the same in both bundles: things a resource-level redactor never sees
"node-1/dmesg.log": "[ 0.000000] Linux version 6.18.33-talos\n",
"node-1/controller-runtime.log": "level=info msg=\"pulled image\" registry=registry.example.internal\n",
"node-1/processes": " PID STATE THREADS CPU-TIME VIRTMEM RESMEM COMMAND\n"
" 2211 S 9 0.41 1.2 GB 48 MB /usr/bin/dns-sync --aws-key=AKIAIOSFODNN7EXAMPLE\n",
"node-1/kubernetes-logs/kube-system/metrics-proxy-7d9f.log":
"proxying GET /metrics Authorization: Bearer EXAMPLEtokenEXAMPLEtoken123\n",
}
def build(path, registry_spec):
with zipfile.ZipFile(path, "w") as z:
z.writestr("node-1/resources/registryconfigs.cri.talos.dev.yaml", registry_spec)
for name, body in COMMON.items():
z.writestr(name, body)
build("support-before.zip", REGISTRY_SPEC)
build("support-after.zip", REDACTED_SPEC)
with open("support-encrypted.zip.age", "wb") as f:
f.write(b"age-encryption.org/v1\n-> X25519 (header only, for the scanner test)\n")
$ python3 bundle_scan.py support-before.zip support-after.zip support-encrypted.zip.age
support-before.zip: 4 finding(s)
support-before.zip!node-1/resources/registryconfigs.cri.talos.dev.yaml:13: secret-looking field: EXAM...(21 chars)
support-before.zip!node-1/resources/registryconfigs.cri.talos.dev.yaml:14: secret-looking field: Y2kt...(40 chars) [decodes to user 'ci-bot' + password]
support-before.zip!node-1/processes:2: AWS access key id: AKIA...(20 chars)
support-before.zip!node-1/kubernetes-logs/kube-system/metrics-proxy-7d9f.log:1: bearer token: EXAM...(27 chars)
support-after.zip: 2 finding(s)
support-after.zip!node-1/processes:2: AWS access key id: AKIA...(20 chars)
support-after.zip!node-1/kubernetes-logs/kube-system/metrics-proxy-7d9f.log:1: bearer token: EXAM...(27 chars)
support-encrypted.zip.age: 1 finding(s)
support-encrypted.zip.age: age-encrypted, cannot scan (only the recipients can read it)
(exit 1)
The redacted bundle drops the two registry findings and keeps the other two. That's the point of the section above: the vendor's redaction worked, and the process list and pod log still carry secrets. The encrypted file can't be scanned, which is correct. Only its recipients can read it.
Pattern scanning has limits. It will miss secrets with no recognisable shape and flag some harmless strings. Use it as a gate before sharing, not as proof a bundle is clean, and open the flagged files yourself.
What to do
- Upgrade Talos to 1.13.3, 1.12.9 or later, and prefer 1.14 so bundles are encrypted by default. Leave encryption on unless you have a reason not to.
- Rotate credentials that appeared in shared bundles. Search your old GitHub issues, Slack threads and tickets for bundles from affected versions. Registry credentials with push access come first.
- Scan before sharing, from any tool. Run the script above, or your secret scanner, over the archive, and remove or redact what it finds.
- Share privately when you can. Ask the maintainers for a private channel or an encrypted upload. A public issue is the worst place for a bundle.
- Keep secrets out of flags and logs. Command lines and request logs end up in bundles, process listings and crash reports. Our secrets management guide covers the alternatives, and our post on container registry credential leaks covers why registry credentials are worth protecting.
- If you maintain a support tool, redact by default, test for secret-looking fields, and encrypt the output to the people who'll read it.
The fair view
Talos handled this well, and arguably better than the report needed. A low-severity leak produced a broader redaction audit, a regression test that checks every document type, and default encryption, which removes the whole class of risk for bundles shared by mistake.
The wider point isn't about Talos. Support bundles exist to capture everything, and people share them under pressure, with an outage running and a maintainer asking for logs. The tool's redaction is one safeguard. A quick scan before you attach the file is the other, and it catches the secrets no vendor can know about.