Access Remote Machines over SSH with rstream

Reach remote machines over private tunnels, published TCP addresses, or TLS entrypoints while preserving the normal OpenSSH security model.


This guide shows how to keep a normal SSH workflow even when the remote machine is not publicly reachable, or when exposing SSH directly would create unnecessary exposure. The same pattern works for homelabs, embedded devices, personal computers, and administrative hosts behind NAT, private addressing, or restrictive firewalls.

The transport model is simple. The remote machine establishes the outbound connection to the rstream edge network, while the SSH client stays local. OpenSSH contributes ProxyCommand, which lets a helper process supply the network path. In this workflow, rstream nc fills that role. It dials the tunnel and hands the resulting byte stream to ssh, preserving the normal SSH client workflow.

That separation matters for the security model. rstream provides the transport path and the access layer around the tunnel, but it does not terminate SSH. Host key verification, user authentication, and session encryption stay end-to-end between the SSH client and the remote sshd.

Private tunnel dialing is available on Pro, Enterprise, and self-hosted deployments. When SSH is not required, WebTTY can address the same general access problem on all plans through either the browser interface or the CLI workflow.

The walkthrough starts with the minimal project preparation required on both machines. Background on installation, authentication, and context management is covered in Installation, CLI Login, and CLI Workflow.

Prepare access to the project

For this walkthrough, both machines need an rstream CLI configuration that resolves to the same project. For a quick hosted validation, the simplest setup on each machine is:

rstream login
rstream project use <project-endpoint>

That setup is convenient for an initial test. For long-lived servers and devices, a dedicated project-scoped context is usually the better choice so the machine only holds the access it actually needs.

Validate the SSH path on one machine

On the remote machine, open a private bytestream tunnel to the local SSH daemon:

rstream forward 22 --bytestream --no-publish --name ssh-server

This keeps the process attached to the terminal and forwards the private tunnel to 127.0.0.1:22. The name ssh-server is only an example. Any stable tunnel name works, and that name becomes the identifier dialed later by the SSH client.

On the client machine, validate the path with an SSH command that delegates transport to rstream nc:

ssh -o 'ProxyCommand rstream nc rstrm://ssh-server' admin@ssh-server "printf 'hello world\n'"

This asks ssh to delegate transport to rstream nc through ProxyCommand. In this example, ssh-server is the tunnel name and admin is the SSH account on the remote machine; both should be adjusted to match the environment. ssh launches the helper locally, wires its input and output streams to that process, and then continues the normal SSH handshake on top of the byte stream returned by rstream nc. The trailing printf keeps the first check short and confirms quickly that the path works. Removing the trailing command opens a regular interactive shell instead:

ssh -o 'ProxyCommand rstream nc rstrm://ssh-server' admin@ssh-server

The first connection still performs normal SSH host key verification. SSH continues to verify the remote host identity itself, and the session remains encrypted end-to-end between the local SSH client and the remote daemon.

At this point, the remote machine has opened only an outbound rstream connection and the local SSH client has completed a normal SSH handshake through that private path. No inbound SSH port has been opened on the remote network.

The examples above use the default client-to-edge tunnel transport. On machines that move between networks or sit behind unstable uplinks, the publishing side can opt into QUIC tunnel transport:

RSTREAM_TUNNEL_TRANSPORT=quic rstream forward 22 --bytestream --no-publish --name ssh-server

This changes the transport between the remote machine and the rstream engine. It does not publish SSH, does not terminate SSH, and does not change OpenSSH authentication or host-key verification.

For private tunnels, the dialing client also owns a client-to-edge leg. If the operator laptop is moving between Wi-Fi and cellular, or if the path is sensitive to interface changes, enable QUIC on the rstream nc side as well:

ssh -o 'ProxyCommand env RSTREAM_TUNNEL_TRANSPORT=quic rstream nc rstrm://ssh-server' admin@ssh-server

