aws advent
Search
  • 2018
  • 2016
Menu

Tag Archives: STS Keys

Using Federated Login to provide AWS CLI/API access

by Norm MacLennan 03. December 2016 2016 0

Author: Norm MacLennan

Editors: Zoltán Lajos Kis, Michael Weinberg

One of the best, most compelling features of AWS is all of the tools and APIs available for automation of your infrastructure. Engineers and administrators can take an empty account and have it ready to run scalable production workloads in mere minutes thanks to these tools.

But there’s a dark side as well. Everyone has seen the stories of the bots continuously scouring GitHub for IAM access keys, leading to stolen data, public embarrassment, and thousands of dollars in bills. AWS themselves offer advice on dealing with exposed keys.

“Don’t show your keys during presentations” and “don’t commit your credentials to source control” are pieces of advice that sound easy to follow, but all it takes is one small mistake to compromise your account. Wouldn’t it be great if long-lived IAM access keys could be done away with entirely?

Federated Login for Management Console Access

Providing each person with an IAM user and a set of access keys works great for some organizations. But others may be managing hundreds of users and/or several AWS accounts.

Organizations with such a footprint generally also already have centralized user management with Active Directory, Google GSuite, or some other identity provider. At that point, managing AWS users and access control from that same centralized point becomes very attractive. Otherwise, role trusts and access policies can become a rat’s nest – not to mention a security nightmare waiting to happen – and users may find the process of constantly exchanging credentials and switching between accounts confusing and frustrating.

Luckily, AWS offers several strategies for federated login through SAML or OpenID Connect identity providers like Microsoft ADFS and Google GSuite. There is a more-complete list of SAML providers in the AWS docs.

Federated login lets administrators delegate control of user management and access control for AWS accounts to traditional identity providers like Active Directory. That means administrators don’t need to manage separate IAM users for people just needing to use the AWS console. Which is cool, but doesn’t solve our problem of eliminating IAM access keys – API/SDK users still need keys to use.

Federated Login for STS keys

Federated login uses the Security Token Service (STS) under the hood – it uses SAML or OIDC to generate a temporary sign-in URL for getting the user to the console. That means it can also be used to generate raw STS keys using the AssumeRoleWithSaml API. STS keys are functionally identical to long-lived IAM access keys, but they have a clearly-defined expiration time – between 15 minutes and 12 hours from when they were issued.

Of course, the hard part of using assume-role-with-saml from the command-line is that you need to provide an XML document called a SAML assertion, which is generated by your identity provider upon authentication. AWS provides example scripts in some blog posts on how to do this for ADFS and certain other identity providers, but they are pretty specific to the individual identity provider, so I won’t cover either of them here.

By and large, though, the scripts automate the HTML login form for the identity provider to generate the SAML response, then they send that to AWS to assume the requested role.

Providing users with a script like this means that they can generate STS keys with the same credentials they already use for other systems. And those keys will automatically expire after a short time to limit risk if they ever do get compromised, or even if they just leave the organization.

In this way, careful account administrators can do away with IAM users entirely and limit their attack surface.

A Working Example with Auth0

Where I work, we have dozens of accounts, hundreds of users, and a few different identity providers. Our situation and our needs are possibly atypical but probably not unique. After a few different attempts, this is the solution that we ended up with.

Auth0 as an Identity provider

We opted to use Auth0 as our identity provider. Or, to be more specific, our identity provider broker. Similar schemes can be achieved with other on-prem and SaaS identity providers, but the below will deal explicitly with Auth0.

Auth0 is a SaaS identity provider hub that allows users to plug in and aggregate a large number of other identity providers (including ADFS, GSuite, or even a local database) on the backend and extend them to provide authentication and authorization into their own applications or – in this case – flexibly integrate with third-party applications like AWS.

It was also an easy answer for us because Auth0 was already in use in a few areas of our company, so your mileage may vary. But the main value proposition for us was that it allowed us to abstract away differences between different identity providers like ADFS and GSuite and allow users across different back-end identity providers to authenticate to the same AWS accounts where necessary.

The basic architecture

For those unfamiliar with Auth0, there will be a few terms to define. Many of these have analogues in traditional identity providers.

  • Connection: A backend identity provider, i.e. ADFS or GSuite. An Auth0 account can have many connections of many different types.
  • Client: Confusingly, this is essentially “an application.” In this case, each AWS account maps directly to a client. Clients can have one to many Connections enabled to govern who can authenticate.
  • Client metadata (previously app_metadata): Each client can have several key-value pairs associated with it. These can be used by Rules or third-party application integrations.
  • Rule: Rules are a chain of Javascript functions that are run during authentications to Clients. These can make use of client metadata, user information, and more to perform custom authorization actions and more.

Logging into the Console

