PF-09 · AWS · Python
AWS firewall request automation
A CLI that checks whether a firewall request is already open, previews changes only for what is blocked, and verifies the real traffic.
- Role
- Design and implementation
- Period
- Personal project · released Nov 2026
- Cloud
- AWS
- Scale
- 3-step CLI · 4 input formats · 4 resource types
- Key tech
- Python · boto3 · AWS · CloudWatch Logs · Athena · Excel
- 3
- Steps: confirm, modify, flowlog
- 4
- Input formats
- 4
- Resource types identified
At a glance
Problem
Firewall requests often asked for flows that were already open, which added duplicate rules, and finding the related resources and rules meant searching the console account by account.
Approach
netops confirm resolves addresses to AWS resources and evaluates security group rules and routes. Only blocked flows go to modify, which prints a plan first. flowlog searches CloudWatch Logs or Athena for the actual traffic.
Result
By default nothing changes. Adding a rule needs --apply and a typed apply, rules can only be added, and a security group without free slots is never chosen.
Up close
Rules and routes decide
What this code does: If any of the security group checks evaluated earlier fails, the verdict becomes a failure with “Security group rule needed” as the reason. When the source is an AWS resource but the destination is outside AWS, it finds the route table of the source subnet and checks for a route to the destination, recording the next hop if one exists or which table lacks it if not. A missing route is recorded separately as “Route needed”, which separates cases that opening a security group would never fix.
if any(not c.ok for c in verdict.checks):
verdict.ok = False
verdict.issues.append("Security group rule needed")
if source.resources and not target.resources:
for resource in source.resources:
ec2 = self.sessions.client("ec2", resource.account)
result = route_analysis.evaluate(
route_analysis.find_table(ec2, resource.subnet_id, resource.vpc_id), dst
)
detail = (
f"via {route_analysis.next_hop(result.routes[0])}"
if result.ok
else f"no route in {result.table_id or 'any table'}"
)
verdict.checks.append(Check("route", resource.label, result.ok, detail))
if not result.ok:
verdict.ok = False
verdict.issues.append("Route needed")
return verdict
Apply only adds rules
What this code does: Calls only the API that adds an inbound or outbound rule, depending on the planned direction. There is no call that deletes or edits a rule, so this tool cannot cut existing traffic. If the same rule already exists and AWS returns InvalidPermission.Duplicate, it is treated as “already present” and skipped, while any other error is raised as is instead of being hidden.
def apply(self, change: Change) -> str:
ec2 = self.sessions.client("ec2", change.account)
call = (
ec2.authorize_security_group_ingress
if change.direction == "ingress"
else ec2.authorize_security_group_egress
)
try:
call(GroupId=change.sg_id, IpPermissions=change.permissions)
except ClientError as error:
if error.response["Error"]["Code"] == "InvalidPermission.Duplicate":
return "already present"
raise
return "applied"What I learned
In a change tool, the safe action has to be the default that needs no flag. With --dry-run, one forgotten flag means a real change, but when the default run changes nothing and applying needs both --apply and a typed apply, a mistake never becomes a change. Narrowing changes to adding rules only follows the same idea: the less a tool can do, the less harm it can cause when it is wrong. And instead of opening a rule as soon as a request arrives, checking first whether the path is already open, and recording whether a block comes from a rule or a route, stopped duplicate rules from piling up and left only the changes that were really needed.