I tried out the new Transit Gateway feature, Policy-Based Routing (PBR)

I tried out the new Transit Gateway feature, Policy-Based Routing (PBR)

On July 30, 2026, Transit Gateway Policy-Based Routing (PBR) became generally available. It is a feature that allows switching the route table referenced by packets based on conditions such as source, destination, protocol, and port. I verified the mutual exclusivity with route table associations, the behavior of match conditions, and the scope of CloudFormation support.
2026.07.31

This page has been translated by machine translation. View original

Introduction

On July 30, 2026, AWS Transit Gateway Policy-Based Routing (PBR) became generally available.

https://aws.amazon.com/about-aws/whats-new/2026/07/aws-transit-gateway-policy-based-routing/

PBR is not a feature that directly specifies the next hop for packets. It is a feature that selects which route table a packet references based on conditions such as source, destination, protocol, and port. The specifications are summarized in Policy table concepts, and how to create entries is described in Creating a policy table entry.

In this article, I examined the relationship with existing route table associations, the behavior and default values of match conditions, rule evaluation order, handling of unmatched packets, and the scope of CloudFormation support.

Verification Details

Verification Environment

Architecture diagram

Verification was conducted in us-east-1, and the policy table is associated with the attachment of source VPC-A (10.0.0.0/16). Depending on the conditions, either the normal or inspect route table is selected. The inspect route table has a blackhole static route for VPC-B (10.1.0.0/16), so traffic where inspect is selected is dropped at the TGW.

In the connectivity tests below, Ncat: TIMEOUT. indicates unreachable. Connection refused and receiving the payload (LAB-OK-80) mean that the destination VPC was reached, i.e., normal was selected.

Policy Tables are Mutually Exclusive with Route Tables

I tried associating a policy table with an attachment that already had a route table associated.

aws ec2 associate-transit-gateway-policy-table \
  --transit-gateway-policy-table-id tgw-ptb-xxxxxxxxxxxxxxxxx \
  --transit-gateway-attachment-id tgw-attach-xxxxxxxxxxxxxxxxx

The following error occurred.

An error occurred (IncorrectState) when calling the AssociateTransitGatewayPolicyTable operation:
Cannot have both PolicyTableAssociation and RouteTableAssociation on the same TransitGateway Attachment:
tgw-attach-xxxxxxxxxxxxxxxxx

A policy table and a route table cannot be associated simultaneously with the same attachment. The policy table documentation also explicitly states: An attachment can be associated with either a policy table or a route table, but not both.

Match Conditions and Default Values

The match conditions consist of five items: source CIDR, source port, destination CIDR, destination port, and protocol, all of which are optional. Of these, four items—destination port, source CIDR, source port, and protocol—were verified to work in the connectivity tests described below (destination CIDR alone was not measured independently).

Use create-transit-gateway-policy-table-entry to create entries.

aws ec2 create-transit-gateway-policy-table-entry \
  --transit-gateway-policy-table-id tgw-ptb-xxxxxxxxxxxxxxxxx \
  --policy-rule-number 100 \
  --policy-rule '{"DestinationCidrBlock":"10.1.0.0/16","Protocol":"6","DestinationPortRange":"80"}' \
  --target-route-table-id tgw-rtb-xxxxxxxxxxxxxxxxx1

When only some fields are specified, such as --policy-rule '{"Protocol":"1"}', unspecified fields are filled in as * in the response.

Field Response Value
SourceCidrBlock *
SourcePortRange *
DestinationCidrBlock *
DestinationPortRange *
Protocol 1

--policy-rule '{}' with all items omitted is also accepted, and it becomes State: active with all fields set to *.

There are protocol constraints for port specification. Specifying an actual port value for non-TCP/UDP returns a validation error from the API.

InvalidParameterValue: SourcePortRange and DestinationPortRange can only be
specified when Protocol is TCP (6) or UDP (17)

For non-TCP/UDP, explicitly specifying * as in {"Protocol":"1","DestinationPortRange":"*"} succeeds and results in State: active. The supported protocols can be confirmed from the error message returned when an unsupported value is passed.

Protocol must be one of ICMP (1), TCP (6), UDP (17), GRE (47), or '*'

