This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Documentation

Welcome to the Org Kickstart documentation. This section covers everything you need to deploy and manage a governed AWS Organization using Terraform.

In This Section

1 - Overview

What Org Kickstart is and why you’d use it.

What is Org Kickstart?

Org Kickstart is an opinionated Terraform module for bootstrapping and managing AWS Organizations. It is PrimeHarbor’s alternative to AWS Control Tower — designed to give you the good parts of a landing zone without the cost, complexity, and lock-in that Control Tower brings.

Deployed into a brand new AWS Management (Payer) account, a single terraform apply can stand up a fully governed AWS Organization with security services enabled, organizational guardrails in place, and an account factory ready to provision new accounts.

Org Kickstart Architecture

Why not just use Control Tower?

Control Tower is a massive beast designed to support highly regulated enterprises. For most organizations, it brings significant drawbacks:

  • Expensive: Heavy reliance on AWS Config means costs scale with the number of accounts and regions.
  • Inflexible: Modifications require navigating AWS Service Catalog customizations.
  • Slow to update: Features like Organization CloudTrail and GuardDuty Delegated Admin lagged behind AWS best practices for years.
  • Complex: You effectively need a PhD in AWS Service Catalog to change anything.

Most organizations deploy Control Tower because their AWS Solutions Architect recommended it, or because they needed an account factory. Org Kickstart gives you the account factory, the security posture, and the governance — without the baggage.

What Org Kickstart does

Org Kickstart deploys and manages:

  1. Security Account — Creates a dedicated security account and delegates GuardDuty, Macie, Inspector, Security Hub, SSO, and CloudFormation StackSets to a dedicated Security Account. Configures all security services in every default region across all accounts.
  2. CloudTrail — Creates a CloudTrail bucket in the Security Account and enables an Organizations CloudTrail in the Management Account.
  3. Alternate Contacts — Sets Billing, Operations, and Security contacts for all AWS accounts.
  4. Organizational Units — Creates four default OUs (Workloads, Governance, Sandbox, Suspended) plus any custom OUs you define.
  5. AI Opt-Out Policy — Creates and applies a default AI opt-out policy at the root OU.
  6. Account Management — Manages AWS accounts and OU placement from Terraform.
  7. Audit Role — Deploys an Audit Role via CloudFormation StackSet to all accounts, trusting the Security Account.
  8. Billing Reports — Creates an S3 bucket for billing data and an Athena-compatible CUR report.
  9. Organization Services — Enables all important AWS Organization Integrated Services.
  10. Org Policies — Manages Service Control Policies, Declarative Policies, and Resource Control Policies with Terraform templating support.
  11. SSO / Identity Center — Creates an AdministratorAccess Permission Set, an admin group, and assigns both to every account.

What is required vs. optional?

Cannot be disabled:

  • Security Account
  • Four default OUs (Governance, Workloads, Sandbox, Suspended)
  • AI Opt-Out Policy
  • Core Organization Integrated Services

Can be disabled via variables:

  • CloudTrail management: cloudtrail_bucket_name = null
  • SSO management: disable_sso_management = true
  • Audit Role StackSet: deploy_audit_role = false
  • Security services: security_services object with disable_* flags
  • CUR reports: cur_report_frequency = "NONE"
  • Alternate contacts: omit contact blocks from tfvars

Where should I go next?

2 - Getting Started

Prerequisites, bootstrapping, and first deployment.

This section walks you through getting Org Kickstart deployed into a new AWS account.

Prerequisites

Before running Org Kickstart you need:

  • Terraform >= 1.0 (< 2.0)
  • AWS CLI configured with credentials for the Management (Payer) account
  • An S3 bucket for Terraform remote state
  • A few manual “artisanal” steps completed in the AWS console (see Bootstrap)

Steps

  1. Complete the Bootstrap steps in the AWS Console
  2. Set up your local repository — copy the local-deploy sample, pin the module version, and create your <env>.tfvars / <env>.tfbackend
  3. Initialize Terraform:
    make env=your-org tf-init
    
  4. Create the Security Account first (required before full apply):
    terraform apply -var-file="your-org.tfvars" -target module.security_account
    
  5. Deploy everything:
    make env=your-org tf-execute
    
    This runs tf-plan followed by tf-apply — saving the plan, applying it, and writing output-your-org.json to your state bucket.

For subsequent updates, use:

make env=your-org update

Using with an Existing Organization

If you already have an AWS Organization, see Importing an Existing Org for guidance on importing existing resources into Terraform state.

Example tfvars

See the Reference page for a full annotated example. The examples/local-deploy directory in the repository contains a sample layout with a Makefile, backend config template, and helper scripts for running Org Kickstart from your workstation. To pin to a specific release, see Releases.

2.1 - Bootstrap a New Account

Manual steps required in the AWS Console before running Terraform.

Before Org Kickstart can be deployed, a few steps must be completed via ClickOps in your new AWS Management (Payer) account. Terraform cannot perform these actions automatically.

Root Account Tasks

Log into the root user of your new AWS “payer” account and complete the following:

  1. Add MFA to root
  2. Enable IAM access to billing
  3. Go to Organizations and create an Organization
  4. Go to AWS IAM Identity Center (SSO) and enable it
  5. Add yourself as a user in Identity Center
  6. Create a Permission Set named TempAdministratorAccess (4-hour session recommended)
  7. Assign the Permission Set to the Payer/Management Account for your user
  8. Activate trusted access for CloudFormation StackSets — click “Activate trusted access with AWS Organizations to use service-managed permissions” (must be done via console)

Log out of root and never use it again.

Note: As of June 2026, aws login works for the root user with Terraform — but the root user cannot assume IAM roles, and Org Kickstart assumes roles (for example OrganizationAccountAccessRole into the security account). That is why you must run Terraform as an IAM Identity Center user or an IAM User rather than as root.

On Your Machine

  1. Check email and activate your IAM Identity Center account
  2. Add MFA to your Identity Center account
  3. Configure AWS credentials in your environment:
    aws configure sso
    # or
    export AWS_PROFILE=your-sso-profile
    
  4. Create the S3 bucket that will hold the Terraform state. It must exist before the first terraform init, since the S3 backend lives in it:
    aws s3 mb s3://org-kickstart-fooli
    
    This is the bucket you set as backend_bucket. By default (manage_state_bucket = true) Org Kickstart then adopts it into Terraform and enforces versioning, public-access-block, and encryption — see the Parameter Reference.

You are now ready to deploy Org Kickstart.

Next Steps

  • Create your tfvars file — see the Reference for all variables and a full example
  • Run terraform init and your first apply — see Getting Started

2.2 - Set Up Your Local Repository

Create your private deployment repo from the local-deploy sample and learn the Makefile workflow.

Once the Bootstrap steps are complete, set up a private repository to hold your organization’s configuration and deploy Org Kickstart from your workstation.

1. Copy the sample

Copy the examples/local-deploy directory into your own private git repository. It provides:

  • main.tf — calls the Org Kickstart module
  • import.tf — adopts the AWS Organization, the payer account, and the state bucket
  • Makefile — the env-based workflow (below)
  • sample.tfvars / sample.tfbackend — templates to copy
  • scripts/ and granted.yml — helper scripts and the Granted registry manifest

2. Pin the module version

main.tf pins the module to a tagged release:

source = "github.com/primeharbor/org-kickstart?ref=0.3.1"

See Releases for tracking the rolling latest branch versus pinning to a tag.

3. Create your environment files

Pick a short environment name ($env) for your org — for example fooli. Copy the samples so their names match it:

cp sample.tfvars    fooli.tfvars
cp sample.tfbackend fooli.tfbackend
  • Edit fooli.tfvars — see the Parameter Reference and the Example tfvars for all options.
  • Set the state bucket name in fooli.tfbackend — the bucket you created with aws s3 mb during Bootstrap. By default (manage_state_bucket = true) Org Kickstart adopts and manages it.