Our scripting hooks up the identity provider to AWS the same way in every account regardless of if it will use ADFS or Google under the hood, which is one of the benefits of Auth0. Then the connections for ADFS or Google are enabled based on which types of users will be logging in.

We create a client for each AWS account as described in the Auth0 documentation and attach account-specific client metadata to the client to enable the login. Then we create an IAM Identity Provider in the AWS account as per the AWS documentation.

We start with a basic Auth0 client as described in auth0-client.json:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
  "addons": {
    "samlp": {
      "audience": "https://signin.aws.amazon.com/saml",
      "mappings": {
        "email": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
        "name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"
      },
      "createUpnClaim": false,
      "passthroughClaimsWithNoMapping": false,
      "mapUnknownClaimsAsIs": false,
      "mapIdentities": false,
      "nameIdentifierFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent",
      "nameIdentifierProbes": [
        "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
      ]
    }
  },
  "callbacks": [
    "https://signin.aws.amazon.com/saml"
  ],
  "jwt_configuration": {
    "alg": "HS256",
    "lifetime_in_seconds": 36000,
    "secret_encoded": true
  },
  "token_endpoint_auth_method": "client_secret_post",
  "app_type": "non_interactive"
}

The important parts in the above are part of the samlp object. These configure the “SAML2 Web App” integration. That being said, most of it is just boilerplate.

When we create the client, we perform a bit of customization. Adding client metadata with information about the account and which security group will be logging in by default. This will be used later by the rule.

See this example create-client.rb (you can see the auth0-ruby documentation and management API docs for more information):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
require 'auth0'
require 'json'
 
auth0 = Auth0Client.new(
  :client_id => 'eX4mpl3Cl13nt'
  :token => 'some-jwt',
  :domain => "nromdotcom.auth0.com"  
)
 
client = JSON.parse(File.read(File.expand_path('./auth0-client.json', __FILE__)))
client["name"] = 'aws-myaccount'
client["client_metadata"] = {
  "aws_default_group" => 'domain\\aws_admins',
  "aws_account_number" => '123456789012'
}
 
auth0.create_client("aws-myaccount", client)

This creates an Auth0 client that will be used for SAML authentication. The client metadata is used by the Auth0 rule to identify which account to place the user into and determine if the user is authorized to assume that role.

From the AWS account side, we then hook up an identity provider and a couple of IAM roles using information from the new Auth0 client.

aws-trust.json:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {},
      "Action": "sts:AssumeRoleWithSAML",
      "Condition": {
        "StringEquals": {
          "SAML:aud": "https://signin.aws.amazon.com/saml"
        }
      }
    }
  ]
}

create-idp.rb:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
require 'open'
require 'aws-sdk'
 
iam = Aws::IAM::Client.new
# Fetch the SAML metadata for the client created in create-client.rb
client_metadata = open("https://nromdotcom.auth0.com/samlp/metadata/#{client_id}").read
 
# Use it to create an IAM Identity Provider and a role
idp = iam.create_saml_provider({
  saml_metadata_document: client_metadata,
  name: name
}).saml_provider_arn
 
role_trust_policy = JSON.parse(File.read(File.expand_path('./aws-policy.json', __FILE__)))
role_trust_policy["Statement"][0]["Principal"] = {"Federated" => provider_arn}
 
iam.create_role({
  path: '/ADFS/domain/',
  role_name: 'aws_admins',
  assume_role_policy_document: role_trust_policy.to_json
  }).role.role_name

Now there is one last piece of the puzzle. An Auth0 rule to perform additional authorization and place users into the proper account based on which client they’re authenticating against. The rule runs for every authentication and pulls information from the client metadata set above. It then ensures the user is a member of the aws_default_group in their identity provider. If the user is a member, it logs the user into a role named for that group (created above), otherwise it errors out.

