awslabs / awslabs/aws-sdk-rust
Add MFA support when profile assumes role
- 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
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