Saltar a contenido

Deploy IAM Roles across an AWS Organization as code

En entornos con múltiples cuentas de AWS, la gestión de roles puede ser un desafío. AWS IAM nos ofrece una solución robusta para gestionarlo en cada cuenta, pero cuando se trata de implementar roles de manera consistente en todas las cuentas de una organización, la tarea puede volverse compleja.

En este post vamos a ver cómo desplegar roles de IAM de forma automática en todas las cuentas de una organización de AWS como código, usando CloudFormation, Organizations y Terraform.

architecture architecture

Es bastante común tener herramientas que necesiten tener unos permisos de IAM determinados en todas las cuentas donde las queramos implementar, como por ejemplo una herramienta interna que necesite acceder a todas nuestras cuentas. En el momento en el que tengamos que actualizar estos permisos, mantenerlo es todo un desafío. Así que con ayuda de AWS Organizations y AWS CloudFormation StackSets podemos solucionarlo.

Para poder explicar los pasos, vamos a utilizar un ejemplo concreto. Nuestro objetivo es desplegar el role security-cspm, con dos policies:

  • SecurityAudit: AWS managed policy.
  • security-cspm-sechub-import: Customer managed policy, con los siguientes permisos:
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "SecurityHubImport",
          "Effect": "Allow",
          "Action": [
            "securityhub:BatchImportFindings"
          ],
          "Resource": "*"
        }
      ]
    }
    

Requisitos

Necesitaremos tener una organización de AWS creada y nuestras cuentas como miembros de esta organización.

Una vez tengamos la organización, necesitaremos habilitar la opción de Activar todas las funciones. Si gestionáis vuestra organización como código 👍, podéis crear la organización y habilitar todas las funciones con el recurso aws_organizations_organization:

resource "aws_organizations_organization" "org" {
  ...
  feature_set = "ALL"
}

Enable Trusted Access

A continuación necesitaremos habilitar la integración de AWS CloudFormation StackSets con AWS Organizations.

Este paso, por alguna razón que desconocemos, require una acción manual 😢. Habilitando el trusted access desde Terraform, o desde el menú de organizations, no es suficiente.

Enable Trusted Access Issue

Es necesario ir a la consola de CloudFormation > StackSets y darle a Activate trusted access. Activate Trusted Access

Si habéis usado Terraform para crear vuestra organización, necesitaréis añadir el siguiente service principal en el argumento aws_service_access_principals del recurso aws_organizations_organization, e.g:

resource "aws_organizations_organization" "org" {
  aws_service_access_principals = [
    ...
    "member.org.stacksets.cloudformation.amazonaws.com",
    ...
  ]

  feature_set = "ALL"
}

Warning

Recordad que se debe incluir el nuevo service principal junto con los existentes. Para verificar los que ya están habilitados puedes usar el siguiente comando:

aws organizations list-aws-service-access-for-organization

Delegar CloudFormation StackSets

Para evitar realizar los despliegues desde la cuenta management de nuestra organización, delegaremos la administración de StackSets a otra cuenta, desde la que crearemos gestionaremos los recursos de CloudFormation necesarios.

Tip

Recordad que debemos intentar reducir el uso de la cuenta de management todo lo posible. Well Architected Framework - Security Pillar SEC01-BP02

Para delegar el servicio podéis usar el siguiente recurso de Terraform:

resource "aws_organizations_delegated_administrator" "stacksets" {
  account_id        = var.security_account_id
  service_principal = "member.org.stacksets.cloudformation.amazonaws.com"
}

O también podéis usar el siguiente comando si no gestionáis la cuenta de management as code:

aws organizations register-delegated-administrator \
  --service-principal=member.org.stacksets.cloudformation.amazonaws.com \
  --account-id="securityAccountId"

Esta acción solo es necesaria hacerla una vez, y desde una única región. Podéis comprobar que se ha delegado con el siguiente comando:

aws organizations list-delegated-administrators \
  --service-principal=member.org.stacksets.cloudformation.amazonaws.com
Output
{
  "DelegatedAdministrators": [
    {
      "Id": "222222222222",
      "Arn": "arn:aws:organizations::111111111111:account/o-myorgid/222222222222",
      "Email": "[email protected]",
      "Name": "security-account",
      "Status": "ACTIVE",
      "JoinedMethod": "CREATED",
      "JoinedTimestamp": "2024-10-11T16:30:05.938000+02:00",
      "DelegationEnabledDate": "2024-10-11T16:48:07.033000+02:00"
    }
  ]
}

Desplegar Cross-Account IAM Roles

Una vez tenemos el servicio integrado con AWS Organizations y delegado a nuestra cuenta, lo primero que tendremos que hacer será definir los recursos en un template de CloudFormation, para ello crearemos un fichero role.yaml, que después leeremos desde Terraform. El siguiente fichero declara el role y las policies de IAM que queremos:

role.yaml
AWSTemplateFormatVersion: "2010-09-09"
Parameters:
  RoleName:
    Type: String
    Description: IAM Role Name
  RoleDescription:
    Type: String
    Description: IAM Role Description
    Default: ""
  PolicyName:
    Type: String
    Description: IAM Managed read and write policy name
  PolicyDescription:
    Type: String
    Description: IAM Managed Policy Description
    Default: ""
  IAMPath:
    Type: String
    Description: IAM Role and Policy Path
    Default: "/"
  TrustedAccount:
    Type: String
    Description: AWS trusted account ID
  TrustedRole:
    Type: String
    Description: Allowed IAM Role ARN
