Simplifying Cross-Account Communication in AWS: A Practical Guide

In the fast-paced world of cloud computing, Amazon Web Services (AWS) is a popular choice for businesses looking to build secure, scalable, and strong applications.

As businesses expand in the AWS world, the need for effective cross-account communication becomes increasingly important.

This allows for central management, resource sharing, and smooth collaboration across multiple AWS accounts.

Understanding cross-account communication in AWS can be tricky, especially for those new to the platform.

But worry not! This guide is here to break it down for you. No matter your experience level, think of this guide as your helpful friend, assisting you in tackling the challenges of cross-account communication and getting the most out of your AWS setup. We’ll start by covering the fundamental theoretical aspects before delving into solving a real-world scenario.

Why Cross-Account Communication?

Cross-account communication is crucial in various situations within the AWS ecosystem. Let’s see why it matters:

  1. Workload Separation and Isolation: Organizations often spread their applications across multiple AWS accounts to better organize and protect their work. Keeping workloads in different accounts helps manage who has access, permissions, and where resources are used.
  2. Handling Scalability and Complexity: As organizations expand, managing all services within a single AWS account can quickly become chaotic. Cross-account communication acts as a solution, providing scalability and simplifying the management of resources. Think of it as building additional floors in a building to accommodate growth without overcrowding a single floor.
  3. Centralized Services: Certain services (e.g observability platforms, logging, monitoring) are centralized and serve multiple accounts.
  4. Security Boundaries: Cross-account communication is a powerful tool for controlling access while maintaining tight security measures. For instance, a Lambda function in one account may need to interact with an S3 bucket in another account. This ensures that actions are performed securely, with controlled access, similar to having secure checkpoints between different sections of a facility.
  5. Sharing Data Across Accounts: In collaborative environments, different teams or departments often require secure data sharing across accounts. Consider a scenario where a data engineering team in one account needs access to data stored in another account. Cross-account communication enables secure data exchange, fostering collaboration and efficient workflows. 

In essence, cross-account communication acts as a versatile tool, addressing organizational needs ranging from effective workload management and security enforcement to seamless collaboration and data sharing across the AWS ecosystem.

Enabling Cross-Account Access

AWS provides various ways to enable communication between different accounts. The key methods involve using resource-based policies and IAM roles.

Resource-based policies

Some AWS services, like S3, allow you to directly attach resource-based policies to the resource you want to share. These policies specify who (which principal) can access the resource. Unlike identity-based policies for roles, resource-based policies focus on the resource itself.

Cross-Account IAM Roles

Not all services support resource-based policies. In such cases, you can opt for cross-account IAM roles for centralized permission management. These roles include a trust policy that permits IAM principals in another AWS account to assume the role.

For more information on resource-based and identity-based policies, check out this AWS user guide.

Let’s transition from theory to hands-on problem-solving.

The Scenario

Imagine a situation where important assets, like video files, are securely stored in an S3 bucket within account A. Now, let’s say there’s a Lambda function in account B that needs to create pre-signed URLs for safe and authorized downloads of these assets. The difficulty here is in establishing a secure communication link between these two AWS accounts. The task is to ensure a reliable and protected means for the Lambda function in account B to access and generate pre-signed URLs for the assets stored in the S3 bucket of account A.

First things first: Setting Up Lambda with Roles and Permissions

Let’s create the Lambda function with the necessary roles and permissions in our AWS account (account B) using Terraform.

Lambda IAM role

IAM roles are crucial components in how users, services, and resources interact within Amazon Web Services. Unlike IAM users, roles don’t have long-term credentials but rely on temporary credentials generated during role assumption.

Here’s the Terraform code to create the IAM role for our Lambda function:

resource “aws_iam_role” “presigned_url_generator_lambda_role” {
  name = “presigned_url_generator_lambda_role”
  assume_role_policy = jsonencode({
    Version = “2012-10-17”,
    Statement = [{
      Action = “sts:AssumeRole”,
      Effect = “Allow”,
      Principal = {
       Service = “lambda.amazonaws.com”
      }
    }]
  })
}

Lambda function

Now, let’s create the Lambda function using Terraform:

resource “aws_lambda_function” “presigned_url_generator” {
  function_name = “presigned_url_generator”
  role          = aws_iam_role.presigned_url_generator_lambda_role.arn
  handler       = “presigned_url_generator.lambda_handler”
  runtime       = “python3.8”
  depends_on    = [aws_iam_role_policy_attachment.S3_access_assume_role_policy_attachment]
  filename      = “${path.module}/src/presigned-url-generator.zip”

  environment = {
    variables = {
      S3_BUCKET = “name_of_the_s3_bucket_in_account_A”
    }
  }
}

Lambda Role Custom Policy

In our account, we need to attach the following custom policy to the Lambda role. This policy explicitly grants permission for the Lambda role to assume the S3 Access role in account A.

{
    “Statement”: [
        {
            “Effect”: “Allow”,
            “Action”: “sts:AssumeRole”,
            “Resource”: “arn:aws:iam::{ACCOUNT_A_ID}:role/{S3ACCESS_ROLE}”,
            “Sid”: “S3AccessAssumeRolePolicy”
        }
    ],
    “Version”: “2012-10-17”
}

