A pod is crashing, someone asks to see the log, and the fastest thing you can do is select the terminal and paste. This is what goes with it that you did not mean to send — and how to take that out before it lands in a channel that never forgets.
Redact before you copy, not after you paste. Run the output through a
redactor that works on text — the one below runs inside this page, and the same rules run
offline in your terminal — then read the result yourself. kubectl logs is
where Kubernetes credentials actually leak, and they leak in plain text: a connection string
with a password in it, an Authorization header some middleware decided to log, a
JWT inside a dumped request. Those are shapes a scanner can find.
What a scanner cannot find is base64. kubectl get secret -o yaml
returns every value base64-encoded, and base64 is an encoding, not encryption:
base64 -d reads it, and so does anyone you send it to. The redactor here does not
decode it. I tested that, and the result is below. If you paste a Secret manifest in and it
comes back looking clean, that is the tool being blind, not the log being safe.
Built for exactly this: a kubectl logs tail, a kubectl describe pod
dump, a crash trace on its way into a bug report. Credentials come back as numbered placeholders,
so the same token in two places is still visibly the same token and the log is still debuggable.
It runs in this page — no upload, no network call, no storage.
One credential — a Postgres DSN and a payments key — reaches your clipboard through four different commands, and a text redactor has a completely different amount to work with in each. Every line below is real input and the real output the detectors above produced from it.
kubectl get secret -o yamlSees nothingIn
DATABASE_URL: cG9zdGdyZXM6Ly9hcHA6czNjcjN0cHdAZGItcHJpbWFyeS5pbnRlcm5hbDo1NDMyL2FwcA==
Out
DATABASE_URL: cG9zdGdyZXM6Ly9hcHA6czNjcjN0cHdAZGItcHJpbWFyeS5pbnRlcm5hbDo1NDMyL2FwcA==
Unchanged. That blob decodes to
postgres://app:s3cr3tpw@db-primary.internal:5432/app, which the same tool
redacts instantly in its decoded form. The encoding is the entire difference.
kubectl describe pod, literal env varPassword outIn
DATABASE_URL: postgres://app:s3cr3tpw@db-primary.internal:5432/app
Out
DATABASE_URL: postgres://app:[PASSWORD_1]@db-primary.internal:5432/app
The password goes, the scheme, user, host, port and database name stay — which is what makes the line still worth reading. Note what survives: your internal hostname is not a credential and nothing here will remove it for you.
kubectl logs, header dumpWhole value outIn
DEBUG checkout req.headers.authorization="Bearer sk_live_51H8xQ2AbCdEfGhIjKlMnOpQr"
Out
DEBUG checkout req.headers.authorization="[SECRET_1]"
The whole header value, scheme word included. This is the one that matters: an application log is plain text by construction, so it is both the likeliest leak and the one a redactor is genuinely good at.
kubectl --v=8, request headersToken outIn
round_trippers.go:473] Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6Ilg5cUxmMlIifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50In0.RmFrZVNpZ25hdHVyZQ
Out
round_trippers.go:473] Authorization: Bearer [JWT_1]
A service-account token is a JWT, so it is caught by shape with no name
needed nearby. The word Bearer is deliberately kept — knowing the scheme
helps whoever debugs this, and it is not the secret.
Three of the four are readable to a pattern scanner and one is not, and the one that is not is the one people think is the safe format because it does not look like a password. Base64 is the gap. Everything else on this page follows from that.
kubectl logs deploy/checkout -c app --tail=200 --since=15m beats the whole stream.
kubectl describe secret payments-api — which prints key names and byte counts
and no values at all — beats kubectl get secret -o yaml when all anyone needs
to know is that a key exists. A field you never fetched is a field you cannot leak.kubectl logs ... > pod.log.npx logscrub pod.log on the file. Same detectors, same output, no network in either
direction.db-primary.payments.svc.cluster.local describes your topology, that
checkout-7d9f8b6c4-x2ktp names an internal service and its replica set, or that an
image tag gives away the registry you use. Those are judgement calls and they stay yours.A Kubernetes Secret does not encrypt anything at the point you read it. data:
values are base64, which exists so arbitrary bytes survive YAML, not so they survive a reader.
Anyone with the manifest gets the value back with one pipe. Two consequences, and the second one
is the one that catches people:
First, a Secret manifest is not safer to paste than the plaintext it encodes. It is the same
disclosure wearing a costume. Second, base64 defeats a naive text scanner completely, because
every pattern a scanner knows — AKIA, ghp_, sk_live_,
eyJ, postgres://user:pass@ — is destroyed by the encoding. The
prefixes that make a credential identifiable only work on the bytes
themselves.
So: the redactor on this page does not decode base64. I ran a real Secret manifest through it to check rather than assume, and here is exactly what came back:
data: DATABASE_URL: cG9zdGdyZXM6Ly9hcHA6czNjcjN0cHdAZGItcHJpbWFyeS5pbnRlcm5hbDo1NDMyL2FwcA== STRIPE_SECRET_KEY: [SECRET_1]
Both lines hold a live credential. One was replaced and one was not, and the difference is the
field name: STRIPE_SECRET_KEY matches an assignment rule that reads names
ending in _key or containing secret, so its value went regardless of what
the value looked like. DATABASE_URL matches nothing, so its base64 sat there
untouched. Where this tool does redact a Secret manifest, it is reading the name, never the value.
That is not decoding, and you should not treat it as coverage.
The honest workflow, then: do not share Secret manifests. If you genuinely need
to show that a key is present and populated, kubectl describe secret gives you the key
names and their byte lengths and nothing else, which is almost always the actual question. If you
need to show that a value is wrong, decode it yourself, look at it, and say what is wrong
in words.
Some of these commands mask things and some do not, and the pattern is not intuitive — the command with “secret” in the name is the one that hands you everything.
| Command | What comes out |
|---|---|
kubectl logs |
Your application's bytes, unmodified. Nothing is masked because nothing knows what any of it means. This is the usual leak and the one worth scrubbing every single time. |
kubectl get secret -o yaml |
Every value, base64, no masking. If the object was created with
kubectl apply it also carries a last-applied-configuration
annotation holding the same data again, so deleting one field before you paste does not do
what you hoped. |
kubectl describe secret |
Key names and byte counts. No values. This is the safe one, and it answers the question people usually mean. |
kubectl describe pod |
Env vars sourced from a Secret print as a reference —
<set to the key 'x' in secret 'y'> — not a value. So do ones sourced
from a ConfigMap. But an env var written as a literal value: in the manifest
prints in full, and that is where the DSN with the password in it shows up. It also
prints the node name and node IP. |
kubectl describe configmap |
Every value, in full, no masking whatsoever. ConfigMaps are not Secrets, which is precisely why people keep putting connection strings, webhook URLs and “temporary” tokens in them. |
kubectl config view |
Masks certificate and key data as DATA+OMITTED and tokens as
REDACTED. Adding --raw prints all of it — that is what the
flag is for, and it is the flag people copy out of an answer without reading why it was
there. |
kubectl --v=7 and above |
Request headers. --v=8 adds request and response bodies and
--v=9 stops truncating them, so kubectl get secret --v=8 prints the
base64 values a second time inside the raw response. Recent client library versions mask the
Authorization value in this output; older ones and other tools built on the same
library do not. Check the bytes in front of you rather than trusting that. |
kubectl cluster-info dump |
Logs and object definitions for everything it can reach, in one enormous artifact. Fine for a support case; never something to paste unread. |
An HTTP middleware that logs request headers. An ORM that prints the connection string when the connection fails. An exception handler that dumps the whole request object, params included. A retry that logs the URL it is retrying, query string and all. None of these are misconfigured — they are all default-ish behaviour that is fine until someone shares the output. This is the class the tool above is genuinely good at, because it is all plain text.
kubectl describe pod is careful about values that come from a Secret and has
nothing to be careful with when the manifest says value: "postgres://app:hunter2@..."
outright. That is not an exotic mistake; it is what a Deployment looks like before somebody gets
round to moving the credential into a Secret, and “before somebody gets round to it”
is a long time in most clusters.
A kubeconfig carries a client certificate, its private key, and often a long-lived token, all
base64 in one file. Pasting it is handing over cluster access at whatever level that user has.
kubectl config view without --raw masks the dangerous fields already;
if you need the real thing for a support case, redact it and check the result by eye.
--v=8 exists to show you the wire, and the wire carries bearer tokens and the
bodies of Secret objects. It is the densest credential artifact kubectl can produce. Scrub it or
do not keep it.
Measured against the actual detectors, not guessed. Everything in the first list I ran and watched come back replaced; everything in the second list I ran and watched come back untouched.
client-key-data in a kubeconfig, and tls.key and
.dockerconfigjson blobs in a Secret — these have dedicated rules, so their
base64 goes even though base64 is not decoded.Authorization header values, in a header dump or a JSON-serialised request,
including the Basic form.user:pass@host half of a DSN in any scheme,
leaving the rest of the URL readable.<vendor>_live_<random> convention, so a vendor nobody has heard of yet is
caught by shape.password= / token= / api_key= style assignments,
including the YAML block-scalar form where the value sits on the following indented lines.client-certificate-data in a kubeconfig stays in the output
while client-key-data next to it is replaced. That split is deliberate — the
certificate is the public half and the key is the half that grants access — but if you are
redacting a kubeconfig to hide who you are as well as what you can do, the certificate
still names you.10.x, 172.16–31.x
and 192.168.x are skipped on purpose: pod and service CIDRs are in every Kubernetes
log ever written and flagging them would bury the real findings. A public node IP is replaced.Node: ip-52-91-14-203.ec2.internal/52.91.14.203 the dotted address is replaced and
the identical address spelled with hyphens inside the hostname is not. Cloud providers put the
address in the name; a rule that matched it there would match half the DNS names in the
world.Run a kubectl describe pod block through it and this happens:
in: STRIPE_SECRET_KEY: <set to the key 'stripe' in secret 'payments-api'> Optional: false out: STRIPE_SECRET_KEY: [SECRET_1] to the key 'stripe' in secret 'payments-api'> Optional: false
The field name looks like a credential assignment, so the rule fires and swallows the
<set token — it has redacted kubectl's own redaction marker. Nothing real is
lost and the line stays readable, but it is wrong, and a tool that only ever showed you its wins
would not be worth trusting on the misses either. If you want the longer version of that argument,
there is a whole corpus of ordinary log output that must not be
flagged, which is how rules like this get narrowed.
The reason this page names Slack is that chat feels like a conversation and behaves like a database.
You are answering one colleague; the message lands in front of everyone in the channel, and in a public channel, in front of everyone who joins later and scrolls back. Full history is the default, not an option someone turned on.
A credential pasted today is a search hit for postgres:// months later, run by
someone who was not there. That is the actual threat model for chat: not an attacker reading
over your shoulder, but ordinary search by ordinary colleagues, plus every integration and bot
with read access to that channel.
By the time you delete it, the message has been pushed to phones and mailed out as a notification, both of which carry the text. On paid plans it is also inside whatever export or compliance tooling the workspace has. Delete it anyway — but delete it after you rotate, not instead.
Practically: post the scrubbed version as a snippet or an attached file rather than an inline paste, so it is one collapsed object rather than a wall of scrollback, and so a later reader has to open it deliberately. Use a thread or a direct message when the log only concerns one person. And if you are about to paste something and are not sure, the quick check for what leaks in a stack trace is the general version of this page.
The detectors in the box above are the same ones in the command-line package and in the downloadable offline copy — generated from a single source, so no version of this is quietly weaker than another.
The full Log Redactor Every detector, each one switchable, with a diff of what changed and a note on anything it saw and deliberately declined. Same browser-only guarantee: nothing is uploaded. Run it without a browsernpm install logscrub for the command line and CI, or the offline single-file
copy for a machine with no network. Free, MIT, no account.
In a pipeline the useful shape is a step that scrubs artifacts before they are uploaded, so a build log that captured an env dump never becomes a public URL — the same failure as this page's, one layer out.
It does not cover stopping the secret from reaching the log in the first place, which is the real fix and lives in your logging configuration — header allow-lists, redacting serialisers, not printing the DSN on a connection error. Scrubbing on the way out is a seatbelt, not a solution.
It does not cover cluster hardening: RBAC so that fewer people can read Secrets at all, encryption at rest for etcd, or an external secret store that keeps values out of the API object. Those change what a leak costs; this page only changes what leaves your terminal.
And it does not cover redacting screenshots or screen recordings, which no text tool can help with, or whether an exposure obliges you to tell anyone. That last one is a question for your organisation, not for me.