AWS best practices
Automating Cross-Account Role creation to access users’ account
Operating within users’ accounts is a serious matter. We want to make sure that we get all the credentials we need to deliver great user experience, but limit our access only to what the user feels comfortable with, and make sure there’s nothing we can do that we do not plan on doing. All this should happen automatically and seamlessly for the user. How do we get that done?
To be able to do that we need to answer 2 questions:
- How do we get the relevant credentials and keys to our user’s account?
- How do we operate within our user’s account once we have the above credentials?
Getting relevant access to our user’s account (Technical Background)
The most basic way is to just ask your users to:
- Create a role for you with the required credentials
- Create a user in their system for you with the above roles, and limit the user to the above role
- Generate an access key for that user
- Manually send you the access credentials for the user they’ve created for you
This is, of course, error-prone in so many ways that it’s basically useless.
What we want is an automated process that will create an appropriate role in our user’s account, allow the user to review them, and limit the access only to us. That can be achieved using a cross-account IAM role.
To do that, you have to:
- Send your account number and a unique customer external ID to the customer so they’ll be able to configure your access rights
- Ask your customer to create a role with your required permission and attach the necessary policies to that role
- Have your customer send you back the ARN for the role they created for you
This is much better than the previous solution, but it’s still very error-prone and manual. So what we need is a way to automate that process.
So now we get to the gist of our solution: How to automate the process of creating a dedicated role for our service at the user’s account and get the credentials, with as little human intervention as possible.
Automating Cross account roles creation
In overview, this is how the flow is going to look like:
- A user comes into our website and enters her
- A Lambda is triggered which generates a specific CloudFormation template for this user
- We create a link that will take the user directly to the AWS CloudFormation console, with the template already loaded, in her own account
- The user clicks the “Create Stack” button to create the policy
- As part of the execution of the stack, we get an automated SNS notification from AWS with the ARN of the role that was created.
- We get the SNS message by using a Lambda to listen on the SNS topic we created for these notifications
- We’re done!
What do we need
The things we’re going to need are:
- A CloudFormation template that generates the role we need with our policies
- A publicly readable s3 bucket where the said template will reside
- An SNS topic to which the stack execution results will be sent
- A Lambda to customize the template per user
- A Lambda to intercept the SNS message and update our database
AWS SNS currently doesn’t support cross-region messaging. This means that if your topic is created in us-east-1, and the user runs her stack in eu-west-1, you won’t get the message. Since IAM roles are global, it doesn’t matter where your user is running the stack.
For the sake of this example, we assume that your SNS and Lambda function, as well as the user’s stack run, occur in the same region. We’ll discuss possible solutions to this issue in future posts
1. The CloudFormation Template
Let’s use a basic CloudFormation template with minimal EC2 capabilities.
There are 3 main parts to this template:
- Parameters is where we define the parameters we’ll update when customizing the template for each user
- The CrossAccountRole object is responsible for describing who can access it (
AssumeRolePolicyDocument) and what it can do (
- The PhoneHomeCustomResource object basically tells AWS what to send back to your (in this case, the
AccountIdof the user who's running the template, the
ARNof the role we created and the
ExternalID), and where to send it to (
ServiceToken). Any property we would add to the Properties object would be sent with the message.
2. The Bucket
That’s pretty straight forward: Take the
template.json file and put it in a bucket, with public read access. Make a note of the objectUrl of the file, we'll use it later
3. An SNS Topic
Go to AWS -> SNS and create a topic called
customer-resources-topic. Note the topic's ARN, we'll use it later.
4. The CustomizeUserTemplate Lambda
Our Lambda’s job seems pretty basic: For each user, generate a unique
externalId token, and return to the user a url that will redirect her directly to the CloudFormation console, with the correct parameters.
AWS provides a direct way to do it, and the url looks like this:
Let’s quickly get over them all:
- region — where the stack is to be deployed. Make sure this is the same region as your SNS, or your message won’t arrive
- stack_name — The name the user stack will have. Preferably something like “<OUR_COOL_COMPANY>_role” — It should be indicative enough so the user will understand what this stack is doing in her environment.
- template_location — the objectUrl of the template we placed in the bucket
- external_id — The unique user id you created for the user
- trusted_account — The account id of the account from which you’ll execute the role
- sns_arn — The ARN of your sns topic
And this is our Lambda:
Basically, that (should have been) all. But the experienced AWS devs among you have probably already recognized a problem: We created an SNS topic and we want messages sent from a customer account sent to it. This means that we need to authorize the customer account to write to our SNS topic!
So here is our new Lambda:
But, not so fast. If you want your Lambda to be able to really allow access to SNS topic to other users, the Lambda’s role should have that permission! So go to your Lambda -> permissions -> click on the role, and ‘add inline policy’:
"Resource": "<YOUR SNS ARN>"
Now we’re done :)
5. The RegisterUser Lambda
This Lambda function’s job is to register in our database the ARN of the role that was created for us at the user account and the ExternalId. This Lambda needs to be triggered by our SNS topic, and this is how an update message looks like:
"Subject": "AWS CloudFormation custom resource request",
What do we need for it:
- Connect the SNS topic to the Lambda, so it will be triggered by it
- Parse the message and update our database (or where ever you keep it) with the
- Respond correctly to the CustomResource call. (more about this after the Lambda)
Here is our Lambda:
What does it mean “Respond correctly to the CustomResource call”?
CloudFormation resource is an interesting topic by itself, but what’s important to understand in our case is this: CustomResources are actually AWS’s way of enabling users to create external resources as part of a stack. When a stack is created (in this case, when a user runs the stack to create our role), we’ll get a ‘Create’ message. If the user will delete her stack, we’ll get a ‘Delete’ message — and the same goes for ‘Update’.
The crucial thing to understand is that this is part of the stack creation process. This means that AWS is waiting for us for the successful completion of the stack creation/update/delete process. If we won’t respond promptly to the request, the stack creation process will fail and our hard-earned role will be deleted.
This picture from the AWS blog sums up everything nicely
We created a Lambda function that receives a user’s account ID (1), generates a personalized template for her and a unique
externalId, and redirects her directly to the CloudFormation console, with the template loaded from the bucket (4). When the user executes the stack (5), the stack creates the role we defined in our template, and send us back the roleArn and the additional data we required (6). We then save the RoleArn, AccountID and the externalId (7) we generated so we can later utilize it using AWS STS, which will allow us to operate seamlessly within the user account.