Skip to main content
Back
DateRead13 min

How to Deploy AWS Declarative Policies with Terraform and the CLI

This page covers how to deploy DECLARATIVE_POLICY_EC2 policies using Terraform and the AWS CLI: enabling the policy type, writing and structuring the policy document, attaching it at the right scope, and validating enforcement. For the conceptual background on how declarative policies differ from SCPs and RCPs, see our post on cloud security controls and how they map to frameworks.

Key Takeaways

  • Deployment has three prerequisites that must be in place before a policy can attach: feature_set = "ALL" on the organization, the DECLARATIVE_POLICY_EC2 policy type enabled at the root, and an IAM identity with the correct permissions, including report generation permissions that most guides omit.
  • In Terraform, enabled_policy_types is a declarative list. If you add DECLARATIVE_POLICY_EC2 without including every already-active policy type, Terraform will disable the omitted ones on the next apply. This is the most operationally risky step in the deployment.
  • Declarative policies enforce configuration state at the service level, not through IAM. They govern service-linked roles and hold automatically when AWS introduces new APIs, but some attributes, IMDSv2 defaults among them, are prospective only and don't affect existing instances.
  • The account status report is the right tool to run before attaching org-wide. It shows the current configuration state across all accounts in scope so you can assess the blast radius before enforcement takes effect.

Prerequisites

Before deploying, confirm the following:

Terraform AWS Provider: >= 6.21.0, released November 2025. That is the release where DECLARATIVE_POLICY_EC2 became documented for aws_organizations_organization and aws_organizations_policy. The value validates against the AWS SDK enum rather than a provider-side allow list, so earlier versions may accept it, but 6.21.0 is the version to pin.

AWS Organizations configuration: Your organization must have feature_set = "ALL". Consolidated billing accounts cannot use policy types.

IAM permissions required: The identity running Terraform or the CLI commands needs the following permissions in the management account (or a delegated administrator account):

  • organizations:CreatePolicy
  • organizations:AttachPolicy
  • organizations:EnablePolicyType
  • organizations:ListPolicies
  • organizations:DescribeEffectivePolicy
  • ec2:StartDeclarativePoliciesReport
  • ec2:GetDeclarativePoliciesReportSummary

The last two are required to generate and retrieve the account status report, along with ec2:DescribeDeclarativePoliciesReports if you want to list existing reports. Note the ec2: prefix. The report operations belong to EC2, not to Organizations. If you scope your Terraform or CLI role to only the first five, the report commands in the Validation section will return AccessDeniedException.

For the account status report, AWS creates a service-linked role (AWSServiceRoleForDeclarativePoliciesEC2Report) in the management account automatically the first time you enable trusted access. You don't need to provision this role manually.

Trusted access: You must enable trusted access for EC2 before the policy can generate account status reports. This happens automatically when you enable the policy type through the console; if you're using the CLI, it requires a separate API call.

Where Declarative Policies Sit in the AWS Policy Stack

Understanding what declarative policies do differently from SCPs and RCPs helps you avoid deploying the wrong control for your intent.

Service Control Policies (SCP)Resource Control Policies (RCP)Declarative Policies
ControlsPrincipal permissions at the API levelResource access permissions at the API levelService-level configuration state
Governs service-linked roles?NoNoYes
Affects the management account?NoNoYes
Persists across new APIs?No. New API actions need explicit policy coverageNoYes. The configuration state holds automatically
Example use caseDeny ec2:CreateVpc outside approved regionsRequire encrypted connections to S3 bucketsEnforce IMDSv2 for all new EC2 instances org-wide
Where enforcedIAM evaluationIAM evaluationService control plane

The key distinction is that declarative policies enforce a configuration outcome at the service level, not a permission boundary. An SCP can deny the DisableSerialConsoleAccess API call, but a declarative policy makes the question moot by declaring what the serial console state must be.

Step 1: Enable the Policy Type in Your Organization

Declarative policies must be enabled at the organization root before you can attach them. In Terraform, this is managed on aws_organizations_organization.

Important: enabled_policy_types is a declarative list. If your organization already has SCPs or other policy types enabled and you manage aws_organizations_organization with Terraform, you must include every currently-enabled policy type in the list, not just DECLARATIVE_POLICY_EC2. Terraform will attempt to disable any type not present in the list on the next apply. This is covered in more detail in the Common Errors section below.

Terraform
# terraform >= 1.0 | hashicorp/aws >= 6.21.0

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 6.21.0"
    }
  }
}

resource "aws_organizations_organization" "org" {
  feature_set = "ALL"

  enabled_policy_types = [
    "SERVICE_CONTROL_POLICY",
    "DECLARATIVE_POLICY_EC2",
    # Add other policy types your org already uses
  ]
}

CLI equivalent:

bash
# Enable the policy type at the organization root
aws organizations enable-policy-type \
  --root-id $(aws organizations list-roots --query 'Roots[0].Id' --output text) \
  --policy-type DECLARATIVE_POLICY_EC2

# Enable trusted access for EC2 (required for account status report)
aws organizations enable-aws-service-access \
  --service-principal ec2.amazonaws.com

Step 2: Write the Policy Document

All EC2 declarative policies use ec2_attributes as the top-level JSON key. Under that key, you declare one or more service attributes. Each attribute that you configure becomes enforced across every account in scope.

The @@assign operator sets a value. When policies inherit across OUs, child policies can override parent values unless the parent uses @@operators_allowed_for_child_policies to restrict inheritance.

The following examples cover the three most commonly deployed configurations.

Enforcing IMDSv2 Organization-Wide

This is the most impactful single declarative policy most organizations deploy. Without it, new EC2 instances can launch with IMDSv1 enabled, which exposes instance credentials to SSRF-based attacks.

Before applying http_tokens_enforced: enabled in production, verify no existing instances in scope are making IMDSv1 calls. The AWS docs recommend running instance-metadata-transition-to-version-2 tooling first. Test the policy against a non-production OU before enforcing org-wide.
json
{
  "ec2_attributes": {
    "instance_metadata_defaults": {
      "http_tokens": {
        "@@assign": "required"
      },
      "http_put_response_hop_limit": {
        "@@assign": "2"
      },
      "http_endpoint": {
        "@@assign": "enabled"
      },
      "http_tokens_enforced": {
        "@@assign": "enabled"
      }
    }
  }
}

A hop limit of 2 is the minimum recommended value when http_tokens is required. A value of 1 can cause failures for containerized workloads that need to reach IMDS from inside a container.

Blocking VPC Public Access

This policy controls whether internet gateway traffic can reach VPCs in all accounts in scope. The mode key takes three values: off, block_ingress, and block_bidirectional. The choice between the two blocking modes matters operationally.

block_ingress blocks all inbound internet traffic through internet gateways. Outbound traffic through NAT gateways and egress-only internet gateways is still allowed. This is the right mode for environments that need outbound internet access (for package downloads, external API calls, and so on) but should never accept inbound connections from the internet.

block_bidirectional blocks all traffic in both directions through internet gateways and egress-only internet gateways. Use this for environments that are fully private and should have no internet reachability at all, even for outbound. NAT gateways are also blocked in this mode.

The example below uses block_ingress, which is the more common starting point:

json
{
  "ec2_attributes": {
    "vpc_block_public_access": {
      "internet_gateway_block": {
        "mode": {
          "@@assign": "block_ingress"
        },
        "exclusions_allowed": {
          "@@assign": "enabled"
        }
      }
    }
  }
}

Setting exclusions_allowed to enabled allows account owners to create subnet or VPC-level exclusions where legitimate public internet access is needed. Setting it to disabled makes the policy absolute with no account-level override path.

Blocking Public Sharing of EBS Snapshots

json
{
  "ec2_attributes": {
    "snapshot_block_public_access": {
      "state": {
        "@@assign": "block_all_sharing"
      }
    }
  }
}

block_all_sharing retroactively treats any already-public snapshots as private. block_new_sharing is less disruptive but leaves existing public snapshots exposed.

Step 3: Deploy with Terraform

Save your policy JSON to a file (for example, policies/declarative-ec2.json), then reference it in aws_organizations_policy. The attachment resource specifies where the policy applies: the organization root, an OU, or a specific account.

Terraform
# Create the policy
resource "aws_organizations_policy" "ec2_baseline" {
  name        = "ec2-baseline-controls"
  description = "Org-wide EC2 baseline: IMDSv2 enforcement, VPC BPA, EBS snapshot blocking"
  type        = "DECLARATIVE_POLICY_EC2"
  content     = file("${path.module}/policies/declarative-ec2.json")

  tags = {
    ManagedBy   = "terraform"
    Environment = "all"
  }
}

# Attach to the organization root (applies to all accounts)
data "aws_organizations_organization" "current" {}

resource "aws_organizations_policy_attachment" "ec2_baseline_root" {
  policy_id = aws_organizations_policy.ec2_baseline.id
  target_id = data.aws_organizations_organization.current.roots[0].id
}

To attach to a specific OU instead of the root, replace target_id with the OU ID:

Terraform
resource "aws_organizations_policy_attachment" "ec2_baseline_production_ou" {
  policy_id = aws_organizations_policy.ec2_baseline.id
  target_id = "ou-xxxx-yyyyyyyy"  # Replace with your OU ID
}

Step 4: AWS CLI Equivalent

If you're not using Terraform, the same deployment requires three CLI commands: create the policy, retrieve its ID, then attach it.