Resources:
  Role:
    Type: 'AWS::IAM::Role'
    Properties:
      RoleName: !Ref RoleName
      Description: !Ref RoleDescription
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              AWS:
                !Ref TrustedAccount
            Action:
              - 'sts:AssumeRole'
            Condition:
              ArnLike:
                'aws:PrincipalArn': !Ref TrustedRole
      Path: !Ref IAMPath
      ManagedPolicyArns:
        - !Ref RolePolicy
        - arn:aws:iam::aws:policy/SecurityAudit
  RolePolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: !Ref PolicyName
      Description: !Ref PolicyDescription
      Path: !Ref IAMPath
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: SecurityHubImport
            Effect: Allow
            Action:
              - securityhub:BatchImportFindings
            Resource: '*'

A continuación necesitaremos definir el StackSet de CloudFormation, usando el fichero que acabamos de crear como template:

data "aws_caller_identity" "this" {}

resource "aws_cloudformation_stack_set" "cspm_role" {
  name             = "security-cspm-role"
  description      = "Deploy Security CSPM Role across all organization accounts"
  permission_model = "SERVICE_MANAGED" // (1)!
  call_as          = "DELEGATED_ADMIN" // (2)!

  auto_deployment {
    enabled = true // (3)!
  }

  capabilities = [
    "CAPABILITY_NAMED_IAM", // (4)!
  ]

  template_body = file("${path.root}/role.yaml") // (5)!
  parameters = { // (6)!
    RoleName          = "security-cspm"
    RoleDescription   = "Audit and Import findings to SecurityHub"
    PolicyName        = "security-cspm-sechub-import"
    PolicyDescription = "Allow import SecurityHub findings"
    IAMPath           = "/security/cspm/"
    TrustedAccount    = data.aws_caller_identity.this.id // (7)!
    TrustedRole       = "arn:aws:iam::${data.aws_caller_identity.this.id}:role/my-source-role"
  }
}
  1. Permite desplegar recursos de CloudFormation en todas las cuentas con un role gestionado por AWS Organizations.
  2. Especifica que se actúa desde la cuenta delegada.
  3. Permitirá desplegar el role automáticamente en las cuentas nuevas que añadamos a la organización.
  4. Necesario cuando el template contiene recursos de IAM manejados por nosotros.
  5. El template que hemos creado anteriormente.
  6. Definimos los parámetros definidos en nuestro template role.yaml.
  7. Usamos el data source para pasar la cuenta desde la que ejecutamos este código automáticamente.

Y por último definimos las instancias de StackSet que crearan el recurso en las cuentas:

data "aws_organizations_organization" "this" {}

resource "aws_cloudformation_stack_instances" "cspm_role" {
  stack_set_name = aws_cloudformation_stack_set.cspm_role.name
  call_as        = "DELEGATED_ADMIN"

  deployment_targets {
    organizational_unit_ids = [ data.aws_organizations_organization.this.roots[0].id ] // (1)!
  }
}
  1. Usamos el data source para pasar el organization ID automáticamente.
Tip

Es recomendable usar los argumentos operation_preferences para mejorar la eficiencia al crear stacks en organizaciones con bastantes cuentas. Esto nos permitirá poder paralelizar cuentas. Como ejemplo:

resource "aws_cloudformation_stack_set" "cspm_role" {
  ...
  operation_preferences {
    max_concurrent_percentage    = 50
    failure_tolerance_percentage = 50
  }
  ...
}

resource "aws_cloudformation_stack_instances" "cspm_role" {
  ...
  operation_preferences {
    concurrency_mode          = "SOFT_FAILURE_TOLERANCE"
    max_concurrent_percentage = 50
  }
  ...
}

Una vez tenemos el código, podremos ejecutar un terraform plan & terraform apply y ya lo tendremos! 🥳

A modo de resumen, os dejamos el siguiente diagrama con la arquitectura que hemos creado: architecture architecture

Terraform Module

Si queréis simplificar todo este proceso podéis crear un módulo de Terraform como el que hemos publicado. De esta forma podréis reutilizarlo todo el código simplemente cambiando el template y los parámetros.

El módulo está disponible en el registry de Terraform, por lo que podéis usarlo desde vuestro código de la siguiente forma:

data "aws_caller_identity" "this" {}
data "aws_organizations_organization" "this" {}

module "cspm_role" {
  source = "unicrons/organization-iam-role/aws"

  stack_set_name        = "security-cspm-role"
  stack_set_description = "Deploy Security CSPM Role across all organization accounts"
  template_path         = "${path.root}/role.yaml"

  template_parameters = {
    RoleName          = "security-cspm"
    RoleDescription   = "Audit and Import findings to SecurityHub"
    PolicyName        = "security-cspm-sechub-import"
    PolicyDescription = "Allow import SecurityHub findings"
    IAMPath           = "/security/cspm/"
    TrustedAccount    = data.aws_caller_identity.this.id
    TrustedRole       = "arn:aws:iam::${data.aws_caller_identity.this.id}:role/my-source-role"
  }

  organizational_unit_ids = [ data.aws_organizations_organization.this.roots[0].id ]
}

Para ver todas las opciones de configuración disponibles, echad un ojo a los parámetros de entrada.


Y esto es todo amigos! Si os queda alguna duda o tenéis algún comentario no dudéis en escribirnos.

Saludos, y que la fuerza os acompañe.