Server Setup
openme server setup, SPA server Linux, openme daemon, openme init, openme serve, nftables SPA, iptables SPA
Install
The openme server requires nftables or iptables to manage firewall rules and is therefore only supported on GNU/Linux. For client-only use on iOS, android, macOS or Windows, see the CLI client setup or the native desktop apps.
From a Debian / Ubuntu package
Pre-built .deb packages are available for Debian, Ubuntu, and derivatives. Download the package for your architecture from GitHub Releases and install it with dpkg:
curl -LO https://github.com/merlos/openme/releases/latest/download/openme_latest_amd64.deb
sudo dpkg -i openme_latest_amd64.debcurl -LO https://github.com/merlos/openme/releases/latest/download/openme_latest_arm64.deb
sudo dpkg -i openme_latest_arm64.debcurl -LO https://github.com/merlos/openme/releases/latest/download/openme_latest_armhf.deb
sudo dpkg -i openme_latest_armhf.debcurl -LO https://github.com/merlos/openme/releases/latest/download/openme_latest_i386.deb
sudo dpkg -i openme_latest_i386.debcurl -LO https://github.com/merlos/openme/releases/latest/download/openme_latest_riscv64.deb
sudo dpkg -i openme_latest_riscv64.debThe .deb package installs the binary to /usr/bin/openme and includes initializes the server and starts the systemd service automatically. You can skip the next two sections and proceed to Add a client →.
From a pre-built binary
Download the latest release from GitHub Releases:
curl -Lo openme https://github.com/merlos/openme/releases/latest/download/openme-linux-amd64
chmod +x openme
sudo mv openme /usr/local/bin/curl -Lo openme https://github.com/merlos/openme/releases/latest/download/openme-linux-arm64
chmod +x openme
sudo mv openme /usr/local/bin/curl -Lo openme https://github.com/merlos/openme/releases/latest/download/openme-linux-arm
chmod +x openme
sudo mv openme /usr/local/bin/curl -Lo openme https://github.com/merlos/openme/releases/latest/download/openme-linux-386
chmod +x openme
sudo mv openme /usr/local/bin/curl -Lo openme https://github.com/merlos/openme/releases/latest/download/openme-linux-riscv64
chmod +x openme
sudo mv openme /usr/local/bin/Initialise the Server
openme init generates a fresh Curve25519 keypair and writes /etc/openme/config.yaml with sensible defaults.
sudo openme init --server myserver.example.comOptions:
| Flag | Default | Description |
|---|---|---|
--server |
(required) | Public hostname or IP — used in generated client configs |
--port |
54154 |
UDP knock port and TCP health port |
--firewall |
nft |
Firewall backend: nft or iptables |
--force |
false |
Overwrite an existing config |
The command prints the server’s Curve25519 public key — you’ll need this when provisioning clients, but openme add handles it automatically.
Start the Server
sudo openme serveFor production, install as a systemd service (the .deb package does this automatically). The unit below matches the one shipped in the package:
# /etc/systemd/system/openme.service
[Unit]
Description=openme Single Packet Authorization server
Documentation=https://openme.merlos.org/docs/getting-started/server-setup
After=network.target
[Service]
Type=simple
ExecStart=/usr/bin/openme serve
Restart=on-failure
RestartSec=5
# Run as the dedicated 'openme' system account.
User=openme
Group=openme
# Grant only the capability required for firewall manipulation.
CapabilityBoundingSet=CAP_NET_ADMIN
AmbientCapabilities=CAP_NET_ADMIN
# Create /run/openme (owned by openme:openme) for the session state file.
RuntimeDirectory=openme
RuntimeDirectoryMode=0750
# Harden the service surface.
ProtectSystem=full
PrivateTmp=true
NoNewPrivileges=true
[Install]
WantedBy=multi-user.targetRuntimeDirectory=openme makes systemd create /run/openme/ (owned by openme:openme, mode 0750) at start-up and remove it on stop. openme writes its session state file there automatically.
sudo systemctl enable --now openme
sudo systemctl status openmeHarden the Firewall
The whole point of openme is that no TCP ports are permanently open.
Apply a default-deny policy before opening anything, then only allow the UDP knock port.
nftables
# Flush and rebuild a minimal ruleset
sudo nft flush ruleset
sudo nft add table inet filter
# Base chains
sudo nft add chain inet filter input '{ type filter hook input priority 0; policy drop; }'
sudo nft add chain inet filter forward '{ type filter hook forward priority 0; policy drop; }'
sudo nft add chain inet filter output '{ type filter hook output priority 0; policy accept; }'
# Allow established / related traffic and loopback
sudo nft add rule inet filter input ct state established,related accept
sudo nft add rule inet filter input iifname "lo" accept
# By default openme opens the udp_port for knocks, but if you set open_knock_port: false then add this rule to allow the knock packets:
#sudo nft add rule inet filter input udp dport 54154 acceptPersist the ruleset:
sudo nft list ruleset | sudo tee /etc/nftables.conf
sudo systemctl enable --now nftablesiptables / ip6tables
# ── IPv4 ──────────────────────────────────────────────────────────────────
# Flush existing rules and set default DROP on INPUT and FORWARD
sudo iptables -F
sudo iptables -X
sudo iptables -P INPUT DROP
sudo iptables -P FORWARD DROP
sudo iptables -P OUTPUT ACCEPT
# Allow established / related traffic and loopback
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
sudo iptables -A INPUT -i lo -j ACCEPT
# By default openme opens the udp_port for knocks, but if you set open_knock_port: false then add this rule to allow the knock packets:
#sudo iptables -A INPUT -p udp --dport 54154 -j ACCEPT
# ── IPv6 ──────────────────────────────────────────────────────────────────
sudo ip6tables -F
sudo ip6tables -X
sudo ip6tables -P INPUT DROP
sudo ip6tables -P FORWARD DROP
sudo ip6tables -P OUTPUT ACCEPT
sudo ip6tables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
sudo ip6tables -A INPUT -i lo -j ACCEPT
# By default openme opens the udp_port for knocks, but if you set open_knock_port: false then add this rule to allow the knock packets:
#sudo ip6tables -A INPUT -p udp --dport 54154 -j ACCEPTPersist across reboots (Debian/Ubuntu):
sudo apt install iptables-persistent
sudo netfilter-persistent saveThese rules drop all incoming TCP connections, including SSH.
Run them from the console or a recovery environment, or add a temporary SSH allowance first:
# Temporary SSH rule — remove it once openme clients are verified
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT
sudo ip6tables -A INPUT -p tcp --dport 22 -j ACCEPTOnce your openme clients are working, remove the temporary SSH rules:
sudo iptables -D INPUT -p tcp --dport 22 -j ACCEPT
sudo ip6tables -D INPUT -p tcp --dport 22 -j ACCEPTFor nftables, if you added a temporary SSH rule:
# Find the rule handle
sudo nft -a list chain inet filter input
# Delete by handle (replace <handle> with the number shown)
sudo nft delete rule inet filter input handle <handle>Monitor Live Sessions
openme sessions shows which clients currently have open firewall rules and when their allowance expires, plus the last-seen time for clients who knocked previously but whose rule has since expired.
sudo openme sessionsExample output:
Session state as of 2026-03-17 14:23:01
ACTIVE SESSIONS
────────────────────────────────────────────────────────────────────────────
CLIENT IP PORTS OPENED EXPIRES IN
──────────────────────────────────────────────────────────────────────────
alice 203.0.113.4 22/tcp 14:22:48 7s
bob 198.51.100.7 22/tcp, 2222/tcp 14:23:00 29s
LAST SEEN (no active session)
────────────────────────────────────────────────────────────────────────────
CLIENT LAST KNOCK AGO
──────────────────────────────────────────────────────────────────────────
carol 2026-03-17 13:55:02 28m1s ago
Refresh continuously with --watch (Ctrl-C to stop):
sudo openme sessions --watchThe session state is read from /run/openme/sessions.json (written by the running server). Both commands default to this path, so no extra flags are needed when the systemd unit uses RuntimeDirectory=openme.
The state file is written with permissions 0600 (owner read/write only). Use sudo to run openme sessions unless the invoking user owns the file.
Verify
Once a client is configured and has knocked, you can verify the full round trip:
# Knock and immediately check the health port (end-to-end test)
openme status --knock
# Check health port only (requires a prior knock within knock_timeout)
openme statusThe health port is never permanently open. It is opened by the firewall only after a valid knock, for the knock_timeout duration (default 30s). A closed health port means either no knock has been sent yet, or the knock_timeout has expired.
Next: Add a client →