bash
# Create the policy
aws organizations create-policy \
  --type DECLARATIVE_POLICY_EC2 \
  --name "ec2-baseline-controls" \
  --description "Org-wide EC2 baseline: IMDSv2, VPC BPA, EBS snapshot blocking" \
  --content file://policies/declarative-ec2.json

# Capture the policy ID from the output
POLICY_ID=$(aws organizations list-policies \
  --filter DECLARATIVE_POLICY_EC2 \
  `--query 'Policies[?Name==`ec2-baseline-controls`].Id' `
  --output text)

# Retrieve the root ID
ROOT_ID=$(aws organizations list-roots \
  --query 'Roots[0].Id' \
  --output text)

# Attach to the organization root
aws organizations attach-policy \
  --policy-id $POLICY_ID \
  --target-id $ROOT_ID

Console Path

If you prefer to verify or troubleshoot through the console, navigate to AWS Organizations > Policies > Declarative policies. The console provides a visual editor that lets you configure each attribute without writing raw JSON. For initial assessment before enforcing, the console's Account status report is the most efficient way to see the current configuration across your organization before attaching.

Common Errors When Applying

enabled_policy_types accidentally disables existing policy types in Terraform

This is the most common failure mode when adding declarative policy support to an existing Terraform-managed organization. If aws_organizations_organization is already in your state and you add enabled_policy_types = ["DECLARATIVE_POLICY_EC2"] without including SERVICE_CONTROL_POLICY and any other currently-active types, Terraform will treat the omitted types as removals and disable them on apply. Before changing enabled_policy_types, run aws organizations list-roots and inspect the PolicyTypes array to confirm exactly which types are currently enabled, then include all of them in your Terraform list.

PolicyTypeNotEnabledException

The policy type isn't enabled on your organization root. Run aws organizations enable-policy-type with the root ID before attempting to create or attach a declarative policy. In Terraform, this means aws_organizations_organization needs to be applied before aws_organizations_policy.

AccessDeniedException on create-policy

The IAM identity doesn't have organizations:CreatePolicy or isn't operating from the management account or a delegated administrator account. Declarative policy management is restricted to these accounts by design.

InvalidInputException on policy content

The JSON structure is malformed or uses unsupported attribute keys. Common causes are using camelCase instead of snake_case for attribute names (the AWS API uses snake_case throughout), missing the @@assign operator, or specifying a value outside the allowed set (for example, setting http_tokens to "enforce" instead of "required").

State drift after infrastructure changes

If an account-level setting was modified after the policy was attached and before Terraform detected drift, terraform plan may show a no-op because the declarative policy itself hasn't changed. The effective configuration on individual accounts, however, will have been rolled back to the policy-defined state by the service automatically. This is expected behavior. Run describe-effective-policy on the affected account to confirm.

IMDSv2 enforcement failures on existing instances

Setting http_tokens_enforced to enabled causes launch failures if any in-scope AMIs or launch templates specify http_tokens = optional. Audit existing instances for IMDSv1 usage before enabling enforcement. AWS provides a CloudWatch metric (MetadataNoToken) and DescribeInstances filtering to identify affected instances.

Validating That the Policy Applied Correctly

After attaching, confirm enforcement is working at two levels: the policy is present, and the effective configuration matches your intent.

Verify the effective policy on a specific account:

bash
aws organizations describe-effective-policy \
  --policy-type DECLARATIVE_POLICY_EC2 \
  --target-id <account-id>

This returns the merged effective policy that account inherits from the organization root, any parent OUs, and any directly attached policies. If you see your attributes in the output, the policy is active.

Generate the account status report:

bash
aws ec2 start-declarative-policies-report \
  --target-id <root-id-or-ou-id> \
  --s3-bucket <report-bucket-name>

# List reports, then fetch the summary for the one you want
aws ec2 get-declarative-policies-report-summary \
  --report-id <report-id>

The status report shows whether each account is in a compliant state for each attribute. A high numberOfUnmatchedAccounts value for a given attribute means accounts exist where the current configuration doesn't yet match the enforced value. This is expected immediately after attachment while the service propagates enforcement; re-check after a few minutes.

How Native Connects Declarative Policies to Your Architecture

Deploying declarative policies org-wide is straightforward. The harder problem is knowing which attributes to enforce, which accounts need exclusions, and whether the policies you've deployed actually match the architectural intent behind them, not just the configuration state.

Native maps the controls you've deployed across AWS (including declarative policies, SCPs, RCPs, and permission boundaries) to the zones and baselines you've defined for your environment. If a declarative policy enforces IMDSv2 but your production zone has a baseline requirement that the policy doesn't yet cover, that gap surfaces in Native before it shows up in an audit. That's the shift from configuration management to architectural enforcement.

See how Native works in your environment.

FAQ

What's the difference between block_ingress and block_bidirectional for VPC Block Public Access, and how do I choose?

