Guide · AWS Compliance

AWS Config Managed Rules for MCP Servers

AWS Config managed rules give you detective compliance enforcement across every resource your MCP server touches — DynamoDB tables, ECS task definitions, Lambda functions, VPCs, and IAM roles — without writing a single Lambda evaluator. Three things teams consistently get wrong: Config rules are detective, not preventive (a non-compliant resource gets flagged after it exists, not blocked at creation — pair with SCPs for hard blocks), Config recording must be enabled before rules can evaluate anything (rules created before the recorder is on will stay in NO_CONFIGURATION_ITEMS_SEEN state indefinitely), and the recording scope defaults to all resource types which generates configuration items and storage costs for every EC2 metadata change, not just the resources your MCP server actually owns — scope it to a resource type allowlist from day one.

TL;DR

Enable the Config recorder scoped to your resource types (DynamoDB, ECS, Lambda, EC2, IAM, VPC). Deploy four managed rules: DYNAMODB_TABLE_ENCRYPTION_ENABLED, VPC_FLOW_LOGS_ENABLED, ROOT_ACCOUNT_MFA_ENABLED, and REQUIRED_TAGS. Wire auto-remediation to SSM Automation for the encryption and tagging rules. Use SCPs to prevent creation of unencrypted resources in the first place — Config rules then serve as your continuous drift detector rather than your only enforcement layer.

Config rules vs SCPs — detective vs preventive

AWS Config rules evaluate the current state of your resources and mark them COMPLIANT or NON_COMPLIANT. They run after the resource exists. A developer who creates an unencrypted DynamoDB table will see a NON_COMPLIANT finding in the Config console within minutes — but the table is already live and potentially receiving data. If your compliance requirement is "this must never exist," use a Service Control Policy (SCP) in AWS Organizations to prevent dynamodb:CreateTable unless the request includes SSESpecification.SSEType=KMS.

The correct layering for MCP server compliance is: SCPs prevent (hard blocks at the organization/OU level that cannot be overridden even by account root), Config rules detect (continuous evaluation and drift detection across all existing resources), and SSM Automation remediates (auto-fix non-compliant resources that slipped through or existed before the SCP was applied). Config rules are also useful in accounts where you cannot apply SCPs — member accounts under a delegated admin, accounts in a commercial-to-GovCloud peering setup, or sandbox accounts intentionally excluded from organizational SCPs.

Control typeWhen it firesCan block creation?Covers drift?Best for
SCP At API call time (IAM evaluation) Yes — denies the API call No — only new operations Hard organizational guardrails (never allow root access keys)
Config managed rule On resource change or periodically No — evaluates after creation Yes — re-evaluates existing resources Drift detection, compliance reporting, auto-remediation triggers
IAM permission boundary At API call time (IAM evaluation) Yes — restricts max permissions No Per-role capability ceilings in multi-team accounts
SSM Automation remediation Triggered by Config NON_COMPLIANT event No Yes — fixes existing resources Auto-healing: encrypt table, add tag, enable flow logs

Enabling the Config recorder with a scoped resource type list

By default aws configservice put-configuration-recorder records all supported resource types in the region. For a typical MCP server account this means configuration items for hundreds of resource types you don't own — generating storage in S3 and query costs in Config advanced queries. Define an explicit includeGlobalResourceTypes: false recording group with an resourceTypes allowlist covering only what your MCP infrastructure uses.

import * as cdk from "aws-cdk-lib";
import * as config from "aws-cdk-lib/aws-config";
import * as iam from "aws-cdk-lib/aws-iam";
import * as s3 from "aws-cdk-lib/aws-s3";

export class McpConfigRecorderStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string) {
    super(scope, id);

    // Delivery channel — Config writes configuration snapshots here
    const deliveryBucket = new s3.Bucket(this, "ConfigDelivery", {
      bucketName: `mcp-config-delivery-${this.account}`,
      encryption: s3.BucketEncryption.S3_MANAGED,
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
      lifecycleRules: [{ expiration: cdk.Duration.days(365) }],
    });

