pipenv for tests.bucket_policy output to it.SecurityHubReadOnlyCrossAccount role in every reporting account (see below).account_ids = []), deploy from the management account or
from a delegated administrator for AWS Organizations.cp security-hub/core.tfvars security-hub/<env>.tfvars # edit values
make plan VAR_FILE=<env>.tfvars
make apply VAR_FILE=<env>.tfvars
make build bundles files/*.py plus openpyxl into security-hub/build/ for the arm64
Python 3.13 runtime; plan and apply depend on it. terraform plan fails if that directory
is missing because the archive data source zips it, and only make build creates it.
State is local by default. Add a backend block to versions.tf for shared state.
!!! note “Tags”
tags is applied to every resource through provider default tags. core.tfvars shows
the expected keys: Company, Application, Environment, Owner, CostCenter.
Take the collector_role_arn output and deploy cloudformation/reporting-account-role.yaml
in every account that should appear in the report, including the reporting account itself if
it should be included.
=== “Single account”
```shell
aws cloudformation deploy \
--stack-name security-hub-report-role \
--template-file cloudformation/reporting-account-role.yaml \
--parameter-overrides CollectorRoleArn=<collector_role_arn> \
--capabilities CAPABILITY_NAMED_IAM
```
=== “StackSet (whole Organization)”
Deploy as a service-managed StackSet from the management account with automatic
deployment enabled so new accounts get the role as they join:
```shell
aws cloudformation create-stack-set \
--stack-set-name security-hub-report-role \
--template-body file://cloudformation/reporting-account-role.yaml \
--parameters ParameterKey=CollectorRoleArn,ParameterValue=<collector_role_arn> \
--capabilities CAPABILITY_NAMED_IAM \
--permission-model SERVICE_MANAGED \
--auto-deployment Enabled=true,RetainStacksOnAccountRemoval=false
aws cloudformation create-stack-instances \
--stack-set-name security-hub-report-role \
--deployment-targets OrganizationalUnitIds=<root-or-ou-id> \
--regions <region>
```
IAM is global, so one region per account is enough.
webhook_urls: any number of Slack (hooks.slack.com) or Teams incoming webhooks.create_sns = true (default) creates an encrypted SNS topic in this account; subscribe to
the sns_topic_arn output.create_sns = false plus sns_topic_arn publishes to an existing topic. For a topic in
another account, attach the sns_topic_policy output to it.Every channel receives the same summary; see Operations.
| Name | Default | Purpose |
|---|---|---|
region |
required | Deployment region |
cust_name |
required | Resource name prefix and report label |
account_ids |
[] |
Accounts to report on; empty means the whole Organization |
report_regions |
required | Regions to read findings from in each account |
out_bucket |
required | Destination bucket |
webhook_urls |
[] |
Slack/Teams webhooks (sensitive) |
create_sns |
true |
Create an SNS topic here |
sns_topic_arn |
null |
Existing topic when create_sns = false |
report_schedule |
cron(0 2 1 * ? *) |
EventBridge schedule |
max_concurrency |
10 |
Accounts collected in parallel |
log_retention_days |
30 |
Lambda log retention |
tags |
{} |
Applied to every resource via provider default tags |
| Name | Purpose |
|---|---|
state_machine_arn |
Start an ad-hoc run with aws stepfunctions start-execution |
collector_role_arn |
Pass as CollectorRoleArn to the CloudFormation role template |
sns_topic_arn |
Topic to subscribe to, if one was created |
bucket_policy |
Statement to attach to out_bucket |
sns_topic_policy |
Policy for an SNS topic in another account when create_sns = false |