Reach Cockpit safely over WireGuard, not the internet

Put the WireGuard interface in its own firewalld zone, allow Cockpit only there, and prove that the internet cannot reach it.

Part 4 of 5 in Cockpit on openSUSE. Last updated

In short: Never forward port 9090: Cockpit's login is your system password. Forward only WireGuard's UDP port, put the wg0 interface in its own firewalld zone (for example internal), allow the cockpit service in that zone and remove it from public. Optionally bind cockpit.socket to the tunnel address with a drop-in (ListenStream= plus FreeBind=yes). Then browse to https://10.8.0.1:9090 with the tunnel up, and check that it times out with the tunnel down.

Why not just forward port 9090?

Cockpit logs in with your system accounts and, once you switch on administrative access, can do everything root can. On the open internet that login page gets password guessing within hours, and any future Cockpit bug is exploitable by anyone. A WireGuard tunnel turns this around: the router forwards one UDP port, and without a valid key nothing on the server even answers.

The plan

Addresses used in this guide
WhatValue
Tunnel network10.8.0.0/24
Server in the tunnel10.8.0.1 on interface wg0
Forwarded on the routerUDP 51820 only
Cockpithttps://10.8.0.1:9090, tunnel only
One UDP port open to the world; Cockpit only answers inside the tunnel.

WireGuard on the server

Install the tools, write /etc/wireguard/wg0.conf and start the tunnel (all as root). The WireGuard config generator creates the server and client configs with keys, in your browser:

sudo zypper install wireguard-tools
sudoedit /etc/wireguard/wg0.conf           # paste the server config
sudo chmod 600 /etc/wireguard/wg0.conf     # it holds the private key
sudo systemctl enable --now wg-quick@wg0
sudo wg show                               # peers and last handshake

Keys are plain files. Made in a scratch folder with umask 077, so even the public keys are private to you (they do not need to be, but it does no harm):

$ umask 077; wg genkey > server.key; wg pubkey < server.key > server.pub
$ wg genkey | tee phone.key | wg pubkey > phone.pub; ls -l
total 16
-rw------- 1 user user 45 Sep 28 06:40 phone.key
-rw------- 1 user user 45 Sep 28 06:40 phone.pub
-rw------- 1 user user 45 Sep 28 06:40 server.key
-rw------- 1 user user 45 Sep 28 06:40 server.pub

Real output on openSUSE Tumbleweed, wireguard-tools v1.0.20260223, 2026-09-28; user and host names and addresses redacted.

A public key is derived from its private key, so you can always check that a pair belongs together, and wg refuses anything that is not a key:

$ wg pubkey < phone.key; cat phone.pub
oXtKsyjAE+mjS12dr5yz/2q9s0MRegdWTHQg6ikSfBE=
oXtKsyjAE+mjS12dr5yz/2q9s0MRegdWTHQg6ikSfBE=
$ echo "not a key" | wg pubkey; echo "exit=$?"
wg: Key is not the correct length or format
exit=1

Real output on openSUSE Tumbleweed, wireguard-tools v1.0.20260223, 2026-09-28; user and host names and addresses redacted.

These keys were made for this page and thrown away; never publish a key you use.

firewalld: Cockpit only in the tunnel zone

These commands need root. Read what each one does before you run it; on a server you reach over SSH, keep a console at hand in case a rule is wrong.

sudo firewall-cmd --permanent --zone=public --add-service=wireguard
sudo firewall-cmd --permanent --zone=internal --add-interface=wg0
sudo firewall-cmd --permanent --zone=internal --add-service=cockpit
sudo firewall-cmd --permanent --zone=public --remove-service=cockpit
sudo firewall-cmd --reload
sudo firewall-cmd --get-active-zones
  1. --zone=public --add-service=wireguard: accept the tunnel itself (UDP 51820) on the normal network card.
  2. --zone=internal --add-interface=wg0: everything that arrives through the tunnel is judged by the internal zone.
  3. --zone=internal --add-service=cockpit: allow 9090/tcp there, and only there.
  4. --zone=public --remove-service=cockpit: close 9090 on the normal network card, in case it was opened earlier. firewalld only warns if it was not.
  5. --reload applies the permanent config; --get-active-zones should list wg0 under internal.

internal also allows a few services by default (such as ssh); check with sudo firewall-cmd --zone=internal --list-all. Want Cockpit on the LAN as well? Give the LAN subnet a zone with --add-source and allow cockpit there too, as in the firewalld basics guide. The firewalld port helper builds such rules with the undo commands.

What the two firewalld names stand for, straight from the files firewalld ships. The internal zone allows these services before you add cockpit:

$ cat /usr/lib/firewalld/services/wireguard.xml
<?xml version="1.0" encoding="utf-8"?>
<service>
  <short>WireGuard</short>
  <description>WireGuard is the simple, fast and modern VPN. The port needs to be open if a peer has this host explicitly configured as endpoint.</description>
  <port protocol="udp" port="51820"/>
</service>
$ grep "service name" /usr/lib/firewalld/zones/internal.xml
  <service name="ssh"/>
  <service name="mdns"/>
  <service name="samba-client"/>
  <service name="dhcpv6-client"/>

Real output on openSUSE Tumbleweed, firewalld, 2026-09-28; user and host names and addresses redacted.

Optional: listen on the tunnel address only

The firewall is enough on its own. For a second layer, make Cockpit listen only on the tunnel address, with a socket drop-in (needs root):

sudo systemctl edit cockpit.socket
[Socket]
ListenStream=
ListenStream=10.8.0.1:9090
FreeBind=yes
sudo systemctl daemon-reload
sudo systemctl restart cockpit.socket
sudo ss -tln | grep 9090                   # 10.8.0.1:9090 only

The empty ListenStream= clears the default "all addresses" entry. FreeBind=yes lets the socket start at boot even before wg0 has its address. This is the method the Cockpit documentation describes.

Test it from outside

Use a phone on mobile data, so you really come from the internet:

  1. WireGuard on: https://10.8.0.1:9090 shows the Cockpit login.
  2. WireGuard off: the same address times out, and so does your public IP on port 9090.
  3. On the server: sudo wg show lists the phone's latest handshake.

If the page does not load with the tunnel on, check that the client's AllowedIPs includes 10.8.0.1 (or 10.8.0.0/24), and that wg0 really sits in the internal zone. SSH works the same way; the SSH hardening guide explains why port 22 should not be forwarded either.

Frequently asked questions

Can I put Cockpit behind a reverse proxy instead?

Yes, Cockpit documents proxy setups, but a proxy on the internet still exposes the login page to everyone. A VPN hides the service completely: without a valid WireGuard key, nothing on port 9090 answers.

My WireGuard connection is managed by NetworkManager. Does --add-interface still work?

NetworkManager assigns zones to the interfaces it manages, so set the zone on the connection instead: sudo nmcli connection modify wg0 connection.zone internal, then bring the connection up again.

The browser warns about the certificate every time. Can I fix that?

Put a certificate and key for the name you use into /etc/cockpit/ws-certs.d/ (as name.crt and name.key) and restart cockpit.socket. For a tunnel-only address, accepting the self-signed certificate once per browser is also fine.