Subscribe and receive upto $1000 discount on checkout. Learn more
Subscribe and receive upto $1000 discount on checkout. Learn more
Subscribe and receive upto $1000 discount on checkout. Learn more
Subscribe and receive upto $1000 discount on checkout. Learn more
Secure Remote Administration Without Exposing SSH

How to Secure Remote Administration Without Exposing SSH

It usually starts innocently. One Linux server becomes two. Then a handful. Then a fleet spread across environments, regions, and teams. At first, remote administration feels simple: open SSH, restrict by IP, rotate keys, and move on. But over time, the edges fray. People travel. IPs change. Vendors need access. Incident response needs speed. And suddenly “just expose SSH” becomes a permanent exception that never gets cleaned up.

In enterprise environments, the risk is not theoretical. Internet-facing SSH attracts constant scanning, password spraying, and exploit attempts. Even when we harden SSH correctly, the exposure itself becomes a liability: it expands the attack surface, complicates firewall policy, and creates noisy logs that hide real signals. The goal is not to make SSH “stronger” on the public internet. The goal is to stop putting it there.

We are going to solve this with a bastion host concept implemented as a controlled access layer: administrators connect to a dedicated bastion over a private overlay network, and only then reach internal systems. SSH remains in use, but it is no longer exposed to the internet. The bastion becomes the single, auditable choke point for remote administration.

Architecture we are implementing

  • Bastion host (Linux): the only server that administrators can reach remotely, but not via public SSH. It will accept connections over a private VPN interface.
  • Internal servers (Linux): application, database, and infrastructure nodes. SSH is reachable only from the bastion and/or the VPN interface, never from the public internet.
  • Administrators: connect to the bastion using a VPN client, then use SSH from bastion to internal servers.

We will use WireGuard as the private overlay network because it is simple, fast, and production-friendly. This is not “port forwarding” and we will not rely on it. We will instead use a dedicated VPN interface and strict firewall rules so SSH is only reachable on private paths.

Prerequisites and assumptions

Before we touch commands, we need to be explicit about what we assume. This is where most “secure access” plans fail in production: the environment is not consistent, and the plan quietly depends on missing details.

  • Platform: Linux. The commands below assume a Debian/Ubuntu-style system with apt and systemd. If we are on RHEL-family, the same concepts apply but package and firewall tooling differs.
  • OS baseline: a maintained LTS release (for example Ubuntu 22.04/24.04 or Debian 12), fully patched.
  • Privileges: we have a user with sudo on the bastion and on each internal server.
  • Network:
    • The bastion has a public IP (or is reachable via a public load balancer) for the VPN endpoint only.
    • Internal servers are reachable from the bastion over private routing (VPC/VNet/subnet) or at least can be reached from the bastion’s network.
  • SSH posture:
    • We use SSH keys, not passwords.
    • We will disable password authentication and root login where appropriate.
  • Change control: we schedule a maintenance window because we will change firewall rules and SSH exposure.
  • Key management: we will generate WireGuard keys on each node and protect private keys with correct permissions.

We will implement three roles in this guide:

  • Bastion: WireGuard server endpoint + SSH jump point.
  • Internal server: WireGuard peer + SSH restricted to bastion/VPN.
  • Admin workstation: WireGuard peer (we will show Linux commands for the client side to keep everything Linux-consistent).

Step 1: Establish a clean baseline on all servers

We will first update packages and install the tools we will rely on. This reduces drift and avoids debugging issues caused by missing utilities. We will do this on the bastion and on every internal server.

sudo apt-get update
sudo apt-get -y upgrade
sudo apt-get -y install wireguard ufw openssh-server ca-certificates

We have now ensured the OS is patched and that WireGuard, UFW (firewall), and OpenSSH are present. This also makes the rest of the steps predictable across reboots because services are managed by systemd.

We will verify that SSH is installed and running, because we will later restrict it rather than break it.

systemctl status ssh --no-pager
ss -lntp | awk 'NR==1 || /:22s/'

We have confirmed whether SSH is listening and which process owns port 22. If SSH is not running, we fix that before proceeding, because later restrictions should be deliberate, not accidental.

Step 2: Define the WireGuard addressing plan

