Designing Transfer-Friendly Firewall Rules
Line 47 allows any source to port 21. It was added late one evening during a month-end outage by someone whose only goal was to make the error go away. And it worked, and the ticket closed, and line 47 has been there ever since. I have written a line 47 myself; I do not remember what it was for either. That is how a rule set ends up with "any to any on 21" and a passive range of ten ports that fails every month-end. Its allowlist still contains a partner who left two contracts ago. Nobody can say why any of it exists. Every one of those is a future ticket, and some of them are a future security incident.
This article is about writing the rules deliberately. It gives you a least-privilege rule set for each transfer protocol and a method for sizing the passive range so it neither starves nor sprawls. It gives you a sane approach to partner allowlists and the egress rules that outbound jobs need and usually lack. It covers the small number of rules worth logging and a documentation habit that makes every rule explain itself. The examples use Windows Defender Firewall and nftables because both are built in and both express the same ideas any firewall product can. Nothing here requires a particular product; everything here requires a reason.
This is the fourth article in our Firewalls, NAT, and File Transfer series. It assumes you know why FTP needs a passive range at all — the opening article covers that — and builds on it.
Three Principles Before Any Rule
Least privilege means every rule allows the smallest set of traffic that makes the service work: specific ports, specific destination, and wherever possible specific sources. A rule that allows "any" in a field you could have narrowed is a rule that will be exploited by something you did not intend. "Any" is not a source. It is a shrug.
Default deny means the last rule in the list drops everything that no earlier rule allowed. Every serious firewall works this way. The design consequence is that you must think about each flow explicitly, because the default will not do it for you. That includes the ones you take for granted — DNS, time synchronization, the outbound half of an inbound service. Default deny has one job and takes it seriously.
Rule order matters. Firewalls evaluate rules top to bottom and act on the first match. A broad deny above a narrow allow silently kills the allow. A broad allow above a narrow deny makes the deny decorative. Transfer rules should be grouped together, specific before general. The logging deny for transfer ports should be immediately after the allows so that anything falling through is caught and recorded.
A Least-Privilege Rule Set per Protocol
The table is the core of this article. It gives the inbound rules a server needs for each protocol. The source column is where least privilege lives, and "partners" means a named address group rather than the internet at large.
| Protocol | Inbound allow (to server) | Source | Notes |
|---|---|---|---|
| SFTP | TCP 22 | Partners, or any if the service is public | One rule. Nothing else. |
| HTTPS | TCP 443 | Partners or any | One rule. Do not open 80 unless you redirect it. |
| FTP, passive | TCP 21 and TCP 50000–50100 | Partners | Range must match the server setting exactly; helper optional |
| FTPS, explicit | TCP 21 and TCP 50000–50100 | Partners | Helper off for this rule; server announces public address |
| FTPS, implicit | TCP 990 and TCP 50000–50100 | Partners | Same passive range can be shared with explicit FTPS |
| FTP, active (legacy) | TCP 21 inbound; TCP from port 20 outbound | Named legacy devices only | Documented exception; helper required on the client side |
Here is the SFTP row and the FTPS rows as real rules. On a Windows server, PowerShell:
New-NetFirewallRule -DisplayName "XFER-IN SFTP partners" -Direction Inbound -Protocol TCP `
-LocalPort 22 -RemoteAddress 198.51.100.0/24,192.0.2.15 -Action Allow
New-NetFirewallRule -DisplayName "XFER-IN FTPS control" -Direction Inbound -Protocol TCP `
-LocalPort 21 -RemoteAddress 198.51.100.0/24 -Action Allow
New-NetFirewallRule -DisplayName "XFER-IN FTPS passive range" -Direction Inbound -Protocol TCP `
-LocalPort 50000-50100 -RemoteAddress 198.51.100.0/24 -Action Allow
And here are the same three on a Linux firewall in front of the server, in nftables. The partner list is held in a named set so that a partner change edits one line:
table inet filter {
set partners { type ipv4_addr; flags interval; elements = { 198.51.100.0/24, 192.0.2.15 } }
chain input {
type filter hook input priority 0; policy drop;
ct state established,related accept
iif lo accept
ip saddr @partners tcp dport 22 accept comment "XFER-IN SFTP partners"
ip saddr @partners tcp dport { 21, 50000-50100 } accept comment "XFER-IN FTPS control+passive"
tcp dport { 21, 22, 990, 50000-50100 } log prefix "XFER-DENY " drop
}
}
Notice the shape shared by both: an allow per protocol, restricted by source, followed by a logging deny that covers the same ports. The established,related line at the top is what lets replies through without further rules. It is the reason nothing needs to be written for the outbound half of these inbound services.
Sizing the Passive Range
The passive port range is the block of high ports the FTP or FTPS server hands out for data connections. The server uses one port per transfer or listing while it is in progress. Too small and the server runs out under load. Too large and the firewall has an unnecessarily wide opening and the security team has a reasonable question. The passive range configuration guide covers the mechanics of setting it; here is how to choose the size.
Each concurrent data connection occupies one port for the duration of the transfer. When the connection closes, the port lingers for a short while in the TIME_WAIT state before it can be reused. That is a minute or two on most systems. So the working number is the peak number of simultaneous transfers plus the ports still cooling down from transfers that just finished. A practical rule:
- Estimate peak concurrent sessions — the month-end burst, not the quiet afternoon. Count listings as transfers; a client that refreshes a directory every few seconds uses ports too.
- Multiply by three to cover
TIME_WAITand bursts. Twenty concurrent sessions become sixty ports. - Round up to a clean block and give it room. Use 100 ports for a small server, 500 for a busy partner exchange, 1000 for a large public service. Beyond that, the server's connection limits are the real constraint.
Choose a block that no other service on the host uses. It should be above the ephemeral range the OS picks from for its own outbound connections so the two never collide. Keep the range identical in three places: the server setting, the firewall rule, and the NAT forward if there is one. Most servers express the range as a start and end port in their configuration; copy those two numbers, verbatim, into the rule. When the range is exhausted, clients see a 425 or 421 reply rather than a silent hang. That is a useful tell that distinguishes "range too small" from "range not open." For once, the failure arrives with a reply attached.
Remember: the passive range must match in every place it appears — server configuration, firewall rule, NAT forward. A mismatch fails intermittently, depending on which port the server picks, and intermittent failures are the ones that take weeks to find. Write the range down once and copy it everywhere.
Partner Allowlists
An allowlist is a rule whose source field names specific addresses, so that only those addresses can reach the port. For partner transfer services it is the single most effective control available. It removes the entire internet's worth of password-guessing from the server's logs. It turns every unexpected connection into a firewall log line rather than an authentication attempt. The design points:
- Use named address groups, one per partner. A group called
partner-northwindcontaining the partner's two egress addresses is self-documenting. A bare198.51.100.20in a rule is a mystery within a year. - Allowlist the address the partner's traffic actually leaves from — their firewall's public address, not their server's private address. Partners frequently give the wrong one; the partner coordination article has the questions that get the right one.
- Expect several addresses per partner. Redundant firewalls, NAT pools, and cloud egress produce multiple sources. Ask, and allow them all.
- Hostnames do not belong in firewall rules. Most firewalls resolve a hostname once at rule load; the rule then silently refers to a stale address. Use addresses, and treat a partner's address change as a change request.
- Layer it. A network allowlist on the firewall and an IP allow list on the server itself are not redundant. The second catches mistakes in the first and gives the application's log a clean record of who was refused. Sysax Multi Server supports IP allow and block lists on the server side for exactly this purpose.
A partner may be unable to give a stable address — mobile users, carrier-grade NAT, a cloud service with a changing egress. In that case, do not weaken the rule to "any" for everyone. Put that partner on a separate rule with a source of any, restricted to a protocol with strong authentication (SFTP with keys, ideally). Lean on the server-side defenses described in IP allowlisting and geo-blocking and its neighboring articles.
Egress Rules for Outbound Jobs
Egress is outbound traffic — connections your own systems open toward the internet. Many networks allow all egress by default, and many security programs are now closing that door. At that point, every scheduled transfer job that quietly pushed files to a partner at two in the morning starts failing. Outbound jobs need rules just as inbound services do, and they are easy to get subtly wrong. The job does not know the door closed; it just stops coming home.
For a job host at 10.0.20.30 that pushes to three partners, the egress rule set looks like this, again in nftables on the network firewall:
chain forward {
type filter hook forward priority 0; policy drop;
ct state established,related accept
# SFTP to two partners
ip saddr 10.0.20.30 ip daddr { 198.51.100.20, 192.0.2.15 } tcp dport 22 accept comment "XFER-OUT sftp jobs"
# FTPS to a legacy partner: control port plus THEIR passive range
ip saddr 10.0.20.30 ip daddr 203.0.113.200 tcp dport { 21, 40000-40200 } accept comment "XFER-OUT ftps legacy partner"
# the job host must resolve names
ip saddr 10.0.20.30 ip daddr 10.0.0.53 udp dport 53 accept comment "DNS for job host"
ip saddr 10.0.20.30 log prefix "XFER-OUT-DENY " drop
}
Three details catch people. First, an outbound FTP or FTPS job needs the partner's passive range opened outbound, not yours. Ask them for it. If they cannot say, you are forced into "any high port to that one address," which is still far better than "any to any." Second, DNS: the job host resolves the partner's hostname before it can connect. A job that fails with "name not found" after a firewall change is usually this. Third, if the job host runs several jobs under a scheduler such as Sysax FTP Automation, every destination in every job profile needs a line. The rule set is the natural place to discover a destination nobody remembered. The broader question of how outbound and inbound flows should be arranged across a DMZ is covered in inbound versus outbound flows.
Kestrel Payroll got two weeks' notice that default-allow egress was ending, which is more than most teams get. The transfer administrator pulled every destination out of the scheduler's job profiles and found eleven, where the service documentation listed eight. Two were partners that had moved to new addresses without anyone updating the register. The eleventh was a test host from the original build, still receiving an empty file every night, long after anyone remembered the test. Ten egress rules went in and the test job was deleted. The first month-end after the change passed without a single transfer ticket. The rule set had done its second job, which was to make the team read its own transfer inventory.
Logging the Rules That Matter
Firewall logging is a budget: log everything and the useful lines drown; log nothing and the troubleshooting playbook has nothing to read. For transfer traffic, three kinds of rule earn a log line.
- The deny for transfer ports, shown above with the
XFER-DENYprefix. This is the line that answers "did the partner's connection even reach us, and what did we do with it?" It is the single most valuable transfer log there is. - New connections from partners to the control port — one line per session, not per packet. It gives a timeline of who connected when, independent of the server's own log, which matters when the server log is the thing in dispute.
- Outbound denies from job hosts, so that a blocked job is visible in the firewall log at the same time it fails in the scheduler.
Do not log the established,related accept — that is every packet of every transfer. Do rate-limit the deny logs so a port scan cannot fill the disk. For this, nftables accepts limit rate 10/second in the log rule. And ship the log somewhere central with the server's own logs, so that the two can be read side by side. The article centralizing logs explains how. A logged deny looks like this, and its fields — interface, source, destination, port, flags — are exactly what the playbook needs:
Mar 14 02:10:07 fw1 kernel: XFER-DENY IN=eth0 OUT= SRC=203.0.113.77 DST=10.0.5.20 PROTO=TCP SPT=51920 DPT=22 SYN
Read that line and the story is immediate: an address not in the partner set tried SFTP at two in the morning and was refused. If 203.0.113.77 turns out to be a partner's new firewall, the fix is one line in the address set. If not, the rule did its job. Either way, for once, the firewall left a note.
Documenting Why Each Rule Exists
A rule without a reason cannot be safely removed, so it never is, and rule sets grow until nobody dares touch them. The cure is cheap: every transfer rule carries, in its name or comment field, enough to answer "who asked, for what, until when." The pattern used throughout this article — a prefix like XFER-IN or XFER-OUT, then the protocol and partner — is the minimum. Behind it, keep a rule register: one row per rule, in whatever document your team already maintains for the service.
| Rule name | Allows | Requested by / ticket | Review or expiry |
|---|---|---|---|
| XFER-IN SFTP partners | TCP 22 from set partners |
Partner onboarding, ticket 4471 | Review each quarter with partner list |
| XFER-IN FTPS control+passive | TCP 21, 50000–50100 from partners |
Legacy partner exchange, ticket 3980 | Remove when partner migrates to SFTP |
| XFER-IN FTP active scanner | TCP 21 from 10.0.40.12 only | Document scanner, active-only firmware | Expires when scanner is replaced |
Two habits keep the register honest. Give temporary rules an expiry date and a calendar reminder the day they are created. The "temporary" test rule that outlives the test is the most common rule-set rot there is. I have written several; at least one is still in production somewhere. And review the partner sets against the partner contract list on a schedule. Offboarding a partner in the server without removing them from the firewall leaves a door that only the firewall log will ever mention. The Group V series on documenting transfer flows shows where this register fits among the rest of the service's documentation.
A Worked Rule Set
Pulling it together: a Windows transfer server at 10.0.5.20 in a DMZ, public address 203.0.113.80. It serves SFTP to a partner group and explicit FTPS to one legacy partner at 203.0.113.200. An internal job host at 10.0.20.30 pushes nightly files out. The complete transfer rule set on the perimeter firewall, in order:
- Accept
established,related. - DNAT:
203.0.113.80TCP 22, 21, 50000–50100 to10.0.5.20. - Allow inbound TCP 22 to
10.0.5.20from setpartners. Log new connections, one line per session. - Allow inbound TCP 21 and 50000–50100 to
10.0.5.20from203.0.113.200; FTP helper disabled for this rule. - Allow outbound from
10.0.20.30to the job destinations on their ports, plus DNS. - Log and drop anything else to TCP 21, 22, 990, 50000–50100 with prefix
XFER-DENY, rate-limited. - Log and drop any other outbound from
10.0.20.30with prefixXFER-OUT-DENY. - Default deny.
On the server itself, Windows Defender Firewall carries the same inbound allows with the same sources, so that a perimeter mistake does not expose the service. The server's own configuration announces 203.0.113.80 with passive range 50000–50100. Eight rules, every one named, every one in the register. That is a transfer-friendly firewall. It is also, not by accident, one with no line 47.
The Short Version
One allow per protocol, restricted to named partner sets. A passive range sized from peak concurrency times three, identical in the server, the firewall, and the NAT forward. Allowlists built from the addresses partners actually leave from, layered with the server's own IP lists. Egress rules for job hosts that include the partner's passive range and DNS. A logged, rate-limited deny after the allows, and no logging of established traffic. And a name and a register entry for every rule, with expiry dates on anything temporary.
When a rule set built this way still produces a failing transfer, the firewall troubleshooting playbook is the next article. It leans on the XFER-DENY log you just configured. When the rule you need to change belongs to a partner, coordinating firewall changes with partners follows. And if the passive range is the part still causing grief, application helpers and ALGs explains the device that may be second-guessing it.
Frequently Asked Questions
How many ports should my passive range have?
Do I need to open port 20 for passive FTP?
Can I put a partner's hostname in the firewall rule instead of an address?
Should I log every packet on the transfer ports?
What does an outbound scheduled job need on the firewall?
Why keep firewall rules on the server as well as the perimeter?
From the Sysax team: we build secure file transfer software for Windows. Sysax Multi Server is an FTP, FTPS, SFTP, and HTTPS server. Sysax FTP Automation handles scheduled, scripted transfers. Free trials are on the download page.