The two sides are independent. The remote publisher and SSH client each run automatic selection by default, so either side can use QUIC or fall back to TLS according to its own network path. rstream doctor -o json reports both reachability checks and the mode that would be selected locally.

Use SSH config for regular access

After the initial validation, move the transport hook into SSH config. This file is usually ~/.ssh/config on Linux and macOS, and %USERPROFILE%\.ssh\config on Windows:

Host ssh-server
  HostName ssh-server
  User admin
  ProxyCommand rstream nc rstrm://%h

The operational command then becomes a normal SSH invocation:

ssh ssh-server

The %h token expands to the SSH host name. Keeping the SSH host name and the tunnel name aligned makes the configuration compact and predictable. Replace admin with the actual SSH user on the remote machine. If the SSH host name and the tunnel name differ, replace %h with a fixed tunnel name such as rstrm://prod-admin-01. If a specific SSH key is required, add the usual IdentityFile directive. If the local SSH alias must differ from the remote host identity, HostKeyAlias works exactly as it would in a direct SSH deployment.

To make QUIC the default for this SSH alias, keep the same stanza and put the environment override in the helper command:

Host ssh-server
  HostName ssh-server
  User admin
  ProxyCommand env RSTREAM_TUNNEL_TRANSPORT=quic rstream nc rstrm://%h

Organize discovery with names and labels

With more than one machine, names and labels quickly become operational tooling. A convention such as ssh-homelab-01, ssh-prod-01, or ssh-router-01 keeps the same identifier usable in the tunnel list, in ssh, and in scripts. Labels remain entirely operator-defined. The example below uses service, env, and role, but the actual taxonomy should match the environment and automation model already in use:

rstream forward 22 \
  --bytestream --no-publish --name ssh-prod-01 \
  --label service=ssh --label env=prod \
  --label role=admin

The tunnel list then becomes an inventory surface. A broad view of the fleet can be obtained with:

rstream tunnel list --filter 'labels.service=ssh'

And a narrower view can be derived from the same label set:

rstream tunnel list --filter 'labels.service=ssh,labels.env=prod' -o json

The table output is useful for quick inspection. JSON output is better when labels are consumed by scripts or other operational tooling.

For automation, keep the label model stable before relying on it in scripts. A useful convention is to combine one service label, one environment label, and one ownership or site label so inventory queries stay readable as the fleet grows.

Generalize the client configuration to a fleet

Once tunnel names follow a consistent convention, the client-side SSH configuration can be generalized with a single block:

Host ssh-*
  User admin
  ProxyCommand rstream nc rstrm://%h

The same SSH client can then reach multiple machines without adding one stanza per server:

ssh ssh-homelab-01
ssh ssh-prod-01

This pattern works when the SSH host name and the tunnel name are aligned. When different groups require different usernames, keys, or local policies, narrower Host blocks can be layered on top of the same model. The single-machine form remains useful for exceptions, while the wildcard form scales better for a fleet.

Move the remote side from forward to run

rstream forward defines the tunnel directly on the command line and keeps it online while the process runs. That makes it a good fit for interactive setup, quick validation, and short-lived operator workflows. rstream run reads the tunnel definition from a file, which is easier to reuse in scripts, service managers, and repeatable machine setup.

On Linux and macOS, the default CLI state lives under ~/.rstream, so keeping the SSH tunnel file there avoids scattering operational files. Create ~/.rstream/ssh-tunnels.yaml on the remote machine:

mkdir -p ~/.rstream
cat > ~/.rstream/ssh-tunnels.yaml <<'EOF'
version: 1
tunnels:
  - name: ssh-server
    forward: 127.0.0.1:22
    tunnel:
      publish: false
      type: bytestream
      labels:
        service: ssh
        env: prod
        role: admin
EOF

The same setup can then be started manually with:

rstream run --apply ~/.rstream/ssh-tunnels.yaml

On Windows, the same file can be stored under the current user profile:

New-Item -ItemType Directory -Force "$env:USERPROFILE\.rstream" | Out-Null
@'
version: 1
tunnels:
  - name: ssh-server
    forward: 127.0.0.1:22
    tunnel:
      publish: false
      type: bytestream
      labels:
        service: ssh
        env: prod
        role: admin
'@ | Set-Content "$env:USERPROFILE\.rstream\ssh-tunnels.yaml"

And started manually with:

rstream run --apply "$env:USERPROFILE\.rstream\ssh-tunnels.yaml"

The declarative tunnel model is covered in YAML.

Start the tunnel automatically

The startup mechanism depends on the operating system and on whether the tunnel must start after user login or at machine boot without an interactive session. On Linux, the user-scoped systemd service below can stay active across logout and reboot once linger is enabled. On macOS, the example uses a LaunchAgent and starts when that user logs in. On Windows, the scheduled task below starts when that user signs in. If the tunnel must start at machine boot without an interactive login, use a system-level service model instead.

Linux

On Linux, the most direct option is a user-scoped systemd service. This keeps the service tied to the same user account that already owns ~/.rstream/config.yaml and ~/.rstream/ssh-tunnels.yaml:

RSTREAM_BIN="$(command -v rstream)"
mkdir -p ~/.config/systemd/user
cat > ~/.config/systemd/user/rstream-ssh.service <<EOF
[Unit]
Description=rstream SSH tunnel
After=network-online.target
Wants=network-online.target
 
[Service]
Type=simple
ExecStart=${RSTREAM_BIN} run --apply ${HOME}/.rstream/ssh-tunnels.yaml
Restart=always
RestartSec=5
 
[Install]
WantedBy=default.target
EOF

Enable it with:

systemctl --user daemon-reload
systemctl --user enable --now rstream-ssh.service
loginctl enable-linger "$(id -un)"

loginctl enable-linger allows the user service to stay active after logout and across reboots.

macOS

On macOS, a per-user LaunchAgent matches the same user-owned CLI setup as ~/.rstream. It starts when that user logs in rather than as a system daemon at machine boot:

RSTREAM_BIN="$(command -v rstream)"
mkdir -p ~/Library/LaunchAgents
cat > ~/Library/LaunchAgents/io.rstream.ssh.plist <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>Label</key>
    <string>io.rstream.ssh</string>
    <key>ProgramArguments</key>
    <array>
      <string>${RSTREAM_BIN}</string>
      <string>run</string>
      <string>--apply</string>
      <string>${HOME}/.rstream/ssh-tunnels.yaml</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
  </dict>
</plist>
EOF
launchctl bootout "gui/$(id -u)" ~/Library/LaunchAgents/io.rstream.ssh.plist 2>/dev/null || true
launchctl bootstrap "gui/$(id -u)" ~/Library/LaunchAgents/io.rstream.ssh.plist
launchctl kickstart -k "gui/$(id -u)/io.rstream.ssh"

Windows

On Windows, a scheduled task tied to the current user matches the same user-scoped setup while reusing %USERPROFILE%\.rstream. This example starts the tunnel when that user signs in. If the tunnel must start at machine boot without an interactive sign-in, use a task principal that can run without an interactive session, or switch to a system-level service model.

$tunnels = "$env:USERPROFILE\.rstream\ssh-tunnels.yaml"
$rstream = (Get-Command rstream).Source
$action = New-ScheduledTaskAction -Execute $rstream -Argument "run --apply `"$tunnels`""
$trigger = New-ScheduledTaskTrigger -AtLogOn
Register-ScheduledTask `
  -TaskName "rstream SSH Tunnel" `
  -Action $action `
  -Trigger $trigger `
  -User $env:USERNAME `
  -Force
Start-ScheduledTask -TaskName "rstream SSH Tunnel"

After installation, restart the machine or the service manager session and verify that rstream tunnel list --filter 'labels.service=ssh' still reports the tunnel online.

