Fix AWS Roles Anywhere access when using per-session MFA#60319
Fix AWS Roles Anywhere access when using per-session MFA#60319marcoandredinis merged 8 commits intomasterfrom
Conversation
3ab518e to
d0a2307
Compare
d0a2307 to
43516ca
Compare
Tener
left a comment
There was a problem hiding this comment.
I'm not sure this approach is secure. This comments suggest we should ensure the issued certs are kept in-memory, which isn't the scenario displayed by this PR.
func setUserSingleUseCertsTTL(actx *grpcContext, req *authpb.UserCertsRequest) {
if isLocalProxyCertReq(req) {
// don't limit the cert expiry to 1 minute for db local proxy tunnel or kube local proxy,
// because the certs will be kept in-memory by the client to protect
// against cert/key exfiltration. When MFA is required, cert expiration
// time is bounded by the lifetime of the local proxy process or the mfa verification interval.
return
}
@Joerger would you mind taking a look?
I could require users to use the That makes me wonder whether the credentials are safer: in-memory but accessible from a TCP connection or in a file, in the user's home directory. |
Agreed, we should not leave long lived MFA-verified certs, which in this case hold plain text AWS Roles Anywhere credentials, in the filesystem. Usually we get around that with However, as Marco pointed out, I don't see exactly how we can make this work with Looking at the docs, I see a warning that basically points out this exact vulnerability concern:
I think we would need to wrap this into I don't see how this feature can support long-lived MFA certs without compromising on its current security invariants, but I still think it's worth investigating deeper. Otherwise, we could take the current approach in this PR and be clear in documentation through warnings that per-session MFA has diminished security effectiveness for AWS Roles Anywhere. Additionally, we should probably hard code the cert expiration to Edit: some more ideas:
|
|
@rob-picard-teleport what are your thoughts on this? |
|
It took me some time to get up to speed on the flows here, so here's my attempt at documenting my understanding. First, this flowchart is what happens when the user runs flowchart
onAppLogin -->|new: requester name is credential process if aws-arn provided| appLogin --> ClusterClient.IssueUserCertsWithMFA
ClusterClient.IssueUserCertsWithMFA -->|if mfa not required then ReusableMFAResponse is nil| ClusterClient.generateUserCerts -->|if ReusableMFAResponse not nil, MFA required, purpose is single use| GenerateUserCerts
GenerateUserCerts -->|purpose is single use| generateUserSingleUseCerts
generateUserSingleUseCerts -->|ttl is capped at teleport.UserSingleUseCertTTL| userSingleUseCertsGenerate
userSingleUseCertsGenerate --> generateUserCert
generateUserCert --> generateCert
generateCert --> generateAWSClientSideCredentials
generateAWSClientSideCredentials -->|notAfter for AWS creds is set to session ttl| generateAWSRolesAnywhereCredentials
GenerateUserCerts -->|else| ServerWithRoles.GenerateUserCerts
ServerWithRoles.GenerateUserCerts --> ServerWithRoles.generateUserCerts
ServerWithRoles.generateUserCerts --> generateUserCert
So the first issue is that we generate a single use cert with a 1 minute TTL, which is then propagated to the AWS Roles Anywhere credential request, and fails validation because it must be at least 15 minutes. We have an implicit rule that no single-use Teleport certificates are written to disk with an expiry greater than We enforce this rule here: func setUserSingleUseCertsTTL(actx *grpcContext, req *authpb.UserCertsRequest) {
if isLocalProxyCertReq(req) {
// don't limit the cert expiry to 1 minute for db local proxy tunnel or kube local proxy,
// because the certs will be kept in-memory by the client to protect
// against cert/key exfiltration. When MFA is required, cert expiration
// time is bounded by the lifetime of the local proxy process or the mfa verification interval.
return
}
maxExpiry := actx.authServer.GetClock().Now().Add(teleport.UserSingleUseCertTTL)
if req.Expires.After(maxExpiry) {
req.Expires = maxExpiry
}
}DB local proxy and kube local proxy are not bound here, because they are not written to disk. In the proposed change, we essentially say:
This solves for the validation error by flooring the TTL to the minimum valid one, but creates a new problem in that the certificate, and the valid AWS credentials generated by that certificate, will be saved to disk with a 15 minute TTL, violating our implicit rule. A partial solution to this would be to do all of that, but then stop short of saving the credentials. This would keep our rule intact, and we would no longer see validation errors. With this, if I have per-session MFA enabled, and I use the If we want to enable longer terminal sessions, we need to have some way of saving the credentials in memory, without making them available on disk or via TCP (I agree with earlier comments, that this is not a secure alternative). Could we persist the AWS credentials in environment variables instead of saving them in the Teleport cert?
This way, we tie the concept of the session in per-session MFA to the terminal session itself. We'd need the user experience to include some sort of initial command and eval to set the environment variables for that terminal session though. I'm not sure what that would look like exactly. I'm trying to think if there's any benefit to also having data in the certificate on disk. If we also want to tie this into having data in the certificate, we could encrypt the credentials with a local session key, save the encrypted credentials in the certificate, persist the session key as an environment variable, and then decrypt on future invocations from That way, the certificate alone can't do anything important, so it's okay to persist for the full 15 minutes as we do for local proxy cases. Two more questions:
|
Does it mean a new AWS Session would be created for each API call? Other per-session mfa flows have a broad concept of a session: the user only needs to do a tap when they start an action.
This has to be compatible with whatever tool you use.
Where do we set the env vars? Maybe I didn't understood your suggestion.
It's a bit more than that. teleport/tool/tsh/common/app.go Lines 109 to 111 in 43516ca
Sure 👍 My own suggestion after reading this:
Keep everything else as is: same flow, credentials (only valid for 15 minutes) stored in disk. |
Exactly, per-session MFA would result in one MFA tap for each of those shell sessions. I don't know enough about the GUI app requirements, or how they work today to have ideas there yet. UX wise, I'm open to suggestions, but I'm thinking something like: $ eval(tsh apps login --aws-role <role-arn> --format=shell-session-init <app-name>)
# ... MFA tap ...
$ aws <command>
# ... no tap ...
$ aws <command>
# ... no tap ...Where the first command (totally a placeholder in terms of the specific arguments and command) emits the env variable for the session key or creds.
This breaks per-session MFA too much. The goal of per-session MFA is that a stolen certificate (e.g. from malware) can't be used on its own. The benefit of the proposed solution above with environment variables is that it's actually never written to disk, not even for the 1 minute TTL. |
I'm not talking about a specific GUI app, but as a general type of applications. My point is: if you change Might be an ok trade off: if you want Per-Session MFA, then there's less flexibility on how you can use the credentials.
That looks good 👍 Suggested UX: Access with per-session mfa What about the AWS Session TTL? Do we keep it as is: as long as allowed or decrease it to 15min? |
43516ca to
cd21887
Compare
|
I'd love to have something elegant for other apps like that. In theory we could have some kind of opt-in "unsafe" mode but I'd rather have the user just configure per-session MFA or disable it in certain cases, etc.
Maybe default to 15 minutes but allow them to configure with a flag? Or maybe it's configured in the resource? |
|
I'll use the current TTL implementation, so whatever is allowed by the Teleport Role (AWS Session may be limited further by the IAM Role Max Session Duration or the IAM Roles Anywhere Profile Max Session Duration) |
cd21887 to
8b749d4
Compare
8b749d4 to
116210c
Compare
|
This is ready for another review |
6f2e647 to
f8a188e
Compare
f8a188e to
a5f1fdb
Compare
| var singleUseCerts bool | ||
| if app.GetAWSRolesAnywhereProfileARN() != "" { | ||
| singleUseCerts, err = isMFARequiredForAppAccess(cf.Context, tc, appInfo.RouteToApp) | ||
| if err != nil { | ||
| return trace.Wrap(err) | ||
| } | ||
|
|
||
| // When using single use certs (aka per-session MFA), tsh cannot write the certificate or AWS credentials to disk. | ||
| // Instead, ask user to use the `--env` flag which only outputs the credentials, in an eval friendly format. | ||
| if singleUseCerts && !cf.AppLoginAWSEnvOutput { | ||
| return trace.BadParameter(`AWS access is configured to use per-session MFA and credentials are only available to a single session. Pass the --env flag to the previous command and export the credentials using eval. | ||
| Example: | ||
| eval "$(tsh apps login %s --aws-role %s --env)" | ||
|
|
||
| You can now run the AWS CLI or other AWS SDK based tools as usual. | ||
| Example: | ||
| aws sts get-caller-identity`, | ||
| shsprintf.EscapeDefaultContext(app.GetName()), | ||
| shsprintf.EscapeDefaultContext(appInfo.RouteToApp.AWSRoleARN), | ||
| ) | ||
| } | ||
|
|
||
| appCertParams.RequesterName = proto.UserCertsRequest_TSH_APP_AWS_CREDENTIALPROCESS | ||
| } |
There was a problem hiding this comment.
re: clarifying saving to disk logic.
Also, do we need to set the AWS_CREDENTIALPROCESS requester name for non single use certs? It looks like this puts us here, so we are limiting non mfa certs with the mfa_verification_interval. I see that you use this for this new error condition, though is this condition even possible to reach? Could the app have an associated RolesAnywhereProfile without a matching IntegrationSubKindAWSRolesAnywhere?
| var singleUseCerts bool | |
| if app.GetAWSRolesAnywhereProfileARN() != "" { | |
| singleUseCerts, err = isMFARequiredForAppAccess(cf.Context, tc, appInfo.RouteToApp) | |
| if err != nil { | |
| return trace.Wrap(err) | |
| } | |
| // When using single use certs (aka per-session MFA), tsh cannot write the certificate or AWS credentials to disk. | |
| // Instead, ask user to use the `--env` flag which only outputs the credentials, in an eval friendly format. | |
| if singleUseCerts && !cf.AppLoginAWSEnvOutput { | |
| return trace.BadParameter(`AWS access is configured to use per-session MFA and credentials are only available to a single session. Pass the --env flag to the previous command and export the credentials using eval. | |
| Example: | |
| eval "$(tsh apps login %s --aws-role %s --env)" | |
| You can now run the AWS CLI or other AWS SDK based tools as usual. | |
| Example: | |
| aws sts get-caller-identity`, | |
| shsprintf.EscapeDefaultContext(app.GetName()), | |
| shsprintf.EscapeDefaultContext(appInfo.RouteToApp.AWSRoleARN), | |
| ) | |
| } | |
| appCertParams.RequesterName = proto.UserCertsRequest_TSH_APP_AWS_CREDENTIALPROCESS | |
| } | |
| // When using `tsh app login`, certs should generally be saved to disk, whether standard certs or | |
| // single-use MFA-verified 1m TTL certs. However, in cases where we are exceeding the standard | |
| // 1m TTL for single-use certs, we must ensure the certs are not saved to disk. | |
| saveToDisk := true | |
| if app.GetAWSRolesAnywhereProfileARN() != "" { | |
| singleUseCerts, err = isMFARequiredForAppAccess(cf.Context, tc, appInfo.RouteToApp) | |
| if err != nil { | |
| return trace.Wrap(err) | |
| } | |
| if singleUseCerts { | |
| // Use the AWS_CREDENTIALPROCESS requester name to get longer TTL single use certs. | |
| // In exchange, this client must not save the certs to disk. Instead, ask user to use the `--env` | |
| // flag which only outputs the credentials, in an eval friendly format. | |
| appCertParams.RequesterName = proto.UserCertsRequest_TSH_APP_AWS_CREDENTIALPROCESS | |
| saveToDisk = false | |
| if !cf.AppLoginAWSEnvOutput { | |
| return trace.BadParameter(`AWS access is configured to use per-session MFA and credentials are only available to a single session. Pass the --env flag to the previous command and export the credentials using eval. | |
| Example: | |
| eval "$(tsh apps login %s --aws-role %s --env)" | |
| You can now run the AWS CLI or other AWS SDK based tools as usual. | |
| Example: | |
| aws sts get-caller-identity`, | |
| shsprintf.EscapeDefaultContext(app.GetName()), | |
| shsprintf.EscapeDefaultContext(appInfo.RouteToApp.AWSRoleARN), | |
| ) | |
| } | |
| } | |
| } |
There was a problem hiding this comment.
Updated the code as suggested (minus some minor changes).
It's clearer, thank you.
a5f1fdb to
34c6e78
Compare
34c6e78 to
70c047d
Compare
|
@marcoandredinis See the table below for backport results.
|
* Fix per-session mfa for AWS CLI access when using Roles Anywhere * use Fprintf * remove quotes * rename helper, fix typo, added rfd section * improve comments * prevent cert write to disk + test * revert auto gen docs changes * review pt 2
…1273) * Fix per-session mfa for AWS CLI access when using Roles Anywhere * use Fprintf * remove quotes * rename helper, fix typo, added rfd section * improve comments * prevent cert write to disk + test * revert auto gen docs changes * review pt 2
AWS IAM Roles Anywhere integration allows users to access AWS resources by calling the
rolesanywhere.CreateSessionAPI.This API receives a certificate (signed by the
awsraTeleport CA) and returns the AWS Session temporary credentials.We don't want the AWS Session to outlive the Teleport identity expiration date, so we set the
durationSecondsparameter accordingly.The Teleport identity expiration date is calculated based on multiple factors:
However, when using Per-Session MFA, this expiration is only 1 minute, which is rejected by
rolesanywhere.CreateSession(durationSecondsmust be greater than 15 minutes).It would also be a bad UX if the AWS Session expired 1 minute right after being created.
This PR changes the app login flow so that when using per-session MFA, the TTL for the AWS Session is the same as the one defined when not using single use certs.
In order to keep the implicit rule of not writing credentials into disk when using Per-Session MFA, we added a new flag into the
tsh apps login:--env.This flag ensures that AWS credentials are not stored to disk and are only sent to output.
The format used is compatible with
eval. This allow users to easily gain access to AWS using the combination of both:eval "$(tsh apps login .... --env)"Before

After (with commit
6f2e6473dbd994366554afb5da52e7e224cc484c)Changelog: Fixes AWS Roles Anywhere cli access when using per-session MFA
Fixes: #59597