Account Factory List
account_factory_list is the account map: the single source of truth for which member accounts exist, where they sit in the OU tree, and what account-specific values their baseline receives.
It lives in the management account's NTC Account Factory stack, so adding an account, moving it to another OU, or granting it an exemption all happen through a pull request.
File Layout
The account map is kept as JSON in the root of the Account Factory stack, usually one file per baseline scope, named account_list_<scope_name>.json:
account_list_core.json
account_list_sandbox.json
account_list_workloads.json
Grouping the accounts by the scope whose baseline they receive keeps a pull request limited to one set of accounts and the diff readable as the organization grows.
Account Fields
{
"account_name": "aws-example-prod",
"account_email": "accounts+aws-example-prod@example.com",
"ou_path": "/root/workloads/prod",
"close_on_deletion": true,
"account_tags": {
"AccountOwner": "owner@example.com",
"AccountType": "workload"
},
"alternate_contacts": [
{
"type": "SECURITY",
"name": "Security Contact",
"email_address": "security@example.com"
}
],
"customer_values": {}
}
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
account_name | string | yes | - | Account name in AWS Organizations. Unique across all files. |
account_email | string | yes | - | Root email of the account. Unique across all files and unused elsewhere in the organization. |
ou_path | string | yes | - | Target OU, e.g. /root/workloads/prod. The OU must already exist and can nest at most five levels below root. |
close_on_deletion | bool | yes | - | true closes the AWS account when the entry is removed. false only drops it from Terraform management. |
ignore_naming_conventions | bool | no | false | Exempts this account from the optional account_factory_naming_conventions regex checks. |
account_tags | map(string) | no | {} | Tags applied to the account in AWS Organizations. Non-string values are stringified, so false is stored as "false". |
alternate_contacts | list(object) | no | [] | See below. |
customer_values | object | no | {} | Per-account values handed to the baseline. See customer_values. |
These fields are validated in the Account Factory stack, so a duplicate name, a malformed email or a missing OU fails the plan in the pull request.
Renaming an account or moving it to an OU of another baseline scope changes the key of its baseline state. See Moving and Renaming Accounts before changing account_name, account_email or ou_path of an existing account.
Alternate Contacts
| Field | Type | Required | Default |
|---|---|---|---|
type | string | yes | - |
name | string | yes | - |
email_address | string | yes | - |
title | string | no | Mx |
phone_number | string | no | 00000000000 |
type is one of SECURITY, OPERATIONS or BILLING, at most one contact per type.
customer_values
Free-form per-account values, reaching baseline templates as var.current_account_customer_values. Use it for anything a custom template should branch on.
It must be an object. Use {} when an account has none.
One key is reserved: account_baseline_overrides. Everything else is yours.
account_baseline_overrides
Baseline templates are configured once per scope, which is what makes them consistent. Individual accounts sometimes need a different value or a documented exemption: an image builder account that cannot use EBS default encryption, a legacy workload that still needs IMDSv1, an account recording fewer AWS Config resource types to control cost.
"customer_values": {
"account_baseline_overrides": {
"account_hardening": {
"ebs_encryption_by_default": { "enabled": false }
}
}
}
The aws_config and account_hardening templates support overrides today.
The account map lives in the management account's Account Factory stack, so an exemption is granted centrally through a pull request and stays visible in code review. An administrator inside the member account cannot exempt their own account, and cannot disable a control by editing it in the console either, because the baseline re-runs on schedule and reverts the drift.
Precedence
The account map override wins over the scope value, merged shallowly per control. An override that sets only one attribute leaves the others at the scope value.
Where a wrong value surfaces
Overrides are validated, but not in the Account Factory stack. To the Account Factory, customer_values is free-form, so a misspelled control or an invalid value passes the management stack plan and merges into the pull request without complaint.
The validation lives inside the generated baseline file. The failure therefore appears later, when the baseline runs for that account, and it fails the baseline plan with every finding in one message:
Invalid account_baseline_overrides.account_hardening for account aws-example-test:
unknown control 'ebs_encryption_by_defualt'; wrong type for 'iam_password_policy.max_password_age
(expected number)'; invalid value: ec2_imdsv2.http_tokens must be one of required, optional,
no-preference
Only that account is affected. Nothing is applied, and other accounts in the same scope continue to deploy normally.
aws_config
Only config_recording_mode can be overridden per account.
| Attribute | Default | Accepted values |
|---|---|---|
frequency | CONTINUOUS | CONTINUOUS, DAILY |
resource_types | [] | List of AWS::<service>::<type> strings |
exclusion_by_resource_types | [] | List of AWS::<service>::<type> strings |
recording_mode_override.frequency | DAILY | CONTINUOUS, DAILY |
recording_mode_override.resource_types | [] | List of AWS::<service>::<type> strings |
resource_types and exclusion_by_resource_types cannot both be set, AWS rejects that recording group with InvalidRecordingGroupException. A type named in exclusion_by_resource_types is not recorded at all, so naming the same type in recording_mode_override.resource_types has no effect.
account_hardening
Every control accepts enabled to exempt the account from it, plus the settings below.
| Control | Attribute | Default | Accepted values |
|---|---|---|---|
ssm_document_public_sharing | setting_value | Disable | Disable, Enable |
ssm_automation_log_destination | setting_value | CloudWatch | CloudWatch |
ec2_imdsv2 | http_tokens | required | required, optional, no-preference |
ec2_imdsv2 | http_endpoint | no-preference | enabled, disabled, no-preference |
ec2_imdsv2 | http_put_response_hop_limit | -1 | 1 to 64, or -1 for no account-level preference |
ec2_imdsv2 | instance_metadata_tags | no-preference | enabled, disabled, no-preference |
ebs_encryption_by_default | - | - | enabled only |
ebs_snapshot_block_public_access | state | block-all-sharing | block-all-sharing, block-new-sharing |
iam_password_policy | minimum_password_length | 18 | 6 to 128 |
iam_password_policy | require_lowercase_characters | true | true, false |
iam_password_policy | require_numbers | true | true, false |
iam_password_policy | require_uppercase_characters | true | true, false |
iam_password_policy | require_symbols | true | true, false |
iam_password_policy | allow_users_to_change_password | true | true, false |
iam_password_policy | password_reuse_prevention | 24 | 1 to 24 |
iam_password_policy | max_password_age | 90 | 1 to 1095 |
s3_account_public_access_block | block_public_acls | true | true, false |
s3_account_public_access_block | block_public_policy | true | true, false |
s3_account_public_access_block | ignore_public_acls | true | true, false |
s3_account_public_access_block | restrict_public_buckets | true | true, false |
CloudWatch is the only value the SSM API accepts for ssm_automation_log_destination. To switch that control off for an account, set "enabled": false rather than looking for another setting value.
The defaults satisfy the cited Security Hub controls. These values still apply cleanly but no longer pass:
- any
s3_account_public_access_blockflag set tofalse- S3.1 requires all four to be true iam_password_policy.password_reuse_preventionother than24- IAM.16 requires exactly 24iam_password_policy.max_password_ageabove90- IAM.17 requires 90 days or lessec2_imdsv2.http_tokensother thanrequired- EC2.8 requires IMDSv2
Limitations
A control disabled for the whole scope cannot be enabled for a single account. The override has no effect and the baseline run reports a check warning instead of failing, because the same account entry may be intended for another scope in which the control is enabled.
For aws_config, the scope-level config_recording_mode_account_override input is deprecated. Move those entries to customer_values.account_baseline_overrides.aws_config.config_recording_mode.
Example
account_list_workloads.json, showing every account_hardening control on the first account and an aws_config override on the second:
[
{
"account_name": "aws-example-test",
"account_email": "accounts+aws-example-test@example.com",
"ou_path": "/root/workloads/test",
"close_on_deletion": true,
"account_tags": {
"AccountOwner": "owner@example.com",
"AccountType": "workload",
"ManagedBy": "OpenTofu"
},
"alternate_contacts": [
{
"type": "SECURITY",
"name": "Security Contact",
"email_address": "security@example.com"
}
],
"customer_values": {
"account_baseline_overrides": {
"account_hardening": {
"ssm_document_public_sharing": {
"enabled": true,
"setting_value": "Enable"
},
"ssm_automation_log_destination": {
"enabled": true,
"setting_value": "CloudWatch"
},
"ec2_imdsv2": {
"enabled": true,
"http_tokens": "optional",
"http_endpoint": "enabled",
"http_put_response_hop_limit": 3,
"instance_metadata_tags": "enabled"
},
"ebs_encryption_by_default": {
"enabled": true
},
"ebs_snapshot_block_public_access": {
"enabled": true,
"state": "block-new-sharing"
},
"iam_password_policy": {
"enabled": true,
"minimum_password_length": 20,
"require_lowercase_characters": false,
"require_numbers": false,
"require_uppercase_characters": false,
"require_symbols": false,
"allow_users_to_change_password": false,
"password_reuse_prevention": 12,
"max_password_age": 60
},
"s3_account_public_access_block": {
"enabled": true,
"block_public_acls": false,
"block_public_policy": false,
"ignore_public_acls": false,
"restrict_public_buckets": false
}
}
}
}
},
{
"account_name": "aws-example-legacy-test",
"account_email": "accounts+aws-example-legacy-test@example.com",
"ou_path": "/root/workloads/test",
"close_on_deletion": true,
"account_tags": {
"AccountOwner": "owner@example.com",
"AccountType": "workload",
"ManagedBy": "OpenTofu"
},
"alternate_contacts": [
{
"type": "SECURITY",
"name": "Security Contact",
"email_address": "security@example.com"
}
],
"customer_values": {
"account_baseline_overrides": {
"aws_config": {
"config_recording_mode": {
"exclusion_by_resource_types": [
"AWS::EC2::NetworkInterface",
"AWS::EC2::Volume",
"AWS::Config::ResourceCompliance"
]
}
}
}
}
}
]
The first account spells out every control and attribute so the full shape is visible. Nothing is mandatory: name only the attributes that should differ and omit the rest, as the second account does. Anything left out keeps its scope value, and enabled only needs to be set when exempting the account from a control.