Check AWS Config Is Recording in Every Region

Blue and yellow network cables plugged into a row of switch ports in a data centre

Photo by Gavin Allanwood on Unsplash

To check AWS Config is enabled in all Regions, call DescribeConfigurationRecorders and DescribeConfigurationRecorderStatus in every enabled Region and confirm a recorder exists with recording true and no failed last status. Then call DescribeDeliveryChannels, because a recorder can’t start without a delivery channel, and check that IAM global resource types are recorded in exactly one Region.

AWS Config is a Regional service. Turning it on in the console for your main Region leaves every other Region without a configuration history, and those are the Regions where an unexpected resource is most likely to go unnoticed. This example is for engineers who need to check AWS Config is enabled in all Regions and prove it for an audit, not just for the Region they usually work in.

You’ll get a read-only TypeScript script for the AWS SDK for JavaScript v3 that prints one row per Region, in the same format as the other AWS SDK v3 security audit scripts. It completes the set of account-wide checks with the scripts to check CloudTrail is enabled and logging in every AWS Region and check GuardDuty is enabled in every AWS Region: CloudTrail records who called what, GuardDuty flags suspicious activity, and Config records what each resource looked like before and after.

What does “AWS Config is enabled” actually mean in a Region?

There’s no single switch. The script treats a Region as ok only when all of these hold:

  • A customer managed configuration recorder exists. You can have one per account per Region. It stores changes to the resource types in scope as configuration items (CIs).
  • It is recording, and its last status isn’t Failure. A stopped recorder keeps its settings but records nothing, which also means deleted resources can leave stale compliance results behind.
  • A delivery channel exists and delivers. The channel names the S3 bucket for configuration history and snapshots. StartConfigurationRecorder fails with NoAvailableDeliveryChannelException without one.
  • It records all supported resource types. A recorder limited to a list of types, or all types with exclusions, is reported as partial scope rather than as a pass.

The CIS Amazon Web Services Foundations Benchmark asks for this in every Region; AWS Security Hub maps its control Config.1 to recommendation 3.3 in CIS v5.0.0 and v3.0.0.

Why record IAM global resources in only one Region?

IAM users, groups, roles and customer managed policies are global. A recorder that uses the all supported types strategy excludes them unless includeGlobalResourceTypes is true, and they can only be recorded in Regions where AWS Config was available before February 2022. Recording them in several Regions produces the same CIs more than once, and you pay per CI. The Security Hub documentation recommends recording them in a single home Region, so the script prints a warning when it finds none, or more than one. Once IAM resources are recorded, Config shows when a policy changed; the script to find IAM policies that grant admin access shows which ones are too broad today.

What about service-linked recorders?

Some AWS services, including Security Hub, create a service-linked configuration recorder of their own. It is always recording and is managed by that service, and its recording scope decides whether its CIs are free (INTERNAL) or billed (PAID). The script counts these per Region but doesn’t treat one as a replacement for your own recorder, because an INTERNAL recorder doesn’t deliver CIs to your delivery channel.

What does the script do?

  1. Lists RegionsDescribeRegions for the enabled Regions, or --regions=.
  2. Reads the customer managed recorderDescribeConfigurationRecorders and DescribeConfigurationRecorderStatus with no name, which return the customer managed recorder if there is one.
  3. Works out the recording scopeFrom recordingGroup: all types, all with exclusions, or an inclusion list, plus whether IAM global types are in scope and the recording frequency.
  4. Checks deliveryDescribeDeliveryChannels and DescribeDeliveryChannelStatus for the bucket and the last configuration history delivery.
  5. Counts service-linked recordersListConfigurationRecorders, keeping the entries that have a servicePrincipal.

Prerequisites

Which IAM permissions does it need?

config-coverage-policy.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ReadConfigCoverage",
      "Effect": "Allow",
      "Action": [
        "ec2:DescribeRegions",
        "config:DescribeConfigurationRecorders",
        "config:DescribeConfigurationRecorderStatus",
        "config:DescribeDeliveryChannels",
        "config:DescribeDeliveryChannelStatus",
        "config:ListConfigurationRecorders"
      ],
      "Resource": "*"
    }
  ]
}

All six actions are read-only. The free IAM policy generator for TypeScript SDK code builds the same list from the script’s imports.

The script to check AWS Config is enabled in all Regions

