<!--date=20250208 -->


<h1>Unix Networking and Security</h1>
<p>Most of this is learned by breaking things. SSH config in particular has a way of locking you out of a remote box at the worst possible time. These are the commands and concepts I actually use, with the caveats that took trial and error to internalize.</p>

<h2>Checking What's Listening</h2>
<p>Before anything else — know what's running on your machine:</p>
<pre>
$ ss -tlnp          # TCP, listening, numeric, show process — the modern tool
$ ss -ulnp          # UDP version
$ netstat -tlnp     # older alternative (same idea, on systems without ss)
$ lsof -i :22       # which process is using a specific port
</pre>
<p><code>ss</code> replaces <code>netstat</code> on modern Linux. It's faster and shows more information. Both are available on most systems.</p>

<h2>Network Interface Management</h2>
<p>The <code>ip</code> command is the modern tool. <code>ifconfig</code> still works but has been deprecated for years:</p>

<table>
  <thead><tr><th>Command</th><th>Purpose</th></tr></thead>
  <tbody>
    <tr><td><code>ip addr show</code></td><td>All interfaces and their addresses</td></tr>
    <tr><td><code>ip addr show eth0</code></td><td>Specific interface</td></tr>
    <tr><td><code>ip link set eth0 up</code></td><td>Bring interface up</td></tr>
    <tr><td><code>ip link set eth0 down</code></td><td>Bring interface down</td></tr>
    <tr><td><code>ip addr add 192.168.1.100/24 dev eth0</code></td><td>Temporary address (lost on reboot)</td></tr>
    <tr><td><code>ip addr del 192.168.1.100/24 dev eth0</code></td><td>Remove an address</td></tr>
    <tr><td><code>ip route show</code></td><td>Routing table</td></tr>
    <tr><td><code>ip route add default via 192.168.1.1</code></td><td>Set default gateway</td></tr>
  </tbody>
</table>

<h2>SSH — Key-Based Auth</h2>
<p>Password-based SSH auth is a security liability on any internet-facing server. Keys are the right answer:</p>
<pre>
# Generate a key — ed25519 is better than the old RSA default
$ ssh-keygen -t ed25519 -C "jason@hostname"

# Copy public key to the remote server (requires password login once)
$ ssh-copy-id user@remote_host

# Or manually — useful when ssh-copy-id isn't available
$ cat ~/.ssh/id_ed25519.pub | ssh user@host \
    "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys"
</pre>

<h2>Hardening sshd_config</h2>
<p>Edit <code>/etc/ssh/sshd_config</code>. Always keep a second SSH session open while testing — if you lock yourself out of a remote machine, it's a datacenter trip or a support ticket:</p>
<pre>
PermitRootLogin no              # never log in as root directly
PasswordAuthentication no       # keys only
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
AllowUsers jason                # whitelist specific users
MaxAuthTries 3                  # limit brute-force attempts
LoginGraceTime 30               # close unauthenticated connections quickly
</pre>

<pre>
# Test the config before reloading — catches syntax errors
$ sshd -t

# Reload (not restart — keeps existing sessions alive)
$ systemctl reload sshd
</pre>

<h2>SSH Client Config (~/.ssh/config)</h2>
<p>Saves a lot of typing and enables advanced workflows like jump hosts:</p>
<pre>
Host myserver
    HostName 203.0.113.10
    User jason
    IdentityFile ~/.ssh/id_ed25519
    Port 2222

Host bastion
    HostName bastion.example.com
    User admin
    IdentityFile ~/.ssh/id_ed25519

Host *.internal
    User admin
    ProxyJump bastion          # tunnel through bastion host automatically
</pre>
<p>After this, <code>ssh myserver</code> just works. <code>ssh app01.internal</code> automatically tunnels through the bastion without any extra flags.</p>

<h2>Firewalls</h2>

