Scrub a kubectl log before you share it

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.

Paste the log. Nothing leaves your browser.

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.

The same secret, four ways out of the cluster

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.

What the redactor on this page does with each
kubectl get secret -o yamlSees nothing

In

  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 out

In

      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 out

In

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 out

In

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.

Do this

  1. Fetch the narrowest thing that answers the question. 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.
  2. Get it out as text, not as a screenshot. A screenshot cannot be redacted by any tool, cannot be searched by you later, and carries your terminal title bar, your prompt and your current context along with the log. Redirect it: kubectl logs ... > pod.log.
  3. Redact before it leaves the machine. Paste it into the box above, or run npx logscrub pod.log on the file. Same detectors, same output, no network in either direction.
  4. Read the output yourself. The tool finds credential shapes. It does not know that 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.
  5. If the unredacted version already went out, rotate. Deleting the message is not the fix — it was delivered to notification emails and mobile pushes before you deleted it. The order that limits the damage is: invalidate at the issuer, read the audit log, clean up last.

base64 is an encoding, and that is the whole problem

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.

What kubectl redacts for you, and what it does not

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.

CommandWhat 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.

Where the secret actually was

The application log, nearly always

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.

The env var someone wrote as a literal

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.

The kubeconfig, pasted to prove a connection problem

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.

The verbose run somebody did while debugging

--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.

What the redactor here catches in Kubernetes output, and what it does not

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.

Caught

Not caught, and you should know which

One false positive I can reproduce

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.

Sharing it into Slack specifically

The reason this page names Slack is that chat feels like a conversation and behaves like a database.

A channel is not a person

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.

It is indexed, and search has no expiry

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.

Deleting is not recalling

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 same thing, offline and in CI

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 browser npm 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.

What this page does not cover

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.