4. The Makefile workflow

Every target takes env=<your env>:

make env=fooli tf-init      # terraform init with your backend config
make env=fooli tf-plan      # write a saved plan
make env=fooli tf-apply     # apply the saved plan and write outputs
make env=fooli tf-execute   # tf-plan followed by tf-apply
make env=fooli update       # tf-init + tf-execute — your day-to-day command

The first apply creates the organization structure, OUs, and the security account.

Next steps

2.3 - Importing an Existing Organization

How to adopt Org Kickstart into an existing AWS Organization.

Org Kickstart can manage an existing AWS Organization. Several resources must be imported into Terraform state before running terraform apply.

The minimum required imports are the organization, the management account, and the Security Account (if it already exists). You may also want to import existing accounts, CloudTrail buckets, billing buckets, and SSO configuration.

Automated Import

The scripts/import_org.sh script generates an import-org.tf file and a TFVars snippet for your existing accounts.

# From the org-kickstart directory
./scripts/import_org.sh

Review the generated import-org.tf carefully before running Terraform.

Steps

  1. Run import_org.sh and review import-org.tf
  2. Add your CloudTrail or billing bucket to the import file if applicable
  3. Review SCPs you want to import:
    aws organizations list-policies --filter SERVICE_CONTROL_POLICY \
      --query 'Policies[].[Id,Name]' --output text
    
  4. Iterate with tf-plan until no unwanted changes appear:
    make env=your-org tf-plan
    make env=your-org tf-show    # review the plan output
    
  5. Once satisfied, apply:
    make env=your-org tf-apply
    
  6. Incrementally enable additional features

Manual Import Examples

Organizational Units

ROOT_OU=$(aws organizations list-roots --query Roots[0].Id --output text)
aws organizations list-organizational-units-for-parent \
  --parent-id $ROOT_OU \
  --query 'OrganizationalUnits[].[Id,Name]' --output text
import {
  to = module.organization.aws_organizations_organizational_unit.TF_VARS_KEY
  id = "ou-xxxx-xxxxxxxx"
}

IAM Identity Center (SSO)

To opt out of managing an existing Identity Center, set disable_sso_management = true in your tfvars. This is recommended for existing orgs with complex SSO configurations.

IDENTITY_STORE_ID=$(aws sso-admin list-instances \
  --query Instances[0].IdentityStoreId --output text)
SSO_INSTANCE_ARN=$(aws sso-admin list-instances \
  --query Instances[0].InstanceArn --output text)
import {
  to = module.organization.aws_identitystore_group.admin_group
  id = "$IDENTITY_STORE_ID/$GROUP_ID"
}
import {
  to = module.organization.aws_ssoadmin_permission_set.admin_permission_set
  id = "$PERMISSION_SET_ARN,$SSO_INSTANCE_ARN"
}
import {
  to = module.organization.aws_ssoadmin_managed_policy_attachment.admin_policy_attachments
  id = "arn:aws:iam::aws:policy/AdministratorAccess,$PERMISSION_SET_ARN,$SSO_INSTANCE_ARN"
}

CloudTrail

aws cloudtrail list-trails --query Trails[].TrailARN --output text
import {
  to = module.organization.aws_s3_bucket.cloudtrail_bucket[0]
  id = "YOUR_EXISTING_BUCKET_NAME"
}
import {
  to = module.organization.aws_cloudtrail.org_cloudtrail[0]
  id = "TRAIL_ARN"
}

Service Control Policies

aws organizations list-policies --filter SERVICE_CONTROL_POLICY \
  --query 'Policies[].[Id,Name]' --output text

# Get OUs targeted by a policy
aws organizations list-targets-for-policy --policy-id p-xxxxxx
import {
  to = module.organization.module.scp["POLICY_BLOCK_IDENTIFIER_FROM_TFVARS"].aws_organizations_policy.org_policy
  id = "p-xxxxxx"
}

Disabling Features for Initial Import

When importing an existing org, you may want to temporarily disable features to prevent unintended changes on the first apply:

  • CloudTrail: cloudtrail_bucket_name = null
  • SSO: disable_sso_management = true
  • Audit Role StackSet: deploy_audit_role = false
  • Security Services: use the security_services block with disable_* flags

2.4 - Example tfvars

A complete annotated example tfvars file.

The following is a complete example tfvars file showing all major configuration options. Copy this as a starting point and customize for your organization.

organization = {
  organization_name                 = "my-org"
  payer_name                        = "My Org Management Account"
  payer_email                       = "aws+payer@example.com"
  security_account_name             = "my-org-security"
  security_account_root_email       = "aws+security@example.com"
  cloudtrail_bucket_name            = "my-org-cloudtrail"
  cloudtrail_loggroup_name          = "CloudTrail/DefaultLogGroup"
  billing_data_bucket_name          = "my-org-cur"
  cur_report_frequency              = "DAILY"   # DAILY, HOURLY, or MONTHLY
  session_duration                  = "PT8H"
  admin_permission_set_name         = "AdministratorAccess"
  admin_group_name                  = "AllAdmins"
  disable_sso_management            = false
  deploy_audit_role                 = true
  audit_role_name                   = "security-audit"
  audit_role_stack_set_template_url = "https://s3.amazonaws.com/pht-cloudformation/aws-account-automation/AuditRole-Template.yaml"
  declarative_policy_bucket_name    = "my-org-account-status"
  vpc_flowlogs_bucket_name          = "my-org-flowlogs"
  macie_bucket_name                 = "my-org-macie-findings"

  # Custom OUs (in addition to the four required OUs)
  organization_units = {
    "Platform" = {
      name             = "Platform"
      is_child_of_root = true
    }
  }

  # AWS Accounts
  accounts = {
    dev = {
      account_name          = "my-org-dev"
      account_email         = "aws+dev@example.com"
      monthly_budget_amount = 500
    }

    prod = {
      account_name   = "my-org-prod"
      account_email  = "aws+prod@example.com"
      parent_ou_name = "Workloads"
    }

    sandbox = {
      account_name   = "my-org-sandbox"
      account_email  = "aws+sandbox@example.com"
      parent_ou_name = "Sandbox"
    }

    sso = {
      account_name    = "my-org-sso"
      account_email   = "aws+sso@example.com"
      parent_ou_name  = "Governance"
      delegated_admin = ["sso.amazonaws.com"]
    }
  }

  # Alternate contacts applied to all accounts (can be overridden per account)
  global_billing_contact = {
    name          = "Finance Team"
    title         = "CFO"
    email_address = "billing@example.com"
    phone_number  = "+14041234567"
  }

  global_security_contact = {
    name          = "Security Team"
    title         = "CISO"
    email_address = "security@example.com"
    phone_number  = "+14041234567"
  }

  global_primary_contact = {
    full_name       = "IT Operations"
    company_name    = "My Org, Inc."
    address_line_1  = "123 Main Street"
    city            = "Atlanta"
    state_or_region = "GA"
    postal_code     = "30332"
    country_code    = "US"
    email_address   = "aws@example.com"
    phone_number    = "+14041234567"
  }

  # Service Control Policies
  service_control_policies = {
    deny_root = {
      policy_name        = "DenyRoot"
      policy_description = "Denies use of root user"
      policy_json_file   = "policies/DenyRootSCP.json"
    }

    security_controls = {
      policy_name        = "DefaultSecurityControls"
      policy_description = "Base security controls for all accounts"
      policy_json_file   = "policies/SecurityControlsSCP.json.tftpl"
      policy_vars = {
        audit_role_name = "security-audit"
      }
    }
  }

  # Resource Control Policies
  resource_control_policies = {
    s3_data_perimeter = {
      policy_name        = "S3DataPerimeter"
      policy_description = "Restricts S3 to principals inside the org"
      policy_json_file   = "policies/RCP_S3DataPerimeter.json.tftpl"
      policy_vars = {
        org_id = "o-xxxxxxxxxxxx"
      }
    }
  }

  # Declarative Policies
  declarative_policies = {
    deny_public_ami = {
      policy_name        = "Block_Public_AMIs"
      policy_description = "Deny public sharing of all AMIs"
      policy_type        = "DECLARATIVE_POLICY_EC2"
      policy_json_file   = "policies/EC2ImageBPA_DCP.json"
      policy_targets     = ["Workloads", "Governance", "Sandbox"]
    }
  }

  # Security Services
  security_services = {
    disable_guardduty   = false
    disable_securityhub = false
    disable_macie       = false
  }

  # CloudFormation stacks to deploy into the payer account.
  # Use this for billing notifications, cost alarms, or other payer-scoped
  # automation. Exactly one of template_file or template_url is required.
  payer_cloudformation_stacks = {
    billing_alerts = {
      stack_name    = "slack_billing_alerts"
      template_file = "cloudformation/slack-Template.yaml"
      regions       = ["us-east-1"]
      parameters = {
        pExecutionRate = "cron(0 09 * * ? *)"
        # JSON parameter values can use a heredoc; the module canonicalizes
        # JSON-valued parameters so whitespace differences don't cause drift.
        pEventInput = <<-EOT
          {
            "threshold": "10",
            "alert_percent": "20"
          }
        EOT
        pSlackWebhookSecret = "SlackWebHook"
        pRuleState          = "ENABLED"
        pAccountDescription = "My-Payer"
      }
    }
  }

  # CloudFormation stacks to deploy into the security account via the
  # OrganizationAccountAccessRole. Same schema as payer_cloudformation_stacks.
  security_account_stacks = {
    findings_processor = {
      stack_name    = "security-findings-processor"
      template_file = "cloudformation/findings-processor-Template.yaml"
      regions       = ["us-east-1"]
      parameters = {
        pSlackWebhookSecret = "SlackWebHook"
        pSeverityThreshold  = "MEDIUM"
      }
    }
  }

  # Billing Alerts
  billing_alerts = {
    levels = {
      level1  = 100
      level2  = 500
      oh_shit = 1000
    }
    subscriptions = ["billing-alerts@example.com"]
  }

  budget_defaults = {
    alert_recipients      = ["finance@example.com"]
    currency              = "USD"
    warning_percentage    = 80
    organizational_budget = 1000
  }
}