Let’s create the policy and attach it to our presigned URL generator Lambda function.

data “aws_iam_policy_document” “S3_access_assume_role_policy_doc” {
  statement {
    actions   = [“sts:AssumeRole”]
    effect    = “Allow”
    resources = [“arn:aws:iam::{ACCOUNT_A_ID}:role/{S3ACCESS_ROLE}”]
    sid       = “S3AccessAssumeRolePolicy”
  }
}

resource “aws_iam_policy” “S3_access_assume_role_policy” {
  name        = “S3AccessAssumeRolePolicy”
  description = “Custom policy for cross-account communication”
 policy      = data.aws_iam_policy_document.S3_access_assume_role_policy_doc.json
}

resource “aws_iam_policy_attachment” “S3_access_assume_role_policy_attachment” {
  name       = “S3_access_assume_role_policy_attachment”
  policy_arn = aws_iam_policy.S3_access_assume_role_policy.arn
  roles      = [aws_iam_role.presigned_url_generator_lambda_role.name]
}

Replace ACCOUNT_A_ID with the actual AWS account ID where the S3 bucket is, and S3ACCESS_ROLE with the name of the actual S3 access role.

Setting Up Trust Relationships

The trust policy defines which principals (entities) can assume a particular role and under what conditions. It is a specific type of resource-based policy for IAM roles focusing on the relationship between the role and the entities that trust it.

In account A, let’s create an S3 access role that has a trust policy allowing the Lambda role in account B to assume it, maintaining security boundaries.

Cross-Account IAM role for S3 Access

Imagine we have a bucket named classic-movies. Let’s create an IAM role for accessing the bucket, and attach a policy that allows permissions required for pre-signed URL generation of the movie objects. Also, we’ll ensure a trust policy is in place, permitting the Lambda role in account B to assume this newly created IAM role.

resource “aws_iam_role” “s3_access_role” {
  name = “s3_access_role”

  assume_role_policy = jsonencode({
    Version = “2012-10-17”
    Statement = [{
      Action = “sts:AssumeRole”
      Effect = “Allow”
      Principal = {
        AWS = “arn:aws:iam::{ACCOUNT_B_ID}:role/{LAMBDA_ROLE_NAME}”
      }
    }]
  })

  tags = {
    Name = “S3 Access Role”
  }
}