We will use a dedicated private subnet for the VPN overlay. This subnet is not routed on the public internet and is only meaningful inside WireGuard. Keeping it separate from existing RFC1918 ranges used in the enterprise reduces collisions.

  • WireGuard network: 10.44.0.0/24
  • Bastion (wg0): 10.44.0.1/24
  • Admin workstation: 10.44.0.10/32
  • Internal server example: 10.44.0.20/32

This plan is intentionally simple. In production, we can allocate per-team ranges or per-environment ranges, but the mechanics remain the same.

Step 3: Configure WireGuard on the bastion

3.1 Detect the bastion’s public interface and IP

Before we write firewall rules, we need to know which interface is actually facing the internet. Guessing here is how we lock ourselves out. We will print the default route interface and the public IP as seen externally.

EXT_IFACE=$(ip route show default 0.0.0.0/0 | awk '{print $5; exit}')
echo "External interface: ${EXT_IFACE}"

PUBLIC_IP=$(curl -fsS https://ifconfig.me || true)
echo "Public IP (best effort): ${PUBLIC_IP}"

We now have EXT_IFACE for firewall binding. The public IP is informational; in many enterprises it may be a NAT or load balancer address, which is fine.

3.2 Generate WireGuard keys on the bastion

We will generate a private/public keypair for the bastion. The private key must be readable only by root. WireGuard will refuse unsafe permissions in many environments, and even when it does not, we should not rely on luck.

sudo install -d -m 0700 /etc/wireguard
sudo sh -c 'umask 077; wg genkey | tee /etc/wireguard/bastion.key | wg pubkey > /etc/wireguard/bastion.pub'
sudo chmod 0600 /etc/wireguard/bastion.key
sudo chmod 0644 /etc/wireguard/bastion.pub
sudo cat /etc/wireguard/bastion.pub

We have created /etc/wireguard/bastion.key and /etc/wireguard/bastion.pub with correct permissions. The public key is safe to share with peers; the private key is not.

3.3 Create the bastion WireGuard configuration

We will now define wg0 on the bastion. This will listen on UDP 51820 and assign the bastion the VPN address 10.44.0.1/24. We will also enable IP forwarding so the bastion can route traffic between VPN peers and internal networks when needed.

First, we will enable IPv4 forwarding persistently across reboots.

sudo tee /etc/sysctl.d/99-wg-forwarding.conf >/dev/null <<'EOF'
net.ipv4.ip_forward=1
EOF
sudo sysctl --system
sysctl net.ipv4.ip_forward

We have made forwarding persistent and applied it immediately. The verification confirms the kernel setting is active.

Now we will create /etc/wireguard/wg0.conf. We will include placeholders only where unavoidable: peer public keys must come from the actual peers. We will add peers after we generate their keys.

BASTION_PRIV_KEY=$(sudo cat /etc/wireguard/bastion.key)

sudo tee /etc/wireguard/wg0.conf >/dev/null <<EOF
[Interface]
Address = 10.44.0.1/24
ListenPort = 51820
PrivateKey = ${BASTION_PRIV_KEY}

# Hardening: do not rely on implicit firewall state.
# We will manage firewall separately with UFW.
EOF

sudo chmod 0600 /etc/wireguard/wg0.conf

We have created the WireGuard interface configuration with correct permissions. At this point, it has no peers yet, so it will not accept any authenticated connections until we add them.

3.4 Start WireGuard and verify the interface

We will bring up wg0 using systemd so it persists across reboots. Then we will verify that the interface exists and is listening.

sudo systemctl enable --now wg-quick@wg0
systemctl status wg-quick@wg0 --no-pager
sudo wg show
ip addr show wg0
ss -lunp | awk 'NR==1 || /:51820s/'

We have enabled WireGuard to start on boot, confirmed service health, confirmed the interface address, and confirmed UDP 51820 is listening.

Step 4: Configure the firewall on the bastion

Now we will make the bastion behave like a bastion: it should accept the VPN endpoint from the internet, but it should not accept SSH from the internet. SSH should be reachable only via the VPN interface.

We will use UFW for clarity and auditability. In enterprises, we often also enforce security groups/NACLs upstream; we should still keep host firewall rules because they remain effective even when upstream rules drift.

First, we will set a default-deny stance for inbound traffic and allow outbound. Then we will allow WireGuard UDP 51820 on the external interface. Finally, we will allow SSH only on the WireGuard interface.

sudo ufw --force reset
sudo ufw default deny incoming
sudo ufw default allow outgoing

# Allow WireGuard from anywhere (we can later restrict to corporate egress IPs if available).
sudo ufw allow in on "${EXT_IFACE}" to any port 51820 proto udp

# Allow SSH only over the VPN interface.
sudo ufw allow in on wg0 to any port 22 proto tcp

sudo ufw --force enable
sudo ufw status verbose

We have now made SSH non-public on the bastion. The only internet-facing service we intentionally allow is WireGuard. SSH is reachable only after a successful VPN connection, which is exactly the control point we want.

We will verify that SSH is not reachable on the external interface by checking listening sockets. SSH may still listen on 0.0.0.0:22, but the firewall will block it on the external interface. That is acceptable, but in high-control environments we may also bind SSH to specific interfaces later.

ss -lntp | awk 'NR==1 || /:22s/'
sudo ufw status numbered

We have confirmed SSH is listening and that UFW rules enforce the intended access paths.

Step 5: Add an administrator VPN peer (Linux client)

We will now create a WireGuard peer for an administrator workstation. This is the moment the bastion becomes usable without exposing SSH. We will generate keys on the admin system, then add the admin public key to the bastion configuration.

5.1 Generate keys on the admin workstation

We will install WireGuard and generate a keypair. We will keep permissions strict because the private key grants network access.

sudo apt-get update
sudo apt-get -y install wireguard resolvconf

sudo install -d -m 0700 /etc/wireguard
sudo sh -c 'umask 077; wg genkey | tee /etc/wireguard/admin.key | wg pubkey > /etc/wireguard/admin.pub'
sudo chmod 0600 /etc/wireguard/admin.key
sudo chmod 0644 /etc/wireguard/admin.pub
sudo cat /etc/wireguard/admin.pub

We now have the admin public key ready to be added to the bastion. The private key stays on the admin workstation.

5.2 Add the admin peer to the bastion

We will append a [Peer] section to the bastion’s wg0.conf. This authorizes the admin key and assigns it the VPN IP 10.44.0.10/32. We will also use PersistentKeepalive to keep NAT mappings stable for roaming clients.

On the bastion, we will paste the admin public key into a shell variable first, so the command remains copy/paste-safe and we avoid editing mistakes.

ADMIN_PUB_KEY="PASTE_ADMIN_PUBLIC_KEY_HERE"

sudo tee -a /etc/wireguard/wg0.conf >/dev/null <<EOF

[Peer]
PublicKey = ${ADMIN_PUB_KEY}
AllowedIPs = 10.44.0.10/32
PersistentKeepalive = 25
EOF

sudo systemctl restart wg-quick@wg0
sudo wg show

We have authorized the admin workstation as a peer and restarted WireGuard to apply the change. The wg show output should now list the peer, even before it connects.

5.3 Configure the admin WireGuard client

We will now create the admin’s wg0.conf. We need the bastion’s public key and reachable endpoint. We will retrieve the bastion public key from the bastion and then build the client config.

On the bastion, we will print the bastion public key:

sudo cat /etc/wireguard/bastion.pub

We now have the bastion public key to place into the admin configuration.

On the admin workstation, we will create /etc/wireguard/wg0.conf. We will set AllowedIPs to the VPN subnet so the admin can reach VPN peers. We will not route all internet traffic through the bastion; we are building an administration plane, not a general-purpose tunnel.

ADMIN_PRIV_KEY=$(sudo cat /etc/wireguard/admin.key)

# Set the bastion endpoint explicitly. If PUBLIC_IP is unknown, we use DNS.
BASTION_ENDPOINT_HOST="PASTE_BASTION_PUBLIC_DNS_OR_IP_HERE"
BASTION_PUB_KEY="PASTE_BASTION_PUBLIC_KEY_HERE"

sudo tee /etc/wireguard/wg0.conf >/dev/null <<EOF
[Interface]
Address = 10.44.0.10/32
PrivateKey = ${ADMIN_PRIV_KEY}
DNS = 1.1.1.1

[Peer]
PublicKey = ${BASTION_PUB_KEY}
Endpoint = ${BASTION_ENDPOINT_HOST}:51820
AllowedIPs = 10.44.0.0/24
PersistentKeepalive = 25
EOF

sudo chmod 0600 /etc/wireguard/wg0.conf
sudo systemctl enable --now wg-quick@wg0
systemctl status wg-quick@wg0 --no-pager
sudo wg show
ip addr show wg0

We have brought up the admin VPN interface and enabled it on boot. The wg show

5.4 Verify we can reach the bastion over the VPN and use SSH privately

We will now verify basic connectivity to the bastion’s VPN IP and then SSH to that VPN IP. This is the key behavioral change: we are administering without exposing SSH publicly.

ping -c 3 10.44.0.1
ssh -o StrictHostKeyChecking=accept-new -o PreferredAuthentications=publickey -o PasswordAuthentication=no 10.44.0.1

We have confirmed the VPN path works and that SSH is reachable over the private overlay. If SSH keys are not yet installed for the admin user on the bastion, SSH will fail with a permission error, which is expected and fixable by adding the admin’s SSH public key to the bastion user’s ~/.ssh/authorized_keys.

Step 6: Configure an internal server to accept SSH only from the bastion/VPN

Now we will bring an internal server into the same controlled plane. The internal server will join WireGuard as a peer and then restrict SSH so it is reachable only from the bastion and/or the VPN interface. This is where the bastion concept becomes real: internal servers stop being remotely reachable except through the controlled path.

6.1 Generate WireGuard keys on the internal server

We will generate a keypair on the internal server with strict permissions.

sudo install -d -m 0700 /etc/wireguard
sudo sh -c 'umask 077; wg genkey | tee /etc/wireguard/internal1.key | wg pubkey > /etc/wireguard/internal1.pub'
sudo chmod 0600 /etc/wireguard/internal1.key
sudo chmod 0644 /etc/wireguard/internal1.pub
sudo cat /etc/wireguard/internal1.pub

We now have the internal server’s public key to add to the bastion.

6.2 Add the internal server as a peer on the bastion

We will authorize the internal server on the bastion and assign it 10.44.0.20/32. This allows the bastion to route to it over the VPN overlay.

INTERNAL1_PUB_KEY="PASTE_INTERNAL1_PUBLIC_KEY_HERE"

sudo tee -a /etc/wireguard/wg0.conf >/dev/null <<EOF

[Peer]
PublicKey = ${INTERNAL1_PUB_KEY}
AllowedIPs = 10.44.0.20/32
EOF

sudo systemctl restart wg-quick@wg0
sudo wg show

We have added the internal server peer and applied the configuration. The bastion is now ready to accept the internal server’s authenticated connection.

6.3 Configure WireGuard on the internal server

We will create wg0.conf on the internal server. The internal server will connect to the bastion endpoint and will allow routing to the VPN subnet. This gives us a stable management IP for SSH that does not depend on the internal server’s LAN addressing.

First, we need the bastion public key. If we do not already have it, we can retrieve it from the bastion as shown earlier.

INTERNAL1_PRIV_KEY=$(sudo cat /etc/wireguard/internal1.key)
BASTION_PUB_KEY="PASTE_BASTION_PUBLIC_KEY_HERE"
BASTION_ENDPOINT_HOST="PASTE_BASTION_PUBLIC_DNS_OR_IP_HERE"

sudo tee /etc/wireguard/wg0.conf >/dev/null <<EOF
[Interface]
Address = 10.44.0.20/32
PrivateKey = ${INTERNAL1_PRIV_KEY}

[Peer]
PublicKey = ${BASTION_PUB_KEY}
Endpoint = ${BASTION_ENDPOINT_HOST}:51820
AllowedIPs = 10.44.0.0/24
PersistentKeepalive = 25
EOF

sudo chmod 0600 /etc/wireguard/wg0.conf
sudo systemctl enable --now wg-quick@wg0
systemctl status wg-quick@wg0 --no-pager
sudo wg show
ip addr show wg0

We have brought up the internal server’s VPN interface and enabled it on boot. Once it handshakes with the bastion, the bastion will be able to reach 10.44.0.20.

6.4 Restrict SSH on the internal server

We will now enforce the core security outcome: SSH on the internal server must not be reachable from the public internet. In many enterprise networks, internal servers do not have public IPs, but we still enforce host-level policy because networks change and mistakes happen.

We will use UFW to allow SSH only from the bastion’s VPN IP and optionally from the VPN subnet. The strictest approach is to allow only the bastion VPN IP. That keeps the bastion as the single hop for SSH, which is the cleanest bastion model.

sudo ufw --force reset
sudo ufw default deny incoming
sudo ufw default allow outgoing

# Allow SSH only from the bastion over the VPN overlay.
sudo ufw allow from 10.44.0.1 to any port 22 proto tcp

# Optionally allow ICMP from the VPN subnet for diagnostics (can be omitted in stricter environments).
sudo ufw allow from 10.44.0.0/24 to any proto icmp

sudo ufw --force enable
sudo ufw status verbose

We have now made SSH reachable only from the bastion’s VPN identity. Even if someone can reach the internal server on a network path, SSH will not accept inbound connections unless they originate from the bastion’s VPN IP.

We will verify SSH is still running and that firewall rules are active.

systemctl status ssh --no-pager
ss -lntp | awk 'NR==1 || /:22s/'
sudo ufw status numbered

We have confirmed SSH is healthy and that the firewall is enforcing the intended policy.

6.5 Verify end-to-end access through the bastion

Now we verify the full bastion flow: admin connects to VPN, SSH to bastion over VPN, then SSH from bastion to the internal server’s VPN IP.

From the admin workstation:

ping -c 3 10.44.0.20

If we allowed direct VPN peer-to-peer reachability, this ping may succeed. In stricter bastion-only designs, we may later restrict peer-to-peer and require all access to go through the bastion. For now, the decisive test is SSH via the bastion.

From the bastion:

ping -c 3 10.44.0.20
ssh -o StrictHostKeyChecking=accept-new -o PreferredAuthentications=publickey -o PasswordAuthentication=no 10.44.0.20

We have confirmed the bastion can reach the internal server over the VPN overlay and that SSH is accessible only through the bastion path.

Step 7: Harden SSH for enterprise administration

Even though SSH is no longer exposed publicly, we still harden it because internal threats, lateral movement, and credential theft are real. We will apply a conservative SSH configuration on the bastion and internal servers.

We will first back up the current SSH configuration, then apply a minimal hardened config. We will keep it compatible with enterprise automation and avoid breaking common key-based access.

sudo cp -a /etc/ssh/sshd_config /etc/ssh/sshd_config.bak.$(date +%F)

sudo tee /etc/ssh/sshd_config >/dev/null <<'EOF'
Include /etc/ssh/sshd_config.d/*.conf

Port 22
Protocol 2

PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
ChallengeResponseAuthentication no
UsePAM yes

PubkeyAuthentication yes
AuthenticationMethods publickey

X11Forwarding no
AllowTcpForwarding no
PermitTunnel no

ClientAliveInterval 300
ClientAliveCountMax 2

LogLevel VERBOSE
EOF

sudo sshd -t
sudo systemctl restart ssh
systemctl status ssh --no-pager

We have replaced the SSH daemon configuration with a hardened baseline, validated syntax with sshd -t, and restarted SSH. Password-based logins are now disabled, root login is disabled, and forwarding features are turned off to reduce abuse potential.

We will verify that SSH still accepts key-based authentication by connecting over the VPN path (admin to bastion, bastion to internal). If we do not yet have keys installed, we must add them before disabling passwords in production.

Operational checks and ongoing verification

In production, we do not trust a one-time setup. We verify continuously and we make the verification cheap.

WireGuard health checks

On the bastion, we check peer handshakes and data transfer.

sudo wg show
sudo systemctl status wg-quick@wg0 --no-pager

We should see recent handshakes for active peers and increasing transfer counters during use.

Firewall posture checks

On the bastion, we confirm only UDP 51820 is allowed from the internet-facing interface and SSH is allowed only on wg0.

sudo ufw status verbose
ss -lunp | awk 'NR==1 || /:51820s/'
ss -lntp | awk 'NR==1 || /:22s/'

We have confirmed the intended exposure: WireGuard is reachable, SSH is not publicly reachable due to firewall policy.

Persistence across reboots

We confirm that both WireGuard and SSH are enabled and will return after a reboot.

systemctl is-enabled wg-quick@wg0
systemctl is-enabled ssh

We have confirmed service enablement state, which is a common gap in “works now” setups.

Troubleshooting

Symptom: WireGuard shows no handshake on the client

  • Likely causes:
    • UDP 51820 is blocked upstream (cloud security group, perimeter firewall) or on the bastion host firewall.
    • Wrong bastion endpoint DNS/IP in the client config.
    • Keys do not match (client public key not added to bastion, or wrong bastion public key on client).
    • System time skew causing confusing authentication behavior in some environments.
  • Fix:
    • On bastion, verify listening: ss -lunp | awk 'NR==1 || /:51820s/'
    • On bastion, verify firewall: sudo ufw status verbose
    • On bastion, verify peer exists: sudo wg show
    • On client, verify endpoint resolves: getent ahosts PASTE_BASTION_PUBLIC_DNS_OR_IP_HERE
    • Restart interfaces after changes: sudo systemctl restart wg-quick@wg0

Symptom: VPN connects, but SSH to 10.44.0.1 or 10.44.0.20 fails

  • Likely causes:
    • UFW rules do not allow SSH on wg0 (bastion) or from 10.44.0.1 (internal server).
    • SSH keys are not installed for the target user.
    • SSH hardening disabled password auth before keys were in place.
  • Fix:
    • On bastion: sudo ufw status numbered and confirm allow in on wg0 to any port 22.
    • On internal server: sudo ufw status numbered and confirm allow from 10.44.0.1 to any port 22.
    • Confirm SSH is running: systemctl status ssh --no-pager
    • Confirm key-based auth: ensure ~/.ssh/authorized_keys exists with correct permissions (700 for ~/.ssh, 600 for authorized_keys).

Symptom: After reboot, VPN is down

  • Likely causes:
    • wg-quick@wg0 not enabled.
    • Config file permissions are too open and WireGuard refuses to load.
  • Fix:
    • Enable service: sudo systemctl enable --now wg-quick@wg0
    • Fix permissions: sudo chmod 0600 /etc/wireguard/wg0.conf
    • Check logs: journalctl -u wg-quick@wg0 --no-pager -n 200

Common mistakes

Mistake: Allowing SSH on the external interface “temporarily”

Symptom: Security scans show port 22 open on the bastion’s public IP.

Fix: Remove any UFW rule that allows 22/tcp generally, and keep only the wg0-scoped rule.

sudo ufw status numbered
# Delete the offending rule number(s) that allow 22/tcp broadly:
# sudo ufw delete <RULE_NUMBER>

# Ensure the correct rule exists:
sudo ufw allow in on wg0 to any port 22 proto tcp
sudo ufw status verbose

We have restored the intended posture: SSH is reachable only over the VPN interface.

Mistake: Reusing the same WireGuard private key across multiple systems

Symptom: Peers flap, handshakes appear inconsistent, and access becomes unpredictable.

Fix: Generate unique keys per node and update the bastion peer list accordingly. Keys are identities in WireGuard; duplication breaks the model.

Mistake: Disabling SSH passwords before confirming key access

Symptom: We can connect to the VPN, but SSH rejects all logins and we lose administrative access.

Fix: Use console access or existing management channels to add SSH keys to authorized_keys, then re-apply the hardened SSH config. In controlled rollouts, we validate key access first, then disable passwords.

Mistake: Forgetting upstream firewall rules

Symptom: Everything looks correct on the bastion, but clients never handshake.

Fix: Ensure perimeter controls allow UDP 51820 to the bastion. Host firewall rules cannot override upstream blocks.

How do we at NIILAA look at this

This setup is not impressive because it is complex. It is impressive because it is controlled. Every component is intentional. Every configuration has a reason. This is how infrastructure should scale — quietly, predictably, and without drama.

At NIILAA, we help enterprises design and implement bastion-based remote administration that removes public SSH exposure, enforces least privilege, and stays operable under real conditions: audits, incidents, team growth, and changing networks. We design the access plane, implement hardened configurations, integrate identity and logging expectations, and maintain the solution so it remains secure long after the initial rollout.

Website: https://www.niilaa.com
Email: [email protected]
LinkedIn: https://www.linkedin.com/company/niilaa
Facebook: https://www.facebook.com/niilaa.llc

Leave A Comment

All fields marked with an asterisk (*) are required