check-aws-config-enabled-in-all-regions.ts

// check-aws-config-enabled-in-all-regions.ts
// Reports, for every enabled Region, whether the customer managed AWS Config recorder exists, is
// recording, what it records (all types, exclusions or a list; IAM global types; frequency) and
// whether a delivery channel is delivering. Also counts service-linked recorders. Read-only.
// Usage: npx tsx check-aws-config-enabled-in-all-regions.ts [--regions=us-east-1,eu-west-1]
import { EC2Client, DescribeRegionsCommand } from "@aws-sdk/client-ec2";
import {
  ConfigServiceClient,
  DescribeConfigurationRecordersCommand,
  DescribeConfigurationRecorderStatusCommand,
  DescribeDeliveryChannelsCommand,
  DescribeDeliveryChannelStatusCommand,
  paginateListConfigurationRecorders,
  type RecordingGroup,
} from "@aws-sdk/client-config-service";

const regionArg = process.argv.slice(2).find((a) => a.startsWith("--regions="))?.split("=")[1];
const IAM_TYPES: string[] = ["AWS::IAM::User", "AWS::IAM::Group", "AWS::IAM::Role", "AWS::IAM::Policy"];

interface Row {
  Region: string;
  Recorder: string;
  Recording: string;
  Scope: string;
  IamGlobal: boolean;
  Frequency: string;
  Channel: string;
  LastDelivery: string;
  ServiceLinked: number;
  Verdict: string;
}

async function listRegions(): Promise<string[]> {
  if (regionArg) return regionArg.split(",").map((r) => r.trim()).filter(Boolean);
  const out = await new EC2Client({}).send(new DescribeRegionsCommand({})); // enabled Regions only
  return (out.Regions ?? []).map((r) => r.RegionName ?? "").filter(Boolean).sort();
}

// Works out the effective recording strategy, including the older allSupported-only form.
function describeScope(g: RecordingGroup | undefined): { scope: string; full: boolean; iam: boolean } {
  const strategy = g?.recordingStrategy?.useOnly ?? (g?.allSupported ? "ALL_SUPPORTED_RESOURCE_TYPES" : "INCLUSION_BY_RESOURCE_TYPES");
  if (strategy === "ALL_SUPPORTED_RESOURCE_TYPES") {
    return { scope: "all types", full: true, iam: g?.includeGlobalResourceTypes === true };
  }
  if (strategy === "EXCLUSION_BY_RESOURCE_TYPES") {
    const excluded: string[] = g?.exclusionByResourceTypes?.resourceTypes ?? [];
    return { scope: `all except ${excluded.length}`, full: false, iam: IAM_TYPES.some((t) => !excluded.includes(t)) };
  }
  const included: string[] = g?.resourceTypes ?? [];
  return { scope: `only ${included.length} types`, full: false, iam: IAM_TYPES.some((t) => included.includes(t)) };
}

async function checkRegion(region: string): Promise<Row> {
  const cfg = new ConfigServiceClient({ region });
  // Without a name, these calls return the customer managed recorder (at most one per Region).
  const recorder = (await cfg.send(new DescribeConfigurationRecordersCommand({}))).ConfigurationRecorders?.[0];
  const status = (await cfg.send(new DescribeConfigurationRecorderStatusCommand({}))).ConfigurationRecordersStatus?.[0];
  const channel = (await cfg.send(new DescribeDeliveryChannelsCommand({}))).DeliveryChannels?.[0];
  const channelStatus = (await cfg.send(new DescribeDeliveryChannelStatusCommand({}))).DeliveryChannelsStatus?.[0];

  let serviceLinked = 0;
  for await (const page of paginateListConfigurationRecorders({ client: cfg }, {})) {
    serviceLinked += (page.ConfigurationRecorderSummaries ?? []).filter((r) => r.servicePrincipal).length;
  }

  const { scope, full, iam } = describeScope(recorder?.recordingGroup);
  const history = channelStatus?.configHistoryDeliveryInfo;
  const lastDelivery = history?.lastStatus ?? "-";
  const verdict = !recorder ? "NOT ENABLED"
    : !status?.recording ? "RECORDER STOPPED"
    : status.lastStatus === "Failure" ? `FAILING: ${status.lastErrorCode ?? "unknown error"}`
    : !channel ? "NO DELIVERY CHANNEL"
    : lastDelivery === "Failure" ? `DELIVERY FAILING: ${history?.lastErrorCode ?? "unknown"}`
    : !full ? "recording, partial scope"
    : "ok";

  return {
    Region: region,
    Recorder: recorder?.name ?? "-",
    Recording: recorder ? (status?.recording ? "yes" : "NO") : "-",
    Scope: recorder ? scope : "-",
    IamGlobal: recorder !== undefined && iam,
    Frequency: recorder?.recordingMode?.recordingFrequency ?? (recorder ? "not set" : "-"),
    Channel: channel?.s3BucketName ?? "-",
    LastDelivery: lastDelivery,
    ServiceLinked: serviceLinked,
    Verdict: verdict,
  };
}

