The Setup, and Why the Question Is Fair
The design is a common one. Two SQL Server nodes in an Availability Group, clients connecting through the AG listener on the production network. Each node has a second NIC on a dedicated backup VLAN so that full backups go directly to the backup server without saturating the production path. The backup NIC has no default gateway, which is exactly right: it should not route anywhere.
The concern raised against it is also exactly right. A second NIC is a second attack surface, and the backup VLAN has a property that makes it interesting to an attacker: both SQL Servers are on it. Compromise node A, and node B is one hop away on a segment that nobody is watching as closely as the production network.
Here is the part that trips people up. "No default gateway" is not isolation. It only means traffic leaving that interface cannot be routed off the subnet. Everything on the subnet is still fully reachable, because same-subnet traffic never needs a gateway in the first place. The backup VLAN is a flat layer 2 segment, and by default every host on it can talk to every other host on every port.
So if SQL Server is listening on all IP addresses, which is the default, it is listening on the backup IP too. Node A can open a TCP session to node B on port 1433 across the backup VLAN, and the production firewall never sees it.
Where People Look First: Listen All = No
The obvious place to look is SQL Server Configuration Manager, under SQL Server Network Configuration, Protocols for the instance, TCP/IP properties. On the Protocol tab there is Listen All. Set it to No, and the IP Addresses tab stops being decorative: each IP entry (IP1, IP2, IPn) gets its own Active, Enabled and TCP Port values, and the engine binds only to the addresses where Enabled is Yes.
That does technically solve the stated problem. It also brings four things with it that are worth knowing before committing to it.
| It is an allow-list, not a deny-list | There is no "listen on everything except this one NIC" option. You enumerate every address the instance is allowed to bind to. Every future address, every re-IP, every added interface has to be remembered and added, on both nodes, or the instance quietly stops answering where you expected it to. |
| It only takes effect on service restart | The network configuration is read when the engine starts. On an AG that means a failover, or a maintenance window per node. This is not a change you make and validate in five minutes. |
| The listener IP is a cluster resource, not a NIC address | The AG listener's IP is owned by the Windows cluster and is brought online only on the node currently hosting the primary. It is not a permanent address on either node's adapter. Pinning the engine to a static address list and letting the cluster manage a floating address are two different mental models, and mixing them is where the surprises come from after a failover. |
| It stops SQL connections, it does not close the host | The engine no longer binds to that IP, which is a real improvement. But the machine is still fully reachable on that VLAN on every other port: SMB, WinRM, RDP, the backup agent itself. SQL Server was never the only way into a SQL Server. |
Why "Handle It Inside SQL Server" Is Not the Boundary
The other instinct is to solve it in the database engine: a logon trigger that inspects client_net_address and rolls back any session originating from the backup subnet. It works, in the narrow sense that it does reject those sessions.
It is still the wrong layer, for reasons that have nothing to do with taste:
- It runs after authentication. The TCP session was accepted, the login was validated, credentials crossed the wire. You are rejecting at the end of the handshake, not preventing it.
- A logon trigger is a well-known way to lock yourself out of an instance. One bug, one failed dependency, one full transaction log inside the trigger, and the only way back in is the Dedicated Admin Connection.
- It costs something on every single login, instance-wide, to defend one path.
- It does nothing at all for the other ports on that same host.
Access control inside the database decides what an authenticated principal may do. It is not the right instrument for deciding which network segments may reach the service at all. Those are different questions, and they belong at different layers.
The Boundary Belongs on the Network
The clean framing is the one the original question already arrived at: this is a segmentation problem, not a SQL Server configuration problem. The backup VLAN exists to carry backup traffic between each node and the backup server. That is the entire legitimate traffic pattern. Everything else on that segment, node to node traffic included, is by definition not required.
Which gives a very specific target state: on the backup VLAN, each SQL node can reach the backup server, and nothing else. Not the other node, not on 1433, not on 445, not on anything.
| Control | What it does | Where it belongs |
|---|---|---|
| Private VLAN / isolated ports | Node to node traffic on the backup segment never reaches the other node at layer 2. Both nodes still reach the backup server on the promiscuous port. | Primary control. The strongest and the most durable, because it survives every host-level mistake. |
| ACL on the VLAN interface | Permits only the backup product's ports between node and backup server, denies the rest. Simpler to deploy than PVLANs on most switch gear. | Primary control where PVLANs are not practical. |
| Host firewall scoped per interface | Blocks the SQL ports on the backup adapter on each node individually. | Defense in depth. Cheap, fast, reversible, no restart needed. A good second layer, not a substitute for the first. |
| Listen All = No | Engine binds only to an explicit address list. | Optional third layer, if you accept the maintenance burden. Not where I would start. |
The Host-Level Layer, Concretely
Windows Defender Firewall can scope a rule to a specific adapter, which is exactly what is needed here and is far less well known than it should be. New-NetFirewallRule takes -InterfaceAlias, so the SQL ports can be blocked on the backup NIC without touching the production NIC at all. Block rules are evaluated before allow rules, so this wins over any broad "allow 1433" rule that already exists.
# Block the SQL Server surface on the backup adapter only.
# Adjust -InterfaceAlias to the real adapter name (see Get-NetAdapter).
$params = @{
DisplayName = 'Block SQL Server on backup NIC'
Direction = 'Inbound'
Action = 'Block'
Protocol = 'TCP'
LocalPort = 1433, 1434, 5022
InterfaceAlias = 'Backup'
Profile = 'Any'
}
New-NetFirewallRule @params
# UDP 1434 is the SQL Browser. Block it on that adapter too.
$params = @{
DisplayName = 'Block SQL Browser on backup NIC'
Direction = 'Inbound'
Action = 'Block'
Protocol = 'UDP'
LocalPort = 1434
InterfaceAlias = 'Backup'
Profile = 'Any'
}
New-NetFirewallRule @params
Port 5022 in that list is the AlwaysOn database mirroring endpoint, assuming the default. Replication traffic between replicas should cross the production or a dedicated replication network, never the backup VLAN, so blocking it there is deliberate rather than incidental. Confirm the actual endpoint port first instead of trusting the default:
SELECT name, type_desc, port, state_desc
FROM sys.tcp_endpoints
WHERE type_desc = 'DATABASE_MIRRORING';
Two more host-level details on multi-homed servers that are easy to miss and cause real confusion later:
- Do not register the backup NIC in DNS. In the adapter's advanced TCP/IP settings, clear "Register this connection's addresses in DNS". Otherwise the server name resolves round-robin to the backup address, and clients periodically try to connect over a path with no gateway. The result is intermittent timeouts that look like a SQL Server problem and are not.
- Set
SkipAsSourceon the backup address so Windows does not pick it as the source address for unrelated outbound connections:Set-NetIPAddress -IPAddress 10.x.x.x -SkipAsSource $true. Together with the DNS setting, this keeps the backup path from leaking into traffic that has no business being on it.
The Cluster Network Role, Which Nobody Mentions
This is the part of the question that touches failover directly, and it is worth checking before anything else. When a new network appears on cluster nodes, the Windows cluster classifies it automatically, and it will happily use the backup VLAN for cluster communication. Check what role it currently has:
Get-ClusterNetwork | Select-Object Name, Address, Role, Metric
Role is 0 for none, 1 for cluster communication only, and 3 for cluster and client communication. A backup network should normally be 0 or 1, never 3. If it is 3, the cluster is willing to serve client traffic over that segment, which is precisely the exposure being discussed.
(Get-ClusterNetwork -Name 'Backup').Role = 1 # cluster only, no client access
# or
(Get-ClusterNetwork -Name 'Backup').Role = 0 # exclude from cluster use entirely
0 reflexively. Role 1 keeps the backup network available as a redundant heartbeat path, which is genuinely useful: heartbeat traffic is tiny, and giving up a redundant path makes the production network a single point of failure for quorum. Setting it to 0 is right when the segment is untrusted enough that you want no cluster traffic on it at all. That is a judgement call about the segment, and it should be made deliberately rather than by accident.
If you block ports on the backup adapter with the host firewall, note that a cluster network still set to role 1 needs its own traffic to pass. Blocking specific SQL ports by number, as in the example above, rather than blanket-blocking the adapter, keeps that distinction intact.
Verifying It, Before and After
Every change here is worth confirming from both directions rather than assumed. First, what the instance is actually bound to right now:
Get-NetTCPConnection -State Listen |
Where-Object LocalPort -in 1433, 1434, 5022 |
Select-Object LocalAddress, LocalPort, OwningProcess
0.0.0.0 in LocalAddress means the engine is bound to every address on the box, backup NIC included. The SQL Server ERRORLOG says the same thing in its own words, in the "Server is listening on" lines written at startup.
Second, which addresses connections are actually arriving on. local_net_address is the server-side address that accepted the session, so it tells you directly whether anything real is coming in over the backup path:
SELECT
c.local_net_address,
c.local_tcp_port,
c.client_net_address,
s.login_name,
s.program_name,
COUNT(*) AS sessions
FROM sys.dm_exec_connections AS c
JOIN sys.dm_exec_sessions AS s ON s.session_id = c.session_id
WHERE c.local_net_address IS NOT NULL
GROUP BY c.local_net_address, c.local_tcp_port,
c.client_net_address, s.login_name, s.program_name
ORDER BY c.local_net_address;
Run that for a few days before changing anything. If some forgotten job or monitoring agent has been connecting over the backup address all along, you want to find that out now, not during the change window.
Third, prove the block from the attacker's position. From node A, against node B's backup address:
Test-NetConnection -ComputerName 10.x.x.20 -Port 1433
Test-NetConnection -ComputerName 10.x.x.20 -Port 445
Both should fail once the segmentation is in place. If 1433 fails and 445 succeeds, you have moved the problem rather than solved it, which is the whole argument for putting the control on the network instead of inside SQL Server.
Get-sqmAlwaysOnHealthReport reports replica states, synchronization health and listener configuration across the AG in one pass, which makes a clean before and after comparison easy to keep as evidence. Related background: Quorum, Witness, and Failover Recovery.
Order of Operations
- ☐ Inventory what actually connects over the backup address (
local_net_address), for long enough to be sure - ☐ Check the cluster network role for the backup VLAN, and set it deliberately to
1or0 - ☐ Turn off DNS registration on the backup adapter and set
SkipAsSourceon its address - ☐ Get the network layer right first: PVLAN isolation, or a VLAN ACL permitting only backup traffic to the backup server
- ☐ Add the per-adapter host firewall rules as the second layer, on both nodes, identically
- ☐ Test node to node reachability on 1433, 445 and the mirroring port, from both directions
- ☐ Fail over, then test again. A control that only holds while node A is primary is not a control
- ☐ Only consider
Listen All = Noafter all of that, and only with a way to keep the address list correct over time
The Bottom Line
SQL Server does give you a way to do this, and Listen All = No is a legitimate setting. But it is an allow-list that needs a service restart to change and manual upkeep to stay correct, applied to a problem that is not really about which addresses the engine binds to. The actual problem is that two production database servers share an unfiltered layer 2 segment, and SQL Server is only one of several ways that fact can be used.
Fix it where the traffic is: allow only what the backup path legitimately needs, block node to node traffic on that segment outright, and add a per-adapter host firewall rule as the second layer. The AG listener and failover behaviour never enter into it, because nothing the cluster owns has been touched. That is what makes it the cleaner answer, not just the more convenient one.
Microsoft's documentation on configuring a server to listen on a specific TCP port covers the engine-side mechanics, and is worth reading precisely so the decision to put the boundary somewhere else is made with the details in front of you.