I set up AWS accounts in a hub and spoke model for access using AWS Organizations for login. This meaning that I have an AWS organization that is under my master account. I have my SSO federated login in a single login account whose only purpose is to do the federated login. After logging in, the user can assume roles in the other accounts from that login account.
The intent is to separate concerns and reduce risk by doing so. In an ideal world, there would be separation between billing, organization, access to the other accounts. AWS doesn’t fully support this setup. You need to control the organization and billing from the master billing account, and protect your AWS login.
Infrastructure / Architecture
With that in mind, I set things up with a service and lane approach
foo-master – Master billing, asset control and organizational control
foo-login – Central login account for federated roles.
foo-management – for centralized automation of other accounts
foo-myservice-cd – CI/CD account for service myservice
foo-myservice-dev – Development account
foo-myservice-qa – QA account
foo-myservice-prod – Production account
So access is granted to all the accounts via the roles in the foo-login account via federation (using an IDP like Google or Okta). Login in via the external system (or AWS SSO) to federated roles in this account.
The roles in the foo-login account are set up to allow assuming roles into the other accounts, and those roles in turn are set up to trust the role in that account, and grant permissions in that account. So we deploy a CloudFormation stack in the foo-management account that builds the roles for a service in each of the accounts based on the access level needed foo-readonly , foo-poweruser and foo-admin as example would be deployed to the DEV, QA and Prod accounts.
The user authenticates which gives them credentials in the foo-login that allow them to assume into these accounts.
Note: This can lead to confusion (particularly in the console. The initial login only has rights to assume other roles and has no other permissions. In the console, you have to “Switch Roles” to get into the account where you are going to do your work.
I use scripts that will create the accounts for each lane. The script provides parameters to build the roles and trust relationships I need for this hub-and-spoke method. The first script (which is typically python or Go) is used to create the service accounts (in the example above that is the foo-myservice-* accounts. That script also does initial setup stuff like removing default VPCs, turning on/off services, and general housekeeping tasks.
The script uses the organizations API to create the accounts. In each service account we use CloudFormation to build a stack in the login account. This stack contains the roles that are able to assume, granting permissions in each service account.
So for our example above the login roles would be things like myservice-readonly or myservice-poweruser. The create script has these values from creating those accounts. The account creation script grabs the new account IDs and passes them to the stack.
Login roles (CloudFormation)
In the foo-management account the parameters we need are the account IDs which are passed by parameter. To keep them unique, we also have a parameter for the service name:
# Name of the team/service we are adding
Description: Name of the service
# Manage account (for automation)
Description: Enter the CI/CD account ID (cd)
<meta charset="utf-8"> # Dev account ID
Description: Development (dev) Account ID
Default: ''<meta charset="utf-8"> # Dev account ID
Description: QA (qa) Account ID
Default: ''<meta charset="utf-8"> # QA account ID
<meta charset="utf-8"> ServiceProd1AccountID:
Description: Prod 1 (prod-1) Account ID
<meta charset="utf-8"> ServiceProd2AccountID:
Description: Prod 2 (prod-2) Account ID
These conditions are used to set the trust relationships, so the policy for the role has a bunch of Fn::If statements that either add the ARN of the role, or use the special property for CloudFormation of “AWS::NoValue” to not add the role for that condition.
The AssumeRolePolicy document is what connects us to the IDP (in this example it’s Google Apps, the name of the role would end up being myservice-readonly Which in this example would have the ability to assume a role named readonly in each of the service accounts.
Service Roles in Accounts (CloudFormation)
And the last script deploys the roles that the above would assume into each service account. Again this is a CloudFormation stack that allows assumption from the role in the login account with something like:
Basically you give this role the permissions it needs (in this case the generic AWS ReadOnlyAccess) and make sure it can only be assumed by the role that is in the login account (myservice-readonly for this one).
Once you have everything set up, you should have all of your login code as infrastructure CloudFormation scripts and a good setup for AWS login.
In this post recall a recent problem caused by an incomplete understanding of the Go language, and how I fixed it with some help from Github CoPilot. I wa delighted to resolve unexpected consequences in Go.
In this post I describe the happy scavenger hunt for tickets to see a Covid-19 delayed concert with James Taylor and Jackson Browne. Note: This post was actually written in 2021, I just found it unpublished in my site, so added a few Firefly images and pushed Publish ...