block_ingress prevents internet gateways from routing inbound traffic into VPCs and subnets in scope. Outbound traffic through NAT gateways and egress-only internet gateways is still permitted, so workloads that need to make outbound calls to external services or pull packages from the internet continue to work. This is the right mode for the majority of cloud environments.

block_bidirectional blocks all traffic through internet gateways in both directions, including NAT gateways and egress-only internet gateways. No traffic crosses the internet gateway boundary in any direction. Use this for environments that are fully private by requirement, where even outbound internet access is out of scope. Applying block_bidirectional without first auditing which workloads depend on NAT gateway outbound paths will cause disruption.

As a general rule, start with block_ingress and tighten to block_bidirectional for specific high-sensitivity OUs where you've confirmed no legitimate outbound internet dependency exists.

Does DECLARATIVE_POLICY_EC2 only cover EC2, or does it apply to other services too?

The name is slightly misleading. The policy type covers EC2, EBS, and VPC configurations as a single group. Specifically: VPC Block Public Access, VPC Encryption Controls, EC2 serial console access, EC2 AMI block public access, EC2 allowed images settings, EC2 instance metadata defaults (IMDSv2), and EBS snapshot block public access. Amazon continues to add attributes, which is one of the core advantages of declarative policies over SCPs: when new API operations are introduced for a covered attribute, the existing policy automatically governs them without any changes on your end.

Do declarative policies apply to existing resources, or only new ones?

It depends on the attribute. Some attributes apply immediately to all in-scope accounts regardless of existing state: VPC Block Public Access begins enforcing on existing VPCs, EBS snapshot blocking retroactively treats already-public snapshots as private when set to block_all_sharing, and serial console access takes effect immediately. Instance metadata defaults, by contrast, apply only to new EC2 instance launches. Existing instances that were launched with IMDSv1 remain unchanged until they're stopped and restarted or explicitly modified. Always check the AWS documentation for the specific attribute you're enforcing before attaching org-wide.

What happens when I detach a declarative policy?

The service rolls back the affected configuration to whatever state it was in before the policy was attached. This rollback is automatic. If an account's VPC Block Public Access setting was off before you attached a policy setting it to block_ingress, detaching the policy returns it to off. One important exception: VPC Encryption Controls rollback at the account level is automatic, but individual VPCs in enforce mode may not successfully transition back to their previous state depending on their current resource configuration. Check the AWS docs for the VPC Encryption Controls attribute specifically before detaching.

Does the declarative policy apply to the management account?

Yes, and this is where declarative policies differ from SCPs and RCPs, which never apply to the management account. A declarative policy attached at the organization root governs the management account too, so include it when you assess blast radius before attaching.

Can I combine multiple attributes in a single policy document?

Yes, and in most cases you should. You can include any combination of supported attributes under the ec2_attributes key in a single policy document. Combining them reduces the number of policies to manage and keeps your baseline expressed as a coherent unit rather than as a collection of separate single-attribute policies.

json
{
  "ec2_attributes": {
    "serial_console_access": {
      "status": {
        "@@assign": "disabled"
      }
    },
    "snapshot_block_public_access": {
      "state": {
        "@@assign": "block_all_sharing"
      }
    },
    "instance_metadata_defaults": {
      "http_tokens": {
        "@@assign": "required"
      },
      "http_put_response_hop_limit": {
        "@@assign": "2"
      }
    }
  }
}

Can member account administrators override a declarative policy once it's attached?

No. That's the architectural point of declarative policies. Once a policy is in effect for an account, the API operations that would modify the enforced configuration return errors for all principals in that account, including account administrators. The error message will say the action is denied due to an organizational policy. You can configure a custom error message and URL in the policy using the exception_message field under ec2_attributes, which lets you redirect users to an internal wiki or ticketing system to request exceptions through proper channels.

What are the @@append and @@remove operators, and when do I need them?

@@assign replaces the value outright. @@append adds items to a list without removing existing ones, and @@remove removes specific items from a list. These operators primarily matter for the allowed_images_settings attribute, which supports multi-criteria lists. For most attributes (serial console access, VPC BPA, IMDSv2, snapshot blocking), @@assign is all you need since those attributes take a single scalar value. If you're using @@append on a list attribute and a child OU policy uses @@assign, the child's value takes precedence and the parent's appended values are discarded for accounts in that OU.

How do I test enforcement before applying to the entire organization?

There are two approaches. First, use the account status report before attaching: it shows the current configuration for all accounts in scope so you can understand the blast radius of enforcement. Second, attach the policy to a single non-production OU or account first, verify the effective policy with describe-effective-policy, and confirm the expected API behavior before expanding to the organization root. The allowed_images_settings attribute also supports an audit_mode state that identifies noncompliant AMIs without blocking their use, which is useful for assessing readiness on that specific attribute before switching to enabled.

Ready to enforce secure-by-design?