Organizations with dozens of Linux hosts need a single entry point for SSH, file transfer, and port‑forwarding. Managing sshd on each machine, rotating keys, and keeping firewall rules in sync is error‑prone. A centralized gateway reduces operational overhead and improves auditability.
What This Does
ShellHub supplies a web‑based SSH gateway and an accompanying agent that runs on each target device. The gateway (gateway/main.go) accepts client SSH connections, authenticates users, and forwards commands to the agent (agent/main.go). The agent implements the agent/pkg/agentd package, which handles key validation, command execution (agent/server/modes/host/command/command_native.go), SFTP support (agent/sftp.go), and session recording. The UI (ui/) provides a browser interface that talks to the gateway API (gateway/modules.go).
How It Is Wired
Entry point: gateway/main.go loads the configuration (gateway/config.go), registers modules (gateway/modules.go), and starts the SSH server. SSH flow: On connection, gateway/caddy.go creates a Caddyfile.tmpl‑based listener, then gateway/server.go uses github.com/gliderlabs/ssh to accept sessions. Authentication: The agent/server/modes/host/authenticator.go verifies public‑key fingerprints against the agent’s key store (agent/pkg/keygen/keygen.go). Command execution: The agent receives the request via its HTTP API (agent/server/server.go → agent/server/modes/connector/connector.go) and dispatches to agent/server/modes/host/command/command_native.go. Effects:
Database: agent stores key mappings in a local SQLite file (agent/pkg/agentd/tenant.go).
Network: the gateway sets up port forwarding (gateway/caddy.go → gateway/modules.go).
The call graph is a deep tree rooted at gateway/main.go; only a handful of functions (gateway/server.go, agent/server/server.go) have wide blast radius across the stack.
How To Use It
Setup
git clone https://github.com/moses-y/shellhub
cd shellhub
make build # builds both gateway and agent binaries
Or use Docker:
docker compose -f docker-compose.yml up -d
Configuration
.env – contains SHELLHUB_SECRET, DB_URL, etc.
gateway/config.go reads SHELLHUB_GW_PORT and TLS settings.
agent/connector.go expects SHELLHUB_GW_URL to reach the gateway.
Running
# Stand‑alone
go run gateway/main.go
# Agent
go run agent/main.go
Or via the provided bin/docker-compose script to launch both.
Real‑World Use
An infrastructure team can deploy the agent on every server (agent/Dockerfile), then add the device via the UI. Users SSH to gateway.example.com using their private key; the gateway forwards the session to the chosen agent, which executes commands locally and streams output back. SFTP transfers and port forwarding are handled automatically.
Code Health & Issues
Security: The repository contains a .env file that may hold secrets. Review or remove it before committing.
Testing: 1057 test files cover most agent logic, but the UI tests are limited to end‑to‑end (.github/workflows/ui-e2e.yml).
Documentation: 186 doc files exist, yet the README lacks a step‑by‑step deployment guide.
No static analysis report is present; a formal scan would be beneficial.
The Bottom Line
ShellHub is a mature, Go‑based SSH gateway with a React/Tailwind UI. It offers all core SSH features and a clear separation between gateway and agent. The codebase is sizeable but well‑structured, with a strong test suite for the agent. Users must handle the .env file cautiously and may need to extend CI for full coverage. Overall, it is suitable for enterprises needing a single, auditable SSH entry point.
What the analyser found
Deployment readiness
6/7
✓Container image
✓CI pipeline
✓Lockfile committed
✓Test suite
✓README
✓License
✗No committed secrets
Composition
2,483 files
Go830
TSX574
TypeScript367
YAML333
SQL92
JSON35
ReactTailwindDocker
Module dependencies
The 10 most depended-upon modules of 813, from static import analysis. Red outline marks a module in an import cycle.
Ranked by severity × confidence × production reach. Reach is the honest discriminator across a collection that is mostly other people's code: the same finding matters more in something that ships.
critical1
high2
medium5
low1
Rotate the credentials in the committed environment filecritical
.env
SHELLHUB_TLS_KEY_FILE hold generated values
These values are loaded by the application at boot, so a generated value here is a working credential for something that runs.
Fix: Rotate each named credential, remove the file from the index, and replace it with a keys-only example.
Upgrade the pinned dependency carrying a published advisoryhigh14 occurrences
github.com/docker/docker@28.5.3-0.20260325154711-31a1689cb0a1 CVE-2026-33997 (medium) +5 more; github.com/shellhub-io/shellhub@0.0.0 CVE-2026-44425 (medium) +7 more
The pinned version is the one that installs, so a published advisory against it describes this deployment rather than a hypothetical one, and the advisory is also a public description of how to exploit it.
Fix: Upgrade to the fixed version named in the advisory, then commit the lockfile.
Remove the committed .env and rotate what it holdshigh2 occurrences
.env
.env, .env.enterprise
A tracked .env is the most common route for a working key to reach a public clone, and it is the file the app actually loads, so the value is usually live.
Fix: git rm --cached the file, add it to .gitignore, rotate every credential it names, commit a .env.example with empty values.
Install from the lockfile in CImedium
.github/workflows
npm install with a committed package-lock.json
A fresh resolution in CI means the tested dependency set is not the locked one, so the failure only appears after merge.
Fix: Use npm ci, yarn install --immutable, or pnpm install --frozen-lockfile.
Pin the container base image by digestmedium5 occurrences
An untagged or mutable base means today's build and last month's contain different libc and a different CVE set, with no record of which shipped.
Fix: Use image:tag@sha256:<digest> and enable Dependabot's docker ecosystem.
Gate pull requests on a dependency vulnerability scanmedium
.github/workflows
no dependency scan in CI
This is the one gate that would catch a known-vulnerable package before it reaches a build, and no repository in the sample had it.
Fix: Add dependency-review-action on pull_request, or osv-scanner on push and a schedule.
Set persist-credentials: false on checkoutmedium
.github/workflows/build-agent.yml
checkout keeps the token, then dependencies are installed
The token stays in .git/config for every later step, so a malicious postinstall script reads a pushable credential without one ever being passed to it.
Fix: Add with: persist-credentials: false, and pass an explicit token only to the step that pushes.
Add a non-root USER to the imagemedium
agent/Dockerfile
CMD or ENTRYPOINT with no USER directive
A process running as root in the container is root against every mounted volume, and it turns any container escape or writable-mount mistake from a contained problem into a host one.
Fix: Create an unprivileged user, chown what it needs, and end the Dockerfile with USER.
Set timeout-minutes on the workflow jobslow3 occurrences
.github/workflows/build-agent.yml
3 workflow(s) declare no job timeout
A wedged step runs to the six-hour platform default, which on a two-hourly schedule means three runs overlap behind it.
Fix: Add timeout-minutes with a realistic bound to each job.
Checked deterministically against the repository tree and a bounded set of its files: committed credentials, unpinned actions and base images, missing lockfiles and update bots, workflows that discard failures, published advisories against the declared dependencies, runtime configuration, licensing and notebook reproducibility. No language model is involved in this section.