Security & hardening
Login brute-force lockout, TOTP 2FA, TLS, DoS limits, peer ACL, and a hardening checklist.
MeshHold defaults to a publicly-bound Web UI (api.listen_addr =
0.0.0.0:8080) because the homelab and multi-host deployments it targets
need to reach it across a network. That makes the login and the listener
the front door, so the daemon ships several layers of protection. This
page is the map: what's on by default, what you can tune, and how to lock
a node down further.
TL;DR — what's on by default
- Brute-force lockout on the Web UI login (15 failed attempts per IP → 15-minute lockout).
- TLS on by default (a self-signed cert is generated on first start; bring your own or use ACME).
- Slowloris-safe limits: idle-connection timeout, a header-size cap, and a ceiling on concurrent connections.
- libp2p runs in private-network mode — peers without your swarm key can't complete a handshake — with a connection manager and resource manager bounding peer load.
- Security headers on every API response.
- Audit log of security-relevant events, surfaced in the Web UI.
Optional, off by default: TOTP two-factor login and a fail2ban jail (see below).
Login brute-force protection
Failed logins are counted per client IP in a sliding window. Cross the
threshold and that IP is locked out — further attempts get 429 Too Many
Requests with a Retry-After header, before the password is even
hashed, so a locked-out client can't keep burning CPU. The Web UI shows
the remaining attempts and a live countdown while locked.
The same per-IP budget is shared across the password login, the tray bootstrap-exchange, and the vault-unlock endpoint, so an attacker can't dodge the cap by spreading guesses across them.
Defaults and tuning (config.yaml):
api:
auth:
login_max_attempts: 15 # 0 disables the lockout entirely
login_window: "15m" # rolling window the failures are counted in
login_lockout: "15m" # how long the lockout holds once tripped
The admin password itself is stored only as a bcrypt hash (in the
encrypted metadata store, never in config.yaml). Set or change it with
meshhold set-password.
Two-factor authentication (TOTP)
You can require a time-based one-time code (Google Authenticator, Aegis, FreeOTP, …) on top of the password. It's managed from the console only — there is deliberately no Web-UI toggle, so an attacker who steals a live session can't disarm it.
# Enable — prints a QR code, the secret, and one-time recovery codes:
meshhold 2fa setup
# Check state / how many recovery codes remain:
meshhold 2fa status
# Turn it off (reverts to password-only):
meshhold 2fa disable
These work whether the daemon is running (they go through its local
admin-token endpoint) or stopped (they open the metadata store directly,
same as set-password). After enabling, the Web UI login asks for a
6-digit code as a second step. Lost your device? Enter one of the
recovery codes printed at setup (each works once), or run meshhold
2fa disable from a shell on the box.
Transport security (TLS)
TLS is enabled by default. On first start the daemon generates a self-signed certificate under its metadata dir (your browser will warn once; accept it and the SPA caches its bearer). For a real certificate:
api:
tls:
cert_file: /etc/meshhold/tls/fullchain.pem
key_file: /etc/meshhold/tls/privkey.pem
# …or let the daemon fetch one via ACME / Let's Encrypt:
acme_domain: node.example.com
The minimum protocol version is TLS 1.2. The only time TLS is skipped is a pure-loopback bind with no cert configured.
Reverse proxies & client IP
By default the daemon trusts no proxy: it uses the real TCP peer as
the client IP, so a spoofed X-Forwarded-For can't forge the address in
the audit log or get an innocent IP banned by fail2ban. If you run
MeshHold behind a reverse proxy you control, list it so X-Forwarded-For
is honoured from it:
api:
trusted_proxies: ["127.0.0.1", "10.0.0.0/8"]
Denial-of-service limits
The HTTP listener sets an idle-connection timeout and a header-size cap,
and bounds the number of simultaneous connections (api.max_connections,
default 512; 0 = unlimited). We deliberately do not set blanket
read/write timeouts, because the same listener serves long media streams,
Server-Sent Events, and large uploads.
The libp2p host runs a connection manager that trims the peer set back to a low watermark when it grows past a high one, plus a resource manager whose limits auto-scale to the machine's memory and file-descriptor budget:
node:
resources:
conn_low: 128
conn_high: 512
conn_grace_period: "30s"
Peer access control
The swarm key is a private-network pre-shared key: a peer that doesn't hold it cannot complete the Noise handshake, so it never opens a stream. To expel a specific peer, add its ID to the blocklist:
node:
peer_blocklist: ["12D3KooW…"]
To rotate the swarm key for a whole network, use the Change swarm key
button on the network card in the Web UI (or meshhold networks
set-swarm-key), then re-pair your nodes.
Encryption at rest
On desktop and mobile the metadata store (BadgerDB) is encrypted at rest using a master key held in the OS keystore (Windows DPAPI, macOS Keychain, Linux Secret Service, Android Keystore). On a headless Linux server, enable it with a passphrase file — otherwise protect the data directory with full-disk encryption (LUKS) or your provider's volume encryption.
Audit log
Security-relevant events — login successes and failures, lockouts tripped, 2FA enable/disable, vault unlocks, swarm-key rotation, management-key verification, tunnel and port-forward open/close, peer appear/leave — are recorded in a local audit log. Open it from Profile → View logs in the Web UI. Warning- and critical-severity events also fire a push notification to your registered devices. The log is local-only; it is not gossiped to other nodes.
Network-level banning (fail2ban)
The daemon's per-IP lockout refuses the login; fail2ban can drop the offender's packets entirely at the firewall. The Linux package ships a ready-made filter and jail (disabled by default). See Fail2Ban integration for the walkthrough.
A hardening checklist
- Set a strong Web UI password; consider enabling
meshhold 2fa setup. - Keep TLS on; install a real certificate for anything internet-facing.
- Put the node behind a firewall and only expose
8080/tcp(Web UI) and7777/tcp(libp2p) to the networks that need them. - If behind a reverse proxy, set
api.trusted_proxies. - Enable encryption at rest (keystore on desktop/mobile, passphrase file or LUKS on a headless server).
- Enable the fail2ban jail for internet-facing nodes.
- Review Profile → View logs periodically, and act on lockout pushes.