Start with the matrix, not the firewall syntax. A table with roles down one side and services across the other can be reviewed by an application owner, argued over by a security owner and implemented by whoever holds the gateway. That review is cheaper than debugging rules nobody agreed to.

An hour of writing it saves a week of rule archaeology later. The design talks about who reaches which service, which is the part that survives renumbering.

Step 1: Name the Roles Before the Networks

Start from the people, not the switch. Write down each group that connects remotely and the single sentence describing what that group does, such as warehouse staff reading stock levels or a contractor maintaining one integration. If two groups would receive identical access, they are one role for this purpose.

Stop here if a role only exists because of an org chart. A role with no distinct service need becomes a row of identical cells, and identical rows invite someone to grant a whole subnet later for convenience. Split by what people do, not by who signs their expenses. Six roles is usually too many for a small office and three is often right.

Step 2: Turn Each Cell Into a Service Statement

A cell that reads "Applications" is not testable. A cell that reads "HTTPS to the ERP host on 443, from the staff pool" is. Replace every network name with a destination, a protocol and a port, then add the dependency the service cannot work without, such as the internal resolver or the identity provider.

CheckPasses whenStop if
Destination namedHost and port appear in the cellCell names a VLAN or a subnet
Direction statedInitiator and responder are both clearCell implies two-way access
Dependency listedDNS or identity path is explicitWhole infrastructure range is granted
Negative case writtenAt least one denied pair is recordedOnly allow rules exist

The direction column catches the most expensive mistakes. Permission for staff to open an application does not imply permission for application servers to open connections back to staff laptops, and a stateful rule that permits the first direction often gets read as permitting the second.

Step 3: Decide Where Identity Becomes Policy

Someone has to convert an authenticated session into a role the gateway can enforce. Supported options include group policy delivered by the authentication backend, trusted session attributes, or separate address pools per role. All three work; a label sent by the client does not. Pick the mechanism your gateway documents, not the one a forum post describes.

Assigning an address that merely looks like it belongs to a VLAN is the same non-answer in a different costume. The Netgate guide to OpenVPN firewall rules makes the separation plain: allowing the tunnel to come up is a different decision from allowing traffic through it.

Write down which mechanism you chose and where it is configured. When a role changes, that note is the difference between one edit and a hunt through three interfaces. It also tells an auditor where to look first.

Step 4: Watch the Source Address the Destination Sees

Forwarding policy has to match the real source at the boundary. If the gateway translates VPN clients to its own address, every downstream firewall sees one source and loses the ability to make per-role decisions. If it preserves the client address, the return path has to exist for that pool.

Test both directions before writing the final rules, then record the observed source next to each cell. Run the same check from two different roles and compare the result. If both show the gateway address, the role distinction has already been lost. A matrix that assumes preserved addresses while the gateway performs source translation fails at the first downstream ACL.

Step 5: Test Each Cell Before You Ship

Convert every allowed cell into one representative operation and every denied cell into a negative case. Run both from a client in the actual role, not from the gateway console, because the console bypasses the path you are testing.

# allowed: staff role reaches the ERP endpoint
curl -sS -o /dev/null -w '%{http_code}\n' --max-time 5 https://erp.internal.example/health

# denied: the same role must not reach a management host
nc -vz -w 3 10.30.5.11 22

# observed source address at the destination
sudo tcpdump -ni any 'tcp port 443 and host 10.30.20.15'

A negative case that connects is a finding, not a fluke, and it belongs in the matrix as a failed cell until the rule is fixed. Keep the test commands beside the table so the next reviewer reruns them instead of rebuilding the reasoning.

The finished table outlives the gateway it was written for. Rules change with firmware and vendor, while the row that says a contractor reaches one project service stays true until the job does.

Makomi ya ndenge moko