msh CLI Reference

msh is the client CLI for Mezite. It lets an end user log in to a cluster, open SSH sessions, copy files, request access, manage their device-trust status, and join or moderate other sessions.

login

Authenticate to a Mezite cluster and receive short-lived SSH certificates. --user and --password are required for credential-style auth (local username/password and LDAP). Browser-mediated SSO connectors (OIDC, SAML, GitHub) are selected with --auth=<connector> and complete the login via the proxy callback.

Login flows bash
# Local username + password
msh login --proxy=mezite.example.com:3080 \
  --user=admin --password="$MEZITE_PASSWORD"

# LDAP / Active Directory (uses the same --user / --password)
msh login --proxy=mezite.example.com:3080 \
  --auth=corp-ldap --user=alice --password="$LDAP_PASSWORD"

# SSO (OIDC / SAML / GitHub) — opens the browser
msh login --proxy=mezite.example.com:3080 --auth=okta

# Custom certificate TTL (default 12h, hard cap 24h)
msh login --proxy=mezite.example.com:3080 \
  --user=alice --password="$MEZITE_PASSWORD" --ttl=30m

Identity-file export (--out)

Pass --out <path> to write the issued SSH key pair and certificate to disk in OpenSSH identity-file format instead of stashing them in the per-profile cache. This is what CI runners, scheduled jobs, and Machine-ID-style helpers consume.

Export an identity file for CI bash
msh login --proxy=mezite.example.com:443 \
  --auth=ci-machine-id \
  --user=ci-bot --password="$CI_BOT_PASSWORD" \
  --out=/tmp/mezite-identity

# Use it with native ssh
ssh -i /tmp/mezite-identity ubuntu@web-01.mezite

Hardware-key login (--hardware-key)

The --hardware-key flag is recognized by msh login, and roles with require_session_mfa: hardware-key are honoured by the proxy and auth service. However, real PIV hardware-key signing is not yet implemented — invoking the flag against a hardware token currently fails with no hardware key implementation available; real PIV support not yet built. The flag, RBAC policy hooks, and certificate extension are in place so that integration can land without churning the public surface; treat the feature itself as unimplemented today.


ssh

Open an interactive SSH session, or run a one-off remote command, against a registered node.

Open a session bash
# Connect by hostname
msh ssh ubuntu@web-01
msh ssh --login=ubuntu web-01

# Run a one-off command
msh ssh web-01 -- uptime

# Pre-approve a session-MFA challenge in the same invocation
msh ssh --mfa-code=123456 web-01

# Attach a justification (logged in the audit event)
msh ssh --reason="incident IR-204" web-01

# Invite extra participants into the session (for moderated roles)
msh ssh --invited=alice,bob web-01

# Cross-cluster: target a node in a trusted leaf cluster
msh ssh --cluster=production web-01

scp

Copy files between a workstation and a node through the Mezite proxy. Every transfer is authenticated with your certificate, logged in the audit trail, and — under a role with file-transfer moderation — gated by a per-file approval workflow.

File transfer bash
# Upload
msh scp ./deploy.tar.gz ubuntu@web-01:/tmp/

# Download
msh scp ubuntu@web-01:/var/log/app.log ./

# Recursive
msh scp -r ./config/ ubuntu@web-01:/etc/app/

msh scp does not support remote-to-remote copies; one side must always be local.

Transfer approval (moderated roles)

When the requester's role triggers SFTP moderation, the transfer pauses on the proxy until a designated moderator approves it. The moderator uses:

Moderator side bash
# Approve a pending transfer
msh approve-transfer --session=<session-id> --transfer=<transfer-id>

# Deny it
msh deny-transfer --session=<session-id> --transfer=<transfer-id>

ls

List nodes visible from the active profile. --filter is repeatable and AND-combines key=value matches against node labels.

Inventory bash
msh ls
msh ls --filter=env=production
msh ls --filter=env=production --filter=team=platform
msh ls --all      # include trusted leaf clusters

app

Reach internal applications published through the proxy. HTTP apps are opened in a browser at their public address; TCP apps (databases, caches, custom daemons) are reached by opening a local port that tunnels through the proxy. See the Application Access guide for the full picture.

Application access bash
# List the apps your roles let you reach
msh app ls

# HTTP app: prints the public address to open in a browser
msh app login grafana

