How to Configure CIDR-Based Host Matching in SSH Config Files


2 views

Many infrastructure teams need to route SSH connections through different bastion hosts based on IP ranges. While simple wildcards (10.1.*) work for basic cases, they fail when dealing with non-contiguous or non-octet-aligned CIDR blocks like 10.2.0.0/18.

The OpenSSH client doesn't natively support CIDR notation in ~/.ssh/config host patterns. The wildcard matching only works on:

  • Whole octets (10.1.*.*)
  • Simple patterns (192.168.?)

We can implement CIDR matching using a script in the ProxyCommand directive:

Host 10.2.*
    ProxyCommand bash -c '[[ $(printf "%02x" ${(s:.:)1}) =~ ^0a02(00|40) ]] && 
      ssh -q %r@bastion-%1 -W %h:%p || exit 1'

This zsh/bash solution:

  1. Converts IP to hex (10.2.0.1 → 0a020001)
  2. Uses regex to match CIDR ranges (first 18 bits for /18)
  3. Selects bastion host based on matched group

For more complex scenarios, a Python script provides better CIDR handling:

Host 10.3.*
    ProxyCommand python3 -c '
import ipaddress, sys, subprocess
ip = ipaddress.ip_address(sys.argv[1].split("%h")[1])
if ip in ipaddress.ip_network("10.3.0.0/18"):
    subprocess.run(["ssh","-q","bastion-foo","-W",sys.argv[1]])
elif ip in ipaddress.ip_network("10.3.64.0/18"):
    subprocess.run(["ssh","-q","bastion-bar","-W",sys.argv[1]])
else:
    sys.exit(1)
    ' %h:%p

While these solutions work, they add latency to the SSH connection startup. For high-performance needs, consider:

  • Pre-compiling IP ranges in the script
  • Using compiled languages like Go for the proxy command
  • Implementing connection multiplexing

For Linux systems, network namespaces can provide cleaner routing:

ip netns add vpc1
ip -n vpc1 route add 10.1.0.0/16 via $BASTION_IP
ssh -o "ProxyCommand=ip netns exec vpc1 nc %h %p" 10.1.5.6

While SSH config files support wildcards for host matching, they don't natively understand CIDR notation. The typical approach using patterns like 10.1.* works for simple cases but fails when you need:

  • Non-octet-aligned subnets (e.g., /18, /22)
  • Precise network boundary matching
  • Multiple bastion hosts for different subnets

Use SSH's Match exec to run a script that checks IP ranges:

Match host 10.* exec "ipcalc -n %h | grep -q '^NETWORK=10.2.0.0/18$'"
    ProxyCommand ssh -q bastion-foo -W %h:%p

Match host 10.* exec "ipcalc -n %h | grep -q '^NETWORK=10.2.64.0/18$'"
    ProxyCommand ssh -q bastion-bar -W %h:%p

Create a reusable script (~/bin/ssh-cidr-check):

#!/bin/bash
ip=$1
cidr=$2

# Install ipcalc if needed: apt-get install ipcalc
ipcalc -n "$ip" | grep -q "^NETWORK=${cidr}$"
exit $?

Then reference it in your config:

Match host 10.* exec "/bin/bash ~/bin/ssh-cidr-check %h 10.2.0.0/18"
    ProxyCommand ssh -q bastion-foo -W %h:%p

For systems without ipcalc, use this Python one-liner:

Match host 10.* exec "python3 -c 'import ipaddress,sys; sys.exit(0 if ipaddress.ip_address(sys.argv[1]) in ipaddress.ip_network(sys.argv[2]) else 1)' %h 10.2.0.0/18"
    ProxyCommand ssh -q bastion -W %h:%p

For environments with thousands of hosts:

  • Cache CIDR calculations
  • Use compiled languages (Go/Rust) for the check script
  • Limit Match directives to broad host patterns first