Quickstart: Self-Hosted
Prefer the managed path? The Quickstart overview splits managed and self-hosted side by side — the managed path is less operational work over time if you don't want to run the hub yourself.
This guide walks you through a complete Mezite deployment on a single machine — from installing the server to SSH-ing into your first node. It should take about five minutes.
By the end you will have a running Mezite auth/proxy server, an enrolled agent node, an admin user, and a working SSH connection through the Mezite proxy.
Development setup only. Every step below uses--insecure/MEZITE_AUTH_H2C=true/--password-on-the-command-line conveniences that are fine on a single dev box and unsafe on anything else.--insecuredisables TLS verification, plaintext h2c presents the gRPC API in the clear, and passing the admin password on the command line leaks it into the process table and shell history. Before exposing this cluster to any other machine, work through the Step 7: Harden for production section: terminate TLS, drop--insecureeverywhere, replaceMEZITE_ADMIN_PASSWORDwith the mezctl bootstrap flow, and close port 22 on the node. See the Systemd, Reverse Proxy, and Architecture guides for the production shape.
Prerequisites
- Linux or macOS host
- Podman or Docker (only needed if using PostgreSQL — SQLite requires no external services)
- Mezite binaries installed (see Installation)
Step 1: Install Mezite
If you have not already installed the Mezite binaries, grab them now. The quickest method on Linux:
curl -fsSL https://releases.mezite.com/latest/mezite-linux-amd64.tar.gz \
| tar -xz -C /usr/local/bin/
# Confirm installation
mezhub version Step 2: Choose a Database
Mezite stores all cluster state — users, roles, certificate authorities, audit events — in a database. You can use SQLite (zero dependencies, great for getting started) or PostgreSQL (recommended for production).
Option A: SQLite (default, simplest)
No external services needed. SQLite is the default — Mezite creates the database file automatically:
MEZITE_CLUSTER_NAME=my-cluster \
MEZITE_ADMIN_PASSWORD='S3cureP@ssw0rd' \
MEZITE_AUTH_H2C=true \
mezhub
That's it — no database to install. The SQLite file is created at /var/lib/mezite/mezhub.db. Skip to Step 3.
Option B: PostgreSQL
Start a local PostgreSQL instance with Podman (or Docker):
podman run -d --name mezite-postgres \
-e POSTGRES_USER=mezite \
-e POSTGRES_PASSWORD=mezite \
-e POSTGRES_DB=mezite \
-p 5432:5432 \
postgres:16
# Wait a few seconds for PostgreSQL to become ready
podman logs -f mezite-postgres 2>&1 | grep -m1 "ready to accept connections" Then start mezhub with PostgreSQL configuration:
MEZITE_CLUSTER_NAME=my-cluster \
MEZITE_DB_DRIVER=postgres \
MEZITE_DB_HOST=localhost \
MEZITE_DB_PORT=5432 \
MEZITE_DB_USER=mezite \
MEZITE_DB_PASSWORD=mezite \
MEZITE_DB_NAME=mezite \
MEZITE_DB_SSLMODE=disable \
MEZITE_ADMIN_PASSWORD='S3cureP@ssw0rd' \
MEZITE_AUTH_H2C=true \
mezhub
# You should see output like:
# {"level":"info","msg":"starting mezhub","cluster":"my-cluster"}
# {"level":"info","msg":"database connected"}
# {"level":"info","msg":"migrations applied"}
# {"level":"info","msg":"certificate authorities initialized"}
# {"level":"info","msg":"default admin user created"}
# {"level":"info","msg":"gRPC auth server listening","addr":"0.0.0.0:3025"}
# {"level":"info","msg":"HTTPS proxy listener started","addr":"0.0.0.0:3080"}
# {"level":"info","msg":"SSH proxy listener started","addr":"0.0.0.0:3023"}
# {"level":"info","msg":"tunnel listener started","addr":"0.0.0.0:3024"}
Leave this terminal running (or add & to background the process).
Open a new terminal for the remaining steps.
You can also use a config file: mezhub --config=mezite.yaml.
See the Configuration Reference for details.
Step 3: Log In with msh
The msh client authenticates against the proxy, receives a short-lived
SSH certificate, and stores it locally. The --insecure flag skips
TLS verification for local development.
msh login --proxy=localhost:3080 --user=admin --password='S3cureP@ssw0rd' --insecure
# Output:
# Logged in as admin
# Roles: admin
# Valid until: 2026-03-31T04:00:00Z Step 4: Add a Node
To enroll a remote machine, install the mezd binary on it and join
it to the cluster. First, generate a join token from the server using mezctl.
mezctl needs an auth token. Log in first, then export the token:
# Log in and save the session token
export MEZITE_AUTH_TOKEN=$(mezctl --insecure login --username=admin --password='S3cureP@ssw0rd')
# Create a join token for the node agent
mezctl --insecure tokens create --type=static --roles=node --ttl=1h
# Output:
# Token: d4f8a2e1-7b3c-4d9e-a5f6-1234567890ab
# Valid for: 1h
# Use this token to join a node to the cluster.
On the target node, install mezd and start it with the token:
# Install mezd on the target node
curl -fsSL https://releases.mezite.com/latest/mezite-linux-amd64.tar.gz \
| tar -xz -C /usr/local/bin/ mezd
# Start the agent, pointing it at your proxy's tunnel address.
MEZITE_JOIN_TOKEN=d4f8a2e1-7b3c-4d9e-a5f6-1234567890ab \
MEZITE_AUTH_ADDR=your-server:3025 \
MEZITE_PROXY_ADDR=your-server:3024 \
mezd start
# Output:
# {"level":"info","msg":"joined cluster","cluster":"my-cluster","node":"webserver-01"}
# {"level":"info","msg":"reverse tunnel established","proxy":"your-server:3024"} Step 5: SSH to the Node
With the agent running and connected, you can now SSH through the Mezite proxy.
# List available nodes
msh ls --insecure
# Output:
# Node Name Address Labels
# -------------- ---------------- ----------------
# localhost 127.0.0.1 env=local
# webserver-01 192.168.1.50 env=staging
# SSH to a node
msh ssh --login=root webserver-01
# You are now connected through the Mezite proxy.
# This session is being recorded for audit.
webserver-01 $ Step 6: (Optional) Publish an Internal App
Beyond SSH, Mezite can publish an internal web or TCP application to your users without a VPN. This is optional — skip it if you only need SSH. The Application Access guide covers the full flow; here is the shortest path on a self-hosted cluster.
First, pick a parent domain for app hostnames, publish a wildcard DNS
record pointing at the proxy, and have a TLS certificate that covers it.
Then point mezhub at the domain:
proxy:
apps_domain: apps.example.com # *.apps.example.com -> the proxy (wildcard DNS + TLS)
apps_oidc_connector: okta # SSO connector that gates browser app access
# apps_keypair: # optional — only if the main HTTPS cert
# cert_file: /etc/mezite/apps-wildcard.crt # doesn't already cover the
# key_file: /etc/mezite/apps-wildcard.key # *.apps.example.com wildcard
Restart mezhub to pick up the change, then register an app and
bind it to the agent you enrolled above:
mezctl --insecure apps register \
--name=grafana \
--uri=http://10.0.3.12:3000 \
--public-addr=grafana.apps.example.com \
--labels=env=staging \
--agent-hostname=webserver-01
# Open it in a browser — the proxy gates the visit through your SSO connector:
# https://grafana.apps.example.com
If apps_domain is left unset, application access stays off and
nothing else about the cluster changes.
What Just Happened?
Here is the full certificate flow you just exercised:
- mezhub started with both Auth and Proxy services. On first boot it ran database migrations and generated User CA, Host CA, and SPIFFE CA key pairs (ECDSA P-256), storing them encrypted in the database (SQLite or PostgreSQL).
- msh login authenticated against the Auth service via the
Proxy, received a short-lived SSH certificate signed by the User CA, and saved
it under
~/.mezite. The certificate encodes the user's identity, roles, and allowed principals. - mezd used its join token to register with the Auth service, received a host certificate signed by the Host CA, and established a reverse tunnel to the Proxy on port 3024.
- msh ssh opened an SSH connection to the Proxy on port 3023. The Proxy verified the user's certificate against the User CA, resolved the target node, and forwarded the connection through the agent's reverse tunnel. The agent verified the user's certificate and started the SSH session. The session was recorded and an audit event was written to the database.
User CA (ECDSA P-256) Host CA (ECDSA P-256)
│ │
▼ ▼
User Cert Host Cert
identity: admin host: webserver-01
roles: [admin] cluster: my-cluster
principals: [root, admin] valid: 24h
valid: 12h │
│ │
│ presented to ──▶ Agent presented to ──▶ msh
│ (agent trusts User CA) (msh trusts Host CA)
▼ ▼
Mutual verification: both sides reject expired, revoked, or unknown certs Step 7: Harden for Production
The setup above is fine for a single dev box. Before exposing this cluster to anyone else, walk through the items below.
- Terminate TLS. Either put
mezhubbehind a TLS-terminating load balancer / reverse proxy (Nginx, HAProxy, Cloudflare, AWS ALB) and keepMEZITE_AUTH_H2C=true, or provision a real certificate formezhubitself and dropMEZITE_AUTH_H2C. See the Reverse Proxy guide for the recommended topology. - Set
proxy.public_addr. Required for WebAuthn and for the cluster'sGetServerInfoenrollment URL. Set it to the public hostname the proxy is reachable on (e.g.mezite.example.com:443). - Drop
--insecure. Everymshandmezctlcall should validate the proxy's certificate against your CA. Roll your~/.mezitedirectory before doing so to make sure no stale insecure session lingers. - Replace
MEZITE_ADMIN_PASSWORD. The bootstrap admin password should be a one-time value. Once a real admin account exists (created withmezctl users create), removeMEZITE_ADMIN_PASSWORDfrom the environment and disable or delete the bootstrap account. Enroll an MFA factor on the new admin withmezctl users add-mfa. - Set
MEZITE_CA_KEY_PASSPHRASE. CA signing keys are encrypted at rest only when this is set. Without it, a database dump leaks signing material. - Set
MEZITE_AUDIT_HMAC_KEY. Required for tamper-detection on the audit chain. Treat the value as a secret of the same sensitivity as the database password. - Close direct port 22 on each node. With the agent active,
the only path into the node should be through the proxy. Block port 22 inbound
at the firewall / security group and remove keys from
/root/.ssh/authorized_keys//home/*/.ssh/authorized_keysto prevent fall-through to legacy SSH. The agent itself does not need port 22 open — it owns the session-establishment side of SSH. - Run
mezhubas a service. Foreground invocation is fine for the tutorial, but production should run under systemd, Kubernetes, or your container orchestrator of choice with restart policy and log capture configured. See the Systemd / Kubernetes / Podman deployment guides. - Plan database backups. All cluster state lives in the database
file. For SQLite, snapshot the file with
sqlite3 .backupon a schedule. For PostgreSQL, use your existing PG backup tooling.
Next Steps
- SSH Access Guide — Deep dive into SSH certificate authentication, session recording, and advanced SSH features.
- RBAC Guide — Create roles with fine-grained label-based permissions and deny rules.
- SSO Guide — Configure OIDC, SAML, LDAP, or GitHub OAuth for enterprise authentication.
- Audit & Recording — Query audit logs and replay SSH sessions.
- Application Access — Publish internal web and TCP apps through the proxy with SSO, RBAC, and identity injection.
- Configuration Reference — Tune session TTLs, logging, TLS, and more.
- Systemd Deployment — Run Mezite as a system service in production.