    // Config recorder IAM role — must be able to read all resources it records
    const recorderRole = new iam.Role(this, "ConfigRecorderRole", {
      assumedBy: new iam.ServicePrincipal("config.amazonaws.com"),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWS_ConfigRole"),
      ],
    });

    // Scoped recorder — only the resource types your MCP server uses
    // Avoids recording EC2 metadata changes, Elastic Load Balancer listener rules, etc.
    const recorder = new config.CfnConfigurationRecorder(this, "McpRecorder", {
      name: "mcp-config-recorder",
      roleArn: recorderRole.roleArn,
      recordingGroup: {
        allSupported: false,
        includeGlobalResourceTypes: false, // IAM recorded separately — see note below
        resourceTypes: [
          "AWS::DynamoDB::Table",
          "AWS::Lambda::Function",
          "AWS::ECS::Cluster",
          "AWS::ECS::TaskDefinition",
          "AWS::ECS::Service",
          "AWS::EC2::VPC",
          "AWS::EC2::Subnet",
          "AWS::EC2::SecurityGroup",
          "AWS::EC2::FlowLog",
          "AWS::IAM::Role",          // global resource type — requires includeGlobalResourceTypes:true
          "AWS::IAM::Policy",        // or record IAM separately in us-east-1 only
          "AWS::SecretsManager::Secret",
          "AWS::SSM::Parameter",
          "AWS::CloudTrail::Trail",
          "AWS::KMS::Key",
        ],
      },
    });

    // NOTE: IAM is a global service. If includeGlobalResourceTypes is false,
    // AWS::IAM::Role and AWS::IAM::Policy will not be recorded even if listed.
    // To record IAM resources, set includeGlobalResourceTypes:true in exactly
    // ONE region (us-east-1 recommended) and false in all others.
    // Alternatively: use a separate Config recorder in us-east-1 with
    // includeGlobalResourceTypes:true and allSupported:false.

    new config.CfnDeliveryChannel(this, "DeliveryChannel", {
      name: "mcp-config-delivery",
      s3BucketName: deliveryBucket.bucketName,
      configSnapshotDeliveryProperties: {
        deliveryFrequency: "Six_Hours", // snapshot frequency — separate from change recording
      },
    });
  }
}

The four managed rules every MCP server account needs

1. DYNAMODB_TABLE_ENCRYPTION_ENABLED

Marks any DynamoDB table as NON_COMPLIANT if SSEDescription.Status is not ENABLED. This rule has no configuration parameters — it fires on any table without server-side encryption. Note that DynamoDB encrypts all data at rest by default using AWS-owned keys as of 2018; this rule flags tables that are missing encryption only if they predate that change or were created via legacy CLI flags. What the rule does not check: it does not verify that a customer-managed KMS key is used rather than the AWS-managed default key (aws/dynamodb). For customer-managed key enforcement, use the custom rule approach with a Lambda evaluator that checks SSEDescription.KMSMasterKeyArn.

// CDK: deploy DYNAMODB_TABLE_ENCRYPTION_ENABLED with auto-remediation
import * as config from "aws-cdk-lib/aws-config";

new config.ManagedRule(this, "DynamoDbEncryptionRule", {
  identifier: config.ManagedRuleIdentifiers.DYNAMO_DB_TABLE_ENCRYPTION_ENABLED,
  configRuleName: "mcp-dynamodb-encryption-enabled",
  // No inputParameters needed — this rule has no configurable parameters
  // Scope: evaluate every DynamoDB table in the account
});

// IMPORTANT: DYNAMODB_TABLE_ENCRYPTION_ENABLED is a configuration-change-triggered rule.
// It fires when any DynamoDB table configuration item is created or modified.
// New tables get evaluated within ~5 minutes of creation.
// Existing tables at rule creation time are evaluated in the initial compliance run (~1 hour).

2. VPC_FLOW_LOGS_ENABLED

Marks a VPC as NON_COMPLIANT if it has no flow log with FlowLogStatus: ACTIVE. Configuration parameters let you specify the required traffic type (TRAFFIC_TYPE: ALL | ACCEPT | REJECT) and delivery destination (S3 | CloudWatch Logs | Kinesis Data Firehose). A common mis-configuration: creating a flow log that is ACTIVE at the flow log resource level but has a delivery failure because the destination S3 bucket policy does not allow the VPC flow log service principal — Config marks the VPC as COMPLIANT because the flow log resource exists and is ACTIVE, but logs never actually land in S3.

new config.ManagedRule(this, "VpcFlowLogsRule", {
  identifier: config.ManagedRuleIdentifiers.VPC_FLOW_LOGS_ENABLED,
  configRuleName: "mcp-vpc-flow-logs-enabled",
  inputParameters: {
    trafficType: "ALL",              // reject-only misses successful lateral movement
    deliverLogsPermissionArn: "",   // leave empty — rule checks existence, not delivery health
  },
});
// NOTE: this rule only verifies that a flow log resource exists and is ACTIVE.
// It does NOT verify that logs are actually delivered successfully.
// Separately monitor the flow log CloudWatch metric FlowLogsDeliverySuccessPercent
// or check S3 bucket growth to confirm delivery is actually working.

