awslabs / awslabs/aws-sdk-rust

Add MFA support when profile assumes role

Open
#527 5 comments 5 reactions 0 assignees View on GitHub
feature-request p2
Dominant language
Rust
Stars
3.3k
Forks
290
Avg merge
1d 12h
Merged PRs (30d)
3

Description

### Describe the feature

Currently, the SDK regognizes the `role_arn` directive in profiles under the normal config file at `~/.aws/config`. When sending a request, it will correctly attempt to assume the specified role. However, if that role requires an MFA token, the SDK will not request an MFA token from the user, and will fail to assume the role.

The feature request is to add support for MFA tokens, analogous to support in `boto3` for the same.

### Use Case

I use an IAM with limited permissions, which assumes a more privileged role, protected by MFA. The idea is to avoid having long-lived credentials present on my development laptop for a privileged account. When using the AWS CLI, or `boto3` in a python script (run from a tty), I am prompted for my MFA key when first authenticating. I'd like to use the same workflow for programmes written against the Rust SDK.

To give a complete picture, my `~/.aws/config` looks something like this (with long term credentials for the `home` profile in `~/.aws/credentials`):
```
[profile home]
region=eu-west-2
role_arn=arn:aws:iam:::role/MorePrivilegedRole
source_profile=home
mfa_serial=arn:aws:iam:::mfa/jelford-laptop
```

### Proposed Solution

