The Latitude.sh Agent (lsh-agent) is a lightweight daemon that keeps your server's firewall in sync with the Latitude.sh Firewall. It is required on every server assigned to a firewall: the agent periodically fetches the rules you configure in the dashboard and applies them locally using UFW (Uncomplicated Firewall).
- The agent runs as a systemd service and polls the Latitude.sh API on a configurable interval (default: 30 seconds).
- On each cycle it fetches the current rules for the firewall assigned to the server.
- Before applying anything, it verifies that the firewall returned by the API matches the
firewall_id(andproject_id) it was installed with. If the API returns a different firewall — for example a stale assignment left over after the server was released to the pool — the agent refuses to apply it, leaving the existing firewall untouched, rather than enforcing another tenant's rules. - Rules are synchronized with UFW: missing rules are added and stale rules are removed, so the server always matches what is configured in the dashboard.
Notes:
- Only TCP and UDP rules are managed. ICMP traffic is permitted by default and cannot be customized.
- Docker manages its own iptables chains, which take precedence over UFW. Ports published by Docker containers may bypass firewall rules.
- A Linux distribution with systemd:
- Debian/Ubuntu — UFW ships natively, or
- RHEL family (Rocky Linux / AlmaLinux / Oracle Linux 9 and 10) — the installer enables EPEL to provide UFW when it isn't installed yet (
oracle-epel-release-el<N>on Oracle Linux) and disablesfirewalldso UFW owns the firewall
- amd64 or arm64 on Debian/Ubuntu; amd64 only on the RHEL family, where the installer builds the agent from source with an amd64 Go toolchain
- Root access
- A firewall created in the Latitude.sh dashboard with the server added as an assignment
The recommended way to install the agent is through the dashboard: open your firewall, go to the Overview tab, expand Agent Installation, and run the provided command on the server.
Alternatively, run the installer yourself:
curl -fsSL https://packages.lsh.io/install.sh | sudo bash -s -- -firewall <firewall_id> -project <project_id> [-public_ip <public_ip>] [-version <version>](or sudo ./install.sh ... from a clone of this repository). -version pins a release; the latest is installed otherwise.
The script installs the required dependencies and enables UFW with sane defaults (deny incoming, allow outgoing, allow SSH). On Debian/Ubuntu it then installs the lsh-agent package from the apt repository; on the RHEL family it enables EPEL (which provides UFW) unless UFW is already installed, disables firewalld so UFW owns the firewall, and builds the agent from source. Either way it writes /etc/lsh-agent/env and starts the lsh-agent systemd service, checking that it stays up.
Important: make sure the server is added to the firewall in the Latitude.sh dashboard, otherwise the agent will have no rules to sync.
The agent is also published as a .deb package in a signed apt repository, for Ubuntu 22.04+ and Debian 12+ on amd64 and arm64:
# One file sets the repository up, signing key included
sudo curl -fsSLo /etc/apt/sources.list.d/lsh-agent.sources https://packages.lsh.io/apt/lsh-agent.sources
sudo apt-get update
sudo apt-get install -y lsh-agent # or lsh-agent=<version> to pin oneThe package installs the binary, the lsh-agent systemd unit and the default configuration, and restarts the agent on every upgrade. It does not configure the host: the service stays inactive until /etc/lsh-agent/env exists, and UFW is left as it is. To finish the setup:
# Bind the agent to your firewall and project
sudo tee /etc/lsh-agent/env > /dev/null << 'EOF'
FIREWALL_ID=<firewall_id>
PROJECT_ID=<project_id>
EOF
# Enable UFW with the same defaults as install.sh (skip if UFW is already active)
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw --force enable
sudo systemctl restart lsh-agentHosts previously set up with install.sh are migrated when the package is installed: the service switches to the packaged binary, and /usr/local/bin/lsh-agent becomes a link to it.
Note: on Debian 12+ and Ubuntu 24.04+, installing UFW removes
iptables-persistent/netfilter-persistent. If the host relies on them to restore/etc/iptablesrules at boot, useinstall.shinstead, which keeps those rules restored.
sudo systemctl status lsh-agent # check status
sudo journalctl -u lsh-agent -f # follow logs
sudo systemctl restart lsh-agent # restartThe agent reads its configuration from /etc/lsh-agent/config.yaml (see configs/agent.yaml for a documented example). Values can be overridden with environment variables:
| Variable | Description | Default |
|---|---|---|
PROJECT_ID |
Project ID from the Latitude.sh dashboard (required) | — |
FIREWALL_ID |
Firewall ID from the Latitude.sh dashboard (required) | — |
PUBLIC_IP |
Public IP of the server | auto-detected |
AGENT_INTERVAL |
Sync interval (e.g. 30s, 1m) |
30s |
LOG_LEVEL |
Log level (debug, info, warn, error) |
info |
UFW_BINARY |
Path to the UFW binary | /usr/sbin/ufw |
FIREWALL_ENABLED |
Enable/disable rule synchronization | true |
The installer also writes /etc/lsh-agent/env with FIREWALL_ID, PROJECT_ID, and PUBLIC_IP, which the agent loads automatically.
lsh-agent -config /etc/lsh-agent/config.yaml # run with a specific config file
lsh-agent -check-config # validate configuration and exit
lsh-agent -version # print version and exitRequires Go 1.23+.
make build # build ./build/lsh-agent
make build-linux # cross-compile for linux/amd64
make test # run the Go tests
make test-shell # Bats + ShellCheck for install.sh, uninstall.sh and the package scripts
make package # build the .deb into ./dist (snapshot)
make help # list all targetsSee TESTING.md for detailed testing instructions.
Every merge into main updates a draft of the next release (Release Drafter): merged PRs grouped by type, and the next version resolved from their labels. The labels come from the conventional-commit PR titles: feat bumps the minor, ! (e.g. feat!:) the major, and everything else the patch.
To release, open the draft under Releases, review it and publish it. Publishing creates the vX.Y.Z tag, and the release workflow then builds the binaries and .deb packages with GoReleaser, attaches them to the release, and redeploys the apt repository at https://packages.lsh.io/apt with the packages of every published release. Pre-release tags (e.g. v1.2.0-rc.1) get artifacts but are not added to the repository.
The workflow needs:
- GitHub Pages deploying from GitHub Actions, with the custom domain
packages.lsh.io; - a
v*tag rule in thegithub-pagesenvironment, so tag builds can deploy; - the
APT_SIGNING_KEYsecret, the ASCII-armored private key that signs the repository (without a passphrase), stored as a secret of thegithub-pagesenvironment rather than of the repository, so onlymainandv*tags can read it; - a tag ruleset restricting who can create
v*tags, since pushing one publishes a release.
To redeploy the repository without a release (e.g. after rotating the key), run the release workflow manually from main.
curl -fsSL https://packages.lsh.io/uninstall.sh | sudo bash # or: sudo ./uninstall.shThis stops and removes the service, the binary, and the agent files; on Debian/Ubuntu it also purges the lsh-agent package and removes the apt repository the installer added. The script reads /etc/lsh-agent/env (created by the installer) and exits if the file is missing — after a partial installation, remove the service and files manually.
Warning: the uninstall script also resets all UFW rules and disables UFW, leaving the server without a local firewall. Remember to remove the server from the firewall in the dashboard as well.
To remove only the agent and keep UFW and its current rules in place, run sudo apt-get purge lsh-agent instead.
See LICENSE.