backend_bucket = "my-org-terraform-state-123456789012"

Backend Config File

Create a my-org.tfbackend file alongside your tfvars:

bucket = "my-org-terraform-state-123456789012"
key    = "org-kickstart.tfstate"
region = "us-east-1"

Then initialize with:

terraform init -backend-config="my-org.tfbackend"

3 - Examples

Example configurations and the local-deploy sample layout.

Local Deploy Sample

The examples/local-deploy directory contains a sample layout for managing your organization with Terraform from your workstation via its Makefile. It shows how to structure your organization-specific configuration (<env>.tfvars, <env>.tfbackend) alongside the module.

Sample tfvars

See the Example tfvars page for a full annotated example configuration file covering all major options.

Policy Library

Ready-to-use policy files are available in the policies/ directory:

  • DenyRootSCP.json — Deny root user access
  • SecurityControlsSCP.json.tftpl — Base security guardrails
  • DisableRegionsPolicy.json.tftpl — Region restrictions
  • RCP_S3DataPerimeter.json.tftpl — S3 data perimeter
  • EC2IMDSv2Enforce_DCP.json — IMDSv2 enforcement
  • And more…

Importing an Existing Organization

See Importing an Existing Organization for a step-by-step guide on adopting Org Kickstart in an existing AWS Organization.

4 - Concepts

Key concepts and architecture behind Org Kickstart.

AWS Organizations Structure

Org Kickstart manages resources in two AWS accounts simultaneously using Terraform’s multi-provider pattern:

  • Management (Payer) Account — Where Terraform runs. Contains the Organization, OUs, SCPs, RCPs, Declarative Policies, CloudTrail, and SSO/Identity Center.
  • Security Account — A dedicated account that is Delegated Administrator for GuardDuty, Macie, Inspector, Security Hub, CloudFormation StackSets, and optionally SSO.

Required Organizational Units

Four OUs are always created and cannot be disabled:

OU Purpose
Governance Security and payer accounts
Workloads Production workload accounts
Sandbox Development accounts with more freedom
Suspended Accounts pending closure

Policy Types

AWS Organizations supports three types of preventive controls, all managed by Org Kickstart:

Type Abbreviation Scope
Service Control Policies SCP What IAM principals can do
Resource Control Policies RCP What can be done to resources
Declarative Policies DP Baseline configuration of AWS services (EC2 only)

All three are defined using the same Terraform structure and support .tftpl templating.

Security Services

Org Kickstart enables and configures the following security services across all default AWS regions and all accounts in the organization:

  • GuardDuty — Threat detection. Payer and Security account both run detectors.
  • Security Hub — Aggregated security findings. Security Account is delegated admin.
  • Inspector — Vulnerability management. Security Account is delegated admin.
  • Macie — S3 data security. Security Account is delegated admin.

Audit Role

A cross-account audit role is deployed to every account via CloudFormation StackSets. The role trusts the Security Account, allowing centralized read access for security tools and audits without needing IAM users in every account.

Account Factory Pattern

New AWS accounts are defined in the accounts map in tfvars. The account module handles everything automatically:

  1. Creates the AWS Organizations account
  2. Places it in the correct OU
  3. Assigns SSO access (via Identity Center group and Permission Set)
  4. Sets billing, operations, and security alternate contacts
  5. Creates a budget with alert thresholds
  6. Applies SCPs/RCPs/Declarative Policies inherited from the parent OU

5 - Account Configurator

Deploy the pht-account-configurator so new accounts are automatically hardened on creation.

When AWS Organizations creates a new account, a number of AWS-recommended best practices are not turned on by default. The pht-account-configurator is a Step Function + Lambda that listens for the “account created” EventBridge event and configures each new account for you. Org Kickstart can deploy and manage it as an optional add-on.

What it configures

Driven by a config file (see below), the configurator can:

  • Set a CIS-compliant IAM password policy.
  • Enable account-wide S3 Block Public Access.
  • Enable EBS snapshot Block Public Access in every enabled region.
  • Enable EBS default encryption in every enabled region.
  • Delete the default VPCs in every region (except regions you mark to preserve, and any VPC that still has an ENI in it).

Sections omitted from the config file are simply skipped.

How Org Kickstart deploys it

Set the account_configurator block in your organization object. When present, Org Kickstart creates a CloudFormation stack (org-kickstart-account-configurator) from the packaged template and uploads your config file to the Terraform state bucket. Omit the block (or set it to null) to disable the feature.

account_configurator = {
  template                    = "https://s3.amazonaws.com/<state-bucket>/pht-account-configurator/AccountFactory-Template-Transformed-<version>.yaml"
  account_factory_config_file = "<env>-account-config.yaml"
}
Field Description
template S3 URL of the packaged configurator CloudFormation template. The make account-configurator target sets this for you.
account_factory_config_file Path (relative to your repo root) to the YAML config file. Terraform uploads it to the state bucket; the Lambda reads it from there.

Setting it up

