Agentless OpenSSH Nodes

Mezite can broker SSH sessions to machines that run plain OpenSSH — no mezd agent installed. The proxy reaches the node directly on its sshd port, presents the user's Mezite-issued SSH certificate, and records the session. The node keeps its existing sshd and just needs to trust the cluster's User CA.

This is the right path when:

  • The host is a legacy or appliance you cannot install a Go binary on (an embedded device, a vendor-managed jumpbox, a router).
  • A regulated environment forbids new daemons on production hosts.
  • You want to bring an existing fleet of OpenSSH servers under Mezite RBAC/recording without changing the on-host process model.

Agent-based mezd nodes are still the default — they give you reverse-tunnel reachability (no inbound port 22), session moderation, host-user provisioning, and PAM hooks. Use agentless only when those trade-offs are worth it.


How agentless mode works

  • The node's sshd is configured with TrustedUserCAKeys pointing at the Mezite User CA public key, so it accepts SSH user certificates issued by the cluster.
  • The node is registered in the cluster with mezctl nodes add --openssh, which records the dial-address (host:port), labels, and the node's SSH host-key fingerprint.
  • When a user runs msh ssh node-name, the proxy opens a TCP connection to the node's sshd, presents the user's certificate, and verifies the node's host key against the pinned fingerprint.
  • Session recording still happens — the proxy is the in-line MITM for agentless flows, so terminal I/O is captured at the proxy and uploaded to the auth service.

Prerequisites

  • A running Mezite cluster with a reachable proxy.
  • Network path from the proxy to the node's sshd (the proxy must be able to dial the node — agentless mode does not use a reverse tunnel).
  • mezctl and an admin session token.
  • Root or sudo on the node to update sshd_config and drop a CA file.

Step 1: Export the cluster's User CA bundle

sshd needs to trust the cluster's User CA so it accepts user certificates signed by Mezite. Export the public key from the proxy and copy it to the node.

Fetch the User CA public key bash
# Plain SSH authorized_keys format, suitable for TrustedUserCAKeys.
# Works without authentication — this is intentionally a public endpoint.
curl -fsSL https://mezite.example.com/v1/ca/user.pub \
  -o /tmp/mezite-user-ca.pub

# Sanity check: should be one line starting with 'ssh-...'
cat /tmp/mezite-user-ca.pub

On a cluster that rotates the User CA, the endpoint returns the full current trust bundle (one key per line) — both the active key and any keys still in the standby set during a multi-phase rotation. Always pull the current bundle when you re-deploy.


Step 2: Configure sshd on the node

Copy the CA bundle to the node and reference it from sshd_config.

On the node — drop the CA file and update sshd bash
# Install the CA bundle
sudo install -m 0644 /tmp/mezite-user-ca.pub /etc/ssh/mezite-user-ca.pub

# Tell sshd to trust certs signed by this CA
sudo sed -i \
  -e '/^TrustedUserCAKeys/d' \
  -e '$a\TrustedUserCAKeys /etc/ssh/mezite-user-ca.pub' \
  /etc/ssh/sshd_config

# (Optional) accept the cluster's host-CA-signed host cert if you mint one
# Otherwise the proxy uses a pinned host key (Step 3).

# Reload sshd so the new TrustedUserCAKeys takes effect
sudo systemctl reload sshd
# or: sudo service ssh reload

Once sshd is reloaded, a user holding a Mezite certificate can SSH directly to the node — but you still want everything routed through the proxy for RBAC enforcement and recording, so do not open port 22 to user networks. Restrict inbound 22 at the firewall so only the proxy can reach it.


Step 3: Capture the node's host key

Mezite pins the node's sshd host-key fingerprint at enrollment time. On reconnect the proxy verifies the live key matches the pin — a mismatch is treated as the node being replaced (or a MITM) and the connection is refused. Grab the key before enrolling.

On the node — print the host public key bash
# Most distributions ship Ed25519 host keys today
sudo cat /etc/ssh/ssh_host_ed25519_key.pub

# Older boxes or hardened images may have RSA only
sudo cat /etc/ssh/ssh_host_rsa_key.pub

Step 4: Register the node

Use mezctl nodes add --openssh to record the node in the cluster. Pass either the host key inline or via a file.

