Practical Application of AWS Account Azure Active Directory SAML Federation

Purpose

The purpose of this article is to share lessons learned from deploying many AWS Accounts with Azure Active Directory (AAD) idp-initiated SAML 2.0. This article provides a perspective on how to approach the problem, some of the solution’s quirks, and how an end user would use it in a few short examples.

Problem Background

Local users are bad practice, but especially in an enterprise with 85,000 employees. When we created our 5th AWS Account, a team member left the organization, and it became clear that using local AWS IAM identities was unmanageable, and we needed to tie back into the central identity provider. Now that we’ve surpassed 100 active AWS Accounts, we are glad we implemented integration between AWS and AAD early on.

Visual Representation of the Solution

There are plenty of visual representations of SAML, but I wanted to provide a sequence diagram with annotations describing the aws-azure-login tool [explained below].

samluml

Ideal Solution

We face multiple issues with the solution described in this article, though the benefits generally outweigh the issues. The main three problems are the inability to automate the Enterprise Application configuration in Azure, we will need to rotate our certs for 100+ individual Enterprise Applications, and we need to support the aws-azure-login package across Windows, Mac, Ubuntu, Fedora, and whatever the next person has on VirtualBox. This seems like a good solution if getting started on AWS, but it wouldn’t solve all three problems.

Ideally, there would be a fully supported out of the box AAD multi-account AWS solution which supports the command line interface, such as Azure AD integration for AWS Single Sign-On (AWS SSO). With AWS SSO, we could use the AWS supported command line interface for cross-account AWS CLI access, and we would only have one Enterprise Application on the Azure side.

Configure AWS Enterprise Application

Let’s not re-create the wheel 🙂

https://docs.microsoft.com/en-us/azure/active-directory/saas-apps/amazon-web-service-tutorial

Creating Roles and Authorizing Users

Create roles with CloudFormation. If you have many accounts, use StackSets. We disable active provisioning on the Azure side because of an occurrence when IAM was unavailable, Azure received a bad response, and we had to reassign users to their roles across all the accounts. Also, this causes an increase of findings in GuardDuty due to the service’s sensitivity. Once roles have been synced, add a user to a role within the Azure Enterprise Application.

Here’s a CloudFormation for an Administrator role tied to your IDP. Customize this to your liking. This assumed your IDP in AWS is named “AAD”.


AWSTemplateFormatVersion: "2010-09-09"
Description: 'AWS Roles for AAD IDP'
Resources:
  AdministratorRole:
    Type: "AWS::IAM::Role"
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          -
            Effect: "Allow"
            Principal:
              Federated:
                - !Join ['', ["arn:aws:iam::", !Ref 'AWS::AccountId',":saml-provider/AAD"]]
            Action:
              - "sts:AssumeRoleWithSAML"
            Condition:
              StringEquals:
                SAML:aud: 'https://signin.aws.amazon.com/saml'
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AdministratorAccess
      Path: /AAD/
      MaxSessionDuration: "14400"
      RoleName: Administrator

AWS Management Console User Login

A user would log into myapps.microsoft.com and click on a tile to log into AWS. This will launch the user into the AWS Management Console. The tile will look like this:

pzbaa

Command Line Interface User Setup and Login

This requires installation of the aws-azure-login package. Installation instructions are here. The config and credentials files will need to be modified to include IDs from the Azure AD Enterprise Application configuration. Since these don’t contain credentials, you can share them with your team, since authorization is granted at the Azure Enterprise Application level. Note that the azure_default_duration_hours cannot exceed what is specified on the AWS role, or it will fail.

$ cat ~/.aws/config


[profile notdefault]
output=json
region=us-east-1
azure_tenant_id=1234abcd-ab12-ab12-ab12-123456abcdef
azure_app_id_uri=abcd1234-123a-321b-3da3-abcdef123456
azure_default_username=me@mycompany.com
azure_default_role_arn=Administrator
azure_default_duration_hours=4

$ cat ~/.aws/credentials


[notdefault]
aws_access_key_id=
aws_secret_access_key=
aws_session_token=

To get a temporary token from AWS STS, simply run the following command. If you set the azure_default_password variable in ~/.aws/config you can use the --no-prompt flag.

$ aws-azure-login --profile notdefault

At this point, you have a 4 hour token to run any commands you are authorized to run.

$ `​aws ecr get-login --profile notdefault`

Add OpsWorks Users from Role with Friendly Username