# TCP app: open a local listener that tunnels to the app
msh app login redis-internal --port=6379
# then point a client (e.g. redis-cli) at 127.0.0.1:6379; Ctrl-C to stop

# Bind the tunnel to a different local address
msh app login redis-internal --port=6379 --listen=0.0.0.0

# Apps whose role requires per-session MFA: supply or be prompted for a code
msh app login redis-internal --port=6379 --mfa-code=123456

msh app ls shows an AUTH column flagging any app that requires extra per-session proof (mfa or a trusted device). Apps you can't reach do not appear in the list.


config

Generate ~/.ssh/config entries for every node visible from the active profile. Each entry pins the Mezite host CA via UserKnownHostsFile ~/.mezite/known_hosts and routes through msh proxy ssh, so native ssh, rsync, Ansible, and editor SSH integrations all benefit from certificate auth and audit.

SSH config generation bash
msh config              # print to stdout
msh config --append     # append to ~/.ssh/config
msh config --cluster=production  # change the Host alias suffix
msh config --login=deploy        # remote login for trusted-cluster entries

--login sets the remote login used in the generated trusted-cluster entries; it defaults to the first principal on your certificate.


proxy ssh

Use msh proxy ssh as an OpenSSH ProxyCommand to route native ssh(1) through the Mezite proxy. The argument takes a single [user@]host:port token, so OpenSSH's %r@%h:%p substitution drops in directly.

ProxyCommand usage bash
# Typical entry generated by 'msh config'
#   ProxyCommand msh proxy ssh %r@%h:%p

# Manual one-off
ssh -o 'ProxyCommand=msh proxy ssh %r@%h:%p' ubuntu@web-01.mezite

# Direct invocation
msh proxy ssh ubuntu@web-01:22

play

Replay a recorded session in the terminal. Speed is configurable.

Playback bash
msh play <session-id>
msh play --speed=2.0 <session-id>

sessions / recordings

Inspect recorded sessions. msh sessions ls and msh recordings ls are aliases; both surface the same list.

Listing bash
msh sessions ls
msh recordings ls

join

Join an active or pending session in one of three modes. For roles configured with require_session_join, the session is held open at start time until the required moderators have joined; this command is how a moderator answers that hold.

  • observer (default) — read-only. See the terminal; cannot type or approve transfers.
  • peer — interactive. See the terminal and send input.
  • moderator — supervisor. Present for moderated sessions; may approve or deny transfer requests. Requires a join_sessions allow rule permitting moderator for this session's owner.
Session join bash
# Observe a session (default mode)
msh join <session-id>
msh join --mode=observer <session-id>

# Attach in interactive mode (if the role allows)
msh join --mode=peer <session-id>

# Attach as a moderator
msh join --mode=moderator <session-id>

While attached, press Ctrl-C to disconnect. A moderator may press t to terminate the source session.


request

Create, list, and cancel access requests. A request can ask for whole roles (--roles), for access to specific SSH nodes (--resource), or both. At least one of the two must be set.

Access requests bash
# Request one or more roles for the grant window
msh request create --roles=admin --reason="Deploy hotfix"

# Request a single node instead — approval grants access to that node only
msh request create --resource=ssh-node:web-01 --reason="incident XYZ-123"

# --resource is repeatable
msh request create \
  --resource=ssh-node:web-01 \
  --resource=ssh-node:web-02 \
  --reason="debug"

# A node in a trusted cluster: ssh-node:<cluster>/<name>
msh request create --resource=ssh-node:leaf-prod/web-01 --reason="cross-cluster check"

# Roles and resources together
msh request create --roles=ssh-prod --resource=ssh-node:web-01 --reason="scoped deploy"

msh request ls
msh request ls --state=pending
msh request cancel <request-id>

--duration sets how long the grant should last (e.g. 2h); the cluster caps it — see MEZITE_ACCESS_REQUEST_MAX_DURATION on the Configuration page. --urgency takes normal, high, or critical. A resource request grants access to the named node(s) for the request's duration and nothing broader.


device

Manage the device-trust enrollment of the current machine. Enrollment is required before you can log in under any role with device_trust_mode: required — the SSH gate fails closed, so an un-enrolled device is refused rather than quietly downgraded. Under device_trust_mode: optional the login still succeeds, but the issued certificate carries no device ID extension.

