Skip to content

Redact PEM private keys from --debug output and CLI history - #10713

Open
luigi617 wants to merge 3 commits into
feat-cloudfront-cookiesfrom
feat-log-redaction
Open

luigi617 wants to merge 3 commits into
feat-cloudfront-cookiesfrom
feat-log-redaction

Conversation

@luigi617

@luigi617 luigi617 commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Issue #, if available:

Description of changes:

Description of changes

Redacts PEM private keys from debug output and CLI history. Depends on #10716 and #10715.

Security

  • PEM private keys passed inline as argument values (e.g. --private-key "-----BEGIN RSA PRIVATE KEY-----...") are now redacted from --debug output and the CLI history database.
  • Redaction covers RSA, EC, and PKCS#8 private key blocks. An unterminated key (truncated before -----END) is also redacted up to the end of the text.
  • The redaction module (awscli/redaction.py) provides a register_redaction() hook so future commands can add their own patterns.

Testing

  • Unit tests: test_redaction.py covers the redact() function and PEM pattern matching. test_logger.py verifies the logger applies redaction before writing debug output.
  • Integration tests: test_sign.py asserts that no key material appears in --debug output or the history database when aws cloudfront sign is called with an inline PEM key.

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@luigi617
luigi617 force-pushed the feat-log-redaction branch from 9aed102 to f047006 Compare October 1, 2026 19:31
@luigi617
luigi617 marked this pull request as ready for review October 1, 2026 19:55
@luigi617
luigi617 requested a review from a team as a code owner October 1, 2026 19:55
@luigi617
luigi617 added this pull request to stack #10717 October 1, 2026 19:57

@GarrettBeatty GarrettBeatty left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good improvement. I checked with --debug and cli_history = enabled: inline PEM keys are redacted from both for sign, sign-cookies, --private-key=…, mistyped argument names and ssm put-parameter --value, and tracebacks are covered too. The related history/logger/autoprompt/cloudfront tests pass (524).

I found a few ways key material still gets through (details inline), so the changelog's "Redact PEM private keys passed as argument values" overclaims a bit:

  1. Base64-encoded keys: aws acm import-certificate --private-key fileb://key.pem logs "PrivateKey": "LS0tLS1CRUdJTiBSU0Eg…" in the --debug request body and in history (HTTP_REQUEST, API_CALL, CLI_ARGUMENTS).
  2. Unquoted, word-split keys: --private-key $K with no quotes means the header is never contiguous, so the key body appears in the Arguments entered to CLI debug line and in history.
  3. stderr without --debug: argparse errors print the key verbatim, e.g. Unknown options: -----BEGIN RSA PRIVATE KEY-----\nMIIE… (key passed twice) or --hash-algorithm: Found invalid choice '-----BEGIN…'. CI logs capture stderr. This is outside logging, so it could be a follow-up, but it's worth noting in the PR as out of scope.

Also minor: botocore's own Session.set_stream_logger/set_file_logger (awscli/botocore/session.py:629,657) still use a plain logging.Formatter. The CLI doesn't call them today, so nothing leaks now, but anything that does call them will bypass redaction.

Comment thread awscli/redaction.py
# PEM private keys.
register_redaction(
re.compile(
r'-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only matches an armored, contiguous header, so two common cases get through.

Base64 of the PEM (blob parameters):

aws acm import-certificate --certificate fileb://cert.pem --private-key fileb://key.pem --debug
# ... botocore.endpoint - DEBUG - Making request ... "PrivateKey": "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVkt..."

The same value lands in history HTTP_REQUEST, API_CALL and CLI_ARGUMENTS.

An unquoted, word-split key (--private-key $K): argv becomes ['-----BEGIN', 'RSA', 'PRIVATE', 'KEY-----', 'MIIE…', …] and the body shows up in Arguments entered to CLI.

Suggestions: also match base64 starting LS0tLS1CRUdJTi (base64 of -----BEGIN), and/or redact argv values by position after --private-key. Or, if those are out of scope, narrow the changelog to "PEM private keys passed inline".

Comment thread awscli/redaction.py

def _redact_text(text):
for pattern, replacement in _REDACTIONS:
text = pattern.sub(replacement, text)

@GarrettBeatty GarrettBeatty Oct 13, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since register_redaction() is a public hook: replacement goes to re.sub as a template, so a backslash in it (e.g. '\1' or 'C:\…') gets interpreted or raises. Suggest pattern.sub(lambda _: replacement, text). Registration also has no dedupe and no way to unregister, and patterns run in order, so a later pattern can match an earlier replacement. Worth documenting, or deduping on (pattern.pattern, replacement).

Comment thread awscli/redaction.py
"""
if isinstance(value, str):
return _redact_text(value)
if isinstance(value, bytes):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-UTF-8 bytes are returned unchanged, so a body with a stray non-UTF-8 byte next to a PEM block isn't redacted. bytearray/memoryview aren't handled either. Suggest value.decode('utf-8', 'surrogateescape') / .encode('utf-8', 'surrogateescape'), or running a bytes version of the pattern directly.

Comment thread awscli/logger.py

def format(self, record):
# The whole formatted record, including any traceback, is redacted.
return redact(super().format(record))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: super().format(record) caches the unredacted traceback in record.exc_text, so any other handler attached to the same logger reuses the unredacted text. Not reachable with the CLI's handlers today, but cheap to harden by setting record.exc_text = redact(record.exc_text) after formatting.

object(),
],
)
def test_returns_values_without_matches_unchanged(value):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test coverage: there are no negative cases for the bypasses above (a base64-encoded PEM, a word-split argv list, non-UTF-8 bytes containing a PEM block). Even if they're declared out of scope, xfail tests would document the limits.

{
"type": "enhancement",
"category": "``logging``",
"description": "Redact PEM private keys passed as argument values from ``--debug`` output and the CLI history database."

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the base64 and word-split cases (see the comment on redaction.py), suggest "Redact PEM private keys passed inline as argument values…" unless those cases get covered.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants