Docs/ Multi-Account AWS Strategy

Multi-Account AWS Strategy

Federate Vouch into dev, staging, and production AWS accounts

Most startups outgrow a single AWS account quickly. By the time you have a dev and production environment, you have two accounts. Add staging, a shared services account for CI/CD, and a logging account, and you have five. Each account needs an OIDC provider and IAM roles that trust Vouch.

This guide covers deploying Vouch OIDC federation across an AWS Organization using CloudFormation StackSets, Terraform modules, and SCPs to enforce that only Vouch can federate into your accounts.


Architecture

AWS Organization (Management Account)
├── Shared Services (CI/CD, logging)
├── Development
├── Staging
└── Production

Each account gets:
  ├── OIDC Provider (trusting us.vouch.sh)
  └── IAM Roles (VouchDeveloper, VouchReadOnly, etc.)

Every developer uses the same vouch login session. The AWS profile they select determines which account and role they assume.


Option 1 – CloudFormation StackSets

StackSets deploy a CloudFormation template across all accounts in an AWS Organization (or a subset).

Template

AWSTemplateFormatVersion: "2010-09-09"
Description: "Vouch OIDC federation (deployed via StackSet)"

Parameters:
  VouchServerUrl:
    Type: String
    Default: "us.vouch.sh"

  RoleName:
    Type: String
    Default: "VouchDeveloper"

  ManagedPolicyArn:
    Type: String
    Default: "arn:aws:iam::aws:policy/ReadOnlyAccess"
    Description: "Permissions policy to attach to the role"

Resources:
  VouchOIDCProvider:
    Type: AWS::IAM::OIDCProvider
    Properties:
      Url: !Sub "https://${VouchServerUrl}"
      ClientIdList:
        - !Sub "https://${VouchServerUrl}"

  VouchRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Ref RoleName
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Federated: !Sub "arn:aws:iam::${AWS::AccountId}:oidc-provider/${VouchServerUrl}"
            Action: "sts:AssumeRoleWithWebIdentity"
            Condition:
              StringEquals:
                !Sub "${VouchServerUrl}:aud": !Sub "https://${VouchServerUrl}"
      ManagedPolicyArns:
        - !Ref ManagedPolicyArn

Outputs:
  RoleArn:
    Value: !GetAtt VouchRole.Arn

Deploy to all accounts

From the management account:

aws cloudformation create-stack-set \
  --stack-set-name vouch-federation \
  --template-body file://vouch-stackset.yaml \
  --capabilities CAPABILITY_NAMED_IAM \
  --permission-model SERVICE_MANAGED \
  --auto-deployment Enabled=true,RetainStacksOnAccountRemoval=false

aws cloudformation create-stack-instances \
  --stack-set-name vouch-federation \
  --deployment-targets OrganizationalUnitIds=ou-xxxx-xxxxxxxx \
  --regions us-east-1

With --auto-deployment Enabled, new accounts added to the OU automatically receive the Vouch OIDC provider and IAM role.

Per-account role customization

Use different parameter overrides per account to control permissions:

# Development: PowerUserAccess
aws cloudformation create-stack-instances \
  --stack-set-name vouch-federation \
  --accounts 111111111111 \
  --regions us-east-1 \
  --parameter-overrides ParameterKey=ManagedPolicyArn,ParameterValue=arn:aws:iam::aws:policy/PowerUserAccess ParameterKey=RoleName,ParameterValue=VouchDeveloper

# Production: ReadOnlyAccess
aws cloudformation create-stack-instances \
  --stack-set-name vouch-federation \
  --accounts 222222222222 \
  --regions us-east-1 \
  --parameter-overrides ParameterKey=ManagedPolicyArn,ParameterValue=arn:aws:iam::aws:policy/ReadOnlyAccess ParameterKey=RoleName,ParameterValue=VouchReadOnly

Option 2 – Terraform

Module

Create a reusable Terraform module:

# modules/vouch-federation/main.tf

variable "vouch_server_url" {
  type    = string
  default = "us.vouch.sh"
}

variable "role_name" {
  type    = string
  default = "VouchDeveloper"
}

variable "policy_arns" {
  type    = list(string)
  default = ["arn:aws:iam::aws:policy/ReadOnlyAccess"]
}