3. ROOT_ACCOUNT_MFA_ENABLED

Marks the account as NON_COMPLIANT if the AWS account root user does not have MFA enabled. This is a periodic rule (runs every 24 hours) not a configuration-change rule — there is no resource configuration item for the root user's MFA status that Config can watch for changes. Auto-remediation is not applicable for this rule since enabling root MFA requires a human to physically touch the root credentials. Use this rule to generate a finding that gets picked up by Security Hub and routed to your team as a ticket.

new config.ManagedRule(this, "RootMfaRule", {
  identifier: config.ManagedRuleIdentifiers.ROOT_ACCOUNT_MFA_ENABLED,
  configRuleName: "mcp-root-mfa-enabled",
  maximumExecutionFrequency: config.MaximumExecutionFrequency.TWENTY_FOUR_HOURS,
  // Periodic rule — fires every 24 hours, not on resource change
  // Does NOT support auto-remediation (no automated way to enable root MFA)
  // Wire to Security Hub custom insight: "accounts without root MFA"
});

4. REQUIRED_TAGS

Marks any tagged resource as NON_COMPLIANT if it is missing one or more required tags. Supports up to six tag key/value pairs as input parameters. For MCP servers, require at minimum: Environment (prod/staging/dev), Service (the MCP server name), and Owner (the team email). This rule evaluates the 30+ resource types listed in its documentation — DynamoDB, Lambda, ECS, EC2, RDS, etc. — so a single rule covers your entire infrastructure.

new config.ManagedRule(this, "RequiredTagsRule", {
  identifier: config.ManagedRuleIdentifiers.REQUIRED_TAGS,
  configRuleName: "mcp-required-tags",
  inputParameters: {
    tag1Key: "Environment",
    tag1Value: "prod,staging,dev",      // comma-separated allowed values; empty = any value allowed
    tag2Key: "Service",                 // MCP server name — no value restriction
    tag3Key: "Owner",                   // team email — no value restriction
    // tag4Key-tag6Key available for additional required tags
  },
});
// IMPORTANT: REQUIRED_TAGS fires on configuration change for each resource type it covers.
// Resources created before the rule was deployed are NOT evaluated until their
// configuration changes again (e.g., a tag is added/removed) or you call
// start-config-rules-evaluation to force a re-evaluation.
// Force re-evaluation of all existing resources after deploying this rule:
// aws configservice start-config-rules-evaluation --config-rule-names mcp-required-tags

Config recording scope for IAM global resources

