Skip to content

Latest commit

 

History

History
370 lines (288 loc) · 25 KB

README.md

File metadata and controls

370 lines (288 loc) · 25 KB

cli-mfa

This repository contains notes and tools for dealing with MFA using the AWS CLI. The documentation around this is quite good, but scattered, and it's difficult to get a handle on how to use the various facilities effectively. These materials are particularly framed from a DevSecOps viewpoint, with an emphasis on how these facilities affect securely configuring and maintaining the AWS infrastructure, rather than using the infrastructure.

At a high level, it's possible that the optimal way to use the AWS CLI is from an EC2 instance within the environment with an appropriate instance role attached. This does pose the problem that you then need to be concerned with controlling access to that instance, and the audit trail of API calls will record the instance as the principal in calls, rather than the actual user.

There is another complication with that scenario - in order to maintain the instance, and probably IAM assets around it's access, you need to operate from outside the instance itself, probably from an administrator or developer's computer.

Providing a fairly high level of access for a principal to act off their computer is a risky proposition. If you are willing to operate purely through the AWS console, then you can require MFA in addition to the user ID / password pair. We're interested though in good DevSecOps practices, and doing things manually through the console are not best practice.

CLI Use

There are alternatives to the simple in-built facilities provided by the AWS CLI tool, which we'll deal with separately.

At the base of authentication for the AWS CLI are the CLI configuration files. This is documented reasonably well at https://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html, which is a good place to start. There are a handful of important points that are not necessarily obvious:

  • You can put credentials in the the ~/.aws/config file, but it's not recommended, and ~/.aws/credentials takes precedence over ~/.aws/config.
  • credentials in the credentials file are tagged with [name], but config entries tagged with [profile name]
  • [default] config applies to all profiles unless overridden, [default] credential is the default principal identifier
  • $AWS_PROFILE can be used to specify a profile rather than passing it at as a CLI parameter
  • $AWS_ACCESS_KEY_ID and $AWS_SECRET_ACCESS_KEY override entries in config files, and you can use $AWS_SESSION_TOKEN - see https://docs.aws.amazon.com/cli/latest/userguide/cli-environment.html

Storing credentials in the ~/.aws/credentials file is quite risky - it's good practice to ensure that directory and it's contents are only readable by the owner, however the credentials are in plain text, so if the computer is compromised they should be considered to be breached. If the simple credential pair can be further constrained using MFA, the security risk is significantly reduced.

The documentation around using MFA for IAM credentials is not well organised, but it is complete, as long as you can find it:

The first half of the solution is quite simple: you make a call to the STS service to obtain temporary credentials that can be used for other API calls:

$ aws sts get-session-token --serial-number arn:aws:iam::931304388919:mfa/devuser --token-code 150355
{
  "Credentials": {
    "SecretAccessKey": "GS+uUxUeBJ0f7wnFTLP8+C0nTO/iEe5cuFMMj6Lc",
    "SessionToken": "FQoDYXdzEFsaDEBVOb7NNZ0WL0g5xCKwATh8Yo+p0XJQDwO5iMrEd9ajopwuK9ZFK7V61it/e+JrK0RQvgyAedB9R5n7r/fXHg/Ak6YACe9DtlhVpX8Ww8VWbxlMibruc4/DtZKXT8n7UbREfFAnl1rhSD18iFUd39uuuu1dOVtqYwJUob7MzUUMs3vypk66ARWyHcd1H+S0PgnnUbN/ynvhq+BREtEgBX4UIrbxByzYskSC2x6v8oDnrCj+9HHgKGICm/Yj6f0LKIOI7dkF",
    "Expiration": "2018-07-03T21:38:11Z",
    "AccessKeyId": "ASIAJCOAUNBB4H4BZRIQ"
  }
 }

The two parameters explicitly passed to the call are the identifier of the MFA device associated with the principal, and the current token displayed by the device. The values returned in the chunk of JSON can then be used for further calls, but it's a nuisance. Either environmental variables are set:

  • $AWS_ACCESS_KEY_ID == Credentials.AccessKeyId
  • $AWS_SECRET_ACCESS_KEY == Credentials.SecretAccessKey
  • $AWS_SESSION_TOKEN == Credentials.SessionToken

or they can be set in the credentials file and used as a profile:

[temp]
aws_access_key_id=ASIAJCOAUNBB4H4BZRIQ
aws_secret_access_key=GS+uUxUeBJ0f7wnFTLP8+C0nTO/iEe5cuFMMj6Lc
aws_session_token=FQoDYXdzEFsaDEBVOb7NNZ0WL0g5xCKwATh8Yo+p0XJQDwO5iMrEd9ajopwuK9ZFK7V61it/e+JrK0RQvgyAedB9R5n7r/fXHg/Ak6YACe9DtlhVpX8Ww8VWbxlMibruc4/DtZKXT8n7UbREfFAnl1rhSD18iFUd39uuuu1dOVtqYwJUob7MzUUMs3vypk66ARWyHcd1H+S0PgnnUbN/ynvhq+BREtEgBX4UIrbxByzYskSC2x6v8oDnrCj+9HHgKGICm/Yj6f0LKIOI7dkF

At a glance this looks pretty useful - you've got some credentials with a fixed life span even if it's a bit fiddly to get them in play for subsequent CLI calls. Note you can specify a shorter lifespan, as the default is quite long - see https://docs.aws.amazon.com/cli/latest/reference/sts/get-session-token.html for more information.

There's some problems though.

To begin with you cannot use the temporary credentials to use the IAM or STS API (see https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_control-access_getsessiontoken.html). More critically, you still had to have provided credentials for the IAM user in order to be able to request the temporary credential. It's not particularly obvious, but does need to be spelled out: even though you have different credentials, you still operate as the same principal, although all subsequent calls carry an indication that MFA has been enabled.

