Browser SSH Terminal Docker Host Self-Hosted: The Safe, Audited Setup Guide

browser-ssh-terminal-docker-host-self-hosted.md
title Browser SSH Terminal Docker Host Self-Hosted: The Safe, Audited Setup Guide
date
author VahaC
read 13 min read
category Self-Hosting
tags #container #Docker #Homelab #Linux #Security #Stashboard
browser ssh terminal docker host self-hosted

Stashboard’s browser SSH terminal Docker host self-hosted feature opens a real, interactive shell on the machine that runs your containers, straight from the dashboard, with no SSH client needed on the device in front of you. The Stashboard overview introduced it with V5.3 in a few paragraphs. This deep-dive covers the rest. 🖥️

Below: what the browser SSH terminal Docker host self-hosted path connects to, how each session is secured, how the caps, idle timeout and audit trail behave, and how to enable and verify it.

What the Browser SSH Terminal Docker Host Self-Hosted Feature Opens

The shell lands on the Docker host itself, not inside a container, logged in as the SSH user of one of your connections with the key Stashboard already holds for Docker monitoring over SSH (V2.5). Everything the browser SSH terminal Docker host self-hosted shell does, it does as that user: df -h, journalctl -u docker, systemctl restart, a look inside /opt/stacks. 🛠️

Stashboard has three browser terminals, and they’re easy to mix up:

  • Host terminal (V5.3): a shell on the host of an SSH tunnel connection. That’s this guide.
  • Container exec (V5.7): a shell inside a running container via the Docker exec API, on any connection type.
  • LXC console (V6.6): a shell inside a Proxmox container, covered in the browser LXC console guide and built on the same SSH, WebSocket and ticket plumbing.

One correction to the original announcement: V5.7 moved the host terminal off the per-container Terminal tab to the connection level, since a host shell belongs to no single container. Open it from the host card on the Docker page: ⋮ (Host actions) or right-click, then Terminal. The item appears only for SSH connections.

How a Browser SSH Terminal Docker Host Self-Hosted Session Is Secured

A browser WebSocket can’t send an Authorization header, and a login token placed on a query string can end up in proxy logs. So every browser SSH terminal Docker host self-hosted session starts with a two-step handshake:

  1. Connect sends an authenticated POST to /api/docker/connections/{id}/host-shell/ticket with your normal session. The server checks the three gates (below) and that you own the connection.
  2. If everything passes, it mints a ticket: 32 random bytes from a cryptographic RNG, bound to your user ID and that one connection, valid for 30 seconds, and kept only in server memory.
  3. The browser opens wss://…/host-shell/ws?ticket=…&cols=…&rows=…. The URL carries the ticket and terminal size, never your login token.
  4. The server redeems the ticket, deleting it on first use. A replayed, expired or wrong-connection ticket gets a 401. It then re-checks the gates and ownership, since either could have changed since the POST.
  5. It checks the concurrency caps, writes the audit row, and has SSH.NET open an xterm-256color PTY on the host at the size your browser reported.
  6. Binary frames then carry keystrokes and output, and small JSON text frames carry resize requests.

The SSH private key never reaches the browser: it’s encrypted at rest, and the API only ever reports whether a key is set. Since V10.4, personal access tokens get a 403 on the ticket endpoint, so a leaked automation token can’t be turned into a shell. The browser SSH terminal Docker host self-hosted flow never needs a host credential in the page.

Three gates, all required

  • Server-wide switch: Settings → Host terminal. It’s stored in the database and off by default. The optional Stashboard:AllowHostShell config flag only seeds it the first time the setting is created.
  • Per-connection opt-in: Allow host terminal on each connection, also off by default. Enabling one host never enables another.
  • An SSH tunnel connection you own: local-socket and TCP+TLS connections have no host shell to offer.

If any gate fails, the server refuses the ticket request, so the protection isn’t just a hidden button.

Prerequisites

Before you enable the browser SSH terminal Docker host self-hosted shell, you need:

  • A current Stashboard install. The steps below follow the 10.x UI.
  • A Linux Docker host that the Stashboard container can reach over SSH.
  • sudo on that host for the one-time user and key setup.
  • If a reverse proxy sits in front of Stashboard, it must forward WebSocket upgrades (see Troubleshooting). HTTPS is strongly recommended.
  • A clear answer to “who can log in to my Stashboard?”, because from now on that’s also who can get a shell on this host.

Step-by-Step: Set Up the Browser SSH Terminal Docker Host Self-Hosted Path

1. Create the SSH user and key 🔑

On the Docker host, create a dedicated user (skip this if Stashboard already has one) and give it access to the Docker socket:

sudo adduser stashboard
sudo usermod -aG docker stashboard

On your workstation, generate a key and authorize it for that user. This follows Stashboard’s own Help → Docker · SSH tunnel page (see the ssh-keygen manual):

ssh-keygen -t ed25519 -f ./stashboard_docker -C stashboard
ssh-copy-id -i stashboard_docker.pub stashboard@<host>

ssh-copy-id logs in once with the user’s password. If password logins are disabled, append the .pub line to /home/stashboard/.ssh/authorized_keys yourself. Then confirm it works:

ssh -i stashboard_docker stashboard@<host> docker ps

⚠️ The docker group is root-equivalent. Docker’s post-install notes say it outright. Anything this user can reach, the browser shell can reach, so treat the host terminal as a root shell.

2. Add the SSH connection

On the Docker page, click Add connection. Enter a Name, set Docker host to SSH tunnel, and fill in SSH host, SSH port (22), SSH username (stashboard), SSH private key (PEM) (paste the whole stashboard_docker file) and, if you set one, Private key passphrase (optional). Leave Remote socket path at /var/run/docker.sock, or use /run/user/UID/docker.sock for rootless Docker. Click Test connection, wait for Docker host reachable, then click Save connection.

Already monitoring this host over SSH? Skip this step. The browser SSH terminal Docker host self-hosted shell reuses the existing connection and its key.

3. Turn on the server-wide switch

Open Settings → Host terminal (listed under Docker). Read the warning box, tick Enable the host terminal server-wide and click Save. You should see “Host terminal enabled server-wide.” No restart is needed. On its own, this switch opens nothing: the browser SSH terminal Docker host self-hosted shell still needs step 4.

4. Opt the connection in

Back on the Docker page, choose ⋮ → Edit on the host card, tick Allow host terminal (interactive SSH shell on the host) and click Save changes. The checkbox only appears while Docker host is set to SSH tunnel.

5. Open the shell

Choose ⋮ → Terminal on the same card. A dialog titled Host terminal · followed by the connection name opens, with the reminder “You are about to open a shell on the Docker host. Every session is audited.” Click Connect. The status goes from connecting to connected and your prompt appears. That’s the browser SSH terminal Docker host self-hosted shell, live. 🎉

Inside the xterm Window

The browser SSH terminal Docker host self-hosted window is xterm.js 5.5 with its fit add-on, and the PTY runs as xterm-256color, so htop, vim and coloured ls output render properly:

  • Status and buttons: a dot and label (idle, connecting, connected, closed, error) next to Connect, Disconnect or Reconnect.
  • Scrollback: 5,000 lines.
  • Copy and paste: Ctrl+Shift+C copies the selection and Ctrl+Shift+V pastes. Right-click copies when text is selected and pastes when nothing is. Plain Ctrl+C still sends an interrupt to the shell.
  • Size: the PTY is created at the panel’s size when you connect. SSH.NET 2024.2.0 can’t send a live window-change, so after resizing the window, click Disconnect, then Reconnect.
  • Clear endings: the close reason is printed in grey, e.g. [ Shell exited. ] or [ Closed: idle timeout. ].
  • Closing the dialog ends the session. Nothing keeps running in the background.

Concurrency Caps and the Idle Timeout ⏱️

Four settings bound every browser SSH terminal Docker host self-hosted session, and the defaults are conservative:

  • MaxSessionsPerUser = 3: open shells per user, across all of their connections.
  • MaxSessionsPerHost = 5: open shells against one connection.
  • IdleTimeoutSeconds = 600: the server closes a shell after 10 minutes with no bytes in either direction, whether or not the tab is still open. 0 disables it.
  • TicketTtlSeconds = 30: how long a minted ticket stays valid.

Over a cap, the socket opens and closes straight away with a readable reason, such as You already have 3 host-terminal session(s) open (limit 3). The idle timer measures silence, not typing: output counts as activity, so a running htop or tail -f keeps the session alive. Stashboard also sends a WebSocket ping and an SSH keep-alive every 30 seconds so proxies don’t drop a quiet shell early. Caps and tickets live in memory, so a restart resets the counters and voids any unused tickets.

To change the limits, add them to the optional .env next to docker-compose.yml:

STASHBOARD_Stashboard__HostShell__MaxSessionsPerUser=3
STASHBOARD_Stashboard__HostShell__MaxSessionsPerHost=5
STASHBOARD_Stashboard__HostShell__IdleTimeoutSeconds=600
STASHBOARD_Stashboard__HostShell__TicketTtlSeconds=30

Then recreate the container:

docker compose up -d

⚠️ An idle timeout of 0 lets a forgotten tab hold a root-capable shell open indefinitely. Shorten the timeout if you like, but don’t disable it.

The Browser SSH Terminal Docker Host Self-Hosted Audit Trail 📜

Every browser SSH terminal Docker host self-hosted session writes a row to the HostShellSessions table when it starts, before the SSH login even happens, and the same row is finalised when it ends. Each row records:

  • who opened it and against which connection, plus that connection’s name, SSH host and username at the time, so rows survive a rename or delete
  • start and end times, and therefore the duration
  • bytes from the browser (In) and bytes to the browser (Out)
  • the end reason, plus the error text if there was one

Open Audit in the sidebar and pick the Host terminal tab, or click View session history under the terminal to jump there pre-filtered to that connection. Rows are newest-first and read-only, and each user sees only their own sessions. An open shell shows an Active badge. The end reasons are Closed by client, Remote closed (you typed exit), Idle timeout, Closed by server (Stashboard stopping gracefully), Error (for example, SSH authentication failed) and Interrupted (server restart), which a startup sweep applies to rows a crash left open. Sessions also log Host terminal opened and Host terminal closed lines, which you can read with docker logs stashboard-app.

