Redact AWS keys from CloudWatch logs

You are about to paste CloudWatch output, a Lambda log or an aws --debug transcript into an issue, a chat or a model. Here is what in it is actually a credential, which half a scanner can see, and what to do about the half it cannot.

Scrub it, then read the output yourself. The access key id — the AKIA or ASIA string — is the easy half: fixed prefix, fixed length, found by any scanner, and on its own it unlocks nothing. The 40-character secret access key next to it has no prefix, no checksum and no distinctive shape, so it is found by the name in front of it and by nothing else. If it is sitting bare on a line, a pattern scanner — mine included — will not see it. That is the line you have to find with your eyes.

If the credential was live, redaction is not the fix. Scrubbing your copy changes nothing about the log, and CloudWatch Logs has no API for deleting one event — the smallest thing you can delete is a log stream. Rotate the key first and read CloudTrail second; the clean paste only protects the next reader.

Paste the log. Nothing leaves your browser.

Access key ids, anything behind a name like aws_secret_access_key or x-amz-security-token, and public IP addresses come back as numbered placeholders, so the log is still readable enough to debug from. The detectors run in this page; there is no upload, and no request is made with your text in it.

One credential pair, and which half the tool can see
AWS_ACCESS_KEY_ID AKIAIOSFODNN7EXAMPLE 20 characters. A four-letter prefix — AKIA for a long-term IAM user key, ASIA for a temporary one — then sixteen more of A–Z and 0–9. No lowercase. It is the username half. It names an identity; it proves nothing. AWS prints it itself: it is in every CloudTrail record the key ever caused. bare on a line, tool output[AWS_KEY_1] caught
AWS_SECRET_ACCESS_KEY wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY 40 characters. Upper, lower, digits, + and /. No prefix, no checksum, no fixed anything — the same shape as a base64 hash, a build id or a chunk of a minified bundle. It is the password half. Combined with the id, it signs requests as that identity until someone deactivates it. bare on a line, tool outputwJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY unchanged behind its name, tool outputaws_secret_access_key = [SECRET_1] caught

The greppable half is the harmless half. Every scanner in the world finds the AKIA string, and every scanner — including this one — needs a name, a quote or an = next to the secret to find that. So an AKIA in your log is not the finding. It is the signpost: go and look at what is on the same line, in the same JSON object, in the next forty characters.

Both values above are AWS’s own published documentation example pair, which is why they are safe to print here — and why a scanner that keeps a blocklist of famous example keys would tell you this figure is clean. Output shown is what the detectors on this page actually return for those inputs.

The shapes, and what the tool does with each

Verified by running each of these through the same detectors the box above uses. Where it misses, it says so — a redactor that overstates its reach is worse than none, because you stop reading the output.

Access key id caught bare

Twenty characters, no name needed. AKIA is a long-term IAM user key; ASIA is a temporary one from STS, which is what a Lambda function, an ECS task or anyone who ran assume-role is using.

in   AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
out  AWS_ACCESS_KEY_ID=[AWS_KEY_1]

Secret access key caught when named missed bare

The rule reads the name: aws_secret_access_key, "SecretAccessKey":, secret_key=, anything ending in _secret or access_key, in an assignment, a JSON field or a YAML block. Alone on a line it is forty characters of base64 with nothing to distinguish it from a checksum, and it comes back untouched.

in   "SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
out  "SecretAccessKey": "[SECRET_1]"

in   wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
out  wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

The full Log Redactor has an opt-in detector for long high-entropy strings that does catch the bare form. It is off by default because it also redacts your git hashes, your content digests and half of any minified file — the false-positive corpus page is the argument for leaving it off until you know your input warrants it.

Session token caught when named missed bare

Temporary credentials come as a triple: an ASIA id, a secret, and a session token that is hundreds of characters long and, in the current format, begins IQoJb3JpZ2lu. It is caught behind aws_session_token, "SessionToken": or the x-amz-security-token header, and missed when it is dumped on its own.

in   x-amz-security-token:IQoJb3JpZ2luX2VjEJr//////////wEaCXVzLWVhc3Qt
out  x-amz-security-token:[SECRET_1]

Length is its own hazard here. If the CloudWatch console or your terminal hard-wrapped the token across two lines, the match ends at the break: you get a tidy placeholder and the tail of the token in the clear underneath it, which looks handled. Un-wrap before you paste.

SigV4 Authorization header signature not redacted

A signed request trace — curl -v, an SDK debug log, an API Gateway access log with headers on — carries a line like this. The key id inside Credential= is replaced. The signature at the end is not, and neither is the credential scope (date, region, service).

in   Authorization: AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20260902/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=fe5f80f77d5fa3beca038a248ff027d0445342fe2855ddc963176630326f1024
out  Authorization: [SECRET_1] Credential=[AWS_KEY_1]/20260902/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=fe5f80f77d5fa3beca038a248ff027d0445342fe2855ddc963176630326f1024

Two things in that output are worth knowing. The first is cosmetic and mine: the assignment rule sees the word Authorization followed by a colon and replaces the algorithm name AWS4-HMAC-SHA256 as if it were a value. Harmless noise, but it is noise, and I would rather you read it here than wonder.

The second is not cosmetic and is not mine. That signature is a keyed hash over one canonical request on one date. You cannot run the derivation backwards to recover the secret, and it will not sign anything else — so a leaked request signature is genuinely less bad than a leaked secret. Do not confuse that with the next case, where the signature is the credential.

Presigned S3 URL signature not redacted

This is the one to check by hand. A presigned URL is a complete, transferable authorisation: anyone holding it can perform that operation, from any machine, with no key of their own, until it expires — up to seven days for a URL signed with a long-term key, and only until the session ends for one signed with temporary credentials. The tool replaces the key id in X-Amz-Credential and leaves X-Amz-Signature alone.