The configurator is maintained in its own repo and pulled into your Org Kickstart deployment repo as a git submodule. The examples/local-deploy Makefile ships an account-configurator target that does the rest.

  1. Add the submodule (once):

    git submodule add git@github.com:primeharbor/pht-account-configurator.git pht-account-configurator
    
  2. Run the target. Using an environment named fooli:

    make env=fooli account-configurator
    

    This will:

    • check out the submodule if needed;
    • seed fooli-account-config.yaml from the submodule’s sample if you don’t already have one;
    • package the configurator’s CloudFormation template into your Org Kickstart state bucket; and
    • point account_configurator.template and account_configurator.account_factory_config_file in fooli.tfvars at the freshly packaged template and your config file.
  3. Review and deploy. Edit fooli-account-config.yaml to taste, then apply as usual:

    make env=fooli tf-plan
    make env=fooli tf-apply
    

    Re-run make env=fooli account-configurator whenever you want to ship a new version of the Lambda/template; it repackages and updates the template URL, and the next apply rolls it out.

The configuration file

<env>-account-config.yaml controls what the Lambda does. A full example:

# Configure the IAM Password Policy per CIS Benchmarks
account_password_policy:
  update_account_password_policy: true
  password_policy:
      MinimumPasswordLength: 24
      RequireSymbols: True
      RequireNumbers: True
      RequireUppercaseCharacters: True
      RequireLowercaseCharacters: True
      AllowUsersToChangePassword: True
      MaxPasswordAge: 90
      PasswordReusePrevention: 24
      HardExpiry: False

# Delete Default VPCs
default_vpc:
  delete_default_vpc: true
  # Regions in this list will keep their default VPCs
  preserve_vpc_regions:
    - "us-east-1"
    - "eu-central-1"

# Enable Default Encryption of EBS in all enabled regions
enable_ebs_default_encryption: true

# Enable Block Public Access for EBS snapshots in all enabled regions
enable_ebs_block_public_access: true

# Enable Account Wide S3 Block Public Access
enable_account_s3_block_public_access:
    BlockPublicAcls: True
    IgnorePublicAcls: True
    BlockPublicPolicy: True
    RestrictPublicBuckets: true

Running against existing accounts

The Step Function normally runs on account creation, but you can also trigger it manually against an existing account. Be aware of two risks before doing so:

  • Enabling account-wide S3 Block Public Access can break legitimately public S3 buckets.
  • Enabling EBS default encryption may interact with existing custom KMS key configurations.

Pair S3 Block Public Access with an SCP that prevents accounts from disabling it, and route any genuinely public content to a dedicated account.

6 - Tasks

How-to guides for common Org Kickstart operations.

This section contains task-oriented guides for common Org Kickstart operations.

Common Tasks

6.1 - Adding a New Account

How to add a new AWS account to your organization.

Adding a new AWS account is the most common operation in Org Kickstart. All account configuration lives in the accounts map in your tfvars file.

Steps

  1. Add an entry to the accounts map in your tfvars file:

    accounts = {
      # ... existing accounts ...
    
      my_new_account = {
        account_name  = "my-org-new-account"
        account_email = "aws+new-account@example.com"
        parent_ou_name = "Workloads"
        monthly_budget_amount = 500
      }
    }
    
  2. Plan and apply:

    make env=your-org tf-execute
    

    Or step-by-step to review the plan before applying:

    make env=your-org tf-plan
    make env=your-org tf-show
    make env=your-org tf-apply
    make env=your-org granted # (if you're using granted)
    

Org Kickstart will create the AWS account, place it in the correct OU, assign SSO access, set alternate contacts, and apply any policies that target the parent OU. See Granted Support for more information about how org-kickstart can seed your granted registry for you.

Account Options

Option Description
account_name Display name for the AWS account
account_email Root email address (must be globally unique)
parent_ou_name Place the account in this OU (by name)
parent_ou_id Place the account in this OU (by ID)
monthly_budget_amount Budget alert threshold in USD
delegated_admin List of AWS services to delegate admin for
close_on_deletion Whether to close the account when removed from Terraform
primary_contact Override the global primary contact for this account

Notes

  • The account_email must be unique across all AWS accounts globally
  • New accounts are created by AWS Organizations and may take a few minutes to become available
  • The Security Account is managed separately via the security_account block

6.2 - Creating Policies

How to create and attach SCPs, RCPs, and Declarative Policies.

Org Kickstart manages three types of AWS Organizations policies via the same Terraform pattern: Service Control Policies (SCPs), Resource Control Policies (RCPs), and Declarative Policies.

Steps

  1. Create a policy JSON file in the policies/ directory of your deployment repo. For dynamic values, use a .tftpl extension and Terraform template syntax.

    // policies/MyPolicy.json
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Deny",
          "Action": "s3:DeleteBucket",
          "Resource": "*"
        }
      ]
    }
    
  2. Add the policy definition to the appropriate block in your tfvars file:

    service_control_policies = {
      deny_s3_delete = {
        policy_name        = "DenyS3BucketDeletion"
        policy_description = "Prevent deletion of S3 buckets"
        policy_json_file   = "policies/MyPolicy.json"
        policy_targets     = ["Workloads", "Sandbox"]
      }
    }
    
  3. Plan and apply:

    make env=your-org tf-execute
    

    SCPs and RCPs are high-impact changes. Always review the plan before applying:

    make env=your-org tf-plan
    make env=your-org tf-show
    make env=your-org tf-apply
    

    You can also run a security scan of the plan with Checkov before applying:

    make env=your-org checkov
    

Policy Types

Block AWS Type
service_control_policies Service Control Policy (SCP)
resource_control_policies Resource Control Policy (RCP)
declarative_policies Declarative Policy (EC2)

Declarative Policies also require policy_type = "DECLARATIVE_POLICY_EC2".

Targeting OUs

policy_targets accepts a list of OU names or OU IDs. Use "Root" to target the organization root:

policy_targets = ["Root"]                          # all accounts
policy_targets = ["Workloads", "Sandbox"]          # by name
policy_targets = ["ou-xxxx-xxxxxxxx"]             # by ID

Templated Policies

For policies that need org-specific values, use a .tftpl file:

// policies/AuditRoleProtection.json.tftpl
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Deny",
    "Action": ["iam:Delete*"],
    "Resource": "arn:aws:iam::*:role/${audit_role_name}"
  }]
}
service_control_policies = {
  protect_audit_role = {
    policy_name      = "ProtectAuditRole"
    policy_json_file = "policies/AuditRoleProtection.json.tftpl"
    policy_vars = {
      audit_role_name = "security-audit"
    }
  }
}

Sample Policies

See the policies/ directory in the repository for a library of ready-to-use policies. Or check out PrimeHarbor’s respository of Organizational Policies.

7 - Granted Support

Generate an AWS CLI / Granted config from your org and share it with your team as a Granted Registry.

Granted is a tool for quickly assuming roles across many AWS accounts via IAM Identity Center (SSO). Org Kickstart can generate a ready-to-use AWS config from your Terraform outputs, and your org-kickstart repo can double as a Granted Registry so your whole team gets the same set of account profiles.

The examples below use an environment named fooli. Substitute your own env name throughout.

Prerequisites

  • Granted is installed (brew install granted or see the Granted docs).
  • IAM Identity Center is enabled and your org has been deployed at least once, so a terraform apply has produced an output-<env>.json file. The generator reads the account_map, sso_start_url, sso_region, and sso_role_name outputs.

Generating an AWS config with make granted

After an apply, run the granted Makefile target:

make env=fooli granted

This runs scripts/generate_granted_config.sh against output-fooli.json and writes an AWS CLI config to granted/aws-config. The file contains one sso-session block plus one profile per account in your organization:

[sso-session fooli-sso]
sso_start_url = https://fooli.awsapps.com/start
sso_region = us-east-1
sso_registration_scopes = sso:account:access

[profile fooli-payer]
sso_session    = fooli-sso
sso_account_id = 111111111111
sso_role_name  = AdministratorAccess

[profile fooli-sandbox]
sso_session    = fooli-sso
sso_account_id = 222222222222
sso_role_name  = AdministratorAccess

