# Setting Up Your Own SSH Bastion Server

LLMS index: [llms.txt](/en/llms.txt)

---

## Why You Need a Bastion and Where It Lives

A bastion is the single entry point into a private network segment. Instead of exposing SSH on every server to the internet, you funnel traffic through one hardened host with a strict access policy. Typical layout: internet → bastion (public IP) → internal servers (only private subnet, SSH listening on `127.0.0.1` or a private interface).

The bastion sits in a demilitarized zone (DMZ) or a public subnet provided by your hosting platform. Internal machines have no route to the internet through the bastion — return traffic flows only over established connections. This is a baseline model you can deploy on any VPS in about 15 minutes.

## Choosing an OS and Basic Setup

The bastion doesn't need to be heavy. Debian, Ubuntu Server, or AlmaLinux — all work. I usually grab a minimal Ubuntu 22.04 LTS install and bring it to working state by hand.

```bash
# Update and install the minimum utility set
sudo apt update && sudo apt upgrade -y
sudo apt install -y fail2ban ufw curl htop
```

> [!TIP]
> Don't put a GUI, databases, or other services on the bastion. The smaller the attack surface, the better.

Create a non-privileged user for daily work:

```bash
sudo adduser deploy
sudo usermod -aG sudo deploy
```

## SSH Configuration: Keys, Port, Disable Passwords

Generate a key on your workstation if you don't have one yet:

```bash
ssh-keygen -t ed25519 -C "bastion-access"
ssh-copy-id -i ~/.ssh/id_ed25519.pub deploy@bastion_ip
```

On the bastion, edit `/etc/ssh/sshd_config`:

```bash
Port 22220
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
MaxSessions 5
AllowUsers deploy
```

> [!WARNING]
> Before restarting SSH, make sure the key is added and works. Otherwise, you'll lock yourself out of the server.

Validate the config and restart:

```bash
sudo sshd -t
sudo systemctl restart sshd
```

`sshd_config` keys and what they do:

| Parameter | Value | Purpose |
|---|---|---|
| `Port` | 22220 | Non-standard port, reduces log noise |
| `PermitRootLogin` | no | Blocks direct root login |
| `PasswordAuthentication` | no | Keys only |
| `MaxAuthTries` | 3 | Limits authentication attempts |
| `AllowUsers` | deploy | Whitelist of allowed users |

## Firewall and Access Restrictions

UFW is the simplest way to close everything unnecessary:

```bash
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22220/tcp comment "SSH bastion"
sudo ufw enable
```

If the bastion is only for your IP, restrict it further:

```bash
sudo ufw allow from YOUR_IP to any port 22220 proto tcp
```

> [!NOTE]
> If your IP is dynamic, use a VPN instead of exposing the port. Opening SSH to the internet without source restrictions is a bad practice.

For internal traffic, add a rule on the bastion to allow packet forwarding:

```bash
sudo sysctl -w net.ipv4.ip_forward=1
```

Add `net.ipv4.ip_forward = 1` to `/etc/sysctl.conf` so the setting survives a reboot.

## Fail2ban and Brute-Force Protection

Install and configure Fail2ban to protect against password guessing:

```bash
sudo apt install fail2ban -y
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
```

In `/etc/fail2ban/jail.local`:

```ini
[sshd]
enabled = true
port = 22220
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 3600
findtime = 600
```

Restart:

```bash
sudo systemctl restart fail2ban
sudo systemctl enable fail2ban
sudo fail2ban-client status sshd
```

## Logging and Auditing

The bastion must log everything. On Debian/Ubuntu, SSH logs go to `/var/log/auth.log`. For centralized collection, configure rsyslog to ship to a separate SIEM or at least a second server:

```bash
# Add to /etc/rsyslog.conf:
*.* @logserver_ip:514
```

For user action auditing, attach `auditd`:

```bash
sudo apt install auditd -y
sudo auditctl -w /etc/ssh/sshd_config -p wa -k ssh_config
sudo auditctl -w /home -p r -k user_files
```

View events:

```bash
sudo ausearch -k ssh_config
sudo ausearch -k user_files
```

> [!WARNING]
> Logs on the bastion are an attacker's first target. Set up remote shipping as early as possible — otherwise, on compromise, you lose the incident history.

## Port Forwarding and Tunnels Through the Bastion

The bastion's main job is to give access to internal machines without exposing their ports. Three ways to do it:

**1. Port forwarding via SSH tunnel:**

```bash
ssh -L 5432:10.0.1.5:5432 deploy@bastion_ip -p 22220
```

Now local port 5432 on your machine proxies to PostgreSQL on `10.0.1.5`.

**2. SOCKS proxy for access to all internal hosts:**

```bash
ssh -D 1080 deploy@bastion_ip -p 22220
```

Point your browser or `proxychains` at `127.0.0.1:1080`.

**3. Reverse tunnel for accessing your local machine from the bastion network:**

```bash
ssh -R 9090:localhost:8080 deploy@bastion_ip -p 22220
```

This lets someone reach a local service on port 9090 via the bastion.

For persistent tunnels, use `autossh` or configure `~/.ssh/config`:

```
Host bastion
    HostName bastion_ip
    Port 22220
    User deploy
    IdentityFile ~/.ssh/id_ed25519
    ServerAliveInterval 60
    ServerAliveCountMax 3
```

Now `ssh bastion` is all you need to connect, and tunnels are built with a single command.

> [!TIP]
> For team operations, store keys in 1Password or HashiCorp Vault, and grant bastion access through ephemeral sessions with expiring tokens. This reduces the risk of key compromise.
