Picking up where we left off…
In my previous blog I gave a fairly high-level overview of what automated AWS account management could (or rather should) entail. This blog will drill deeper into the processes and give you some real-world code samples of what this looks like.
AWS Organizations and Linked Account Creation:
As mentioned in my last blog, AWS recently announced the general availability of AWS Organizations, allowing you to create linked or nested AWS accounts under a master account and apply policy-based management under the umbrella of the root account. It also allows for hierarchical management (up to five levels deep) of linked accounts by Organizational Units (OU). Policies can be applied at the global level, OU level, and individual account level. It is important to note that conflicting policies always defer to the parent entities permission set. Meaning an IAM user/role in account may have permissions to perform some action, but, if at the Organizations level the account, OU, or global settings deny those actions, the resulting action for the IAM resource will be denied. Likewise, the effective permissions for a resource are a union of the resource’s direct permissions assigned in IAM and the permissions that are controlled by Organizations. This means you can lock linked accounts down to do things like “only manage DNS Route53 resources” or “only manage S3 resources” using Organizations policies. Pretty nice way of segmenting off security and reducing the potential blast radius.
I am going to pick the most common denominator for my following examples… AWS CLI. Though I rarely use it for actual automation code, I figure most folks are familiar with it and it has a pretty intuitive syntax.
Step 1: Enable Organizations on your root account
Ensure that your AWS Profile environment variable is set to your desired root account AWS profile that has the necessary permissions to work with AWS Organizations. Alternatively, if you don’t want to use an environment variable, you can either ensure the default AWS Profile is the one which has permissions on your root account or you can specify the –profile argument when typing your AWS CLI commands. I’m going to use the AWS_DEFAULT_PROFILE environment variable in my examples here (output redacted).
> export AWS_DEFAULT_PROFILE=myrootacctadmin |
> export AWS_DEFAULT_PROFILE=myrootacctadmin
This of course assumes you have a profile set up under your HOME dir in the .aws/credentials file named myrootacctadmin.
Minimally, this will look something like this:
[myrootacctadmin] aws_access_key_id = AKI????????????????? aws_secret_access_key = somesecretaccesskey0somesecretaccesskey0 |
[myrootacctadmin] aws_access_key_id = AKI????????????????? aws_secret_access_key = somesecretaccesskey0somesecretaccesskey0
Now that we have our environment set we can get on with running the AWS CLI commands to create our organization.
Let’s be safe and make sure we don’t already have an organization created under our root account:
$ aws organizations list-roots |
$ aws organizations list-roots
An error occurred (AWSOrganizationsNotInUseException) when calling the ListRoots operation: Your account is not a member of an organization.
As the error message indicates, this account is not currently a part of any organization and will need to be configured to use organizations if we want to use this as our master account and create linked accounts underneath it.
Easy enough, let’s just create our organization…
> aws organizations create-organization { "Organization": { "Id": "o-xxxxxxxxxx", "Arn": "arn:aws:organizations::000000000000:organization/o-xxxxxxxxxx", "FeatureSet": "ALL", "MasterAccountArn": "arn:aws:organizations::000000000000:account/o-xxxxxxxxxx/000000000000", "MasterAccountId": "000000000000", "MasterAccountEmail": "1337h4x0r@gmail.com", "AvailablePolicyTypes": [ { "Type": "SERVICE_CONTROL_POLICY", "Status": "ENABLED" } ] } } |
> aws organizations create-organization { "Organization": { "Id": "o-xxxxxxxxxx", "Arn": "arn:aws:organizations::000000000000:organization/o-xxxxxxxxxx", "FeatureSet": "ALL", "MasterAccountArn": "arn:aws:organizations::000000000000:account/o-xxxxxxxxxx/000000000000", "MasterAccountId": "000000000000", "MasterAccountEmail": "1337h4x0r@gmail.com", "AvailablePolicyTypes": [ { "Type": "SERVICE_CONTROL_POLICY", "Status": "ENABLED" } ] } }
Now that we have created an organization let’s try our list-roots command again to see if we get something different this time…
> aws organizations list-roots { "Roots": [ { "Id": "r-xxxx", "Arn": "arn:aws:organizations::000000000000:root/o-xxxxxxxxxx/r-xxxx", "Name": "Root", "PolicyTypes": [] } ] } |
> aws organizations list-roots { "Roots": [ { "Id": "r-xxxx", "Arn": "arn:aws:organizations::000000000000:root/o-xxxxxxxxxx/r-xxxx", "Name": "Root", "PolicyTypes": [] } ] }
Indeed! our myrootacctadmin account is listed as the root (i.e. master) of our entire organization. This is exactly what we wanted. Now let’s see what AWS accounts are identified as part of this organization…
> aws organizations list-accounts { "Accounts": [ { "Id": "000000000000", "Arn": "arn:aws:organizations::000000000000:account/o-xxxxxxxxxx/000000000000", "Email": "1337h4x0r@gmail.com", "Name": "Satoshi Nakamoto", "Status": "ACTIVE", "JoinedMethod": "INVITED", "JoinedTimestamp": 1524592888.119 } ] } |
> aws organizations list-accounts { "Accounts": [ { "Id": "000000000000", "Arn": "arn:aws:organizations::000000000000:account/o-xxxxxxxxxx/000000000000", "Email": "1337h4x0r@gmail.com", "Name": "Satoshi Nakamoto", "Status": "ACTIVE", "JoinedMethod": "INVITED", "JoinedTimestamp": 1524592888.119 } ] }
As expected, just our root account. It looks kind of lonely there all by itself, so let’s go ahead and create a Linked account underneath it.
Step 2. Create a Linked Account under your Organization
> aws organizations create-account --email brawndo@2ndwatch.com --account-name brawndo { "CreateAccountStatus": { "Id": "car-0123456789abcdef0123456789abcdef", "AccountName": "brawndo", "State": "IN_PROGRESS", "RequestedTimestamp": 1524593026.058 } } |
> aws organizations create-account --email brawndo@2ndwatch.com --account-name brawndo { "CreateAccountStatus": { "Id": "car-0123456789abcdef0123456789abcdef", "AccountName": "brawndo", "State": "IN_PROGRESS", "RequestedTimestamp": 1524593026.058 } }
The actual creation of the account is not instantaneous, and the API responds to the create-account call before the new account creation is complete. While it is pretty quick to complete, unless we ensure that it is completed before performing any additional automation against it, we may receive an error from the API indicating the account is not yet ready. So prior to performing additional configuration on the new account, we need to ensure the State has reached SUCCEEDED. You will generally just loop until the State is equal to SUCCEEDED in your automation code before moving on to the next step. Also, it might be a good idea to catch failures (e.g. State == “FAILED”) and handle those gracefully. The account creation status can be achieved as follows:
> aws organizations describe-create-account-status --create-account-request-id car-0123456789abcdef0123456789abcdef { "CreateAccountStatus": { "Id": "car-0123456789abcdef0123456789abcdef", "AccountName": "brawndo", "State": "SUCCEEDED", "RequestedTimestamp": 1524593026.146, "CompletedTimestamp": 1524593030.253, "AccountId": "111111111111" } } |
> aws organizations describe-create-account-status --create-account-request-id car-0123456789abcdef0123456789abcdef { "CreateAccountStatus": { "Id": "car-0123456789abcdef0123456789abcdef", "AccountName": "brawndo", "State": "SUCCEEDED", "RequestedTimestamp": 1524593026.146, "CompletedTimestamp": 1524593030.253, "AccountId": "111111111111" } }
Congratulations! You’ve just enabled AWS Organizations and created your first linked account!
At this point you should have a couple of emails from AWS in the inbox of the email address used to create the new account. They are standard boiler-plate emails. One of which is a “Welcome to Amazon Web Services” email and the other tells you that your account is ready and has some “getting started” type links.
Step 3: Reset New Linked Account Root Password
Now that your linked account has been created you will need to go through the AWS Reset Root Account Password workflow to make your new account accessible from either the AWS Web Console or the AWS APIs. The recommended approach here is to reset the root account password, enable MFA, Create an IAM user with Administrator privileges, store the root account secrets in a VERY secure place, and only use them as a last resort for account access.
Here’s a shortened URL that will take you directly to the root account password reset page: https://amzn.pw/45Nxe
Step 4: (Optionally) Create Organizational Units
Let’s go through a couple of examples of Organizational Units.
- OU for only allowing S3 services
- OU for only allowing services in us-west-2 and us-east-1 regions
“What if I want to bring my existing accounts under the umbrella of Organizations?” you ask
Good news! You can invite existing AWS accounts to join your organization. Using the API you can issue an invitation to an existing account by Account ID, Email, or Organization. For the sake of simplicity, let’s use an Account ID (222222222222) for the following example (again, using the root/master account AWS profile):
> aws organizations invite-account-to-organization --target Id=222222222222,Type=ACCOUNT { "Handshake": { "Id": "h-0123456789abcdef0123456789abcdef", "Arn": "arn:aws:organizations::000000000000:handshake/o-xxxxxxxxxx/invite/h-0123456789abcdef0123456789abcdef", "Parties": [ { "Id": "xxxxxxxxxx", "Type": "ORGANIZATION" }, { "Id": "222222222222", "Type": "ACCOUNT" } ], "State": "OPEN", "RequestedTimestamp": 1524610827.55, "ExpirationTimestamp": 1525906827.55, "Action": "INVITE", "Resources": [ { "Value": "o-xxxxxxxxxx", "Type": "ORGANIZATION", "Resources": [ { "Value": "1337h4x0r@gmail.com", "Type": "MASTER_EMAIL" }, { "Value": "Satoshi Nakamoto", "Type": "MASTER_NAME" }, { "Value": "ALL", "Type": "ORGANIZATION_FEATURE_SET" } ] }, { "Value": "222222222222", "Type": "ACCOUNT" } ] } } |
> aws organizations invite-account-to-organization --target Id=222222222222,Type=ACCOUNT { "Handshake": { "Id": "h-0123456789abcdef0123456789abcdef", "Arn": "arn:aws:organizations::000000000000:handshake/o-xxxxxxxxxx/invite/h-0123456789abcdef0123456789abcdef", "Parties": [ { "Id": "xxxxxxxxxx", "Type": "ORGANIZATION" }, { "Id": "222222222222", "Type": "ACCOUNT" } ], "State": "OPEN", "RequestedTimestamp": 1524610827.55, "ExpirationTimestamp": 1525906827.55, "Action": "INVITE", "Resources": [ { "Value": "o-xxxxxxxxxx", "Type": "ORGANIZATION", "Resources": [ { "Value": "1337h4x0r@gmail.com", "Type": "MASTER_EMAIL" }, { "Value": "Satoshi Nakamoto", "Type": "MASTER_NAME" }, { "Value": "ALL", "Type": "ORGANIZATION_FEATURE_SET" } ] }, { "Value": "222222222222", "Type": "ACCOUNT" } ] } }
A couple of things of note – The handshake Id is what will be required to accept the invitation on the linked account side. Notice the difference between the RequestedTimestamp (epoch 1524610827.55) and the ExpirationTimestamp (epoch 1525906827.55). 1296000 seconds. Divide that by 86400 seconds in a day and we get 15 days.
At this point you have 15 days to issue an acceptance of the invitation (aka: handshake), from the target AWS account. You could simply log in to the AWS Web Console, navigate to Organizations, and accept the invitation, but that’s not what this article is about now is it? We’re talking automation here! And, as all good DevOpsers know, we utilize security entities that employ PoLP (Principal of Least Privilege) to perform process-specific tasks.
This means we aren’t going to do something ludicrous like adding AWS Access Keys to our root account login (please don’t ever do this). Nor are we going to create an IAM User with Administrator access for this very specific task. You can either create a User or a Role in the target account to accept the handshake, although, creating a Role will require you to assume that Role using STS, which might be overkill. On the other hand, you might use a lambda function to automate the handshake in which case you most certainly would utilize an IAM Role. Either way, the following IAM Policy Document will provide the User/Role with the required permissions to accept (or delete) the invitation:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "iam:CreateServiceLinkedRole", "organizations:AcceptHandshake", "organizations:CancelHandshake", "organizations:DeclineHandshake", "organizations:DescribeHandshake", "organizations:ListHandshakesForAccount" ], "Resource": "*" } ] } |
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "iam:CreateServiceLinkedRole", "organizations:AcceptHandshake", "organizations:CancelHandshake", "organizations:DeclineHandshake", "organizations:DescribeHandshake", "organizations:ListHandshakesForAccount" ], "Resource": "*" } ] }
Using the AWS CLI (leveraging a profile of a User/Role with the aforementioned permissions under the existing target account), you would issue the following command to accept the invitation/handshake:
> aws organizations accept-handshake --handshake-id h-0123456789abcdef0123456789abcdef |
> aws organizations accept-handshake --handshake-id h-0123456789abcdef0123456789abcdef
{ "Handshake": { "Id": "h-0123456789abcdef0123456789abcdef", "Arn": "arn:aws:organizations::000000000000:handshake\/o-xxxxxxxxxx\/invite\/h-0123456789abcdef0123456789abcdef", "Parties": [ { "Id": "222222222222", "Type": "ACCOUNT" }, { "Id": "xxxxxxxxxx", "Type": "ORGANIZATION" } ], "State": "ACCEPTED", "RequestedTimestamp": 1524610827.55, "ExpirationTimestamp": 1525906827.55, "Action": "INVITE", "Resources": [ { "Value": "o-xxxxxxxxxx", "Type": "ORGANIZATION", "Resources": [ { "Value": "1337h4x0r@gmail.com", "Type": "MASTER_EMAIL" }, { "Value": "Satoshi Nakamoto", "Type": "MASTER_NAME" }, { "Value": "ALL", "Type": "ORGANIZATION_FEATURE_SET" } ] }, { "Value": "222222222222", "Type": "ACCOUNT" } ] } } |
{ "Handshake": { "Id": "h-0123456789abcdef0123456789abcdef", "Arn": "arn:aws:organizations::000000000000:handshake\/o-xxxxxxxxxx\/invite\/h-0123456789abcdef0123456789abcdef", "Parties": [ { "Id": "222222222222", "Type": "ACCOUNT" }, { "Id": "xxxxxxxxxx", "Type": "ORGANIZATION" } ], "State": "ACCEPTED", "RequestedTimestamp": 1524610827.55, "ExpirationTimestamp": 1525906827.55, "Action": "INVITE", "Resources": [ { "Value": "o-xxxxxxxxxx", "Type": "ORGANIZATION", "Resources": [ { "Value": "1337h4x0r@gmail.com", "Type": "MASTER_EMAIL" }, { "Value": "Satoshi Nakamoto", "Type": "MASTER_NAME" }, { "Value": "ALL", "Type": "ORGANIZATION_FEATURE_SET" } ] }, { "Value": "222222222222", "Type": "ACCOUNT" } ] } }
The returned JSON struct is the exact same handshake struct returned by the API when we issued the invitation with one important difference. The State property is now reflecting a value of ACCEPTED.
That’s it. You’ve successfully linked an existing account into your Organization under the master billing account.
In the next installment, I will go into depth on the processes involved in automating the Account Initialization, Configuration, and Continuous Compliance.
Thanks for tuning in!
-Ryan Kennedy, Principal Cloud Automation Architect