Install the panel
Build Mistgate, prepare the panel with mistgate setup, run it under systemd and sign in for the first time.
On this page
This page takes a fresh Linux server to a running panel with an owner account. The examples use panel.example.com; check Requirements first. Commands on the panel server run as root.
1. Build the binaries#
On a build machine with Go, Node.js, pnpm, make and git:
git clone https://github.com/Mistgate/mistgate.git
cd mistgate
make buildbin/ now holds the panel (mistgate-linux-amd64, mistgate-linux-arm64) and the node agent (mistgate-node-linux-amd64, mistgate-node-linux-arm64). The admin web app is built into the panel binary. Keep the agent binaries: you need them when you add a node.
if you want nodes to update themselves later, make your release key now and build with it (RELEASE_KEY=<public key> make build). Agents built without a key never update themselves. See Updates.
Copy the panel binary to the server and install it:
scp bin/mistgate-linux-amd64 root@panel.example.com:/root/
ssh root@panel.example.com 'install -m 0755 /root/mistgate-linux-amd64 /usr/local/bin/mistgate'2. Choose how the admin is reached#
The public address shows a decoy site to everyone. The admin is reached in one of three ways, chosen once, when you run mistgate setup:
| Mode | setup flags |
Admin address | When to pick it |
|---|---|---|---|
| Secret path prefix (default) | --public-url https://panel.example.com |
https://panel.example.com/<secret>/, where the secret is 24 random characters |
The simplest: one host name, one certificate. |
| Secret host | --public-url https://panel.example.com --admin-host <secret host> |
https://<secret host>/ |
You want the admin on a host name nobody can guess. It needs a DNS record and a certificate that covers it. |
| Separate listener | --public-url https://panel.example.com --admin-listen 127.0.0.1:8081 |
http://localhost:8081/, through an SSH tunnel |
The admin never appears on the public port at all. |
-
Requests that do not match the admin (a wrong prefix, an unknown host, an unknown path) all get the decoy site, so a prober cannot tell a near miss from a random guess.
-
Always pass
--public-url. It is the base of every subscription link and the address that node install commands carry. Without it the panel hands out no subscription links, and adding a node fails unlessservegets--agent-addr. -
--admin-hostand--admin-listencannot be combined. -
The separate admin listener speaks plain HTTP. Bind it to loopback and reach it through SSH:
ssh -N -L 8081:127.0.0.1:8081 root@panel.example.comThen open
http://localhost:8081/in your browser. Use the same port on your side: passkeys are bound to the addresshttp://localhost:<port>that setup stored. -
--rp-idand--rp-originsoverride the WebAuthn settings that setup derives from the admin address. You rarely need them.
setup stores these addresses once. Running it again keeps them, and Mistgate has no command to change them later. Choose before you go on; starting over means a new data directory and enrolling every node again.
3. Run setup#
mistgate setup --public-url https://panel.example.comIt prints:
Data dir: /var/lib/mistgate
Admin URL: https://panel.example.com/<secret>/
Setup link: https://panel.example.com/<secret>/setup#<token>
The link works once and expires in 30 minutes. Open it in a browser and create your admin (a passkey, or a password with an authenticator code).What setup does:
- Creates the data directory
/var/lib/mistgatewith mode 0700 (--data-dirchanges the path). - Creates
master.key, 32 random bytes with mode 0600. It encrypts every secret the panel stores. - Creates the database
mistgate.dband applies the schema. - Stores the addresses of the chosen mode and generates two more secrets: the TLS name the node agents use to reach the panel, and the secret path prefix of the subscription links.
- Prints a one-time setup link, valid for 30 minutes.
Keep the admin URL to yourself. Setup is safe to run again: it keeps the existing configuration and, as long as no admin exists, prints a fresh setup link.
4. Choose the certificate#
| Option | Flags of serve |
What happens |
|---|---|---|
| Let's Encrypt | --acme-domain panel.example.com |
The panel gets and renews the certificate itself (TLS-ALPN-01 on the public listener, so it must be reachable on port 443). A second listener on :80 answers HTTP-01 and redirects plain HTTP to HTTPS (--acme-http; an empty value turns it off). Certificates are cached in <data-dir>/acme. --acme-email adds a contact address. Using it accepts the Let's Encrypt terms of service. |
| Your own certificate | --tls-cert /path/fullchain.pem --tls-key /path/privkey.pem |
The panel serves your PEM files and reads them again when the certificate file changes (checked at most every 30 seconds), so a renewal by another tool needs no restart. |
| Both | both sets of flags | Names listed in --acme-domain get Let's Encrypt certificates; every other name gets your certificate. Useful for a secret admin host covered by a wildcard certificate of your own. |
--acme-domain takes plain host names only, no wildcards. It can be repeated for several names.
a Let's Encrypt certificate for a secret admin host puts that name into public Certificate Transparency logs. To keep it secret, cover it with a wildcard certificate of your own.
5. The decoy site#
Without anything else the panel shows a built-in "Coming soon" page. Every installation ships the same page, so it is recognisable; your own site is better. Put static files in a directory and pass --decoy-dir:
/and every directory serve theirindex.html.404.htmland429.html, when present, replace the built-in "not found" and "too many requests" pages. Every unknown path, wrong admin prefix and unknown subscription link gets the same 404.robots.txt, when absent, is a built-in one that turns away known AI crawlers.- Only GET and HEAD are served. Files and directories whose name starts with a dot are never served, and responses carry no Last-Modified or ETag headers.
The panel only reads this directory. With the systemd unit below, keep it outside /home and /root (for example in /srv/mistgate-decoy).
6. Try it in the foreground#
mistgate serve --listen :443 --acme-domain panel.example.com--listen defaults to 127.0.0.1:8080, so pass --listen :443 on a real server. The log goes to stderr; look for listening lines for the public listener (and acme-http for port 80). Stop it with Ctrl+C once it works.
7. Run it under systemd#
Save this as /etc/systemd/system/mistgate.service and change ExecStart to the flags you chose:
[Unit]
Description=Mistgate panel
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/local/bin/mistgate serve --listen :443 --acme-domain panel.example.com
Restart=on-failure
RestartSec=5
TimeoutStopSec=30
# The panel writes only its data directory and only needs to bind ports 443 and 80.
NoNewPrivileges=yes
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
ProtectSystem=strict
ReadWritePaths=/var/lib/mistgate
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectClock=yes
ProtectControlGroups=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectHostname=yes
LockPersonality=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
RestrictNamespaces=yes
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX AF_NETLINK
SystemCallArchitectures=native
SystemCallFilter=@system-service
UMask=0077
[Install]
WantedBy=multi-user.targetsystemctl daemon-reload
systemctl enable --now mistgate
journalctl -u mistgate -fWhy it looks like this:
mistgate setupran as root, so the data directory belongs to root, and the service runs as root too. Every capability except binding low ports is dropped, and only/var/lib/mistgateis writable. If you use another--data-dir, changeReadWritePaths.- The panel stops gracefully on SIGTERM. With
Restart=on-failuresystemd restarts it when a listener fails, not when you stop it. - Every flag has a
MISTGATE_*environment variable, so you can move the flags toEnvironment=lines or anEnvironmentFile=. See Configuration. - With
--decoy-dir,--tls-certor--tls-key, the files only need to be readable.
Behind a reverse proxy#
If another web server must own port 443 on this host, Mistgate can run behind it:
- Run the public listener without TLS on loopback (
--listen 127.0.0.1:8080, no--acme-domain, no--tls-cert) and let the proxy terminate TLS forpanel.example.com, passing the Host header and the path through unchanged. - Add
--trusted-proxy 127.0.0.1so that rate limits, sessions and the audit log see the real client address fromX-Forwarded-FororForwarded. - Node agents need TLS from the panel itself. Give them a listener of their own and put its address into the install commands:
--agent-listen :8443 --agent-addr panel.example.com:8443. The proxy must not touch that port.
The agent port is then a port of its own that answers only the secret TLS name; on the shared port 443 it hides behind the decoy site. Prefer the plain setup above when you can.
8. Sign in for the first time#
Open the setup link from step 3 in a browser, the whole link including the part after #.
- Language. Pick English or Russian. You can change it later.
- Create the admin. The LOGIN field (it starts as
admin) is your login and the name the panel shows. Then:- Create passkey: the browser asks for a fingerprint, a face, a PIN or a security key. The key stays on your device. This is the recommended way.
- password + authenticator code: a password of at least 12 characters, then scan the QR code with an authenticator app (Google Authenticator, 1Password, Aegis and the like), type the 6-digit code and press Create admin.
- Done. Choose Add your first node or Later, let me look around first.
The first admin is the owner. The setup wizard also creates an empty group "Everyone" for your first users.
- A login is 3 to 64 characters: a–z, 0–9 and
. _ @ -. - The link is spent only when the admin is created. If it expired or something went wrong, run
mistgate setupagain for a new link. - A passkey works only on the admin address that setup stored. "The browser refused the passkey for this address" means the page was opened under another address.
Later you sign in at the admin URL with Sign in with passkey, or with another way: login, password and code. Five wrong passwords within an hour lock that login for 15 minutes. Lost your phone or your passkey: Security explains mistgate auth reset-login.
9. Back up right away#
Everything the panel knows lives in the data directory:
| File | What it is |
|---|---|
mistgate.db, mistgate.db-wal, mistgate.db-shm |
The database: admins, nodes, profiles, users, devices, traffic, events, the audit log. Secrets inside are encrypted with the master key. |
master.key |
The key that encrypts the stored secrets, including the key of the panel CA that every node trusts. |
acme/ |
Let's Encrypt account and certificates (with --acme-domain only). |
Make a copy now and after every important change. Settings → Backups in the admin shows the same command:
systemctl stop mistgate
tar czf mistgate-backup-$(date +%F).tgz -C /var/lib mistgate
systemctl start mistgatethe copy holds the master key: whoever has it can read every secret of the panel. Keep it encrypted and off the server. Without the data directory every node has to be enrolled again and every user, link and key is gone.
Automatic encrypted backups are Planned. If you made a release key in step 1, keep that file offline and backed up as well.