Our organization uses the AWS architecture for NIST-based assurance frameworks. We couple this with AWS OpsWorks for our bastion host authentication. With OpsWorks, we are able to gate remote private VPC access to users from directly within the AWS Management Console. Once users are provided access, they can create an ssh tunnel to remote to private resources.

OpsWorks allows you to import IAM users; however, federated users who access OpsWorks have auto-generated usernames based on their role and federated username. If the same user is granted permissions to multiple bastion hosts, they will have multiple auto-generated usernames.

NOTE: OpsWorks has its own user store. If an IAM user is imported as an OpsWorks user, then deleted from IAM, the user will still exist in OpsWorks. This solution does not solve the problem of having decoupled users between OpsWorks and Azure AD. When a user is terminated from AAD, the user is still able to authenticate to OpsWorks instances to which they have permissions. The user must be removed from OpsWorks separately.

To import federated users in a consistent manner, we created a simple script to create users with a friendly username, which is simply the prefix of their federated username. I know, I know… it’s read happy.


#!/bin/bash

echo "Which AWS profile?"
read PROFILE

echo "What is the user's role (Administrator, Auditor, Developer)?"
read ASSUMED_ROLE_NAME

echo "What is the user's federated username (example@mycompany.com)?"
read FEDERATED_USER_NAME

echo "What is the user's public ssh key?"
read SSH_PUBLIC_KEY

# if there's an at symbol remove anything after it to create a friendly ssh username, or else aggressively yell at the person running the script
if echo $FEDERATED_USER_NAME | grep '@'; then
  SSH_USERNAME=$(echo $FEDERATED_USER_NAME | cut -d'@' -f 1)
else
  echo -e "WARN: The federated username should be a full email address."
  exit 1
fi

ACCOUNT_ID=$(aws sts get-caller-identity --query Account --profile $PROFILE | cut -d \" -f2)

aws opsworks create-user-profile --iam-user-arn arn:aws:sts::$ACCOUNT_ID:assumed-role/$ASSUMED_ROLE_NAME/$FEDERATED_USER_NAME --profile $PROFILE --ssh-username $SSH_USERNAME --ssh-public-key "$SSH_PUBLIC_KEY" --no-allow-self-management

Using Profiles with Python SDK

When using the Python SDK as a federated user, you are required to generate a new session token, and so this requires you to do an aws-azure-login for the profile you set as your default prior to running the script. This script specifies an AWS profile of notdefault which would represent whichever account you are running the script in. The output of this specific script is a CSV of accounts in “My OU” (assuming your company uses AWS Organizations), and expects the “output_files” directory to already exist. The region_name is redundant with the ~/.aws/config region variable.


#!/usr/bin/env python
import boto3

# AWS Configurations
aws_profile = 'notdefault'
boto3.setup_default_session(profile_name=aws_profile)
client = boto3.client('organizations', region_name='us-east-1')

# Create output file, assumes directory exists
output_file = open("./output_files/myou_accounts.csv", "w")

# Write accounts one by one in pages of 20
paginator = client.get_paginator('list_accounts_for_parent')
pages = paginator.paginate(ParentId='ou-1234-abc12345a')
for page in pages:
    for account in page['Accounts']:
        account_id = account['Id']
        account_name = account['Name']
        account_email = account['Email']
        account_status = account['Status']
        to_file = (account_id + "," + account_name + "," + account_email + "," + account_status)
        output_file.write(to_file + '\n')

# Close CSV
output_file.close()

Loop Through All Profiles

Your boss is breathing down your neck and wants you to check for something specific in all of your AWS Accounts. You need a quick way to run a script to generate the findings into an output file. You have the same access across all of your AWS Accounts.

Plug the azure_default_password for each profile in ~/.aws/config, find the command you want to run, and plug it into the business logic section of the following bash script.

NOTE: This is possible with the SDKs as well, I just used the following script for the sake of providing an example.


#!/bin/bash

echo -e "Start time is $(date)" >> runlog.txt

# get all profiles
cat ~/.aws/config |grep profile | awk -F '[][]' '{print $2}'|awk '{print $2}' > profiles.txt

# log into all awscli profiles and do sumthin
while IFS= read -r x <&3; do   aws-azure-login --no-prompt --profile $x >> runlog.txt
  ############## business logic #######################
  # aws insert command(s) here --profile $x
  #####################################################
done 3< profiles.txt

rm profiles.txt

echo -e "End time is $(date)" >> runlog.txt

exit 0

Summary

If you find this helpful, awesome! If you have proposed ways to build on this, post your ideas! If you know of an alternative solution, feel free to share it!

Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s