in   ...&X-Amz-Credential=ASIAY34FZKBOKMUTVV7A%2F20260902%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Expires=604800&X-Amz-Signature=fe5f80f7…326f1024
out  ...&X-Amz-Credential=[AWS_KEY_1]%2F20260902%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Expires=604800&X-Amz-Signature=fe5f80f7…326f1024

(Query string abridged in the middle; the point is that the signature parameter is byte-for-byte identical on both lines.) If your log prints presigned URLs — download links, upload targets, anything your service handed a browser — treat the whole URL as a secret and delete the line rather than redacting it. X-Amz-Expires tells you how long you have, and it is not a reason to relax.

Account ids and ARNs left alone, deliberately

A 12-digit account id and the ARNs that embed it are identifying, not authenticating. They name your account, your roles, your buckets and often your project. Knowing them lets someone guess at role names or try a confused-deputy angle; it does not let them call anything. The tool leaves them, because redacting every ARN turns an AWS log into unreadable soup and you lose the ability to debug from your own paste.

If a bucket name, an internal hostname or a role name really is confidential in your organisation, that is what the custom terms box in the full Log Redactor is for. No regular expression can guess which of your nouns are secret.

Principal unique ids over-redacted

AROA…, AIDA…, AGPA… and their relatives share the twenty-character shape, so the detector replaces them too. They are not secrets — they are stable unique ids for roles, users, groups and policies, and they turn up in CloudTrail and in IAM policy conditions. Being wrong in this direction costs you a placeholder in a log; being wrong in the other direction costs you a key. The prefix field guide lists which is which.

in   "AssumedRoleId": "AROAY34FZKBOKMUTVV7A:deploy-session"
out  "AssumedRoleId": "[AWS_KEY_2]:deploy-session"

How credentials get into CloudWatch in the first place

Worth knowing, because the redactor treats the symptom and every one of these is fixable at the source.

A Lambda that prints its environment

The Lambda execution environment holds AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_SESSION_TOKEN for the function’s role, injected by the service. Any exception handler that dumps the environment — a “log everything and figure it out later” catch block, a debug print of a config object built from process.env or os.environ — writes live temporary credentials into CloudWatch, with the retention period you set years ago, for everyone with logs:FilterLogEvents to read. This is the single most common way an AWS secret reaches a log, and it is invisible until someone pastes the log somewhere.

An aws --debug transcript pasted into a ticket

Debug output prints the request headers, so a run using temporary credentials puts the whole session token in your terminal, along with the Authorization line and the canonical request. The AWS CLI does not print your secret access key as part of that trace — but the transcript people share is usually a whole terminal capture, and the other things in that window are an env, a cat of a credentials file, or a copied export. Scrub the window, not the command.

If you only want to know which key is in play, aws configure list is the safe question: it prints just the last four characters of each value.

Logs Insights results, copied out

A query result is the raw event text. Anything the shapes above describe is in @message exactly as it was written, and the export button hands you a file of it. Redact after you export and before you attach — and if the credential is really there, exporting it is already the second copy.

The order to do things in

  1. Scrub the paste, then read it. Use the box above, or run the same detectors offline. Then re-read the output for what it could not see: a lone run of about forty base64 characters, a very long unbroken base64 blob, X-Amz-Signature=, Signature=.
  2. Un-wrap long lines before, not after. A token broken across two lines is matched up to the break. The placeholder that appears is the reason you stop looking.
  3. If a live credential was in the original, rotate it now. For a long-term key: IAM → the user → security credentials → deactivate, then delete. For temporary ASIA credentials there is no key to delete — they expire on their own, and if that is not soon enough, revoking the role’s active sessions denies everything issued before this moment. Then read CloudTrail filtered on the access key id, which is the only thing that tells you whether it was used. The full order of operations after a leak is here.
  4. Fix what printed it. Usually one logging line in an error path. Until that changes, every subsequent invocation writes a fresh credential into the same log group.
  5. Then deal with the stored copy. You cannot delete a single CloudWatch event; you can delete the log stream or the log group, and you can set a retention period so this expires instead of accumulating. CloudWatch Logs also has a data-protection feature that masks matching values at ingest, with the AWS secret access key among the identifiers it knows — it applies to events arriving after you turn it on, not to what is already stored, and viewing the unmasked value then needs an extra permission. Useful going forward; not a cleanup.

Running the same thing offline

The box on this page and the tools below share one detector source, so you get the same answer in all three places — which matters when the log you cannot paste into a browser is the one you most need to scrub.

The full Log Redactor Every detector with a switch next to it, a custom terms box for your own bucket and host names, and the complete written account of what it misses. Same browser-only guarantee. The command-line version, for logs that never leave the host Pipe a file through it, keep a local key map so the placeholders are reversible, and run it in CI. No network, no telemetry, no dependencies.

There is also npm install logscrub — the same detectors as a free, MIT-licensed package with no dependencies, for scrubbing inside your own code before the log line is ever written. That is the version that scales past a paste: a CloudWatch export, a whole log group, a nightly job.

What this page does not cover

Detecting a secret in text you already have. If the question is whether a credential in a shared log was used, no redactor helps — that is CloudTrail, filtered on the access key id, and it is the step people skip because rotating feels like the end of the story.

Anything about GuardDuty findings, a compromised instance, or credentials taken from a running machine rather than read out of text. If an attacker had code execution, you do not know what they hold, and the response is a larger one than this page.

And it does not cover the general question of what else leaks out of a log — internal hostnames, customer email addresses, JWTs from your own auth, database URLs with the password in the middle. The two-minute check before you paste anything is the broader version of this page.