⚠️ The trail records metadata, not commands. Byte counts tell you how much happened, not what. If you need command-level records, capture them on the host.

How to Verify Your Browser SSH Terminal Docker Host Self-Hosted Setup ✅

Run these four checks once, before you rely on the browser SSH terminal Docker host self-hosted shell:

  1. Gate check: turn the server-wide switch off and choose ⋮ → Terminal. You should see “Host terminal is disabled on this server” and no Connect button. Turn the switch back on.
  2. Right machine: connect and run whoami; hostname; docker ps. You should see your SSH user and the host’s real hostname. A 12-character hex hostname means you’re inside a container, not on the host.
  3. Audit row: with the shell connected, open Audit → Host terminal in a second browser tab and look for the Active badge. Type exit in the shell, reload that tab, and the row should read Remote closed, with a duration and byte counts.
  4. Token refusal: try to mint a ticket with a personal access token. To get the connection ID, click View session history and copy the connectionId value from the address bar. Expect 403 Forbidden.
curl -i -X POST -H "Authorization: Bearer sb_pat_..." \
  https://stashboard.example.com/api/docker/connections/CONNECTION_ID/host-shell/ticket

Troubleshooting 🔧

Most browser SSH terminal Docker host self-hosted failures come down to one of these:

  • “Terminal connection timed out — the WebSocket upgrade didn’t complete.” Usually your reverse proxy isn’t forwarding the Upgrade and Connection headers. In Nginx Proxy Manager, enable Websockets Support on the proxy host. In plain nginx, follow the WebSocket proxying docs:
location / {
    proxy_pass http://127.0.0.1:8080;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
}
  • “Host terminal is disabled on this server.” Turn on Settings → Host terminal. The panel’s hint may still mention Stashboard:AllowHostShell and a restart, but you need neither.
  • “Host terminal is not enabled for this connection.” Tick Allow host terminal in the connection’s Edit form.
  • No Terminal item in the ⋮ menu. The browser SSH terminal Docker host self-hosted shell exists only for SSH tunnel connections, and this one is local-socket or TCP+TLS. Use container exec, or add an SSH connection.
  • [ SSH connection failed: … ]. Stashboard couldn’t log in over SSH, so check the host, port, user and key. Re-run Test connection and the ssh -i … docker ps check from step 1. The Audit row keeps the full error.
  • Ctrl+Shift+V does nothing. Paste uses the browser’s async Clipboard API, which works only over HTTPS or on localhost. Copy still works everywhere.
  • Lines wrap oddly. You resized after connecting. Reconnect.

Risks and Gating ⚠️

A browser SSH terminal Docker host self-hosted shell is host-level remote code execution by design. That’s why the gates exist:

  • Your Stashboard login becomes a host login. Stashboard has no separate admin role, so anyone who can sign in as a connection’s owner can open its shell. Protect that account with TOTP two-factor (V10.3) or SSO (V10.5).
  • Keep it off the open internet. Reach Stashboard over a VPN, as described in the secure remote access guide, instead of publishing a shell behind a login page.
  • Assume root. The SSH user is in the docker group, so the whole Docker security checklist applies.
  • The host key isn’t pinned. As of 10.5, the SSH hop doesn’t check the host’s key against a known fingerprint, so keep the path between Stashboard and the host on a network you trust.
  • Switching it off doesn’t kill open shells. ⚠️ Gates are checked when a session starts. Turning off the switch or the opt-in blocks new sessions, but an open one keeps running until it ends, times out or Stashboard restarts.
  • Prefer the editor for Compose changes. Hand-editing YAML in a root shell has no safety net. The diff-and-apply Compose editor does.

Browser SSH Terminal Docker Host Self-Hosted FAQ

Does the browser SSH terminal Docker host self-hosted shell work over a local socket?

No. It needs an SSH tunnel connection. For local-socket or TCP+TLS hosts, container exec gives you a shell inside a container instead.

Does a browser SSH terminal Docker host self-hosted session expose my SSH key?

No. The key is decrypted and used only on the server. The browser only ever holds a 30-second, single-use ticket.

Can I script the browser SSH terminal Docker host self-hosted endpoint with an API token?

No. Personal access tokens get a 403 on the ticket endpoint. Opening a shell requires an interactive login.

Why doesn’t the browser SSH terminal Docker host self-hosted window resize?

SSH.NET 2024.2.0 can’t send a live resize, so the PTY keeps its connect-time size. Reconnect after resizing.

Bottom Line

The host terminal turns “where’s my SSH client?” into a couple of clicks without giving up control. All three gates must pass, the ticket is single-use, the key never leaves the server, the caps and idle timeout are enforced server-side, and every session leaves an audit row. Set up the browser SSH terminal Docker host self-hosted path on one host, run the checks above, and only then add a second host. 🚀

New here? Read the Stashboard homelab dashboard overview. Stashboard is open source: code on GitHub, image on Docker Hub.

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.