<h3>ufw (Ubuntu/Debian)</h3>
<pre>
$ ufw status verbose
$ ufw allow 22/tcp
$ ufw allow 80/tcp
$ ufw allow 443/tcp
$ ufw allow from 192.168.1.0/24 to any port 5432   # postgres, local net only
$ ufw deny 23/tcp                                    # block telnet
$ ufw enable
$ ufw reload
</pre>

<h3>iptables</h3>
<pre>
$ iptables -L -n -v                              # list rules with packet counts
$ iptables -A INPUT -p tcp --dport 22 -j ACCEPT
$ iptables -A INPUT -p tcp --dport 80 -j ACCEPT
$ iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
$ iptables -P INPUT DROP                         # default deny

# Persist across reboots
$ iptables-save > /etc/iptables/rules.v4
$ iptables-restore < /etc/iptables/rules.v4
</pre>

<h2>Diagnostic Tools</h2>

<table>
  <thead><tr><th>Command</th><th>Purpose</th></tr></thead>
  <tbody>
    <tr><td><code>ping -c 4 8.8.8.8</code></td><td>Basic reachability</td></tr>
    <tr><td><code>traceroute 8.8.8.8</code></td><td>Path packets take</td></tr>
    <tr><td><code>mtr 8.8.8.8</code></td><td>Live updating traceroute — better than traceroute</td></tr>
    <tr><td><code>dig google.com</code></td><td>DNS lookup</td></tr>
    <tr><td><code>dig @8.8.8.8 google.com</code></td><td>DNS lookup against a specific resolver</td></tr>
    <tr><td><code>curl -I https://example.com</code></td><td>HTTP headers only</td></tr>
    <tr><td><code>curl -v https://example.com</code></td><td>Full HTTP request/response details (includes TLS handshake)</td></tr>
    <tr><td><code>tcpdump -i eth0 port 80</code></td><td>Capture HTTP traffic (needs root)</td></tr>
    <tr><td><code>tcpdump -i eth0 -w dump.pcap</code></td><td>Save capture to file for Wireshark analysis</td></tr>
  </tbody>
</table>

<h2>Fail2ban</h2>
<p>Any SSH port exposed to the internet will get constant brute-force attempts. Fail2ban watches your logs and bans IPs that fail too many times:</p>
<pre>
$ apt install fail2ban
$ cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
</pre>

<p>In <code>/etc/fail2ban/jail.local</code>:</p>
<pre>
[sshd]
enabled  = true
port     = ssh
maxretry = 5
bantime  = 3600     # ban for 1 hour
findtime = 600      # count failures within 10-minute window
</pre>

<pre>
$ systemctl enable --now fail2ban
$ fail2ban-client status sshd                  # current bans
$ fail2ban-client set sshd unbanip 1.2.3.4    # unban an IP
</pre>

<h2>SSH Tunnels</h2>
<pre>
# Local port forwarding — access remote_host:5432 (Postgres) as localhost:5432
$ ssh -L 5432:localhost:5432 user@remote_host

# Remote port forwarding — expose local port 8080 on the remote server
$ ssh -R 8080:localhost:8080 user@remote_host

# Dynamic SOCKS proxy — route traffic through remote_host
$ ssh -D 1080 user@remote_host
# Then configure your browser to use SOCKS proxy at localhost:1080

# Keep tunnels alive
$ ssh -N -f -L 5432:localhost:5432 user@remote_host
# -N: don't execute commands, -f: go to background
</pre>

<h2>References</h2>
<ul>
  <li><a href="https://man.openbsd.org/ssh_config.5" target="_blank" rel="noopener">ssh_config(5) man page ↗</a></li>
  <li><a href="https://www.fail2ban.org/wiki/index.php/Main_Page" target="_blank" rel="noopener">Fail2ban documentation ↗</a></li>
  <li><a href="https://wiki.archlinux.org/title/OpenSSH" target="_blank" rel="noopener">Arch Linux OpenSSH guide ↗</a></li>
</ul>