async function main(): Promise<void> {
  const rows: Row[] = [];
  for (const region of await listRegions()) {
    try {
      rows.push(await checkRegion(region));
    } catch (err) {
      rows.push({ Region: region, Recorder: "?", Recording: "?", Scope: "?", IamGlobal: false, Frequency: "?", Channel: "?", LastDelivery: "?", ServiceLinked: 0, Verdict: `error: ${err instanceof Error ? err.name : String(err)}` });
    }
  }
  console.table(rows);

  const gaps = rows.filter((r) => r.Verdict !== "ok");
  const iamRegions = rows.filter((r) => r.IamGlobal && r.Recording === "yes").map((r) => r.Region);
  console.log(`${rows.length} Regions checked; ${gaps.length} not recording everything.`);
  if (iamRegions.length === 0) console.log("IAM global resource types are not recorded in any Region.");
  if (iamRegions.length > 1) console.log(`IAM global resource types are recorded in ${iamRegions.length} Regions (${iamRegions.join(", ")}): one is enough.`);
  console.log("Read-only: nothing was changed.");
  if (gaps.length) process.exitCode = 2;
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});

How do you run it?

Terminal

npm install @aws-sdk/client-config-service @aws-sdk/client-ec2
npm install --save-dev tsx typescript

AWS_PROFILE=readonly npx tsx check-aws-config-enabled-in-all-regions.ts

# Fail a scheduled job if any of these Regions has a gap
AWS_PROFILE=readonly npx tsx check-aws-config-enabled-in-all-regions.ts --regions=us-east-1,eu-west-1 || echo "AWS Config gap found"

Sample output

Output

┌─────────┬──────────────────┬───────────┬───────────┬─────────────────┬───────────┬──────────────┬────────────────────────────┬──────────────┬───────────────┬───────────────────────┐
│ (index) │ Region           │ Recorder  │ Recording │ Scope           │ IamGlobal │ Frequency    │ Channel                    │ LastDelivery │ ServiceLinked │ Verdict               │
├─────────┼──────────────────┼───────────┼───────────┼─────────────────┼───────────┼──────────────┼────────────────────────────┼──────────────┼───────────────┼───────────────────────┤
│ 0       │ 'ap-southeast-2' │ '-'       │ '-'       │ '-'             │ false     │ '-'          │ '-'                        │ '-'          │ 0             │ 'NOT ENABLED'         │
│ 1       │ 'eu-west-1'      │ 'default' │ 'yes'     │ 'all types'     │ true      │ 'CONTINUOUS' │ 'config-logs-123456789012' │ 'Success'    │ 1             │ 'ok'                  │
│ 2       │ 'us-east-1'      │ 'default' │ 'yes'     │ 'all types'     │ true      │ 'CONTINUOUS' │ 'config-logs-123456789012' │ 'Success'    │ 1             │ 'ok'                  │
│ 3       │ 'us-east-2'      │ 'default' │ 'NO'      │ 'all types'     │ false     │ 'DAILY'      │ 'config-logs-123456789012' │ 'Success'    │ 0             │ 'RECORDER STOPPED'    │
│ 4       │ 'us-west-2'      │ 'default' │ 'yes'     │ 'only 12 types' │ false     │ 'CONTINUOUS' │ '-'                        │ '-'          │ 0             │ 'NO DELIVERY CHANNEL' │
└─────────┴──────────────────┴───────────┴───────────┴─────────────────┴───────────┴──────────────┴────────────────────────────┴──────────────┴───────────────┴───────────────────────┘
5 Regions checked; 3 not recording everything.
IAM global resource types are recorded in 2 Regions (eu-west-1, us-east-1): one is enough.
Read-only: nothing was changed.

