powershelldba.de · Uwe Janke

The Backup NIC Problem: Blocking SQL Traffic on a Dedicated VLAN Without Breaking the AG Listener

Two AlwaysOn nodes behind a listener, each with a second NIC on a dedicated backup VLAN. No default gateway, separate network, much better backup throughput. Then someone asks the uncomfortable question: if one node gets compromised, what stops it from reaching the other node's SQL port straight across that VLAN?

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.
⚠ The maintenance liability is the real cost. A configuration that has to be manually re-verified after every IP change, every node rebuild and every listener change will eventually be wrong on exactly one node, and nobody will notice until a failover. A hand-maintained allow-list spread across a cluster is not a control you can prove in an audit six months later without checking it by hand again.

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:

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:

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
⚠ Do not set this to 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.

🔧 Confirm the AG is genuinely unaffected, not just apparently unaffected: any change touching cluster networks or listener paths deserves a real health check afterwards rather than a successful ping. sqmSQLTool's 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

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.