IAM is a global service — IAM roles, policies, and users are not region-specific. AWS Config records IAM resources only when includeGlobalResourceTypes: true is set in the recorder. If you have Config recorders in multiple regions, setting includeGlobalResourceTypes: true in all of them means IAM configuration items are recorded once per region, multiplying your storage costs and evaluation count without adding value. The recommended pattern: enable includeGlobalResourceTypes: true in exactly one region (typically us-east-1 since that's where IAM API calls land) and false in all other regions. Then deploy IAM-specific Config rules only in us-east-1.

# Verify which region is recording global resources
aws configservice describe-configuration-recorders \
  --query "ConfigurationRecorders[*].{Name:name,AllSupported:recordingGroup.allSupported,GlobalResources:recordingGroup.includeGlobalResourceTypes}" \
  --output table --region us-east-1

# Check if IAM resources are being recorded at all
aws configservice get-resource-config-history \
  --resource-type AWS::IAM::Role \
  --resource-id arn:aws:iam::123456789012:role/mcp-execution-role \
  --region us-east-1  # Must query us-east-1 for IAM resources

# Force re-evaluation of all in-scope resources for a specific rule
aws configservice start-config-rules-evaluation \
  --config-rule-names mcp-required-tags mcp-dynamodb-encryption-enabled \
  --region us-east-1

Wiring Config rules to Security Hub

AWS Security Hub ingests Config rule findings automatically when the SecurityHub integration is enabled. Each NON_COMPLIANT finding from Config appears as a Security Hub finding with ProductFields.RuleId set to the Config rule name. This lets you build Security Hub custom insights to track compliance trends over time, and route findings to Jira, Slack, or PagerDuty via EventBridge rules on Security Hub finding events.

# Enable Security Hub + Config integration (one-time per account per region)
aws securityhub enable-security-hub --enable-default-standards
# Config findings flow automatically once Security Hub is enabled

# EventBridge rule to route critical Config findings to Slack
# (captures NON_COMPLIANT findings for your highest-severity rules)
cat > config-finding-rule.json <<'EOF'
{
  "source": ["aws.securityhub"],
  "detail-type": ["Security Hub Findings - Imported"],
  "detail": {
    "findings": {
      "ProductFields": {
        "RuleId": ["mcp-root-mfa-enabled", "mcp-dynamodb-encryption-enabled"]
      },
      "Compliance": {
        "Status": ["FAILED"]
      }
    }
  }
}
EOF

aws events put-rule \
  --name mcp-config-critical-findings \
  --event-pattern file://config-finding-rule.json \
  --state ENABLED

Checking compliance status programmatically

Use describe-compliance-by-config-rule to get aggregate compliance counts per rule, and get-compliance-details-by-config-rule to get the individual non-compliant resources. The latter returns a paginated list of EvaluationResult objects with the resource ID, resource type, compliance type, and timestamp of the last evaluation — useful for building a compliance dashboard or feeding into a weekly report.

import {
  ConfigServiceClient,
  DescribeComplianceByConfigRuleCommand,
  GetComplianceDetailsByConfigRuleCommand,
} from "@aws-sdk/client-config-service";

const client = new ConfigServiceClient({ region: "us-east-1" });

async function getMcpComplianceSummary(): Promise<void> {
  // Aggregate counts: how many resources are compliant vs non-compliant per rule
  const summary = await client.send(
    new DescribeComplianceByConfigRuleCommand({
      ConfigRuleNames: [
        "mcp-dynamodb-encryption-enabled",
        "mcp-vpc-flow-logs-enabled",
        "mcp-required-tags",
        "mcp-root-mfa-enabled",
      ],
    })
  );

  for (const rule of summary.ComplianceByConfigRules ?? []) {
    const counts = rule.Compliance?.ComplianceContributorCount;
    console.log(`${rule.ConfigRuleName}: ${rule.Compliance?.ComplianceType}`, counts);
  }

  // Get specific non-compliant resources for the tagging rule
  let nextToken: string | undefined;
  do {
    const details = await client.send(
      new GetComplianceDetailsByConfigRuleCommand({
        ConfigRuleName: "mcp-required-tags",
        ComplianceTypes: ["NON_COMPLIANT"],
        NextToken: nextToken,
      })
    );
    for (const result of details.EvaluationResults ?? []) {
      const id = result.EvaluationResultIdentifier?.EvaluationResultQualifier;
      console.log(
        `NON_COMPLIANT: ${id?.ResourceType} / ${id?.ResourceId} — missing required tags`
      );
    }
    nextToken = details.NextToken;
  } while (nextToken);
}

Failure modes and common mistakes

SymptomRoot causeFix
Config rule stuck in NO_CONFIGURATION_ITEMS_SEEN Config recorder not enabled before rule was created, or recorder scope doesn't include the rule's resource type Enable the recorder first, then create rules; verify resource type is in the recorder's resourceTypes list
IAM rules show NO_CONFIGURATION_ITEMS_SEEN in all regions includeGlobalResourceTypes: false in all recorders — no region is recording IAM Set includeGlobalResourceTypes: true in the us-east-1 recorder; deploy IAM rules only in us-east-1
REQUIRED_TAGS rule not evaluating existing resources Configuration-change rules only evaluate resources when they change; existing resources aren't re-evaluated at rule creation Run start-config-rules-evaluation to force an initial evaluation of all in-scope resources
VPC_FLOW_LOGS_ENABLED rule shows COMPLIANT but logs not arriving in S3 Flow log is ACTIVE but S3 bucket policy doesn't allow the flow logs delivery service principal — delivery silently fails Check S3 bucket policy includes delivery.logs.amazonaws.com as principal; verify bucket growth or add CloudWatch delivery metrics
Config recorder generating unexpectedly high S3 costs allSupported: true recording all resource types including high-frequency EC2 metadata changes Switch to explicit resourceTypes allowlist; disable recording for EC2 instance metadata, autoscaling groups, and other high-churn resource types
DYNAMODB_TABLE_ENCRYPTION_ENABLED marks new tables as NON_COMPLIANT DynamoDB default encryption uses AWS-owned keys (not AWS-managed KMS key); the rule requires SSE to be explicitly enabled on some older APIs Confirm your DynamoDB table creation path explicitly sets SSESpecification.Enabled: true; alternatively use a custom rule to check for CMK usage instead