Earlier this year, I decided that I would finally implement the same set of best practices in my own personal AWS accounts that I shared with my customers over the past two years. The intent, to run my own production workloads across the accounts that were effectively idle since their instantiation.

The first step (and the most important) was to address authentication and authorization into the accounts, not only for administration, but for development. While the easy route would be to create an IAM user, assign it the AdministratorAccess managed policy, and continue, I wanted to replicate the same experience as an enterprise using AWS for the first time.

As a GSuite user for many years (set up before it became a pay service), I decided to use it as the base for my identity authority. This would force me to set up federation between it, and AWS, similar to how an enterprise would leverage their existing Active Directory infrastructure. The primarily goal being to reduce (read: eliminate) credential sprawl.

Before setting up the association between GSuite, and my AWS environment, I needed to organize my accounts. At this point, each of them were standalone, without any association between them. To ensure consistency across the accounts, I decided to set up an AWS Organization, and set up a logical heirarchy. When complete, I had three Organizational Units (OU) (e.g., personal, external, common-services), and had moved each account into an OU. I then decided that the environment would have a single point of entry, by federation through an account in the common-services OU for console, CLI, and API use.

pause - Armen, what did you just say?

That’s right! I was going to find a way to use federation, and role assumption for not only console use (child’s play), but through the AWS CLI.

Setting up federation between GSuite, and AWS was straightforward, thanks to this awesome post on the AWS Security blog from 2016.

I made two changes to the blog post implementation to increase the security posture of my infrastructure.

The first was to set the value for customType, as I didn’t think the value of Developer referenced in the post correctly conveyed the intent of the object. To me, setting it to AWS made more sense on line 7 below. On line 8, I set the value as it’s set in the AWS blog article, to a role described in the next paragraph.

1
2
3
4
5
6
7
8
9
10
11
12
13
{
  "customSchemas": {
    "SSO": {
      "role": [
        {
          "type": "custom",
          "customType": "AWS",
          "value": "redacted"
        },
      ]
    }
  }
}

The AWS IAM role value used in the schema entry above has a single custom policy giving the assumer of the role access to a single action of assuming a role.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "sts:AssumeRole",
      "Resource": "*"
    }
  ]
}

The trust relationship for the role is defined just as it is in the AWS blog entry, with no changes.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::redacted:saml-provider/redacted"
      },
      "Action": "sts:AssumeRoleWithSAML",
      "Condition": {
        "StringEquals": {
          "SAML:aud": "https://signin.aws.amazon.com/saml"
        }
      }
    }
  ]
}

With the permission policy in place, the only privilege allowed to federated user is role assumption assumption reducing the risk of impact if the originating identity is compromised. From here, I set up roles to be assumed in each of the accounts for specific usage (e.g., billing, development, infrastructure, security, etc.), and ensured that each target role had conditionals to further reduce the risk of malicious use.

For example, this is trust policy for a role that can be assumed by the entry role:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::redacted:role/saml-sts/armen@kriation.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

