fix(bedrock): stop reflecting AWS profile name in logs and errors

The verbose get_credentials debug log interpolated the real
aws_profile_name, and a missing profile let the boto3 ProfileNotFound
(which echoes the profile value) propagate to the caller and into the
chained traceback. The log now records only whether a profile is set,
and profile-lookup failures map to a generic AwsAuthError raised from
None so the value is not reflected through logs, error messages, or the
traceback chain. Regression tests cover the boolean-only log line and
the generic, chain-suppressed error.
This commit is contained in:
yucheng-berriai 2026-06-17 20:31:21 -07:00
parent 556e8f89c8
commit f58f96f094
2 changed files with 95 additions and 7 deletions

View file

@ -256,7 +256,8 @@ class BaseAWSLLM:
"aws_session_token=[set=%s]\n"
"aws_region_name=%s\n"
"aws_session_name=%s\n"
"aws_profile_name=%s\n"
# log only whether aws_profile_name is set, not its value
"aws_profile_name=[set=%s]\n"
"aws_role_name=%s\n"
"aws_web_identity_token=[set=%s]\n"
"aws_sts_endpoint=%s\n"
@ -266,7 +267,7 @@ class BaseAWSLLM:
aws_session_token is not None,
aws_region_name,
aws_session_name,
aws_profile_name,
aws_profile_name is not None,
aws_role_name,
aws_web_identity_token is not None,
aws_sts_endpoint,
@ -1234,9 +1235,17 @@ class BaseAWSLLM:
import boto3
# uses auth values from AWS profile usually stored in ~/.aws/credentials
with tracer.trace("boto3.Session(profile_name=aws_profile_name)"):
client = boto3.Session(profile_name=aws_profile_name)
return client.get_credentials(), None
# map any failure to a generic error so the profile value is not
# reflected back in the exception message or the chained traceback
try:
with tracer.trace("boto3.Session(profile_name=aws_profile_name)"):
client = boto3.Session(profile_name=aws_profile_name)
return client.get_credentials(), None
except Exception:
raise AwsAuthError(
message="The specified AWS profile could not be found.",
status_code=400,
) from None
@tracer.wrap()
def _auth_with_aws_session_token(

View file

@ -163,6 +163,77 @@ def test_aws_profile_path_not_cached_in_iam_cache():
assert mock_profile.call_count == 2
def test_aws_profile_not_found_raises_generic_error():
"""boto3 ProfileNotFound carries the resolved profile name (which may
be an os.environ/ secret). _auth_with_aws_profile must map it to a generic error that
never reflects the user-supplied/resolved value into logs or tracebacks."""
from botocore.exceptions import ProfileNotFound
secret_profile_value = "super-secret-resolved-profile-name"
fake_boto3 = MagicMock()
fake_boto3.Session.side_effect = ProfileNotFound(profile=secret_profile_value)
base = BaseAWSLLM()
with patch.dict("sys.modules", {"boto3": fake_boto3}):
with pytest.raises(AwsAuthError) as exc_info:
base._auth_with_aws_profile(secret_profile_value)
err = exc_info.value
assert secret_profile_value not in err.message
assert secret_profile_value not in str(err)
assert err.message == "The specified AWS profile could not be found."
# the original ProfileNotFound (which embeds the profile value) must not
# be rendered as a chained cause/context, or a handler that logs the full
# traceback would re-leak it
assert err.__cause__ is None
assert err.__suppress_context__ is True
import traceback
rendered = "".join(traceback.format_exception(type(err), err, err.__traceback__))
assert secret_profile_value not in rendered
def test_get_credentials_does_not_log_aws_profile_value():
"""get_credentials logs aws_profile_name as a [set=True] boolean only. The
resolved profile value can itself be a secret (os.environ/ or an upper-case
env lookup), so it must never be reflected into the verbose debug log. This
fails if the format string is changed to interpolate the value (e.g.
'aws_profile_name=%s')."""
import logging
sentinel = "super-secret-profile-do-not-log"
captured = []
class _ListHandler(logging.Handler):
def emit(self, record):
captured.append(record.getMessage())
handler = _ListHandler()
verbose_logger = logging.getLogger("LiteLLM")
previous_level = verbose_logger.level
verbose_logger.addHandler(handler)
verbose_logger.setLevel(logging.DEBUG)
base = BaseAWSLLM()
try:
with patch.object(
base,
"_auth_with_aws_profile",
return_value=(Credentials("prof-ak", "prof-sk", None), None),
):
base.get_credentials(aws_profile_name=sentinel)
finally:
verbose_logger.removeHandler(handler)
verbose_logger.setLevel(previous_level)
combined = "\n".join(captured)
assert sentinel not in combined
assert "aws_profile_name=[set=True]" in combined
def test_web_identity_path_not_cached_in_iam_cache():
base = BaseAWSLLM()
with patch.object(
@ -880,7 +951,11 @@ def test_different_roles_without_session_names_should_not_share_cache():
},
),
],
ids=["no_region_or_endpoint", "bedrock_region_ignored_for_sts", "explicit_sts_endpoint"],
ids=[
"no_region_or_endpoint",
"bedrock_region_ignored_for_sts",
"explicit_sts_endpoint",
],
)
def test_eks_irsa_ambient_credentials_used(role_kwargs, expected_client_kwargs):
"""
@ -1272,7 +1347,11 @@ def test_sts_endpoint_region_matches_bedrock_region_param():
},
),
],
ids=["no_region_or_endpoint", "bedrock_region_ignored_for_sts", "explicit_sts_endpoint"],
ids=[
"no_region_or_endpoint",
"bedrock_region_ignored_for_sts",
"explicit_sts_endpoint",
],
)
def test_explicit_credentials_used_when_provided(role_kwargs, expected_client_kwargs):
"""