Device trust bash
msh device serial      # print this device's hardware serial
msh device enroll      # enroll this device as a trusted device
msh device status      # report whether this device is enrolled and trusted

bench

Open many real SSH sessions to a node and report how the access path performs under concurrency. Each iteration runs the full path — dial the proxy, complete both SSH handshakes, open a session, run a command — so it exercises audit and session-recording load as well as the network. It reports connect / handshake / exec latency percentiles (p50/p95/p99), throughput, and the error rate.

Load testing bash
# Burst: 250 sessions, 50 at a time
msh bench -n 250 -c 50 web-1

# Soak: run for 10 minutes at 20 concurrent sessions
msh bench --duration 10m -c 20 web-1

# Gate a run against service-level objectives — non-zero exit on violation
msh bench -n 200 -c 25 \
  --max-error-rate 0.01 --connect-p95-ms 750 --exec-p95-ms 500 web-1

# Machine-readable output
msh bench -n 100 --format=json web-1

Choose the load shape with --requests/-n (a fixed number of sessions, default 100) or --duration (run for a fixed time); --duration wins if both are set. --command overrides what each session runs, --login the remote username, and --proxy the proxy address from the current login.


status / logout

Profile status bash
msh status   # show cluster, user, roles, cert expiry
msh logout   # remove local certificates and end the session

version

Print the build stamp of the binary you are running. Every Mezite binary — msh, mezctl, mezd and mezhub — accepts the same subcommand and prints the same shape, so it is the first thing to paste into a bug report.

Build stamp bash
msh version
# Version:    0.2.24
# Git commit: 9344e36a
# Build date: 2026-07-29T18:22:04Z

msh version --format=json
# {
#   "version": "0.2.24",
#   "git_commit": "9344e36a",
#   "build_date": "2026-07-29T18:22:04Z"
# }

These values are stamped into the binary when it is built. A binary you compiled yourself reports its version as dev, and any field the build did not stamp renders as unknown. That is deliberately visible rather than hidden: an unstamped build should be obvious in the field instead of looking like a released one.


Client version compatibility

A cluster publishes the client version it expects, and the oldest client version it still supports. msh login reads that policy and tells you if the binary in your hand is behind it.

  • Older than the version the cluster expects — a single informational line on stderr. Everything keeps working; upgrade when convenient.
  • Older than the minimum the cluster supports — a warning on stderr. Commands may start failing against this cluster, so treat it as a real to-do.
  • Newer than the cluster — nothing is printed. msh status still shows both versions. This is a normal state mid-upgrade and there is never a reason to downgrade a client.

The check never blocks a command and never changes what a command does. If it cannot reach the cluster, or the answer takes more than a moment, it says nothing at all — being offline must not stand between you and a node. The answer is cached in the profile directory and refreshed at most every four hours, plus on every login, so it costs nothing on the interactive path.

A client built from source reports its version as dev, which cannot be compared to a release version, so no comparison is attempted and no message is printed.

Comparing client and cluster bash
msh status
# Cluster:     mezite.example.com
# Proxy:       mezite.example.com:3080
# User:        alice
# Roles:       access
# Valid until: 2026-07-30 08:14:02 BST (11h58m remaining)
# Client:      0.2.20 (cluster expects 0.2.24)

# Skip the comparison for a single command
msh status --no-update-check

# Skip it for every command in this environment
export MEZITE_TOOLS_VERSION=off

Use MEZITE_TOOLS_VERSION=off where the client binary is owned by something else — a package manager, a container image, a configuration-management system — and the version is not yours to change. Operators can also switch the policy off cluster-wide with client_tools.mode: disabled; see Client Tools.

msh does not update itself. Upgrading means installing a new binary the same way you installed the first one — see Installation.


Global flags

msh has exactly one persistent flag, accepted by every subcommand:

  • --no-update-check — Do not compare this client's version against the cluster's policy.

The following look global but are per-command — check msh <command> --help before assuming a command takes one:

  • --insecure — Skip server certificate verification. Dev-only. Accepted by login, ls, play, app login, app ls, and the request subcommands.
  • --cluster <name> — Target a trusted cluster. Accepted by ssh, scp, proxy ssh, and config.
  • --proxy <host:port> — Proxy address. Set at login time on msh login; msh bench takes it as an override. Other commands read the address from the active profile.