This does suggest one route forward - the user (or better the user's group) - could have a policy that allowed calling GetSessionToken without restriction, but then require MFA to be enabled for any other actions. This is feasible if the principal has a fairly limited set of permissions, but could get cumbersome for an administration or development account.

The mfa.sh script is one possible way of making obtaining and using the token a little less cumbersome. Note this is just a sketch, the ARN of the MFA key is hard-wired, which is sub-optimal. Nevertheless, invoking the script with the MFA session token will parse the JSON and launch a new shell with the appropriate environmental variables set to overload whatever credentials are specified in the files.

It's useful when testing this to look at the CloudTrail logs to verify that you wind up with the expected principal - in my case I could see the expected snippet:

"userIdentity": {
    "type": "IAMUser",
    "principalId": "AIDAI2O37N4DXSYCLZBW2",
    "arn": "arn:aws:iam::931304388919:user/devuser",
    "accountId": "931304388919",
    "accessKeyId": "ASIAJCOAUNBB4H4BZRIQ",
    "userName": "devuser",
    "sessionContext": {
        "attributes": {
            "mfaAuthenticated": "true",
            "creationDate": "2018-07-03T14:24:39Z"
        }
    }
},

Hashicorp Vault

Hashicorp Vault is a very interesting option. Bearing in mind that we would like to not have Access Key/Secret Key pairs on the desktop, Vault provides a way in which short-lived credentials can be obtained for use then discarded, without needing access keys on the desktop at any point. I strongly recommend that you read the introduction materials for Vault, as it's an elegant but complex tool that should be approached carefully.

At a high level part of Vault is functionally similar to Secrets Manager - a way to securely hold encrypted secrets held as key/value pairs. It provides quite a bit more functionality though, and that is where the complexity arises. In the following examples I run a vault server instance locally in development mode (note also I'm using version 0.10.2), but this is very definitely not a recommended practice for anything other than testing as we are doing here. vault is intended to run as a service, and one best practice is to run it with a Consul back end. In this scenario, Vault will make use of Consul as a storage engine, rather than managing it's own storage. The big advantage here is that it's quite straightforward to build a highly-available, highly-resilient Consul cluster, and as a result fairly easy to set Vault up as a high-available, highly-resilient service.

It should be emphasised that in a production environment, using the vault server would require provision of authentication keys - generally a userid/password pair, although more complex authentication backends can be built (see https://www.vaultproject.io/docs/auth/index.html). The vault enterprise mode also supports MFA for calling the server. In addition, the vault server has a rich language for expressing policies around how authentication principals can use it, however this is an independent realm of authorisation and authentication outside AWS and whatever the backend authentication service provides, and decidedly non trivial. None of that is dealt with below, you should assume that the activity of setting up and configuring vault is independent of setting up and configuring the rest of your AWS infrastructure. I would highly recommend that the service be set up in an independent and dedicated VPC, or even AWS account.

The advantage in using vault to obtain short-lived AWS credentials is that the individual CLI users do not ever have to have their own user accounts, or their own AWS access keys, reducing the risk of those keys leaking considerably. There is also an advantage for security administrators: vault provides means to easily and quickly disable individual access, or to break the glass and seal access to secrets entirely.

Before proceeding, you will need to download and install Vault. All the work shown below was using version 0.10.2. The vault executable can be placed anywhere in the shell path as desired.

To begin with, in one shell, we start up a vault server in development mode. Again, this is not suitable in any way for production use - among other things any stored secrets or configuration will be discarded when the server terminates:

vault server -dev

This starts up the local server in dev mode listening on port 8200, so we go to another shell to be able to work with it:

$ export VAULT_ADDR=http://127.0.0.1:8200
$ vault status
Key             Value
---             -----
Seal Type       shamir
Sealed          false
Total Shares    1
Threshold       1
Version         0.10.2
Cluster Name    vault-cluster-0ef7d3d6
Cluster ID      aaa2e1c4-b22f-2278-1f20-5f501297348d
HA Enabled      false

Vault uses various "secrets engines" to provide glue to other backends, such as AWS IAM. In general terms the vault API is somewhat RESTful, mainly using read and write to store and fetch secrets. Another useful analogy is to think of vault as following filesystem semantics, with different secrets engines mounted at different points in the file system tree - much of the Vault documentation talks of it as a virtual filesystem.

In order to use the IAM backend, we add the secrets engine, and provide credentials that the server will use on our behalf. Fairly obviously this would be done in production as the service was installed and configured. Note also that it's reasonably sane to store the credentials in the vault server, as the server's encryption is rock solid.

$ vault secrets enable aws
Success! Enabled the aws secrets engine at: aws/

$ vault write aws/config/root \
    access_key=AKIAJWVN5Z4FOFT7NLNA \
    secret_key=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 \
    region=eu-west-2
Success! Data written to: aws/config/root

$ vault secrets list
Path          Type         Accessor              Description
----          ----         --------              -----------
aws/          aws          aws_320d6b0a          n/a
cubbyhole/    cubbyhole    cubbyhole_05da00be    per-token private secret storage
identity/     identity     identity_0f9eb80f     identity store
secret/       kv           kv_a7784dfb           key/value secret storage
sys/          system       system_621e812d       system endpoints used for control, policy and debugging

A best practice here would be to create a specific Vault IAM user, with a carefully constructed policy attached, or alternatively to run the vault server on an EC2 instance with the appropriate role attached, bearing in mind the caveat from the documentation.

Internally, Vault will connect to AWS using these credentials. As such, these credentials must be a superset of any policies which might be granted on IAM credentials. Since Vault uses the official AWS SDK, it will use the specified credentials. You can also specify the credentials via the standard AWS environment credentials, shared file credentials, or IAM role/ECS task credentials. (Note that you can't authorize vault with IAM role credentials if you plan on using STS Federation Tokens, since the temporary security credentials associated with the role are not authorized to use GetFederationToken.)

The import of this may not be immediately obvious - we can avoid storing the configuration inside vault if the vault service is running in an environment where the AWS SDK is able to find the credentials.

The documentation discusses the various policies and permissions that the server needs. The basic use case though is the one we are interested in: providing CLI access on the desktop while protecting access keys. There are two broad patterns of use. In the first case, Vault will create dummy IAM users on our behalf, with directly assigned policies, either using an inline policy (refer to the https://www.vaultproject.io/docs/secrets/aws/index.html for an example), or a policy ARN:

$ vault write aws/roles/ec2readonly arn=arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess
Success! Data written to: aws/roles/ec2readonly

This policy or role is held entirely within vault, and should not be confused with the IAM rule and policy - we store the policy in the vault service, and vault uses it when it makes the appropriate IAM calls.

Having set up a role, we can make a read call against the AWS secrets engine to get an access/secret pair to use against AWS with that role:

$ vault read aws/creds/ec2readonly
Key                Value
---                -----
lease_id           aws/creds/ec2readonly/3751b993-133b-8cf8-0b7d-b87527a9e4f4
lease_duration     768h
lease_renewable    true
access_key         AKIAJEZBHAZJ3XOZLVSQ
secret_key         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
security_token     <nil>

$ export AWS_ACCESS_KEY_ID=AKIAJEZBHAZJ3XOZLVSQ
$ export AWS_SECRET_ACCESS_KEY=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
$ aws ec2 describe-instances
{
    "Reservations": []
}

Please note:

Each invocation of the command will generate a new credential. Unfortunately, IAM credentials are eventually consistent with respect to other Amazon services. If you are planning on using these credential in a pipeline, you may need to add a delay of 5-10 seconds (or more) after fetching credentials before they can be used successfully.

In a different shell, with appropriate credentials (or using the AWS console), we can see that a dummy user has been created (details omitted in this example)

$ aws iam list-users
{
    "Users": [
        {
            "UserName": "vault-root-ec2readonly-1532257268-6823",
            "Path": "/",
            "CreateDate": "2018-07-22T11:01:09Z",
            "UserId": "AIDAJBDK2BH7OPAM6XSDE",
            "Arn": "arn:aws:iam::889199313043:user/vault-root-ec2readonly-1532257268-6823"
        }
    ]
}

$ aws iam  list-attached-user-policies --user-name vault-root-ec2readonly-1532257268-6823
{
    "AttachedPolicies": [
        {
            "PolicyName": "AmazonEC2ReadOnlyAccess",
            "PolicyArn": "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess"
        }
    ]
}

returning to the first shell, revoke the lease and drop the local credentials

$ vault lease revoke aws/creds/ec2readonly/3751b993-133b-8cf8-0b7d-b87527a9e4f4
Success! Revoked lease: aws/creds/ec2readonly/3751b993-133b-8cf8-0b7d-b87527a9e4f4

$ aws ec2 describe-instances
An error occurred (AuthFailure) when calling the DescribeInstances operation: AWS was not able to validate the provided access credentials

$ unset AWS_ACCESS_KEY_ID
$ unset AWS_SECRET_ACCESS_KEY

and we can see that the user is now gone:

$ aws iam get-user --user-name vault-root-ec2readonly-1532257268-6823
An error occurred (NoSuchEntity) when calling the GetUser operation: The user with name vault-root-ec2readonly-1532257268-6823 cannot be found.

You should be able to see that the temporary user has been destroyed.

There are alternative mechanisms available to avoid this creation of a temporary user, via the STS api. This is accessed via a write, and my initial thought was that we can re-use the same policy:

$ vault write aws/sts/ec2readonly ttl=60m
Error writing data to aws/sts/ec2readonly: Error making API request.

URL: PUT http://127.0.0.1:8200/v1/aws/sts/ec2readonly
Code: 400. Errors:

* Can't generate STS credentials for a managed policy; use a role to assume or an inline policy instead

This is annoying, as it means we need to set up an inline policy instead which is functionally equivalent to the managed policy (see ec2readonly.json). Here we overwrite the one we had previously, then make a call to obtain a session token from the STS api.

$ vault write aws/roles/ec2readonly [email protected]
Success! Data written to: aws/roles/ec2readonly

$ vault write aws/sts/ec2readonly ttl=60m
Key                Value
---                -----
lease_id           aws/sts/ec2readonly/96f497e4-5ddb-ea9f-902b-1b923e17ca6b
lease_duration     59m59s
lease_renewable    false
access_key         ASIA46CDIYCJ3ZTW3A5A
secret_key         Va4AGaTpMWQKbgXyHacY5NB7P3B7rHmvksMXZGrL
security_token     FQoDYXdzEEUaDLFkooYUHgcczMReFCLGAluxi6AjoQOXkujrdlpkoMXzNXbR8lw...

$ export AWS_ACCESS_KEY_ID=ASIA46CDIYCJ3ZTW3A5A
$ export AWS_SECRET_ACCESS_KEY=Va4AGaTpMWQKbgXyHacY5NB7P3B7rHmvksMXZGrL
$ export AWS_SESSION_TOKEN=FQoDYXdzEEUaDLFkooYUHgcczMReFCLGAluxi6AjoQOXkujrdlpkoMXzNXbR8lw...

$ aws ec2 describe-instances
{
    "Reservations": []
}

$ aws sts get-caller-identity
{
    "Account": "889199313043",
    "UserId": "889199313043:vault-1532260548-5040",
    "Arn": "arn:aws:sts::889199313043:federated-user/vault-1532260548-5040"
}

It will take a while - allow about 10 minutes - for the trace of the describe-instances to show up in CloudTrail, but when it does you will be able to confirm the identity of the caller:

{
    "eventVersion": "1.05",
    "userIdentity": {
        "type": "FederatedUser",
        "principalId": "889199313043:vault-1532260548-5040",
        "arn": "arn:aws:sts::889199313043:federated-user/vault-1532260548-5040",
        "accountId": "889199313043",
        "accessKeyId": "ASIA46CDIYCJ3ZTW3A5A",
        "sessionContext": {
            "attributes": {
                "mfaAuthenticated": "false",
                "creationDate": "2018-07-22T11:55:48Z"
            },
            "sessionIssuer": {
                "type": "IAMUser",
                "principalId": "AIDAJVOFC6DLBWBOLOE54",
                "arn": "arn:aws:iam::889199313043:user/XXXXXX",
                "accountId": "889199313043",
                "userName": "XXXXXX"
            }
        }
    },
    "eventTime": "2018-07-22T12:02:50Z",
    "eventSource": "ec2.amazonaws.com",
    "eventName": "DescribeInstances",
    "awsRegion": "eu-west-2",
    "sourceIPAddress": "88.98.207.26",
    "userAgent": "aws-cli/1.15.40 Python/2.7.10 Darwin/17.7.0 botocore/1.10.40",
    "requestParameters": {
        "instancesSet": {},
        "filterSet": {}
    },
    "responseElements": null,
    "requestID": "69d4b320-8bd2-462a-92a1-6d1007b9a8e0",
    "eventID": "a96a6165-4bb8-484d-8db8-b4eb75f78901",
    "eventType": "AwsApiCall",
    "recipientAccountId": "889199313043"
}

Experimentation showed that the STS token remains useable even after you do a vault lease revoke, suggesting that if this mechanism is used, a short TTL should be used.

A similar mechanism from the point of view of the caller is available (see https://www.vaultproject.io/docs/secrets/aws/index.html) where instead of using an in-line policy, a role is assumed. This is somewhat nicer, as it ensures the operating policy is visible purely within AWS - using the STS mechanism outlined above, the policy document is held in vault and used when acquiring the token, after which the caller and the policy it is operating with are effectively invisible and anonymous.

One trouble with both mechanisms is that the identity of the actual caller gets lost - the identity of the user on the laptop is not easily visible. If an appropriate authentication back end was put in place for the vault server, and the audit trail periodically captured, it becomes possible to ensure that only authorised users can obtain AWS credentials, and to see who obtained credentials when.

Ideally, using vault to provide credentials should be configured so that different users, and/or temporary credentials, map onto different AWS operating policies. Doing this is non-trivial and somewhat tricky, and your needs will be unique. In many regards, use of vault to obtain AWS credentials is probably more use for scripts rather than ad-hoc CLI use, in which case the question arises as to why those scripts are not being run from authorised EC2 instances rather than the desktop. It feels like vault is most useful for the case where we need privileged users to be bootstrapping up AWS infrastructure in a controlled fashion, after which further configuration and maintenance is done from within AWS via privileged EC2 instances. There are several layers of "bootstrapping" in play in this scenario, as there would need to be an initial phase around setting up the vault service itself!

On a final note, in the examples above, the vault program is being used as the client for the running vault server. This has some drawbacks, as the output is by default designed for human readability. It's possible to specify the output to be in JSON, which would allow the JSON to then be parsed by jq or similar:

$ export VAULT_FORMAT=json
$ vault write aws/sts/ec2readonly ttl=60m
{
  "request_id": "725cbf75-2737-685a-e7c5-ca689a933ee7",
  "lease_id": "aws/sts/ec2readonly/fe77b067-fefd-afb8-8bd1-4fb956933692",
  "lease_duration": 3599,
  "renewable": false,
  "data": {
    "access_key": "ASIA46CDIYCJX5F73F7K",
    "secret_key": "+yjSBdAYpcNAT912lbCCJI0kCSIOFDHasKr710pP",
    "security_token": "FQoDYXdzEEYaDJSAudTfP2BXDAsfiy..."
  },
  "warnings": null
}

Alternatively, the vault server has a RESTful HTTP API which could be used effectively in scripts, although the semantics of the vault client are probably more transparent.

Automating Vault Configuration

Configuring vault is well outside the scope of this current work, however the following article from Hashicorp has some useful starting thoughts: https://www.hashicorp.com/blog/codifying-vault-policies-and-configuration. Another article worth looking at discusses using Terraform to configure Vault, pointing out that the Hashicorp suggestion is only really useful for additive changes, whereas because Terraform is stateful you may be able to get a happier experience with it: https://theartofmachinery.com/2017/07/15/use_terraform_with_vault.html. Note that this article does not encompass setting up a vault cluster, just provisioning of configurations into it.

In either of these cases, it cannot be emphasised enough: the actual secrets should still be added to vault manually. There are other issues around integrating Terraform and Vault that need to be considered, this article is a starting point: https://www.greenreedtech.com/mitigating-terraform-secrets-exposure/. It includes suggestions around mitigating the risks, but the bottom line is that while you might use Terraform to set up and configure vault, reading and writing secrets with Terraform is currently not a good idea.

License

Copyright 2018 Leap Beyond Emerging Technologies B.V.

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.