MoaV v2.0.0
Same protocols. Rebuilt underneath. No new transports — instead a month of work on the things that decide whether a circumvention tool actually holds up: security, reliability, and being able to change the code without breaking it.
100 PRs · 190 commits · 162 files · +16,801 / −25,987 lines (net −9,186) · tests 3 → 24
Your keys, users and certificates are preserved. Upgrading: Migration guide.
Why a full refactor and review
1.9.x worked, but it had accumulated the failure modes of a fast-moving bash project: a 9,483-line dispatcher nobody could safely edit, secrets written world-readable, containers holding privileges they didn't need, entrypoints that ran without strict mode and failed silently, and a provisioning path duplicated three times so a fix in one place quietly missed the others. None of that is visible in a feature list — all of it decides whether the tool works on a bad day, and whether a bug found once stays fixed.
What's better
Modularity — the dispatcher is 89% smaller
moav.sh went from 9,483 lines to 1,076, with the logic in 15 focused lib/*.sh modules (service, users, bootstrap, doctor, cert, migrate, donate, nettune, dns, …). Provisioning had three near-duplicate user generators; there is now one shared path used by the CLI, the dashboard and the bundle generator, so a fix lands everywhere at once. Dead code and redundant bundle artifacts were deleted rather than carried.
Workstreams: #168–#179 (provisioning), #181–#190 (dispatcher), #239 #243 #256
Security — least privilege, and secrets that stay secret
- Secrets are no longer world-readable. State keys are
0600root-owned;.envis0600; the Clash API secret moved to an entrypoint env hand-off. Existing installs are repaired on upgrade, through a single guarded path no start/restart route can bypass. (#241 #261) - User bundles are no longer world-writable.
chmod 777is replaced by deterministic uid-2000 ownership — client private keys were readable and writable by any local account. (#242) - Exporters dropped the raw Docker socket (4 of 4). Metrics collection no longer holds an unrestricted handle on the host. (#226 #227)
- The admin panel is TLS-only, proven functionally rather than assumed. (#228 #229)
- A management-only network isolates the Docker socket-proxy from the 29 containers that terminate untrusted internet traffic.
- An independent adversarial pass before tagging found no critical/high issues and one real medium: sing-box was re-downgrading every state secret's ownership on each start — the actual source of a bug we'd been treating as stale images. (#259 #262)
- Reality/CDN hardening: the render guard anchors to the real JSON field (an 8-hex id inside a UUID could give a false PASS) and the CDN WebSocket path uses 48-bit entropy instead of bash
$RANDOM— it's an active-probing barrier, not a cosmetic string.
Reliability — strict mode, deadlines, no silent failures
- Every container entrypoint runs under strict mode. Getting there meant fixing the landmines first: SIGPIPE under
pipefail,((x++))returning 1 from 0,dashhaving nopipefail(where the usual|| trueguard is fatal), and cert-wait paths that died before the loop they existed for. (#232 #233 #236) - Every container call has a hard deadline, so a wedged container can no longer freeze
moav user addforever. (#243) user addno longer depends on Docker for key material. WireGuard keys are X25519, so they're generated locally with openssl; UUIDs likewise. This removed a whole class of intermittent failure — including one that only reproduced from an interactive terminal, becausedocker exec -iattaches stdin and blocks on a TTY. (#267 #270 #271)- New users are actually applied. sing-box has no
reloadand runs from a config copy taken at start, so a dashboard-created user could receive a perfectly valid bundle that authenticated against nothing. The restart now happens and is verified, with a loud warning if the user isn't live. (#272) - Generated-secret desync (the Reality
short_idtotal-outage class) is hardened: renders re-source state immediately before writing, so an empty.envvalue can't blank a live secret. (#247)
Operability
.env.exampleis one curated file: ~29 commonly-configured variables with descriptions on top, a hard "only change things below if you know what you're doing" separator, everything else below. (#250)- Bundles show only the protocols the user actually has — no more "No X config available" filler. (#248)
- Every config is identifiable in the client app:
MoaV-<server>-<protocol>-<user>, including WireGuard, which takes its tunnel name from the filename. (#273) moav doctorgained real checks and stopped failing healthy hosts; monitoring defaults sensibly; Grafana is branded and no longer crash-loops on a read-only image dir. (#249 #252 #268)- Docs: a v2 migration guide, plus
llms.txtandAGENTS.mdso an AI agent operating a server has accurate, safety-aware instructions. (#253 #257 #266)
Testing — the part that makes it stick
The suite went from 3 scripts to 24 (21 in CI), plus an end-to-end workflow that builds the stack on a real VPS with a real domain and probes every protocol — the merge bar for anything touching provisioning.
The rule that shaped this release: every bug found gets a regression test in the same PR, named after the class it pins. Several of those tests earned their place immediately by catching mistakes made while fixing something else — a false-PASS render guard, a MoaV-- double dash, a pty probe that proved nothing, and an e2e assertion that checked a filename spelling instead of the thing it meant to assert.
Verification
Beyond CI and e2e: a full 1.9.1 → v2 in-place upgrade on a live server (which surfaced four regressions, all fixed and pinned — #244 #245), and repeated fresh v2 installs driven from the install script, with every protocol validated end-to-end from real clients.
Breaking changes
None to protocols or credentials. Operational notes:
moav buildis required when coming from 1.9.x — every image predates v2.- WireGuard/AmneziaWG bundle files are renamed (
moav-<server>-wg.conf); readers accept both, andmoav regenerate-usersconverts existing bundles. - Config-file permissions tighten on first start; the repair is automatic.
Full detail: CHANGELOG.md
Quick Install
curl -fsSL moav.sh/install.sh | bashThis will install MoaV to /opt/moav and guide you through setup.
Documentation
moav.sh/docs — Full documentation
- Setup Guide — Complete installation instructions
- Client Setup — How to connect from devices
- DNS Configuration — DNS records setup
- CLI Reference — All commands and options
- Monitoring — Grafana dashboards and metrics
- Troubleshooting — Common issues and solutions