The account is illustrative, and every row is a common pattern. ap-southeast-2 was never set up. us-east-2 was stopped, and us-west-2 records a short list of types and has lost its delivery channel. IAM global types are recorded twice, which doubles those CIs for no benefit.

What does AWS Config cost to leave on everywhere?

You’re charged per configuration item recorded by the customer managed recorder, plus rule and conformance pack evaluations if you use them. As of September 2026, the AWS Config pricing page and the us-east-1 price list show:

Item (us-east-1) Price
Continuous recording $0.003 per configuration item
Daily (periodic) recording $0.012 per configuration item
Config rule evaluations, first 100,000 $0.001 per evaluation

A quiet Region with little change costs very little. As a worked example, a Region that produces 20,000 continuous CIs in a month costs 20,000 × $0.003 = $60. Daily recording produces at most one CI per resource per 24 hours, and only if its state changed, so at four times the unit price it only saves money on resources that change more than four times a day. The script to get last month’s AWS cost broken down by service shows what Config really costs you after a month.

How do you fix a gap?

The script doesn’t change anything, because enabling Config needs a role, a bucket and a decision about scope. The usual fixes:

  • No recorder. Create one with PutConfigurationRecorder, a delivery channel with PutDeliveryChannel, then call StartConfigurationRecorder. Use the AWS Config service-linked role; Security Hub’s Config.1 fails by default if you don’t.
  • Stopped recorder. StartConfigurationRecorder with its name, once you know why it was stopped.
  • Many accounts. AWS Systems Manager Quick Setup can create the customer managed recorder across organizational units and Regions. Quick Setup can also turn on Default Host Management Configuration for Systems Manager, and the script to find EC2 instances not managed by Systems Manager shows which instances still don’t register.
  • Ownership questions. Config records state; it doesn’t assign owners. The script to find untagged AWS resources with the tagging API is still the quicker answer for ownership questions.

Troubleshooting

  • AccessDeniedException in some Regions. An SCP that blocks unused Regions also blocks these calls there. The guide to troubleshoot AWS IAM access denied errors helps separate an SCP deny from a missing permission.
  • FAILING with an error code. The recorder’s last recording attempt failed. The error code usually points at the recorder’s IAM role; compare it with the AWS Config service-linked role.
  • DELIVERY FAILING. The bucket was deleted, its KMS key isn’t usable, or its policy changed. Recording and delivery have separate statuses, so the recorder can look healthy while the history in S3 has a hole.
  • Security Hub says Config.1 passes but the script says NOT ENABLED. With Security Hub CSPM and Security Hub both enabled, Config.1 always passes because Security Hub records through its own service-linked recorder. Your own recorder is still missing. To see where Security Hub CSPM itself is on, run the script to check Security Hub is enabled in all AWS Regions.

Ask ChatWithCloud instead

For one Region, ask ChatWithCloud “Is AWS Config recording here, and what resource types does it record?” It writes AWS SDK for JavaScript v2 code, runs it locally with your AWS profile and explains the answer. It works in one profile and Region per session, so the script is the better tool for a sweep across all Regions. It runs generated code without a confirmation step, so connect ChatWithCloud to your AWS account with a read-only profile; the guide to analyze your AWS security posture with an AI CLI suggests follow-up questions, and the ChatWithCloud security model explains what leaves your machine.

Frequently asked questions

How do I check if AWS Config is enabled in all Regions?

In each Region, check that DescribeConfigurationRecorderStatus returns a recorder with recording true and that DescribeDeliveryChannels returns a channel. The console shows only the current Region.

Can I have more than one AWS Config recorder per Region?

You can have one customer managed recorder per account per Region. Service-linked recorders created by other AWS services exist alongside it.

Should global resources be recorded in every Region?

No. Record the IAM global resource types in one Region that supports them, and turn includeGlobalResourceTypes off elsewhere to avoid duplicate CIs.

Is AWS Config free?

No. You pay per configuration item recorded by your customer managed recorder, and per rule evaluation. Some service-linked recorders record for free.

Related guides

Ask your AWS account in plain English

Your first 15 runs are free, with no OpenAI key needed.

npx chatwithcloud