Reference

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) and 7777/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.