resource “aws_iam_policy” “s3_access_policy” {
  name        = “s3_access_policy”
  description = “Policy for S3 access”

 policy = jsonencode({
    Version = “2012-10-17”
    Statement = [{
      Action   = [“s3:GetObject”, “s3:ListObjects”]
      Effect   = “Allow”
     Resource = “arn:aws:s3:::classic-movies/*”
    }]
  })
}

resource “aws_iam_policy_attachment” “s3_access_attachment” {
  name       = “s3_access_attachment”
  policy_arn = aws_iam_policy.s3_access_policy.arn
  roles      = [aws_iam_role.s3_access_role.name]
}

Replace ACCOUNT_B_ID with the actual AWS account ID where the Lambda function resides, and LAMBDA_ROLE_NAME with the name of the role associated with the Lambda function.

Role Assumption in Code

Once trust relationships are established, the Lambda function in our production account – account B can programmatically assume the S3 access role in account A using the AssumeRole API of STS. It’s important to note that, when we assume a role, we give up the original permissions and adopt the permissions assigned to the newly assumed role. 

Here’s how you can achieve this in Python:

sts_client = boto3.client(“sts”)
try:
    response = sts_client.assume_role(
        RoleArn=“arn:aws:iam::{ACCOUNT_A_ID}:role/{S3ACCESS_ROLE}”,
        RoleSessionName=“some-session-name”)
except ClientError as e:
    logger.error(“Failed to assume role: %s”, e)
    credentials = response[“Credentials”]

Generating Presigned URLs

With the assumed role’s credentials, the Lambda function in account B can now generate presigned URLs for securely downloading movies from account A’s S3 bucket. Presigned URLs are short-lived and provide temporary access to the specified object.

Here’s how to generate a presigned URL using the assumed credentials:

session = boto3.Session(
    aws_access_key_id=credentials[“AccessKeyId”],
    aws_secret_access_key=credentials[“SecretAccessKey”],
    aws_session_token=credentials[“SessionToken”]
)

s3_client = session.client(“s3”)
try:
    presigned_url = s3_client.generate_presigned_url(
        “get_object”,
        Params={“Bucket”: bucket, “Key”: object_key},
        ExpiresIn=expiry_time_in_seconds
    )
except ClientError as e:
    logger.error(“Failed to generate presigned URL: %s”, e)

logger.info(“The generated presigned URL: %s”, presigned_url)

Caveats: Role Duration Awareness

When working with pre-signed URLs, it’s crucial to be aware of the relationship between the pre-signed URL’s validity and the duration of the Lambda assumed role. Here are some important points to consider:

  • Presigned URL Expiration (ExpiresIn):
      • The ExpiresIn parameter in the pre-signed URL determines how long the URL remains valid.
      • We need to set the expiration time within the Lambda assume role’s duration. If the ExpiresIn exceeds the assumed role duration, the validity is capped at the minimum of the two durations.
      • For security reasons, we must avoid setting excessively long expiration times and choose a reasonable duration based on our use case.
  • Cross-account Role Duration:
    • The duration of the cross-account role assumed by the lambda function affects the overall security of the communication channel.

Lambda Implementation

Here’s the step-by-step guide to implementing the Lambda function using Python:

  1. Navigate to the src/presigned-url-generator directory.
  2. Create a new Python file named presigned_url_generator.py.
  3. Open the newly created file and insert the following Python function:
import logging
import os
from typing import Dict, Any

import boto3
import botocore

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def assume_role(role_arn: str, role_session_name: str) -> Dict[str, Any]:
    “””
    Assume the specified IAM role based on the given role ARN.

    :param role_arn: The ARN of the role to assume.
    :type role_arn: str
    :param role_session_name: A unique name for the assumed role session.
    :type role_session_name: str
    :return: Temporary credentials for the assumed role.
    :rtype: dict
    “””
    sts_client = boto3.client(“sts”)
    try:
        response = sts_client.assume_role(
            RoleArn=role_arn,
            RoleSessionName=role_session_name)
        return response[“Credentials”]
    except botocore.exceptions.ClientError as e:
        logger.error(“Failed to assume role: %s”, e)
        raise

def get_s3_client(credentials: Dict[str, str]):
    “””
    Create an S3 client using the provided access credentials.

    :param credentials: Temporary AWS credentials.
    :type credentials: dict
    :return: S3 client.
    :rtype: boto3.client
    “””
    session = boto3.Session(
        aws_access_key_id=credentials[“AccessKeyId”],
        aws_secret_access_key=credentials[“SecretAccessKey”],
        aws_session_token=credentials[“SessionToken”]
    )
    return session.client(“s3”)

def generate_presigned_url(s3_client: boto3.client, bucket_name: str, object_key: str, expiry_time: int = 3600) -> str:
    “””
    Generate a presigned URL for downloading an S3 object.

    :param s3_client: S3 client.
    :type s3_client: boto3.client
    :param bucket_name: The name of the S3 bucket.
    :type bucket_name: str
    :param object_key: The key of the S3 object.
    :type object_key: str
    :param expiry_time: The expiration time of the presigned URL in seconds, defaults to 3600 seconds.
    :type expiry_time: int
    :return: The generated presigned URL.
    :rtype: str
    “””
    try:
        presigned_url = s3_client.generate_presigned_url(
            “get_object”,
            Params={“Bucket”: bucket_name, “Key”: object_key},
            ExpiresIn=expiry_time
        )
        logger.info(“Generated presigned URL: %s”, presigned_url)
        return presigned_url
    except botocore.exceptions.ClientError as e:
        logger.error(“Failed to generate presigned URL: %s”, e)
        raise

def lambda_handler(event, context):
    “””
    Lambda function to generate a presigned URL.

    :param event: Lambda event.
    :type event: dict
    :param context: Lambda context.
    :type context: LambdaContext
    :return: The generated Presigned URL.
    :rtype: str
    “””    role_arn = “arn:aws:iam::{ACCOUNT_A_ID}:role/{S3ACCESS_ROLE}”
    role_session_name = “UniqueSessionName”
    bucket_name = os.environ.get(“S3_BUCKET”)
    object_key = event.get(“object_key”)


    try:
        credentials = assume_role(role_arn, role_session_name)
        s3_client = get_s3_client(credentials)
        return generate_presigned_url(s3_client, bucket_name, object_key)
    except botocore.exceptions.ClientError as e:
        raise

Let’s zip it

When deploying a Lambda function, AWS Lambda expects the function code to be packaged in a ZIP archive. Here’s how you can create a zip archive for the Lambda function using Terraform:

data “archive_file” “presigned_url_generator_archive” {
 type        = “zip”
  source_dir  = “${path.module}/src/presigned-url-generator”
  output_path = “${path.module}/src/presigned-url-generator.zip”
  excludes    = [“venv”, “*.pyc”]
}

Once deployed, our lambda function can securely generate pre-signed URLs for us to download our favorite classic movies stored in the S3 bucket of account A. 

Conclusion

By carefully following the outlined steps and taking into account the duration of roles, we have effectively set up a secure communication link between two AWS accounts. This ensures that the Lambda function in account B can securely interact with the S3 access role in account A, enhancing the overall security and efficiency of cross-account communication. It’s similar to establishing a guarded pathway, allowing seamless and protected collaboration between different components of our AWS infrastructure.

Picture of Saimon hossain

Saimon hossain

Software Development Engineer

Hire Exceptional Developers Quickly

Share this blog on

Hire Your Software Development Team

Let us help you pull out your hassle recruiting potential software engineers and get the ultimate result exceeding your needs.

Contact Us Directly

Address:

Plot # 272, Lane # 3 (Eastern Road) DOHS Baridhara, Dhaka 1206

Talk to Us
Scroll to Top