There are usually three tools through which a developer first gets acquainted with AWS: the AWS CLI (in the terminal), the AWS SDK (inside code), and CloudShell (in the browser). All three ultimately call the same HTTPS APIs, but they diverge in the details of credential chaining and region resolution. These details show up on the exam as the question "why does our code work in one environment but not in another?".
Today we look at the internal workings of these three tools — the credential provider chain, named profiles, the SDK retry algorithm, and SigV4 signing. The goal is not to memorize a few CLI commands, but to understand the mechanisms that apply identically across every SDK. Once you grasp the mechanism, the same debugging flow works in any language's SDK.
AWS CLI v1 (launched 2013, Python-based) was replaced by v2 in 2020. v2's biggest changes are: (1) a containerized bundled Python runtime (no dependency on the system Python), (2) SSO login integration (aws configure sso), (3) auto-prompt mode (aws --cli-auto-prompt), (4) a client-side pager (long output is automatically piped through less).
When the CLI receives a command, it executes in the following order.
1. Determine region and output format from ~/.aws/config
2. Obtain AK/SK/SessionToken from the credential provider chain
3. Determine the service endpoint (region + service)
4. Build the JSON request, sign it with SigV4
5. Call the endpoint over HTTPS
6. Format the response JSON per --output (table/json/text/yaml)
Understanding why AWS moved from CLI v1 to v2 reveals a tool-design perspective. v1 depended on the Python installed on the system, so conflicts were frequent around the Python 2.7 EOL (January 2020). Touching macOS's system Python collided with Homebrew, and on Linux, ABI compatibility issues made security patching difficult. v2 bundles the Python interpreter statically, isolating it from the user's system. AWS reflected that this decision eliminated thousands of GitHub issues per year.
🔍 Going deeper: CLI v2's credential chaining looks for credentials in the following order, using the first one found: (1) command-line options (
--profile), (2) environment variables (AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_SESSION_TOKEN), (3) the default profile in , (4) SSO profiles in , (5) Web Identity Token File (, EKS IRSA), (6) ECS task role (), (7) EC2 instance metadata (IMDSv2). The defines this same order so it applies consistently across all SDKs (boto3, AWS SDK for Java, Go, .NET, etc.).
Click a choice to reveal the answer and explanation.
Question 1
Inside an EC2 instance, where does boto3 look for credentials?
Question 2
The command `aws s3 ls --profile prod` is configured so that the `prod` profile uses the dev account's access key to assume a Role in the prod account. What is the flow of this call?
Question 3
boto3 returned a `SignatureDoesNotMatch` error. What is the most likely cause?
Question 4
Why is AWS CLI v2's SSO login (`aws configure sso`) safer than IAM User access keys?
Question 5
How do you customize SDK retry behavior in a Lambda function?
Question 6
When you run `aws sts get-caller-identity` inside CloudShell, what does the ARN come back as?
Question 7
While uploading a 5GB file with `aws s3 cp`, the network dropped momentarily. How does CLI v2 handle this?
Question 8
You set a presigned URL's ExpiresIn=86400 (24 hours), but it expires after 1 hour. Why?
~/.aws/credentials~/.aws/configAWS_WEB_IDENTITY_TOKEN_FILEAWS_CONTAINER_CREDENTIALS_RELATIVE_URI💡 Related theory: The credential provider chain is essentially the Chain of Responsibility pattern (GoF, 1994). Each provider checks "can I handle this?" and, if not, passes to the next. Thanks to this pattern, a new provider (e.g., Pod Identity) can be slotted into the chain without changing existing code. If you look at
credentials.pyin the boto3 source, theCredentialResolverclass implements exactly this pattern.
In practice, almost every developer works with multiple AWS accounts simultaneously. Named profiles in ~/.aws/config and ~/.aws/credentials solve this.
# ~/.aws/config
[default]
region = ap-northeast-2
output = json
[profile dev]
region = ap-northeast-2
role_arn = arn:aws:iam::111111111111:role/DevRole
source_profile = default
[profile prod]
region = us-east-1
role_arn = arn:aws:iam::222222222222:role/ProdRole
source_profile = default
mfa_serial = arn:aws:iam::333333333333:mfa/alice
duration_seconds = 3600
[profile sso-admin]
sso_session = mycompany
sso_account_id = 444444444444
sso_role_name = AdministratorAccess
region = us-east-1
[sso-session mycompany]
sso_start_url = https://mycompany.awsapps.com/start
sso_region = us-east-1
sso_registration_scopes = sso:account:accesssource_profile is the key. When you use the dev profile, the CLI first uses the default profile's credentials to call sts:AssumeRole, then makes the real API calls with the returned temporary credentials. If mfa_serial is present, a token code is required, so you can enforce MFA on dangerous accounts like prod.
# Specify the profile per command
aws s3 ls --profile prod
# Pin it for the session via environment variable
export AWS_PROFILE=prod
aws s3 ls
# SSO login (valid for 8 hours)
aws sso login --profile sso-admin⚠️ Trap: The access keys in
~/.aws/credentialsare stored in plain text. If a laptop without disk encryption is lost, that's a straight-up key leak. AWS strongly recommends IAM Identity Center (SSO) +aws configure ssoinstead of IAM User access keys. SSO stores credentials encrypted in the OS keyring (macOS Keychain, Windows Credential Manager, Linux libsecret), and when the token expires, it prompts re-authentication via the browser.
📚 Case study: In the 2019 Capital One incident, the attacker stole the IAM Role's temporary keys from EC2 metadata — but had permanent keys from a laptop's
~/.aws/credentialsbeen there instead, they would have been discovered months later, unrotated. The 2021 Twitch source code leak also had.aws/credentialspushed to GitHub as one of its causes. AWS has since partnered with GitHub so that Push Protection automatically blocks pushes containing AKIA-pattern keys.
When an API call returns throttling or a transient error, the SDK retries automatically. This retry algorithm doesn't appear directly on the exam, but it underlies the answers to scenarios like DynamoDB ProvisionedThroughputExceededException, Lambda 429s, and S3 SlowDown.
| Retry mode | Default attempts | Algorithm | Introduced |
|---|---|---|---|
| Legacy (old) | 4 | exponential backoff | Early SDKs |
| Standard (default) | 3 (4 calls total) | exponential + jitter | 2019 |
| Adaptive (experimental) | 3 | client-side rate limiting + retry | 2020 |
Standard mode uses exponential backoff with jitter. The first retry waits 0-1 seconds, the second 0-2 seconds, the third 0-4 seconds, at random. Without jitter, all clients retry at the same moment and get throttled again all at once — the "thundering herd" problem. This algorithm is detailed in the AWS Architecture Blog's Exponential Backoff and Jitter.
Adaptive mode uses a token bucket algorithm for the client to dynamically limit its own call rate. It slows down preemptively before being throttled by the server. The downside is throughput can be constrained, so standard is the default.
💡 Related theory: AIMD (Additive Increase, Multiplicative Decrease) is an algorithm originating in TCP congestion control (Jacobson 1988). Increase throughput slowly, but cut it quickly on detecting loss. The AWS SDK's adaptive retry applies a similar philosophy to client-side rate limiting. RFC 7567 (IETF AQM Working Group, 2015) covers standardization work on similar mechanisms. Even more interesting is that the fact that omitting jitter re-creates the "thundering herd" was reconfirmed in datacenter traffic analysis [Polly Vavilala et al., SIGCOMM 2017].
🔍 Going deeper: SDK retry decisions hinge on HTTP status codes and error codes. (1) 5xx, 429, 502, 503, 504 are retried. (2)
ThrottlingException,Throttling,RequestLimitExceeded,RequestThrottled,ProvisionedThroughputExceededExceptionare retried. (3) Among 4xx, 400 BadRequest, 403 AccessDenied, and 404 NotFound are not retried (permanent errors). However, some 4xx errors are retryable, like DynamoDB'sTransactionConflictException, so per-service exceptions exist. The full list of retryable errors is inretries/special.pyin the boto3 source.
⚠️ Trap: Leaving SDK retries as-is inside a Lambda function collides with the function timeout. Inside a Lambda with the default 3-second timeout, if DynamoDB returns throttling, the SDK waits up to 0-1s + 0-2s = 3 seconds and Lambda dies first. In such cases, the right approach is to disable SDK retries with
AWS_MAX_ATTEMPTS=1and retry at the function's caller (Step Functions, EventBridge Pipes, SQS DLQ).
Almost all AWS API calls go through SigV4 signing (exceptions: query-string signing for presigned URLs, IoT's MQTT, etc.). If you don't know how SigV4 works, you can't debug things like "why does a timestamp off by more than 15 minutes produce a 403?" or "why is this presigned URL only valid for 5 minutes?".
SigV4 signing steps:
1. Build the Canonical Request
- HTTP method, canonical URI, canonical query string
- canonical headers, signed headers, body hash (SHA256)
2. Build the String to Sign
- "AWS4-HMAC-SHA256"
- timestamp (X-Amz-Date)
- credential scope (date/region/service/aws4_request)
- SHA256(Canonical Request)
3. Derive the Signing Key (5-stage HMAC)
kDate = HMAC("AWS4" + SecretAccessKey, Date)
kRegion = HMAC(kDate, Region)
kService = HMAC(kRegion, Service)
kSigning = HMAC(kService, "aws4_request")
4. Final signature = HMAC-SHA256(kSigning, String to Sign)
5. Include it in the Authorization header, or embed it in a presigned URL's query string
🔍 Going deeper: SigV4's 5-stage key derivation is a deliberate security design. Separating kDate, kRegion, and so on lets you create and use intermediate keys "valid only for this date, this region, this service" without exposing the SecretAccessKey. A pattern of handing such a derived key to an external service (e.g., CloudFront → Lambda@Edge) when delegating partial permissions becomes possible. The 15-minute timestamp skew limit prevents replay attacks — even if an attacker intercepts the packet, it's useless after 15 minutes. A client clock drifting off NTP is the cause of the commonly seen
SignatureDoesNotMatcherror.
📚 Case study: In late 2024, AWS released SigV4a (Signature Version 4 Asymmetric). Based on ECDSA asymmetric signing, the same signature is simultaneously valid against multiple regional endpoints. It lets Multi-Region Access Points (S3) and cross-region traffic be handled in one shot without regenerating the signature per region. SigV4 was HMAC symmetric-key based, requiring a different signature per region — a limitation ECDSA resolved.
💡 Related theory: HMAC is a message authentication code defined in RFC 2104 (Krawczyk et al., 1997). Its structure,
HMAC(K, m) = H((K ⊕ opad) || H((K ⊕ ipad) || m)), is safe against the length extension attacks that plague naive hash chaining. The reason SigV4 uses a 5-stage HMAC chain is that it is the standard pattern for a key derivation function (KDF), providing security properties similar to NIST SP 800-108 (KDF in counter mode).
An S3 presigned URL is a time-limited link the SDK creates by embedding a SigV4 signature into the URL's query string. The recipient can access the object with just that URL, no AWS credentials required.
import boto3
s3 = boto3.client('s3')
url = s3.generate_presigned_url(
'put_object',
Params={'Bucket': 'my-bucket', 'Key': 'upload.bin'},
ExpiresIn=300 # 5 minutes
)
# The client can PUT directly to this URLThe key constraint: a presigned URL's validity cannot exceed the validity of the issuer's credentials. If credentials issued via an EC2 instance profile expire after 1 hour, then even with ExpiresIn=86400 (24 hours), the URL dies after 1 hour. A staple exam question.
⚠️ Trap: If the issuer's IAM permissions are reduced after the presigned URL is created, the URL is invalidated even before it expires. The SDK signs with only the permissions at issuance time, but at actual usage time AWS also evaluates the current permissions. So the debugging scenario "we revoked permissions right after generating a presigned URL and the URL suddenly died" is entirely possible.
CloudShell is a browser-based shell launched in December 2020. You can immediately type aws commands from the console without spinning up a separate EC2. Internally, an Amazon Linux 2-based container is isolated per user, and credentials are automatically inherited from the console login session.
| Feature | Value |
|---|---|
| Persistent storage | 1GB (home directory) |
| Memory | 4GB |
| Session idle timeout | 20-30 minutes |
| Auto-deletion after inactivity | 120 days |
| Free usage time | Unlimited (only the AWS API costs you invoke are billed) |
CloudShell's credentials are temporary credentials derived from the console session, so they expire just like an IAM Role's. That's why typing aws sts get-caller-identity shows an assumed-role ARN. CloudShell rarely appears on the exam, but in practice it's very handy for "running one urgent SQL line".
The golden tool for CLI/SDK debugging is the --debug flag.
aws s3 ls --debug 2>&1 | grep -E "(endpoint|signature|Status|provider)"Running this shows (1) which credential provider the credentials came from, (2) which endpoint was called, (3) the full canonical request of the SigV4 signature, and (4) the HTTP response code and headers. In boto3, boto3.set_stream_logger('', logging.DEBUG) yields the same information.
import boto3
import logging
boto3.set_stream_logger('', logging.DEBUG)
s3 = boto3.client('s3')
print(s3.list_buckets())The most common debugging scenario in practice is "why did the credentials get resolved differently than expected?". Find a line like Found credentials in environment variables. or Found credentials in shared credentials file. in the --debug output and you immediately know which provider won. Next, verify the region resolution with the Endpoint: https://s3.ap-northeast-2.amazonaws.com line. Finally, capturing the StringToSign: block and reproducing the same signature in your own code helps when implementing SigV4 directly outside the SDK.
| SDK | Version | Characteristics |
|---|---|---|
| boto3 (Python) | 1.x | Richest documentation; async requires separate aioboto3 |
| AWS SDK for JavaScript v3 | 3.x | Modular imports (tree shaking), TypeScript first |
| AWS SDK for Java v2 | 2.x | NIO-based async, builder pattern |
| AWS SDK for Go v2 | 2.x | context.Context-based cancellation |
| AWS SDK for .NET | 3.x | Native async/await, IConfiguration integration |
| AWS SDK for Rust | beta | tokio-based, type-safe |
Compared to v1, the v2/v3 SDKs share (1) async-first design, (2) module separation (import only the clients you need), and (3) a middleware system (retry, signing, logging as chainable handlers).
The JavaScript SDK v2 → v3 migration is a particularly big issue: v3 split npm packages per client (@aws-sdk/client-s3, @aws-sdk/client-dynamodb, etc.), and with tree shaking, Lambda package sizes dropped by more than half. v2 imported every service at once, directly impacting Node.js Lambda cold starts, whereas v3 pulls in only the needed clients, so cold-start reductions of 30-50ms are typical.
💡 Memorization tip: SDK version labels follow a confusing pattern. Python → boto3 is v1.x (boto was v1, and boto3 effectively plays the v2 role), for JavaScript, v3 is the modular one, for Java/Go/.NET, v2 is the latest. When AWS official docs say "SDK v2", it's not JavaScript — it's the other languages.
Today's picture is that the CLI and SDKs ultimately operate on the same mechanisms — the credential provider chain, region resolution, SigV4 signing, and retries. The patterns of handling multiple accounts with named profiles, using SSO without long-term keys via IAM Identity Center, and tracing the flow with --debug work identically across every SDK.
In the next article, we consolidate this Week 1 material into exam questions and pinpoint weak spots.