Start with the flow matrix
In a fictional funds project, Prod and Dev need to send files to Services but should not communicate with each other. Before selecting the hub, record source, destination, protocol, port, initiating direction, reply, and owner. Add flows that must fail. This matrix makes isolation verifiable and helps the business identify omitted dependencies. A diagram showing only three connected VPCs does not reveal who may initiate connections or what happens when the next prefix is automatically propagated. Keep the matrix as an acceptance artifact.
Look for aggregate exceptions
A migration changes the target for 172.28.0.0/16 while retaining a 172.28.40.0/24 route to the old path. The batch at 172.28.40.8 still depends on that path. Select the most specific matching prefix first; compare static and propagated priority only for identical destinations. Keep IPv4 and IPv6 separate. In the inventory, record the actual table association of the source subnet: reading another table can produce a convincing explanation for a configuration the packet never uses. Test destinations inside exceptions as well as the aggregate.
Separate association from propagation
In TGW, an attachment association determines the lookup table for traffic entering through it. Propagating Prod’s CIDR into the Services table lets that table know the Prod destination. It does not mean Prod now consults the Services table. Draw two differently colored arrows for these relationships. An attachment may propagate into several tables but associates with one. Reviewing both relationships avoids opening lateral communication when the intention was only to permit replies from a shared service. Record the intended table for every source attachment.
Trace requests and replies
Trace a request from its subnet to the destination attachment, then trace the reverse path. A Services route in Prod’s table does not automatically create a Prod route in the table used by Services. Also check that the source zone is enabled on the VPC attachment. An application can work in one zone and fail in another with apparently identical tables. Record these prerequisites before broadening permissions. A valid route also does not prove that the security group, application, or authentication permits the intended use.
Local exercise with explicit limits
Run the model shown in this lesson with Python 3.13. It uses only in-memory data and does not configure AWS. Predict three destinations before running: one inside the /24 exception, one only in the /16, and one with no route. Then inspect a synthetic Prod, Dev, and Services matrix. Remove the Prod return path, rerun, and explain the difference. The model rejects ties it cannot resolve; it implements no local routes, prefix lists, BGP, filters, appliances, real DNS, or connectivity testing.
Decide acceptance and retirement
Hand RUN the approved matrix, associations, evidence for allowed and prohibited paths, rollback plan, and remaining dependencies. In an authorized rehearsal, test actual batch destinations and every intended zone within an agreed window and stopping criteria. One positive result does not justify retiring a path with unvalidated exceptions. If time runs out, a possible decision is partial acceptance with explicit scope and risk. This example is fictional: the actual controls and owners must follow the organization’s governance and change-management requirements.
"""Selected in-memory reasoning fixtures. No AWS, DNS packets or network tests.
Run with Python 3.13: python3 run.py
Excludes VPC local routes, prefix lists, BGP, appliances, filters and DNS caches.
"""
import ipaddress
import json
def route(address, entries):
"""Entries are (canonical CIDR, target, static|propagated)."""
address = ipaddress.ip_address(address)
matches = []
for cidr, target, origin in entries:
network = ipaddress.ip_network(cidr, strict=True)
if origin not in ('static', 'propagated'):
raise ValueError('unsupported origin')
if address.version == network.version and address in network:
matches.append((network.prefixlen, origin == 'static', target))
if not matches:
return None
priority = max((n, static) for n, static, _ in matches)
winners = [target for n, static, target in matches if (n, static) == priority]
if len(winners)!= 1:
raise ValueError('unsupported equal-priority tie')
return winners[0]
def dns_view(name, zones, rules):
"""Select only a suffix and authority; absence is a fixture label, not a DNS RCODE engine.
zones maps suffix -> exact names. rules is a list of forwarding suffixes.
Excludes wildcards, delegation, DNS record types, aliases and caching.
"""
name = name.lower.rstrip('.')
candidates = []
for kind, suffixes in [('zone', zones), ('forward', rules)]:
for suffix in suffixes:
suffix = suffix.lower.rstrip('.')
if name == suffix or name.endswith('.' + suffix):
candidates.append((len(suffix.split('.')), kind == 'forward', suffix, kind))
if not candidates:
return {'kind': 'public-lookup-not-executed'}
_, _, suffix, kind = max(candidates)
if kind == 'forward':
return {'kind': 'forward', 'suffix': suffix}
present = name in [n.lower.rstrip('.') for n in zones[suffix]]
return {'kind': 'zone', 'suffix': suffix, 'namePresent': present}
def run:
checks = []
def check(name, actual, expected):
if actual!= expected:
raise AssertionError((name, actual, expected))
checks.append({'name': name, 'actual': actual, 'expected': expected, 'passed': True})
entries = [('172.28.0.0/16', 'new', 'static'), ('172.28.40.0/24', 'old', 'static')]
check('specific exception', route('172.28.40.8', entries), 'old')
check('aggregate destination', route('172.28.10.8', entries), 'new')
check('no route', route('192.0.2.1', entries), None)
check('identical CIDR priority', route('172.22.1.4', [('172.22.0.0/16','vpn','propagated'),('172.22.0.0/16','peer','static')]), 'peer')
check('specific propagated beats broad static', route('172.22.1.4', [('172.22.0.0/16','peer','static'),('172.22.1.0/24','vpn','propagated')]), 'vpn')
check('family separation', route('2001:db8::1', [('0.0.0.0/0','ipv4','static')]), None)
check('IPv6 specific match', route('2001:db8:1::8', [('::/0','default','static'),('2001:db8:1::/48','private','static')]), 'private')
check('specific blackhole', route('10.90.7.20', [('10.90.0.0/16','services','static'),('10.90.7.0/24','DROP','static')]), 'DROP')
tables = {'prod': {'services'}, 'dev': {'services'}, 'services': {'prod','dev'}}
check('forward and return', 'services' in tables['prod'] and 'prod' in tables['services'], True)
check('lateral path absent', 'dev' in tables['prod'] or 'prod' in tables['dev'], False)
tables['services'].remove('prod')
check('broken return', 'services' in tables['prod'] and 'prod' in tables['services'], False)
zones = {'example.com': ['api.apps.example.com'], 'apps.example.com': []}
check('more specific private view', dns_view('api.apps.example.com', zones, []), {'kind':'zone','suffix':'apps.example.com','namePresent':False})
check('missing name stays private', dns_view('portal.example.com', zones, []), {'kind':'zone','suffix':'example.com','namePresent':False})
check('same suffix forwarding', dns_view('api.apps.example.com', zones, ['apps.example.com']), {'kind':'forward','suffix':'apps.example.com'})
check('most specific forwarding', dns_view('api.payroll.example.com', {}, ['example.com','payroll.example.com']), {'kind':'forward','suffix':'payroll.example.com'})
check('label boundary', dns_view('notexample.com', zones, []), {'kind':'public-lookup-not-executed'})
check('case and final dot', dns_view('API.APPS.EXAMPLE.COM.', zones, []), {'kind':'zone','suffix':'apps.example.com','namePresent':False})
try:
route('10.1.0.1', [('10.1.0.0/16','a','static'),('10.1.0.0/16','b','static')])
except ValueError:
check('ambiguous tie rejected', True, True)
else:
raise AssertionError('ambiguous tie accepted')
return {'scope':'In-memory fixtures only; no AWS resources or network traffic.', 'checks':checks, 'passed':len(checks)}
if __name__ == '__main__':
print(json.dumps(run, indent=2))
Prod reaches Services, but Services does not know Prod. Repeating the forward route does not fix the timeout.
Common pitfalls
Confusing propagation with association; forgetting replies; testing one aggregate address; counting attachments as isolation evidence.
Related topics: Networks, endpoints, and hybrid connectivity · Resilience, capacity, and recovery · Private access and startup without hidden dependencies
A connection is accepted only when evidence covers the approved flow, its exceptions, and boundaries that must remain closed.
Reference: How Transit Gateway works · SAA-C03