A simplified version of the rule is below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
function awsAuthorization (user, context, callback) {
  // Pull relevant things out of client metadata
  var aws_account_number = context.clientMetadata.aws_account_number,
    default_group = context.clientMetadata.aws_default_group,
    identity_provider = 'arn:aws:iam::' + aws_account_number + ':saml-provider/Auth0',
    requested_aws_role;
 
  // See if user is a member of the default group
  requested_aws_role = context.connection + '/' + default_group.replace(/\\/g,'/').replace(/ /g,'+');
 
  if (user.groups.indexOf(default_group) >= 0) {
    user.awsRole = 'arn:aws:iam::' + aws_account_number + ':role/' + requested_aws_role;
  }
 
  if (!user.awsRole) {
    return callback("You aren't authorized to login to AWS account " + aws_account_number + "!");
  }
 
  // 'delegation' protocol means we are just generating STS keys, relevant below.
  if(context.protocol === 'delegation') {
    // Since we're delegating an existing session, the user
    // already has a role assigned.
    var aws_role = user.awsRole.split(',')[0];
 
    context.addonConfiguration.aws.principal = identity_provider;
    context.addonConfiguration.aws.role = aws_role;
    context.addonConfiguration.aws.mappings = {
      'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier': 'name',
      'https://aws.amazon.com/SAML/Attributes/Role':                          'awsRole',
      'https://aws.amazon.com/SAML/Attributes/RoleSessionName':               'nickname',
      'https://aws.amazon.com/SAML/Attributes/SessionDuration':               'sessionDuration'
    };
 
  // If we aren't delegating, we are logging in to the console
  } else {
 
    // SAML federation requires the role to be "$roleName,$IdPName"
    // so let's join them.
    user.awsRole = [user.awsRole, identity_provider].join(',');
    user.sessionDuration = '43200';
 
    // Set SAML assertions, used by AWS to map our session to a role
    context.samlConfiguration.mappings = {
      'https://aws.amazon.com/SAML/Attributes/Role': 'awsRole',
      'https://aws.amazon.com/SAML/Attributes/RoleSessionName': 'nickname',
      'https://aws.amazon.com/SAML/Attributes/SessionDuration': 'sessionDuration'
    };
  }
 
  callback(null, user, context);
}

With all of this in place in Auth0 and the AWS account, users simply navigate to https://[organization].auth0.com/samlp/[client_id] to log in to the AWS console for the account. We’ve created a simple redirector service to put friendly names in front of those URLs, but that is out of scope of this article.

That allows us to control logging in to the AWS management console for multiple accounts from a single location, but what about STS keys?

Getting STS keys from Auth0

The Auth0 delegation endpoint endpoints for these clients can be used to generate STS keys from anywhere you can make HTTP requests.

sts.rb:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
require 'net/http'
require 'json'
require 'io/console'
require 'pp'
 
puts 'Username:'
user = STDIN.gets.chomp
puts 'Password:'
pw = STDIN.noecho(&:gets).chomp
puts 'Auth0 Organization:'
organization = STDIN.gets.chomp
puts 'Auth0 Client:'
client = STDIN.gets.chomp
 
# Authenticate to the client with username/password
uri = URI("https://#{organization}.auth0.com/oauth/ro")
req = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json')
req.body = {
  client_id: client, username: user, password: pw,
  connection: 'adfs', grant_type: 'password', scope: 'openid'
}.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
  http.request(req)
end
 
# Use the returned JWT to fetch STS keys
uri = URI("https://#{organization}.auth0.com/delegation")
req = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json')
req.body = {
  client_id: client,
  grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
  id_token: JSON.parse(res.body)['id_token'],
  scope: 'openid', api_type: 'aws'
}.to_json
 
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
  http.request(req)
end
 
pp JSON.parse(res.body)['Credentials']

Running that script with valid values will output STS keys that are good for one hour. The script can easily be extended to write these keys to an AWS credentials file or inject them into the environment for use by the AWS CLI or other tooling.

Conclusion

Using STS through Auth0 for users along with Instance Profiles (etc) for systems, we have been able to ensure that there are no IAM users or long-lived IAM access keys in any of our accounts, simplifying management and increasing security of our accounts.

Users gain and lose access to accounts automatically as they join and leave the company or are added and removed to relevant security groups in our directory services. This means account administrators don’t need to spend any extra time or effort managing access to their accounts and users don’t need to worry about accidentally exposing their keys or regularly/manually rotating them.

While organizations that already have an identity provider can gain a lot of value from this or a similar architecture, smaller organizations without such infrastructure might have trouble getting a real return on investment from the work that this takes to set up in the first place. However, it is worth noting that Auth0 also provides an “identity provider” that consists of a database hosted by Auth0, so certain organizations could get the best of both worlds.

 

About the Author:

Norm MacLennan is a SysAdmin-turned-developer who can be found engineering clouds @Cimpress.

About the Editors:

Zoltán Lajos Kis joined Ericsson in 2007 to work with scalable peer-to-peer and self organizing networks. Since then he has worked with various telecom core products and then on Software Defined Networks. Currently his focus is on cloud infrastructure and big data services.


Sidebar

Follow me on Twitter

My Tweets

Tags

2014 alb alexa ami Auth0 boto boto3 cfn-init chatops CloudFormation cloudfront CodePipeline data pipeline DynamoDB ebs ec2 eip elb federated login ha haproxy IAM Infrastructure as Code Lambda mfa multi-account open source packer profiles python react roles route53 s3 sam security serverless Slack SNS sparleformation STS Keys terraform threads userdata vpc
Twitter
To top

Navigation

  • 2018
  • 2016
Search Anything
Close