Profiles are named after the keys in your accounts map (plus the payer and security accounts), so re-running make granted after adding accounts keeps the config in sync. Regenerate it any time the account list changes.

Once the profiles are in your AWS config you can assume any account with Granted:

assume fooli-sandbox
# or log the whole SSO session in first
aws sso login --sso-session fooli-sso

Sharing your repo as a Granted Registry

A Granted Registry lets everyone on the team pull the same account profiles straight from a git repo — no copy/pasting config files. Since your org-kickstart repo already generates the config, it’s the natural place to host the registry.

  1. Confirm the granted.yml manifest. The examples/local-deploy sample already ships one at the repo root pointing at the generated config, so there’s nothing to create — just keep it. It looks like this:

    awsConfig:
        - ./granted/aws-config
    
  2. Commit the generated config. Run make env=fooli granted and commit both granted.yml and granted/aws-config to the repo. (Re-run and commit whenever accounts change.)

  3. Team members add the registry once:

    granted registry add -n fooli-admin -u git@github.com:fooli/fooli-org-kickstart.git
    

    Granted clones the repo, reads granted.yml, and merges the referenced profiles into the user’s ~/.aws/config. From then on they can assume fooli-payer, assume fooli-sandbox, and so on.

  4. Stay current. When the registry repo is updated, team members pull the latest profiles with:

    granted registry sync
    

Tip: keep the registry in a private repo. The generated config contains AWS account IDs and your SSO start URL. Those aren’t secrets on their own, but there’s no reason to publish them.

8 - Reference

Variable reference, policy library, and module documentation.

This section contains low-level reference documentation for Org Kickstart.

In This Section

Source

The complete module documentation is generated from the Terraform source and available in the ModuleDocs.md file in the repository.

Policy Library

Sample policies are included in the policies/ directory of the repository:

File Type Description
DenyRootSCP.json SCP Deny use of root user in all accounts
SecurityControlsSCP.json.tftpl SCP Base security controls (requires audit_role_name)
DisableRegionsPolicy.json.tftpl SCP Restrict to approved AWS regions
DenyUnapprovedInstanceTypes.json SCP Deny non-approved EC2 instance types
DenyUnapprovedServices.json SCP Deny unapproved AWS services
SuspendedAccountsPolicy.json.tftpl SCP Deny all activity in suspended accounts
RCP_S3DataPerimeter.json.tftpl RCP Restrict S3 access to org principals
EC2ImageBPA_DCP.json Declarative Block public sharing of AMIs
EC2SnapshotBPA_DCP.json Declarative Block public sharing of EBS snapshots
EC2IMDSv2Enforce_DCP.json Declarative Enforce IMDSv2 with hop limit of 2

Policies with the .tftpl extension support Terraform template variables via policy_vars.

8.1 - Module Documentation

Auto-generated Terraform module reference — inputs, outputs, resources, and sub-modules.

Requirements

Name Version
terraform >= 1.0, < 2.0
aws >= 6.27.0

Providers

Name Version
aws >= 6.27.0
aws.security-account >= 6.27.0

Modules

Name Source Version
accounts ./modules/account n/a
billing_alerts ./modules/billing_alerts n/a
datatrail ./modules/datatrail n/a
declarative_policies ./modules/org_policies n/a
rcp ./modules/org_policies n/a
scp ./modules/org_policies n/a
security-services ./modules/security_services n/a
security_account ./modules/account n/a

Resources

Name Type
aws_account_alternate_contact.billing resource
aws_account_alternate_contact.operations resource
aws_account_alternate_contact.security resource
aws_account_primary_contact.primary resource
aws_budgets_budget.organization resource
aws_cloudformation_stack.account_factory resource
aws_cloudformation_stack.audit_role_payer resource
aws_cloudformation_stack.payer resource
aws_cloudformation_stack.security resource
aws_cloudformation_stack_set.audit_role resource
aws_cloudformation_stack_set_instance.audit_role resource
aws_cloudtrail.org_cloudtrail resource
aws_cloudwatch_log_group.cloudtrail resource
aws_cur_report_definition.cur_report_definition resource
aws_iam_organizations_features.org resource
aws_iam_role.cloudtrail_to_cloudwatch resource
aws_iam_role_policy.cloudtrail_to_cloudwatch resource
aws_identitystore_group.admin_group resource
aws_kms_alias.macie_key resource
aws_kms_key.macie_key resource
aws_kms_key_policy.macie_key resource
aws_organizations_account.payer resource
aws_organizations_delegated_administrator.cloudformation resource
aws_organizations_delegated_administrator.health resource
aws_organizations_delegated_administrator.securityhub resource
aws_organizations_organization.org resource
aws_organizations_organizational_unit.custom_ous resource
aws_organizations_organizational_unit.governance_ou resource
aws_organizations_organizational_unit.sandbox_ou resource
aws_organizations_organizational_unit.suspended_ou resource
aws_organizations_organizational_unit.workloads_ou resource
aws_organizations_policy.ai_policy resource
aws_organizations_policy_attachment.ai_policy_root resource
aws_ram_sharing_with_organization.enable resource
aws_s3_bucket.billing_logs resource
aws_s3_bucket.cloudtrail_bucket resource
aws_s3_bucket.declarative_policy_bucket resource
aws_s3_bucket.macie_bucket resource
aws_s3_bucket.state_bucket resource
aws_s3_bucket.vpc_flowlogs_bucket resource
aws_s3_bucket_lifecycle_configuration.billing_logs resource
aws_s3_bucket_notification.bucket_notification resource
aws_s3_bucket_ownership_controls.cloudtrail_bucket resource
aws_s3_bucket_ownership_controls.declarative_policy_bucket resource
aws_s3_bucket_ownership_controls.macie_bucket resource
aws_s3_bucket_ownership_controls.vpc_flowlogs_bucket resource
aws_s3_bucket_policy.allow_billing_logging resource
aws_s3_bucket_policy.cloudtrail_bucket_policy resource
aws_s3_bucket_policy.declarative_policy_bucket_policy resource
aws_s3_bucket_policy.macie_bucket_policy resource
aws_s3_bucket_policy.vpc_flowlogs_bucket_policy resource
aws_s3_bucket_public_access_block.billing_logs resource
aws_s3_bucket_public_access_block.cloudtrail_bucket_bpa resource
aws_s3_bucket_public_access_block.declarative_policy_bucket_bpa resource
aws_s3_bucket_public_access_block.macie_bucket_bpa resource
aws_s3_bucket_public_access_block.state_bucket resource
aws_s3_bucket_public_access_block.vpc_flowlogs_bucket_bpa resource
aws_s3_bucket_server_side_encryption_configuration.macie_bucket resource
aws_s3_bucket_server_side_encryption_configuration.state_bucket resource
aws_s3_bucket_versioning.cloudtrail_bucket resource
aws_s3_bucket_versioning.declarative_policy_bucket resource
aws_s3_bucket_versioning.macie_bucket resource
aws_s3_bucket_versioning.state_bucket resource
aws_s3_bucket_versioning.vpc_flowlogs_bucket resource
aws_s3_object.account_factory_config resource
aws_securityhub_account.payer_account resource
aws_securityhub_account.security_account resource
aws_securityhub_configuration_policy.no_enabled_standards resource
aws_securityhub_configuration_policy_association.root_ou resource
aws_securityhub_finding_aggregator.regional_aggregator resource
aws_securityhub_organization_admin_account.delegated_admin resource
aws_securityhub_organization_configuration.security_account resource
aws_sns_topic.cloudtrail_s3_notification_topic resource
aws_ssoadmin_account_assignment.payer_account_group_assignment resource
aws_ssoadmin_managed_policy_attachment.admin_policy_attachments resource
aws_ssoadmin_permission_set.admin_permission_set resource
aws_billing_service_account.main data source
aws_iam_policy_document.allow_billing_logging data source
aws_iam_policy_document.cloudtrail_bucket_policy data source
aws_iam_policy_document.cloudtrail_s3_notification_topic data source
aws_iam_policy_document.declarative_policy_bucket_policy data source
aws_iam_policy_document.macie_bucket_policy data source
aws_iam_policy_document.vpc_flowlogs_bucket_policy data source
aws_organizations_organization.org data source
aws_organizations_organizational_units.all_ous data source
aws_organizations_policies.dp_ec2 data source
aws_organizations_policies.rcps data source
aws_organizations_policies.scps data source
aws_organizations_policy.dp_ec2 data source
aws_organizations_policy.rcps data source
aws_organizations_policy.scps data source
aws_partition.current data source
aws_region.current data source
aws_regions.available data source
aws_ssoadmin_instances.identity_store data source

