ssh-gateway is an agent-facing SSH gateway for remote Linux automation behind bastions. It keeps reusable embedded SSH sessions inside a local daemon, moves authentication material into gateway-managed profiles, and lets agents operate by profile name instead of raw passwords or keys.
It is intentionally not a general-purpose SSH client replacement. The project is optimized for agent workflows, profile-driven safety, and repeatable remote operations.
- Agents that repeatedly spawn one-shot
sshorscpoften hit connection churn, login throttling, or refused sessions. - Bastion chains, delegated hops, and mixed per-hop auth are awkward to express safely in prompts.
- Passwords and private keys should stay in gateway-owned config, not in agent-visible command lines or chat history.
- Secret isolation at the gateway API boundary: the daemon reads passwords, key paths, and optional key passphrases from config; callers send only
profileplus operation arguments. - Redacted profile and session output:
profile show,session inspect, and error payloads never echo raw passwords or passphrases. - Profile-first agent workflow: agents use named profiles instead of embedding secrets in
sshcommands. - Embedded SSH transport with session reuse: direct and bastion profiles use in-process SSH instead of spawning local
ssh.exeorscp. - No local OpenSSH dependency for direct or bastion mode: Windows and Linux direct transports run through the embedded client stack.
- Per-hop auth for bastions and targets: every hop can use its own password or key configuration.
- Delegated
via_profilemode: reuse an upstream host's remote SSH capability when the final target is only reachable from that host. - Managed remote agent lifecycle: version checks, bootstrap, and reuse happen on connect.
- JSON-only CLI: predictable automation surface for
daemon,profile,exec,read,write,upload,download,tunnel, andsession.
- Passwords, private-key paths, and optional key passphrases live in
profiles.yaml,profiles.yml, or legacyprofiles.toml, and are consumed by the local gateway daemon. - CLI and RPC requests carry
profilenames and operation arguments, not raw password, passphrase, or private-key values. profile show,session inspect, and error results redact secret material before returning JSON to the caller.
- This is not a claim of strong isolation against other local processes running under the same OS user.
- This is not a replacement for OS file permissions, secret managers, host hardening, or bastion policy.
- Delegated
via_profilesessions still rely on the upstream host's own SSH capability to reach the final target.
- Keep live configs outside the repository.
- Restrict config file permissions to the expected local user or service account.
- Do not commit real profiles, passwords, or private keys.
- Download a release asset from GitHub Releases and place
ssh-gatewayon yourPATH. - Prepare a profile file. YAML is preferred; start from examples/profiles.yaml.
- Validate the profile before the first run.
- Start the daemon implicitly or explicitly and run remote operations by
profile.
PowerShell:
$env:ARRT_CONFIG_PATH = (Resolve-Path .\examples\profiles.yaml)
ssh-gateway profile validate
ssh-gateway daemon start
ssh-gateway exec --profile direct-with-bastion -- hostname
ssh-gateway session list
ssh-gateway daemon stopBash:
export ARRT_CONFIG_PATH="$PWD/examples/profiles.yaml"
ssh-gateway profile validate
ssh-gateway daemon start
ssh-gateway exec --profile direct-with-bastion -- hostname
ssh-gateway session list
ssh-gateway daemon stopThe config loader uses ARRT_CONFIG_PATH first. If it is unset, the default search order is:
- Windows:
%APPDATA%\opensource\ssh-gateway\profiles.yaml, thenprofiles.yml, then legacyprofiles.toml - Linux:
$XDG_CONFIG_HOME/opensource/ssh-gateway/profiles.yaml, thenprofiles.yml, then legacyprofiles.toml
The repository keeps public-safe examples in examples/profiles.yaml.
profiles:
- name: direct-with-bastion
target:
host: target.internal
user: root
port: 22
auth:
type: password
password: target-password
bastions:
- host: bastion.example.com
user: root
port: 22
auth:
type: key
key_path: ~/.ssh/id_ed25519
### Encrypted private key with a passphrase
```yaml
profiles:
- name: encrypted-key-target
target:
host: secure.internal
user: ops
port: 22
auth:
type: key
key_path: ~/.ssh/id_rsa_2048
passphrase: local-key-passphraseThe passphrase is consumed only by the local gateway daemon. It is not returned by profile show, session inspect, or normal CLI error payloads.
### Delegated `via_profile`
```yaml
profiles:
- name: upstream-bastion
target:
host: bastion.example.com
user: root
port: 22
auth:
type: key
key_path: ~/.ssh/id_ed25519
- name: delegated-target
via_profile: upstream-bastion
target:
host: target.internal
user: root
port: 22
Delegated mode is useful when the upstream host already knows how to ssh target.internal ... and the local machine should not carry an additional target credential. In this mode:
- the delegated profile must not define
auth - the delegated profile must not define
bastions exec,read,write,upload, anddownloadare supportedtunnel openis rejected for delegated sessions
Legacy TOML remains supported for compatibility:
[[profiles]]
name = "legacy"
[profiles.target]
host = "target.internal"
user = "root"
[profiles.auth]
key_path = "~/.ssh/id_ed25519"
passphrase = "local-key-passphrase"All commands print JSON.
| Area | Commands |
|---|---|
daemon |
daemon start, daemon status, daemon stop |
profile |
profile list, profile show <name>, profile validate [name] |
| remote ops | exec, read, write, upload, download |
tunnel |
tunnel open --profile <name> --local <port> --remote <host:port>, tunnel close --id <tunnel-id> |
session |
session list, session inspect --id <session-id>, session close --id <session-id> |
Common examples:
ssh-gateway exec --profile delegated-target -- hostname
ssh-gateway read --profile delegated-target --path /etc/hostname
ssh-gateway write --profile delegated-target --path /tmp/demo.txt --input hello
ssh-gateway upload --profile delegated-target --src ./local.txt --dst /tmp/local.txt
ssh-gateway download --profile delegated-target --src /tmp/local.txt --dst ./local-copy.txt
ssh-gateway tunnel open --profile direct-with-bastion --local 8080 --remote 127.0.0.1:11434
daemon stop returns {"status":"stopping"} when it successfully signals a running daemon and {"status":"not_running"} when nothing is listening.
Release assets are published automatically for every pushed v* tag.
- Windows x64:
ssh-gateway-<version>-x86_64-pc-windows-msvc.zip - Linux x64:
ssh-gateway-<version>-x86_64-unknown-linux-gnu.tar.gz - Checksums:
SHA256SUMS
Typical install flow:
- Download the archive for your platform from Releases.
- Extract
ssh-gatewayorssh-gateway.exe. - Put the binary on your
PATH. - Create a config file from examples/profiles.yaml.
For the bundled Windows skill installer, the default target path is %LOCALAPPDATA%\ssh-gateway\bin\ssh-gateway.exe. skills/ssh-gateway/scripts/install.ps1 also persists that directory into the user PATH by default, so new shells can resolve ssh-gateway without an absolute path.
The repository includes a portable SKILL.md-based skill at skills/ssh-gateway. The skill is meant for agents that support the open skills ecosystem and teaches them to prefer profile-driven ssh-gateway commands over raw ssh.
The skill can also bootstrap the ssh-gateway binary on first use by downloading the latest GitHub Release for the current platform. Agents should prefer ssh-gateway from PATH, then the installer's default target path, and only then reinstall.
If your agent supports npx skills add, prefer installing from the direct GitHub path to the skill directory. This avoids repository-root discovery ambiguity on agents or CLI versions that do not consistently resolve nested skills:
npx skills add https://github.com/TYzzt/ssh-gateway/tree/main/skills/ssh-gateway -gRepository shorthand also works when the CLI discovers nested skills correctly:
npx skills add TYzzt/ssh-gateway --skill ssh-gateway -gExamples for common agents:
npx skills add https://github.com/TYzzt/ssh-gateway/tree/main/skills/ssh-gateway -a codex -g
npx skills add https://github.com/TYzzt/ssh-gateway/tree/main/skills/ssh-gateway -a claude-code -g
npx skills add https://github.com/TYzzt/ssh-gateway/tree/main/skills/ssh-gateway -a cursor -gTo inspect what the CLI sees before installing:
npx skills add TYzzt/ssh-gateway --listTo update an existing install that originally came from npx skills add:
npx skills update ssh-gateway -gnpx skills update does not manage copies installed by the Codex-native install-skill-from-github.py script. If you previously installed the skill that way, remove the old copy and reinstall it through npx skills add if you want standard skills CLI updates later.
There is no mandatory central registry for installation. This repository is already a valid distribution source. skills.sh is useful for discovery and the public leaderboard, but it is not required to install or update this skill.
If you prefer the native Codex skill installer, the repository can also be installed directly from GitHub:
Windows PowerShell:
py -3 "$env:USERPROFILE\.codex\skills\.system\skill-installer\scripts\install-skill-from-github.py" `
--repo TYzzt/ssh-gateway `
--path skills/ssh-gatewayLinux or macOS shell:
python ~/.codex/skills/.system/skill-installer/scripts/install-skill-from-github.py \
--repo TYzzt/ssh-gateway \
--path skills/ssh-gatewayNotes:
- Restart your agent after installing the skill.
- If
ssh-gatewayis missing, the bundled skill scripts can download the latest release binary on first use. - The skill still expects a valid config file to already exist.
- The skill is intentionally thin: it does not replace the CLI, it standardizes how the agent should call it.
- Prefer
npx skills addwhen you want a standard install/update workflow across agents. - For passphrase-protected keys, keep the passphrase in the gateway config instead of pasting it into chat or shell flags.
The repository ships a tag-driven GitHub Actions workflow at .github/workflows/release.yml.
- Trigger: push a tag that matches
v* - Build matrix: Windows x64 and Linux x64
- Steps: checkout, install Rust stable,
cargo test --locked,cargo build --release --locked, package artifacts, create GitHub Release, upload binaries plusSHA256SUMS - Release notes: generated automatically by GitHub
Example:
git tag v0.1.2
git push origin v0.1.2Released under Apache License 2.0.