To fit with the ["Batteries included, but replaceable"](https://awslabs.github.io/smithy-rs/design/tenets.html#batteries-included-but-replaceable) design tenet, I think it would make sense to include:
- a default implementation that requests an MFA token on stdin, if available
- a plugable API to allow users to bring their own implementation for supplying MFA tokens

The general idea is to:
1. Load `mfa_serial` from profile config when present, onto `aws_config::profile::credentials::repr::RoleArn`
2. Add a trait `ProvideMfaToken` that can provide mfa tokens when required in `aws_config::profile::credentials::exec::AssumeRoleProvider::credentials`
3. Add an implementation of `ProvideMfaToken` used by default when stdin is a tty, that sources MFA tokens from stdin
4. Provide an easy hook on `aws_config::default_provider::credentials::Builder` to pass in a custom `ProvideMfaToken`

I'd propose usage looks a bit like this (using my "home" profile name from above), first for the default case:

```
// using default MFA token provider

let creds = credentials::Builder::default().profile_name("home").build();
let region = region::Builder::default().profile_name("home").build();

let config = aws_config::from_env()
.credentials_provider(creds.await)
.region(region)
.load()
.await;

let s3_client = aws_sdk_s3::Client::new(&config);
let buckets = s3_client.list_buckets().send().await.unwrap();
```

And for customizing the `ProvideMfaTokenImplementation` (assuming a `future::ProvideMfaToken` analogous to `future::ProvideCredentials`):

```
struct MyProvideMfaToken{}

impl ProvideMfaToken for MyProvideMfaToken {
fn provide_mfa_token<'a>(&'a self) -> future::ProvideMfaToken<'a> ... {
...
}
}
...

let creds = credentials::Builder::default().profile_name("home").mfa_token(MyProvideMfaToken{}).build();
let region = ...

let config = aws_config::from_env()
.credentials_provider(creds.await)
.region(region)
.load()
.await;

... etc
```

### Other Information

For completeness, here's the detail from tracing when trying to use the profile above with an MFA token, following the proposed "default" code:

```
2022-05-01T12:11:12.105241Z INFO send_operation{operation="ListBuckets" service="s3"}:provide_credentials{provider=default_chain}:lazy_load_credentials:load_credentials{provider=Profile}: aws_config::profile::credentials: constructed abstract provider from config file chain=ProfileChain { base: AccessKey(Credentials { provider_name: "ProfileFile", access_key_id: "", secret_access_key: "** redacted **" }), chain: [RoleArn { role_arn: "arn:aws:iam:::role/MorePrivilegedRole", external_id: None, session_name: None }] }
2022-05-01T12:11:12.105271Z INFO send_operation{operation="ListBuckets" service="s3"}:provide_credentials{provider=default_chain}:lazy_load_credentials:load_credentials{provider=Profile}: aws_config::profile::credentials::exec: first credentials will be loaded from AccessKey(Credentials { provider_name: "ProfileFile", access_key_id: "", secret_access_key: "** redacted **" }) base=AccessKey(Credentials { provider_name: "ProfileFile", access_key_id: "", secret_access_key: "** redacted **" })
2022-05-01T12:11:12.105296Z INFO send_operation{operation="ListBuckets" service="s3"}:provide_credentials{provider=default_chain}:lazy_load_credentials:load_credentials{provider=Profile}: aws_config::profile::credentials::exec: which will be used to assume a role role_arn=RoleArn { role_arn: "arn:aws:iam:::role/MorePrivilegedRole", external_id: None, session_name: None }
2022-05-01T12:11:12.105337Z INFO send_operation{operation="ListBuckets" service="s3"}:provide_credentials{provider=default_chain}:lazy_load_credentials:load_credentials{provider=Profile}: aws_config::profile::credentials: loaded base credentials creds=Credentials { provider_name: "ProfileFile", access_key_id: "", secret_access_key: "** redacted **" }
2022-05-01T12:11:12.107050Z DEBUG send_operation{operation="ListBuckets" service="s3"}:provide_credentials{provider=default_chain}:lazy_load_credentials:load_credentials{provider=Profile}:load_assume_role{provider=AssumeRoleProvider { role_arn: "arn:aws:iam:::role/MorePrivilegedRole", external_id: None, session_name: None }}: aws_endpoint: resolved endpoint endpoint=AwsEndpoint { endpoint: Endpoint { uri: https://sts.eu-west-2.amazonaws.com/, immutable: false }, credential_scope: CredentialScope { region: Some(SigningRegion("eu-west-2")), service: None } } base_region=Region("eu-west-2")
<-- snip: a bunch of http request preparation / sending stuff -->
2022-05-01T12:11:12.199119Z WARN send_operation{operation="ListBuckets" service="s3"}:provide_credentials{provider=default_chain}:lazy_load_credentials:load_credentials{provider=Profile}: aws_config::profile::credentials: failed to load assume role credentials provider=AssumeRoleProvider { role_arn: "arn:aws:iam:::role/MorePrivilegedRole", external_id: None, session_name: None }
2022-05-01T12:11:12.199167Z WARN send_operation{operation="ListBuckets" service="s3"}:provide_credentials{provider=default_chain}:lazy_load_credentials: aws_config::meta::credentials::chain: provider failed to provide credentials provider=Profile error=An error occurred while loading credentials: An error occurred while loading credentials: Error { code: "AccessDenied", message: "User: arn:aws:iam:::user/jelford-laptop is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam:::role/MorePrivilegedRole", request_id: "..." }

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: ConstructionFailure(CredentialsLoadingError(ProviderError { cause: ProviderError { cause: ServiceError { err: AssumeRoleError { kind: Unhandled(Error { code: Some("AccessDenied"), message: Some("User: arn:aws:iam:::user/jelford-laptop is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam:::role/MorePrivilegedRole"), request_id: Some("..."), extras: {} }), ...
<-- snip -->
```

### Acknowledgements

- [X] I may be able to implement this feature request
- [ ] This feature might incur a breaking change

### A note for the community

### Community Note
* Please vote on this issue by adding a 👍 [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to the original issue to help the community and maintainers prioritize this request
* Please do not leave "+1" or "me too" comments, they generate extra noise for issue followers and do not help prioritize the request
* If you are interested in working on this issue, please leave a comment

Contributor guide

Open the contributing guide

Research direction

Start by reading the profile credential paths named in the issue: aws_config::profile::credentials::repr::RoleArn, exec::AssumeRoleProvider::credentials, and default_provider::credentials::Builder. Trace how role_arn and source_profile are loaded, then compare the requested mfa_serial, token-provider hook, stdin behavior, and custom builder integration with the existing credential flow. Done means MFA-protected profile role assumption works and custom token providers can be supplied.

Written by the indexing model from the issue text.

Assessment

Tech stack
aws, rust
Domain
authentication, cloud
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.