Register an agentless node bash
# Inline (authorized_keys format)
mezctl nodes add \
  --openssh \
  --name=db-legacy-01 \
  --host=db-legacy-01.internal \
  --port=22 \
  --labels=env=production,role=db,managed-by=ansible \
  --host-key="ssh-ed25519 AAAAC3Nz...comment" \
  --verify-host-key

# Or from a file copied off the node
mezctl nodes add \
  --openssh \
  --name=db-legacy-01 \
  --host=db-legacy-01.internal \
  --port=22 \
  --labels=env=production,role=db \
  --host-key-file=/tmp/db-legacy-01-host.pub \
  --verify-host-key

# Confirm
mezctl nodes get db-legacy-01

Labels are the same shape as agent-based nodes — RBAC roles match against them in allow.node_labels. Pick a labelling convention that lets you write allow.node_labels: { env: production }-style rules without referencing the agent-vs-agentless distinction (Mezite treats them as one inventory).


Step 5: Connect

From a workstation, the agentless node behaves like any other registered node:

Connect bash
# Show up in inventory
msh ls

# Open a session — same flags as agent-based nodes
msh ssh ubuntu@db-legacy-01

# One-shot remote command
msh ssh db-legacy-01 -- systemctl status postgresql

# Recursive copy
msh scp -r ./schema/ ubuntu@db-legacy-01:/var/lib/postgresql/migrations/

Rotating the host key

If the node's sshd host key is regenerated (e.g. re-imaged VM, host-key rotation script), the pinned fingerprint in the cluster no longer matches and the proxy refuses to dial the node. Re-pin with mezctl nodes rotate-host-key.

Re-pin after host-key change bash
# Inline
mezctl nodes rotate-host-key db-legacy-01 \
  --host-key="ssh-ed25519 AAAAC3Nz...new-comment"

# Or from a file
mezctl nodes rotate-host-key db-legacy-01 \
  --host-key-file=/tmp/db-legacy-01-host-new.pub

# Confirm the new fingerprint is recorded
mezctl nodes get db-legacy-01

Treat host-key rotation as an operational event: it should coincide with an explicit change-management entry, not happen silently. The audit log records the rotation with the operator identity that performed it.


Removing an agentless node

Deregister bash
mezctl nodes rm db-legacy-01

After deregistration, revert the node's sshd_config (remove the TrustedUserCAKeys line) and remove the CA file at /etc/ssh/mezite-user-ca.pub. The firewall rule that limited port 22 to the proxy should stay — you don't want to re-open SSH to user networks.


Limitations vs agent-based nodes

Agentless mode trades flexibility for "no daemon on the box". What you lose:

  • Reverse tunnel. The proxy has to dial the node, so the node must accept inbound TCP on its sshd port from the proxy. Behind NAT or restrictive egress, this might not be possible.
  • Host-user auto-provisioning. create_host_user needs an agent on the node to run useradd / usermod / sudo configuration. Agentless nodes are stuck with whichever local accounts already exist on the box.
  • PAM hooks. Mezite's MEZITE_PAM_SERVICE hook fires inside the agent's session-setup path. Agentless sessions terminate at the node's own sshd, which runs PAM independently — the cluster cannot push PAM configuration onto the node.
  • Enhanced (BPF) command capture. The per-command capture loop runs in the agent process and watches the session shell's process tree. Agentless sessions still record the full terminal I/O at the proxy, but not the per-command structured stream.
  • Moderated sessions / session join. require_session_join and require_session_mfa are enforced at the proxy and work for agentless flows; session-join for a peer to actively share the PTY is best supported via the agent-based path.

Troubleshooting

  • proxy: dial tcp: connection refused — The proxy could not reach the node's port. Check the recorded address with mezctl nodes get and verify firewall rules between the proxy and the node.
  • ssh: handshake failed: host key mismatch — The node's host key has changed since enrollment. Confirm with ssh-keyscan from the proxy host, then re-pin with mezctl nodes rotate-host-key.
  • Permission denied (publickey) — The node's sshd does not yet trust the cluster CA. Re-check TrustedUserCAKeys in sshd_config and reload sshd. Confirm the requested login exists on the box and is allowed by AllowUsers / AllowGroups.
  • See the Agentless OpenSSH section of the Troubleshooting guide for the longer recipe list.

Next steps

  • SSH Access — Day-to-day usage, flags, and config that applies to agent and agentless nodes alike.
  • RBAC — Restrict which nodes the agentless inventory exposes to which roles.
  • Audit Logging — Query proxy-side audit events for agentless sessions.