GRE is also listed as a specifiable value, but GRE match behavior was not verified in this verification.

The results below are from a state where rule 50, added in the "Rules are evaluated in numeric order" section described later, does not yet exist.

For destination port, I tested with rules 100 and 110 that route destination 10.1.0.0/16 / TCP port 80 to normal and 443 to inspect. The catch-all with all conditions set to * is rule 200 (normal). Connectivity tests were performed from 10.0.1.100 in VPC-A. VPC-C (10.2.0.0/16) in the table is a control destination that does not result in a blackhole even if inspect is selected.

ncat -w 4 -v 10.1.1.100 80
Test Matching Rule Selected RT Result
A → B:80 (TCP) 100 (dport 80) normal Payload received (listener active)
A → B:443 (TCP) 110 (dport 443) inspect Ncat: TIMEOUT.
A → B:8080 (TCP) 200 (catch-all) normal Connection refused
A → C:80 (TCP) 200 (catch-all) normal Payload received

Even with the same destination and protocol, destination port 80 was reachable while 443 was dropped. The VPC-B instance was listening on both 80 and 443, and the security group allowed all protocols from the lab CIDR. Only 443 was dropped because rule 110 selected inspect, hitting the blackhole static route.

For source CIDR, I used 3 rules with all targets set to inspect. All were for destination 10.1.0.0/16 / TCP, with destination ports 9011 (rule 51), 9012 (52), and 9013 (53).