Inputs

Name Description Type Default Required
account_configurator Serverless Application to configure new accounts. See https://github.com/primeharbor/pht-account-configurator
object({
account_factory_config_file = string
template = string
})
null no
accounts AWS accounts to provision in the organization.
map(
object({
account_name = string
account_email = string
delegated_admin = optional(list(string), [])
operations_contact = optional(object({
name = string
title = string
email_address = string
phone_number = string
}))
primary_contact = optional(object({
full_name = string
company_name = optional(string)
address_line_1 = string
address_line_2 = optional(string)
address_line_3 = optional(string)
city = string
district_or_county = optional(string)
state_or_region = optional(string)
postal_code = string
country_code = string
phone_number = string
website_url = optional(string)
}))
# parent_ou_id can explicitly override the OU assignment and lookup by name.
# parent_ou_id takes precedence over parent_ou_name
parent_ou_name = optional(string, “Workloads”)
parent_ou_id = optional(string, null)
monthly_budget_amount = optional(number, 0)
budget_alert_recipients = optional(list(string), [])
service_control_policies = optional(list(string), [])
resource_control_policies = optional(list(string), [])
declarative_policies_ec2 = optional(list(string), [])
})
)
n/a yes
admin_group_name Name of the Identity Store Group with all the admin users string "AllAdmins" no
admin_permission_set_name Name of the Default Admin Permission Set to Create string "AdministratorAccess" no
audit_role_name Name of the AuditRole to deploy string "security-audit" no
audit_role_stack_set_template_url URL that points to the Audit Role Policy Template string null no
aws_service_access_principals_to_enable List of AWS service access principals to enable if they’re not part of the default set. list(string) [] no
aws_service_access_principals_to_exclude List of AWS service access principals to exclude from the default set. list(string) [] no
backend_bucket Name of the S3 bucket used for the CloudFormation stacks and Terraform state backend string n/a yes
billing_alerts Triggers for billing alerts and who should receive them.
object({
levels = map(number)
subscriptions = list(string)
})
null no
billing_data_bucket_name Name of the S3 Bucket for CUR reports. Set to null to disable CUR report generation. string null no
budget_defaults Default values for AWS Budgets. Some settings can be overridden in the account definition.
object({
alert_recipients = optional(list(string), [])
currency = optional(string, “USD”)
warning_percentage = optional(number, 85)
organizational_budget = optional(number, 0)
})
{} no
cloudtrail_bucket_name Name of the S3 Bucket to create to store CloudTrail events. Set to null to disable CloudTrail management string null no
cloudtrail_loggroup_name Name of the CloudWatch Log Group in the payer account where CloudTrail will send its events. Set to null to disable CloudTrail to CloudWatch Logs. string null no
cur_report_frequency Frequency CUR reports should be delivered (DAILY, HOURLY, MONTHLY). Set to NONE to disable string "NONE" no
datatrail Details on the DataTrails
object({
bucket_name = string
trail_name = string
enabled = optional(bool, true)
excluded_buckets = list(string)
})
null no
declarative_policies Map of Declarative Policies to create and attach.
map(object({
policy_name = string
policy_type = string
policy_description = string
policy_json_file = string
policy_targets = optional(list(string), [“Root”])
policy_vars = optional(map(string), {})
do_not_attach = optional(bool, false)
}))
{} no
declarative_policy_bucket_name Name of S3 Bucket for Declarative Policy Reports. Set to null to disable Declarative Policy Reports. string null no
default_close_on_deletion If set, the AWS Account will be closed when it’s removed from org-kickstart. Set this with caution. bool false no
deploy_audit_role Boolean to determine if org-kickstart should manage a Security Audit Role. Set to false to disable the creation of an Audit Role stackset. bool true no
disable_sso_management Set to true to disable creating a default Admin Permission Set in Identity Center. bool false no
global_billing_contact Billing alternate contact to be applied to all accounts.
object({
name = string
title = string
email_address = string
phone_number = string
})
null no
global_operations_contact Default operations alternate contact to be applied to all accounts. Can be overridden in account definition.
object({
name = string
title = string
email_address = string
phone_number = string
})
null no
global_primary_contact Default primary account owner to be applied to all accounts. Can be overridden in account definition.
object({
full_name = string
company_name = optional(string)
address_line_1 = string
address_line_2 = optional(string)
address_line_3 = optional(string)
city = string
district_or_county = optional(string)
state_or_region = optional(string)
postal_code = string
country_code = string
phone_number = string
website_url = optional(string)
})
null no
global_security_contact Security alternate contact to be applied to all accounts
object({
name = string
title = string
email_address = string
phone_number = string
})
null no
macie_bucket_name Name of the S3 Bucket to create to store Macie Findings. Set to null to skip creation string null no
manage_state_bucket Manage the S3 bucket named by backend_bucket (the bucket that holds this Terraform state) with
Terraform. The bucket must already exist (it has to, in order to run Terraform at all), so it is
adopted via an import block in the calling module rather than created. When true, Terraform
enforces versioning, public-access-block, and encryption on it. Set to false to leave the bucket
entirely outside Terraform’s management.
bool true no
organization_name Name of the Organization. This is used for resource prefixes and general reference string n/a yes
organization_policy_types_to_exclude List of organization policy types to exclude from the default set. list(string) [] no
organization_units Map of OUs to create.
map(
object({
name = string
is_child_of_root = optional(bool) # This is ignored, it is retained for backwards compatibility
parent_id = optional(string)
})
)
{} no
payer_cloudformation_stacks Map of CloudFormation stacks to deploy into the payer account. Exactly one of template_file (local path relative to path.root) or template_url (S3/HTTPS URL) must be set per stack. If regions is omitted, the stack is deployed only in the base org-kickstart region.
map(
object({
stack_name = string
template_file = optional(string)
template_url = optional(string)
regions = optional(list(string))
parameters = optional(map(string), {})
timeout_in_minutes = optional(number, 15)
on_failure = optional(string, “DO_NOTHING”)
})
)
{} no
payer_email Root Email address for the Organization Management account string n/a yes
payer_name Name of the Organization Management account string "AWS Payer" no
resource_control_policies Map of RCPs to create and attach.
map(
object({
policy_name = string
policy_description = string
policy_json_file = string
policy_targets = optional(list(string), [“Root”])
policy_vars = optional(map(string), {})
do_not_attach = optional(bool, false)
})
)
{} no
security_account Settings for the Security Account.
object({
# These will move from the main module to here at some point.
# account_name = string
# account_email = string
delegated_admin = optional(list(string), [])
operations_contact = optional(object({
name = string
title = string
email_address = string
phone_number = string
}))
primary_contact = optional(object({
full_name = string
company_name = optional(string)
address_line_1 = string
address_line_2 = optional(string)
address_line_3 = optional(string)
city = string
district_or_county = optional(string)
state_or_region = optional(string)
postal_code = string
country_code = string
phone_number = string
website_url = optional(string)
}))
monthly_budget_amount = optional(number, 0)
budget_alert_recipients = optional(list(string), [])
})
n/a yes
security_account_name Name of the Security Account string "Security Account" no
security_account_root_email Root Email address for the security account string n/a yes
security_account_stacks Map of CloudFormation stacks to deploy into the security account. Exactly one of template_file (local path relative to path.root) or template_url (S3/HTTPS URL) must be set per stack. If regions is omitted, the stack is deployed only in the base org-kickstart region.
map(
object({
stack_name = string
template_file = optional(string)
template_url = optional(string)
regions = optional(list(string))
parameters = optional(map(string), {})
timeout_in_minutes = optional(number, 15)
on_failure = optional(string, “DO_NOTHING”)
})
)
{} no
security_services Explicitly disable or not manage a security service
object({
disable_guardduty = optional(bool, false)
disable_macie = optional(bool, false)
disable_inspector = optional(bool, false)
disable_securityhub = optional(bool, false)
disable_stacksets = optional(bool, false)
})
n/a yes
service_control_policies Map of SCPs to create and attach.
map(
object({
policy_name = string
policy_description = string
policy_json_file = string
policy_targets = optional(list(string), [“Root”])
policy_vars = optional(map(string), {})
do_not_attach = optional(bool, false)
})
)
{} no
session_duration Admin Permission Set Session Duration string "PT8H" no
sso_instance_region Region where the AWS SSO instance is configured string "us-east-1" no
sso_start_url AWS SSO start URL (e.g., https://yourorg.awsapps.com/start) string "https://NOT-PROVIDED.awsapps.com/start" no
tag_set Default map of tags to be applied to all resources via all providers map(string) {} no
vpc_flowlogs_bucket_name Name of the S3 Bucket to create to store VPC Flow Logs. Set to null to skip creation string null no

Outputs

Name Description
account_map Map of account names (actual names, not terraform keys) to account IDs
accounts Map of account names to account IDs
cloudtrail_cloudwatch_log_group ARN of the CloudWatch Log Group that has the CloudTrail Management Events
cloudtrail_s3_notification_topic ARN of the SNS Topic that receives S3 notifications of new CloudTrail event objects.
declarative_policy_bucket S3 Bucket used to store declarative policies
macie_key_arn ARN of the KMS Key used by Macie
org_id ID of the AWS Organization
org_name Name of the AWS Organization
ou_name_to_id Map of OU Names to OU IDs
security_account_id ID of the Security Account
sso_instance_arn AWS Identity Center Instance ARN managed by org-kickstart
sso_region AWS Region where SSO Identity Center is configured
sso_role_name Name of the SSO Permission Set (role name) for admin access
sso_start_url AWS SSO start URL (e.g., https://yourorg.awsapps.com/start)

8.2 - Parameter Reference

All Terraform variables for the Org Kickstart module.

All configuration is passed via the organization variable (an object type) and a few top-level variables. Below are the key configuration parameters.

Top-Level Variables

Variable Type Required Description
organization object Yes Main configuration object (see below)
backend_bucket string Yes S3 bucket name for Terraform state

Organization Object

Core Identity

Parameter Type Required Description
organization_name string Yes Short name for the organization
payer_name string Yes Display name for the management account
payer_email string Yes Root email of the management account
security_account_name string Yes Display name for the security account
security_account_root_email string Yes Root email for the new security account

State Bucket

Parameter Type Default Description
manage_state_bucket bool true Manage the backend_bucket (the S3 bucket holding Terraform state) with Terraform. The bucket must already exist; it is adopted via an import block in the calling module (see examples/local-deploy/main.tf), and Terraform then enforces versioning, public-access-block, and AES256 encryption on it. It is protected with prevent_destroy. Set to false to leave the bucket unmanaged.

CloudTrail

Parameter Type Default Description
cloudtrail_bucket_name string required S3 bucket for CloudTrail logs. Set null to disable
cloudtrail_loggroup_name string "CloudTrail/DefaultLogGroup" CloudWatch Log Group name

SSO / Identity Center

Parameter Type Default Description
session_duration string "PT8H" ISO 8601 duration for SSO sessions
admin_permission_set_name string "AdministratorAccess" Name of the admin Permission Set
admin_group_name string "AllAdmins" Name of the admin Identity Center group
disable_sso_management bool false Set true to stop Terraform from managing SSO
sso_instance_region string "us-east-1" Region where the IAM Identity Center instance is configured
sso_start_url string required when SSO is managed AWS access portal URL, e.g. https://yourorg.awsapps.com/start

Audit Role

Parameter Type Default Description
deploy_audit_role bool true Deploy the cross-account audit role StackSet
audit_role_name string "security-audit" Name of the audit role in each account
audit_role_stack_set_template_url string required if deploy S3 URL to the CloudFormation template

Billing

Parameter Type Required Description
billing_data_bucket_name string Yes S3 bucket for CUR reports
cur_report_frequency string "DAILY" DAILY, HOURLY, MONTHLY, or "NONE" to disable
declarative_policy_bucket_name string Yes S3 bucket for declarative policy documents

Security Services

Configure which security services to enable/disable (each defaults to false):

security_services = {
  disable_guardduty   = false
  disable_macie       = false
  disable_inspector   = false
  disable_securityhub = false
  disable_stacksets   = false
}

Log & Findings Buckets

Parameter Type Default Description
vpc_flowlogs_bucket_name string null S3 bucket to create for VPC Flow Logs. null to skip
macie_bucket_name string null S3 bucket to create for Macie findings. null to skip

Organization Services & Policy Types

org-kickstart enables an opinionated set of AWS service-access principals and organization policy types. Adjust the defaults with:

Parameter Type Default Description
aws_service_access_principals_to_enable list(string) [] Extra service principals to enable beyond the default set
aws_service_access_principals_to_exclude list(string) [] Service principals to remove from the default set
organization_policy_types_to_exclude list(string) [] Org policy types to exclude from the default set (e.g. when a partition doesn’t support one)

Account Configurator

Optional integration with pht-account-configurator to harden new accounts on creation — see the Account Configurator guide. Set the account_configurator object, or omit it to disable:

Field Type Description
template string S3 URL of the packaged configurator CloudFormation template
account_factory_config_file string Path (relative to repo root) to the YAML config file; Terraform uploads it to the state bucket

DataTrail

Optional DataTrail (CloudTrail S3 data-event) configuration in the security account. Set the datatrail object, or omit it to disable:

Field Type Default Description
bucket_name string required S3 bucket holding the DataTrail logs
trail_name string required Name of the CloudTrail data-event trail
enabled bool true Whether the trail is enabled
excluded_buckets list(string) required S3 buckets to exclude from data-event logging

Organizational Units

Four OUs are always created: Governance, Workloads, Sandbox, Suspended. Additional OUs can be defined:

organization_units = {
  "MyOU" = {
    name             = "MyOU"
    is_child_of_root = true
  }
  "NestedOU" = {
    name         = "NestedOU"
    parent_id    = "MyOU"   # use parent OU name for direct children of custom OUs
  }
}

Accounts

Each account in the accounts map:

accounts = {
  my_account = {
    account_name          = "my-org-prod"        # AWS account display name
    account_email         = "aws+prod@example.com" # root email (must be unique)
    parent_ou_name        = "Workloads"           # OU name (or use parent_ou_id)
    monthly_budget_amount = 1000                  # optional, in USD
    delegated_admin       = ["service.amazonaws.com"]  # optional

    # Optional: override primary contact for this account
    primary_contact = {
      full_name       = "Account Owner"
      company_name    = "My Org"
      address_line_1  = "123 Main St"
      city            = "Atlanta"
      state_or_region = "GA"
      postal_code     = "30332"
      country_code    = "US"
      email_address   = "owner@example.com"
      phone_number    = "+14041234567"
    }
  }
}

The org-wide default_close_on_deletion (bool, default false) controls whether an account is closed — not just removed from the org — when it leaves org-kickstart. Use with caution.

Security Account

The security account is created and managed like a workload account, but its settings live in a dedicated required security_account block. Its name and root email come from security_account_name / security_account_root_email; everything else is optional:

security_account = {
  delegated_admin         = ["guardduty.amazonaws.com"]  # services it is delegated admin for
  monthly_budget_amount   = 100                          # optional, USD
  budget_alert_recipients = ["security@example.com"]     # optional
  # operations_contact and primary_contact may also be set (same shape as an account entry)
}

Alternate Contacts

Applied org-wide (overridable per account):

global_billing_contact = {
  name          = "Name"
  title         = "CFO"
  email_address = "billing@example.com"
  phone_number  = "+1xxxxxxxxxx"
}

global_security_contact = {
  name          = "Name"
  title         = "CISO"
  email_address = "security@example.com"
  phone_number  = "+1xxxxxxxxxx"
}

global_operations_contact = {
  name          = "Name"
  title         = "VP Engineering"
  email_address = "ops@example.com"
  phone_number  = "+1xxxxxxxxxx"
}

global_primary_contact = {
  full_name       = "Name"
  company_name    = "My Org"
  address_line_1  = "123 Main St"
  city            = "Atlanta"
  state_or_region = "GA"
  postal_code     = "30332"
  country_code    = "US"
  email_address   = "aws@example.com"
  phone_number    = "+1xxxxxxxxxx"
}

Policies

All three policy types follow the same structure:

service_control_policies = {
  my_policy = {
    policy_name        = "MyPolicy"
    policy_description = "Description"
    policy_json_file   = "policies/MyPolicy.json"
    policy_targets     = ["Workloads", "ou-xxxx-xxxxxxxx"]  # OU names or IDs
    policy_vars = {                                         # for .tftpl files
      variable_name = "value"
    }
  }
}

The same structure applies to resource_control_policies and declarative_policies (which also require policy_type = "DECLARATIVE_POLICY_EC2").

Billing Alerts

billing_alerts = {
  levels = {
    level1  = 100   # USD threshold
    level2  = 500
    oh_shit = 1000
  }
  subscriptions = ["email@example.com"]
}

budget_defaults = {
  alert_recipients      = ["email@example.com"]
  currency              = "USD"
  warning_percentage    = 80
  organizational_budget = 1000
}

Payer CloudFormation Stacks

payer_cloudformation_stacks deploys arbitrary CloudFormation stacks into the payer (management) account. It’s the right hook for things like billing notifications, payer-scoped cost-explorer alarms, or other automation that has to live in the payer because of where billing data and Organizations APIs are exposed.

Each map entry creates one stack per region listed. The Terraform resource addresses use a <key>-<region> composite, so adding a region later does not rename existing stacks.

Field Type Default Description
stack_name string required CloudFormation stack name
template_file string conditional Local path to template, relative to the caller’s path.root. Exactly one of template_file or template_url is required
template_url string conditional HTTPS/S3 URL to the template. Exactly one of template_file or template_url is required
regions list(string) base org-kickstart region Regions to deploy the stack into
parameters map(string) {} CloudFormation parameter values
timeout_in_minutes number 15 Stack create/update timeout
on_failure string "DO_NOTHING" One of DO_NOTHING, ROLLBACK, DELETE

All stacks are created with the full set of CloudFormation capabilities (CAPABILITY_IAM, CAPABILITY_NAMED_IAM, CAPABILITY_AUTO_EXPAND).

payer_cloudformation_stacks = {
  billing_alerts = {
    stack_name    = "slack_billing_alerts"
    template_file = "cloudformation/slack-Template.yaml"
    regions       = ["us-east-1"]
    parameters = {
      pExecutionRate = "cron(0 09 * * ? *)"
      # JSON-valued parameters can be written as heredocs. The module
      # canonicalizes any value that parses as JSON, so multi-line
      # formatting does not cause drift on subsequent plans.
      pEventInput = <<-EOT
        {
          "threshold": "10",
          "alert_percent": "20"
        }
      EOT
      pSlackWebhookSecret = "SlackWebHook"
      pRuleState          = "ENABLED"
      pAccountDescription = "My-Payer"
    }
  }
}

Security Account Stacks

security_account_stacks is the same idea as payer_cloudformation_stacks, but the stacks are deployed into the Security Account via the OrganizationAccountAccessRole. Use it for security-scoped automation that needs to live in the delegated-admin account: Security Hub finding processors, GuardDuty event handlers, KMS keys for security data, ChatBot integrations, and similar.

Schema, defaults, composite-key behavior, capabilities, and JSON parameter canonicalization are identical to payer_cloudformation_stacks.

security_account_stacks = {
  findings_processor = {
    stack_name    = "security-findings-processor"
    template_file = "cloudformation/findings-processor-Template.yaml"
    regions       = ["us-east-1"]
    parameters = {
      pSlackWebhookSecret = "SlackWebHook"
      pSeverityThreshold  = "MEDIUM"
    }
  }
}

9 - Contribution Guidelines

How to contribute to Org Kickstart

We welcome contributions to both the Terraform module and this documentation site. All submissions require review via GitHub pull request.

Contributing to the Module

  1. Fork the org-kickstart repo on GitHub
  2. Create a feature branch
  3. Make your changes and test them against a real AWS Organization (see the CLAUDE.md for testing guidance)
  4. Send a pull request with a clear description of the change and why it’s needed

Testing

When modifying the module:

  • Use make env=your-org tf-plan extensively before applying
  • Test in a non-production organization first
  • Use targeted applies for risky changes: terraform apply -var-file="your-org.tfvars" -target <resource>
  • Verify SCPs don’t lock out root or prevent remediation

Contributing to the Docs

The documentation source lives in the org-kickstart-site/ directory alongside the module.

Running the Site Locally

Requirements:

  • Hugo extended >= 0.155.0
  • Node.js (for PostCSS/SCSS processing)
cd org-kickstart-site
make npm        # install Node dependencies (first time only)
make test       # starts dev server at http://localhost:1319/

Use make test-drafts to also render draft and future-dated content.

Editing Pages

  1. Fork the org-kickstart-site repo
  2. Edit files under org-kickstart-site/content/en/
  3. Preview locally with make test
  4. Submit a pull request

You can also click Edit this page in the top right of any documentation page to jump directly to the source file on GitHub.

Creating an Issue

Found a bug or want to request a feature? Open an issue on GitHub.

Useful Resources

10 - Releases

How Org Kickstart is versioned — the latest branch and tagged releases — and how to pin the module source.

Org Kickstart uses a simple versioning model: a rolling latest branch and fixed, tagged releases.

We target quarterly releases, but may cut a new version sooner when significant new AWS organization-management features warrant it.

The latest branch

The default branch is latest. It always holds the most recent changes. Point your module source at it (with no ?ref=) to track the newest code:

module "organization" {
  source = "github.com/primeharbor/org-kickstart"
  # ...
}

Use this if you want the newest features and are comfortable reviewing every terraform plan for changes before you apply.

Tagged releases

Each release is a fixed, immutable git tag — for example 0.3.1. Pin to a tag for a stable, reproducible deployment:

module "organization" {
  source = "github.com/primeharbor/org-kickstart?ref=0.3.1"
  # ...
}

The examples/local-deploy sample pins to the current release by default. Bump the ?ref= value when you’re ready to move to a newer release — and read its release notes for breaking changes first.

Release notes

Per-release changes — new features, breaking changes, and migration steps — are published as release notes on the blog, and in each release’s docs/v<version>-notes.md file in the repository.

Which should I use?

  • Production: pin to a tagged release (?ref=0.3.1) and upgrade deliberately after reviewing the release notes.
  • Evaluating or lab environments: track latest for the newest capabilities.