Choose a published path when clients cannot use rstream

The private workflow remains the better default for direct operator access. It creates no public SSH entrypoint and lets the project decide which authenticated rstream clients may dial the tunnel. Some environments instead require an ordinary SSH command on the client, with no rstream context. A published TCP or TLS tunnel covers that case.

The choice is about the public side of the connection. SSH still performs host-key verification, user authentication, and session encryption end to end in every case.

Publish SSH directly over TCP

A published TCP tunnel gives the remote SSH daemon a public hostname and port. On the remote machine, start with an ephemeral address:

rstream forward 22 --tcp --name ssh-public

The command prints the allocated address. A client can connect to it with OpenSSH alone:

ssh -p <port> admin@<tcp-hostname>

The address exists for the lifetime of that tunnel. When client configuration must survive agent restarts, reserve an address from the project TCP page or from the CLI:

rstream project tcp-address reserve
rstream forward 22 --tcp --tcp-port <reserved-port> --name ssh-public

The reservation keeps the same hostname and port attached to the project while the tunnel connects and disconnects. The SSH command itself does not change.

rstream forwards a published TCP connection as a raw byte stream. It does not add encryption or authentication on the downstream side. That is appropriate for SSH because SSH already provides both, but the public address can receive connection attempts from the Internet. Keep normal SSH hardening in place, including key-based authentication, host-key verification, account policy, and server-side rate limiting where appropriate.

Published TCP is available on Pro and Enterprise projects and can be disabled in project security settings. TCP Tunnels covers address reservation, quarantine, and declarative configuration.

Add an outer TLS session

A published TLS tunnel adds a TLS session between the client helper and the rstream edge before the SSH handshake reaches the remote daemon. This is useful when the connection should pass through TLS-aware network policy or when the public endpoint should use a verified custom hostname. The client needs a TLS-capable byte-stream helper such as ncat, but it does not need an rstream context.

On the remote machine, publish SSH through a terminated TLS tunnel:

rstream forward 22 \
  --bytestream --publish \
  --tls --tls-mode terminated \
  --name ssh-tls

Use the hostname and port printed by the command in the SSH client:

ssh -o 'ProxyCommand ncat --ssl --ssl-verify --ssl-servername %h %h %p' \
  -p <published-port> admin@<published-host>

For a durable hostname outside the rstream namespace, first register and verify it from the project Domains page. Once its managed certificate is ready, select it when opening the tunnel:

rstream forward 22 \
  --bytestream --publish \
  --tls --tls-mode terminated \
  --host ssh.example.com \
  --name ssh-tls

The corresponding SSH configuration remains compact:

Host ssh-production
  HostName ssh.example.com
  Port 443
  User admin
  ProxyCommand ncat --ssl --ssl-verify --ssl-servername %h %h %p

TLS is an outer transport layer in this setup. rstream terminates that layer, while the SSH handshake and SSH session remain end-to-end between the client and sshd. See Custom Domains for hostname verification and certificate lifecycle, and Tunnel Protocols for the TLS tunnel model.

Operational notes

This guide starts with a private path because it minimizes exposure and keeps access inside the rstream project. Published TCP is the simpler alternative when clients need plain OpenSSH, while published TLS adds an outer TLS layer and supports a verified custom hostname. In all three forms, SSH remains responsible for the identity and security of the SSH session itself.

For long-running hosts, rstream run behind the native service manager is usually the better fit. rstream forward remains useful for initial setup, incident response, and temporary maintenance windows.

SSH keys, host keys, local account policies, and server-side hardening continue to apply exactly as before. rstream provides the private transport path and tunnel access layer without requiring port 22 to be published publicly.

The private workflow also shows why rstream nc is useful beyond SSH. Any client that can delegate transport to a helper command can use the same private bytestream model, while the remote side continues to use the same forward and run workflows used elsewhere in rstream.