data "aws_caller_identity" "current" {}

resource "aws_iam_openid_connect_provider" "vouch" {
  url            = "https://${var.vouch_server_url}"
  client_id_list = ["https://${var.vouch_server_url}"]
}

resource "aws_iam_role" "vouch" {
  name = var.role_name

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Federated = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/${var.vouch_server_url}"
        }
        Action = "sts:AssumeRoleWithWebIdentity"
        Condition = {
          StringEquals = {
            "${var.vouch_server_url}:aud" = "https://${var.vouch_server_url}"
          }
        }
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "vouch" {
  count      = length(var.policy_arns)
  role       = aws_iam_role.vouch.name
  policy_arn = var.policy_arns[count.index]
}

output "role_arn" {
  value = aws_iam_role.vouch.arn
}

Usage per account

# environments/dev/main.tf
module "vouch" {
  source      = "../../modules/vouch-federation"
  role_name   = "VouchDeveloper"
  policy_arns = ["arn:aws:iam::aws:policy/PowerUserAccess"]
}

# environments/prod/main.tf
module "vouch" {
  source      = "../../modules/vouch-federation"
  role_name   = "VouchReadOnly"
  policy_arns = ["arn:aws:iam::aws:policy/ReadOnlyAccess"]
}

Restricting federation with SCPs

Use Service Control Policies to ensure that only the Vouch OIDC provider can federate into your accounts. This prevents anyone from registering a rogue OIDC provider:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyNonVouchOIDCProviders",
      "Effect": "Deny",
      "Action": [
        "iam:CreateOpenIDConnectProvider",
        "iam:DeleteOpenIDConnectProvider"
      ],
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "aws:PrincipalOrgID": "${aws:PrincipalOrgID}"
        }
      }
    }
  ]
}

Note: Adjust this SCP to allow your management account or deployment pipeline to manage OIDC providers. The goal is to prevent individual developers from creating unauthorized OIDC providers in member accounts.


Developer setup

Each developer configures a named AWS profile for each account:

# Development account
vouch setup aws \
  --role arn:aws:iam::111111111111:role/VouchDeveloper \
  --profile vouch-dev

# Staging account
vouch setup aws \
  --role arn:aws:iam::333333333333:role/VouchDeveloper \
  --profile vouch-staging

# Production account (read-only)
vouch setup aws \
  --role arn:aws:iam::222222222222:role/VouchReadOnly \
  --profile vouch-prod

This produces the following ~/.aws/config:

[profile vouch-dev]
credential_process = vouch credential aws --role arn:aws:iam::111111111111:role/VouchDeveloper

[profile vouch-staging]
credential_process = vouch credential aws --role arn:aws:iam::333333333333:role/VouchDeveloper

[profile vouch-prod]
credential_process = vouch credential aws --role arn:aws:iam::222222222222:role/VouchReadOnly

Use profiles per command:

# Deploy to dev
cdk deploy --profile vouch-dev

# Check production (read-only)
aws s3 ls --profile vouch-prod

Or set a default:

export AWS_PROFILE=vouch-dev

Role design patterns

By environment

AccountRolePolicyWho
DevelopmentVouchDeveloperPowerUserAccessAll developers
StagingVouchDeveloperPowerUserAccessAll developers
ProductionVouchReadOnlyReadOnlyAccessAll developers
ProductionVouchDeployerCustom deploy policySenior engineers only

Restricting production access by email

Add an email condition to the production deployer role’s trust policy:

"Condition": {
  "StringEquals": {
    "us.vouch.sh:aud": "https://us.vouch.sh",
    "us.vouch.sh:sub": [
      "alice@example.com",
      "bob@example.com"
    ]
  }
}

Only Alice and Bob can assume the VouchDeployer role. Everyone else can still assume VouchReadOnly.


Troubleshooting

“OIDC provider already exists”

The OIDC provider URL must be unique per account. If you already created one manually, the StackSet deployment will fail for that account. Either import the existing resource or delete it before deploying.

Credentials for the wrong account

If aws sts get-caller-identity shows an unexpected account, verify you are using the correct profile:

aws sts get-caller-identity --profile vouch-dev

Check ~/.aws/config for the correct role ARN in each profile.