The CloudTrail entry for when the federation occurs looks like this:

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
52
53
54
{
    "eventVersion": "1.05",
    "userIdentity": {
        "type": "SAMLUser",
        "principalId": "redacted:armen@kriation.com",
        "userName": "armen@kriation.com",
        "identityProvider": "redacted"
    },
    "eventTime": "redacted",
    "eventSource": "sts.amazonaws.com",
    "eventName": "AssumeRoleWithSAML",
    "awsRegion": "redacted",
    "sourceIPAddress": "redacted",
    "userAgent": "AWS Signin, aws-internal/3",
    "requestParameters": {
        "sAMLAssertionID": "redacted",
        "roleSessionName": "armen@kriation.com",
        "durationSeconds": "redacted",
        "roleArn": "arn:aws:iam::redacted:role/saml-sts",
        "principalArn": "arn:aws:iam::redacted:saml-provider/google-kriation"
    },
    "responseElements": {
        "subjectType": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified",
        "issuer": "https://accounts.google.com/o/saml2?idpid=redacted",
        "credentials": {
            "accessKeyId": "redacted",
            "expiration": "redacted",
            "sessionToken": "redacted"
        },
        "nameQualifier": "redacted",
        "assumedRoleUser": {
            "assumedRoleId": "redacted:armen@kriation.com",
            "arn": "arn:aws:sts::redacted:assumed-role/saml-sts/armen@kriation.com"
        },
        "subject": "armen@kriation.com",
        "audience": "https://signin.aws.amazon.com/saml"
    },
    "requestID": "redacted",
    "eventID": "redacted",
    "resources": [
        {
            "ARN": "arn:aws:iam::redacted:role/saml-sts",
            "accountId": "redacted",
            "type": "AWS::IAM::Role"
        },
        {
            "ARN": "arn:aws:iam::redacted:saml-provider/google-kriation",
            "accountId": "redacted",
            "type": "AWS::IAM::SAMLProvider"
        }
    ],
    "eventType": "AwsApiCall",
    "recipientAccountId": "redacted"
}

Note that the principalArn on line 20 is the identity provider, and not a typical user or role Arn that you would see in a regular AssumeRole call (as below). The role being assumed is the entry role that I discussed above.

The CloudTrail entry for the subsequent role assumption looks like this:

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
52
53
54
55
{
    "eventVersion": "1.05",
    "userIdentity": {
        "type": "AssumedRole",
        "principalId": "redacted:armen@kriation.com",
        "arn": "arn:aws:sts::redacted:assumed-role/saml-sts/armen@kriation.com",
        "accountId": "redacted",
        "accessKeyId": "redacted",
        "sessionContext": {
            "attributes": {
                "mfaAuthenticated": "false",
                "creationDate": "redacted"
            },
            "sessionIssuer": {
                "type": "Role",
                "principalId": "redacted",
                "arn": "arn:aws:iam::redacted:role/saml-sts",
                "accountId": "redacted",
                "userName": "saml-sts"
            }
        }
    },
    "eventTime": "redacted",
    "eventSource": "sts.amazonaws.com",
    "eventName": "AssumeRole",
    "awsRegion": "redacted",
    "sourceIPAddress": "redacted",
    "userAgent": "aws-internal/3",
    "requestParameters": {
        "roleArn": "arn:aws:iam::redacted:role/security/security-admin",
        "roleSessionName": "armen@kriation.com"
    },
    "responseElements": {
        "credentials": {
            "accessKeyId": "redacted",
            "expiration": "redacted",
            "sessionToken": "redacted"
        },
        "assumedRoleUser": {
            "assumedRoleId": "redacted:armen@kriation.com",
            "arn": "arn:aws:sts::redacted:assumed-role/security-admin/armen@kriation.com"
        }
    },
    "requestID": "redacted",
    "eventID": "redacted",
    "resources": [
        {
            "ARN": "arn:aws:iam::redacted:role/security/security-admin",
            "accountId": "redacted",
            "type": "AWS::IAM::Role"
        }
    ],
    "eventType": "AwsApiCall",
    "recipientAccountId": "redacted"
}

In comparison to the previous call, this event entry is slightly different, logging the userIdentity object that is assuming the target role (in this case security-admin).

The end result is the same, with the accesskey, expiration, and session token being contained in the response.

Once all the pieces were in place, SSO into my AWS account via the browser was seamless. To make access easier, I bookmarked the link to intiate SSO. The link to intiate SSO is located at the top right corner

The pattern for the link is: https://accounts.google.com/o/saml2/initsso?idpid=[idpid]&spid=[spid]

The idpid is effectively the identifier for the directory that is tied to your GSuite account.

The spid is the identifier for the app that you configured SSO to through the GSuite Apps->SAML Apps pane.

In my next post, I’ll dive into how I configured my development environment to facilitate access into the accounts via the CLI.