Test Matching Rule (SourceCidrBlock) Result Determination
A → B:9011 51 (10.0.1.100/32, exact match with source) Ncat: TIMEOUT. inspect
A → B:9012 200 (52's 10.0.99.0/24 does not contain source) Connection refused normal
A → B:9013 53 (10.0.0.0/16, wider CIDR containing source) Ncat: TIMEOUT. inspect
A → B:9014 200 (no matching rule, control) Connection refused normal

Both the /32 exact match and the /16 that contains the source matched. A CIDR that does not contain the source fell through to the catch-all.

For source port, I prepared rule 61 with a single value (source port 44444, destination port 9021) and rule 62 with a range (44000-45000, destination port 9022). Both were for destination 10.1.0.0/16 / TCP with inspect as the target. The source port was fixed using ncat's -p option.

ncat -w 4 -v -p 44444 10.1.1.100 9021
Test Matching Rule Result Determination
ncat -p 44444 to B:9021 61 (matches single value) Ncat: TIMEOUT. inspect
Auto source port to B:9021 200 (no match for 61) Connection refused normal
ncat -p 44500 to B:9022 62 (within range) Ncat: TIMEOUT. inspect
ncat -p 43000 to B:9022 200 (outside range of 62) Connection refused normal

Source port routing worked with both single values and ranges, and even for the same destination port 9021, the selected route table changed depending on whether the source port was 44444.

For protocol, I created two UDP rules and compared behavior between TCP and UDP. Both were for destination 10.1.0.0/16, with rule 71 routing UDP / destination port 9031 to inspect and rule 72 routing UDP / destination port 9032 to normal. UDP reachability results were confirmed on the destination side.

Test Matching Rule Result Determination
A → B:9031 (TCP) 200 (UDP rule does not apply) Connection refused normal
A → B:9032 (TCP) 200 (UDP rule does not apply) Connection refused normal
A → B:9031 (UDP, 3 packets) 71 0 packets received at B inspect (blackhole)
A → B:9032 (UDP, 3 packets) 72 3 packets received at B (with response) normal

Even with the same destination port, TCP did not match the UDP rules and fell through to the catch-all. With UDP, 9031 branched to inspect and 9032 to normal.

Rules are Evaluated in Numeric Order

With rules 100, 110, 120, and catch-all 200 in place, I added a broad rule 50 with destination 10.1.0.0/16 / TCP and dport set to *. The entries present in the policy table at the time of measurement were as follows.

# Condition Target
50 dst 10.1.0.0/16 / TCP / dport * inspect
100 dst 10.1.0.0/16 / TCP / dport 80 normal
110 dst 10.1.0.0/16 / TCP / dport 443 inspect
120 ICMPv4 inspect
200 all * (catch-all) normal
9200 dst 10.1.0.0/16 / TCP / dport 9200 normal

Rule 9200 is an entry created during the CloudFormation verification described later. Because its number is large, it does not affect the evaluation order.

Before adding rule 50, A → B:80 matched rule 100, selected normal, and was reachable. After adding rule 50, in its steady state, A → B:80 changed to Ncat: TIMEOUT..

The more specific rule 100 (dport 80) was not evaluated; the broader rule 50 (dport *) with the smaller number was applied first. The policy table concepts at the beginning also explicitly state Rules in a policy table are evaluated in ascending numeric order. This is a first-match by numeric order, not a longest-match, and it is incorrect to think "more specific rules win" as in route tables.

Immediately after changing a rule, old and new behaviors may coexist, so all determinations were made in the stable steady state after convergence.

Unmatched Packets are Dropped

I deleted the catch-all rule 200 from the policy table, leaving only rules 100, 110, and 120, and tested connectivity.

Test Matching Rule Result
A → C:80 (TCP) none Ncat: TIMEOUT.
A → B:9003 (TCP) none Ncat: TIMEOUT.
A → B:80 (TCP) 100 Payload received

Similar to when a destination is not found in a route table, packets with no matching entry are dropped (implicit deny).

This drop can be observed from the AWS/TransitGateway namespace in CloudWatch as a 5-minute interval Sum. Results using the TGW dimension are as follows.

Period PacketDropCountNoPolicy PacketDropCountBlackhole
Before test 0 0
During test 8 3
After test 0 0

The 8 for PacketDropCountNoPolicy corresponds to the total of SYN packets and their retransmissions sent in the two trials with no matching rule (C:80 and B:9003). The PacketDropCountBlackhole value of 3 recorded during the same period was generated by the concurrently running A → B:443 trial (where inspect was selected).

Upon checking at the attachment level, all 8 were attributed to attachment A, which has the policy table associated, while B and C showed 0.

CloudFormation Support Scope

The following three resource types are available in CloudFormation.

  • AWS::EC2::TransitGatewayPolicyTable
  • AWS::EC2::TransitGatewayPolicyTableAssociation
  • AWS::EC2::TransitGatewayPolicyTableEntry

I confirmed registration in the registry using describe-type.

aws cloudformation describe-type \
  --type RESOURCE \
  --type-name AWS::EC2::TransitGatewayPolicyTableEntry

The result was as follows.

TypeName: AWS::EC2::TransitGatewayPolicyTableEntry
Type: RESOURCE
ProvisioningType: FULLY_MUTABLE
TimeCreated: 2026-05-05T22:06:08.525Z
handlers: create, read, update, delete, list
required: TransitGatewayPolicyTableId, PolicyRuleNumber, PolicyRule, TargetRouteTableId

After creating a stack that includes the entry type, rule 9200 became State: active and the stack reached CREATE_COMPLETE.

cfn-lint does not recognize the three resource types. With both 1.51.2 and 1.53.0, the following errors occur (as of July 31, 2026).

E3006 Resource type 'AWS::EC2::TransitGatewayPolicyTable' does not exist in 'us-east-1'
E3006 Resource type 'AWS::EC2::TransitGatewayPolicyTableAssociation' does not exist in 'us-east-1'
E3006 Resource type 'AWS::EC2::TransitGatewayPolicyTableEntry' does not exist in 'us-east-1'

While cfn-lint does not recognize the three resource types, registration in the CloudFormation registry and stack creation succeeded. The lint "type does not exist" result does not reflect the actual CloudFormation support status.

Summary

Route branching that could not be differentiated by destination alone can now be configured using Transit Gateway alone. Routing specific port or protocol traffic to a different route table, or sending only certain hosts filtered by source CIDR through a different path, can now be expressed with policy table rules.

On the other hand, since a policy table is mutually exclusive with a route table on a per-attachment basis, it cannot be introduced in a way that adds exceptions to an existing environment. Switching means all traffic on that attachment becomes subject to rule evaluation, and any unmatched traffic is dropped. This becomes a migration task that involves identifying all existing communication requirements, rewriting them as policy rules, and verifying connectivity after the switchover.

https://docs.aws.amazon.com/vpc/latest/tgw/tgw-policy-tables-limitations.html

Share this article

AWSのお困り事はクラスメソッドへ