SSH

Polycrate's native SSH integration: connecting to inventory hosts, interactive host selection, ssh exec, tunnel and copy, shell autocompletion, passphrase support, SSH agent socket mount, key source (sshkeysource) and Ansible become password.

SSH

Polycrate offers native SSH integration for quick access to hosts from the Ansible inventory. This integration enables direct SSH access without manual configuration.

Prerequisites

The SSH integration only works if an inventory.yml exists in the workspace. Polycrate automatically determines:

  • SSH host address (ansible_host)
  • SSH port (ansible_ssh_port, default: 22)
  • SSH user (ansible_user)
  • SSH key from the workspace

Showing available hosts

To show all available hosts in the workspace:

polycrate ssh list

Output:

Available SSH hosts:
- web-1 (192.168.1.10)
- web-2 (192.168.1.11)
- db-1 (192.168.1.20)

Establishing an SSH connection

With hostname

To establish an SSH connection to a specific host:

polycrate ssh <hostname>

Example:

polycrate ssh web-1

Interactive host selection

If no hostname is given, Polycrate shows an interactive selection of all available hosts:

polycrate ssh

Output:

? Select a host to connect to:
  👉 web-1 (ubuntu@192.168.1.10:22)
     web-2 (ubuntu@192.168.1.11:22)
     db-1 (postgres@192.168.1.20:2222)

--------- Host Details ----------
Name:   web-1
Host:   192.168.1.10
User:   ubuntu

  • Navigate through the hosts with the arrow keys
  • Establish the connection with Enter
  • Cancel with Ctrl+C

Tab completion

Polycrate supports shell autocompletion for the SSH command. Tab shows all available hosts:

polycrate ssh <TAB>
# Shows: web-1  web-2  db-1

See Shell autocompletion for setup.

Polycrate: 1. Reads the Ansible inventory 2. Automatically determines the connection information 3. Uses the SSH key from the workspace (id_rsa) 4. Establishes the direct SSH connection

Example workflow

1. Create an inventory

Polycrate supports both common Ansible inventory patterns:

Pattern 1: variables per host

# inventory.yml in the workspace
all:
  hosts:
    web-1:
      ansible_host: 192.168.1.10
      ansible_user: ubuntu
      ansible_ssh_port: 22

    web-2:
      ansible_host: 192.168.1.11
      ansible_user: ubuntu

    db-1:
      ansible_host: 192.168.1.20
      ansible_user: postgres
      ansible_ssh_port: 2222

Pattern 2: variables in all.vars (since 0.30.4)

Shared variables can be defined centrally under all.vars and inherited by all hosts – Ansible-compliant. Host-level values override the group variables.

# inventory.yml – variables in all.vars
all:
  vars:
    ansible_user: sysops
    ansible_ssh_port: 4422
  hosts:
    cr1:
      ansible_host: 10.10.10.101
    cr2:
      ansible_host: 10.10.10.102
# → polycrate ssh cr1 uses sysops@10.10.10.101:4422

2. Show available hosts

polycrate ssh list

3. Connect

polycrate ssh web-1

Running ad-hoc commands

With polycrate ssh exec a single command is executed non-interactively on a remote host — without opening a full shell session. Stdout and stderr are streamed directly to the local terminal; the exit code of the remote command is propagated as the local exit code.

polycrate ssh exec <hostname> <command...>

Examples

# Check system status
polycrate ssh exec web-1 systemctl status nginx

# Query disk usage
polycrate ssh exec db-1 df -h

# Multi-part command (shell interpreter on the remote host)
polycrate ssh exec web-1 -- bash -c "df -h && free -m"

# Filter process list
polycrate ssh exec web-1 ps aux | grep nginx

Limitations

  • No interactive stdin (no pseudo-TTY) — for interactive sessions continue to use polycrate ssh <hostname>
  • Shell expansion (wildcards, pipes) is interpreted locally; for remote expansion use -- bash -c "..."

Port forwarding tunnel

With polycrate ssh tunnel an SSH local port forward is established. This makes services reachable that only listen on the remote host itself — without VPN and without manual ssh -L commands.

polycrate ssh tunnel <hostname> <local-port>:<remote-host>:<remote-port>

The tunnel runs in the foreground and blocks the terminal. CTRL+C terminates it cleanly — the local port is released immediately.

Examples

# Make a PostgreSQL database reachable locally
polycrate ssh tunnel db-1 5432:localhost:5432
# → psql -h localhost -p 5432 -U myuser mydb

# Make a web UI reachable via browser
polycrate ssh tunnel web-1 8080:localhost:80
# → http://localhost:8080

# Service in the internal network via a node as jump host
polycrate ssh tunnel web-1 6379:10.0.0.5:6379
# → redis-cli -p 6379

Typical workflow

# Terminal 1: keep the tunnel open
polycrate ssh tunnel db-1 5432:localhost:5432

# Terminal 2: connect a tool to localhost
psql -h localhost -p 5432 -U postgres

Limitations

  • Local forwarding only (-L) — no remote or dynamic forwarding
  • No background mode — the process deliberately blocks the terminal for full control over its lifetime

Copying files

With polycrate ssh copy files are transferred between the local machine and a host from the inventory. Polycrate resolves connection parameters, key and port automatically from the inventory — no manual scp with key path required.

polycrate ssh copy <source> <destination>

Exactly one of the two arguments must contain a hostname from the inventory, in the format hostname:/path.

# Download a file from the remote host
polycrate ssh copy web-1:/var/log/nginx/access.log ./

# Upload a file to the remote host
polycrate ssh copy ./deploy.sh web-1:/tmp/

# Copy a directory recursively (-r / --recursive)
polycrate ssh copy -r web-1:/etc/nginx/ ./nginx-backup/
polycrate ssh copy -r ./configs/ web-1:/etc/myapp/

Flags

Flag Short Description
--recursive -r Copy directories recursively
--block Block for inventory lookup (optional)
--workspace -w Workspace path

Limitations

  • Remote-to-remote copy (two hostnames) is not supported
  • Exactly one of the arguments must contain a hostname from the inventory; local paths with : in the name are not supported

Usage

Direct access for debugging

# Interactive shell session
polycrate ssh web-1

# Run a single command (non-interactive)
polycrate ssh exec web-1 systemctl status nginx
polycrate ssh exec web-1 tail -n 50 /var/log/application.log

After an Ansible deployment

# 1. Configure servers with Ansible
polycrate run webservers configure

# 2. Individual checks non-interactively
polycrate ssh exec web-1 systemctl is-active nginx

# 3. If needed, an interactive session for manual inspection
polycrate ssh web-1

Database access via tunnel

# Tunnel in a background terminal
polycrate ssh tunnel db-1 5432:localhost:5432

# In the other terminal: local tools against localhost
psql -h localhost -p 5432 -U postgres

Shell autocompletion

Polycrate supports tab completion for all commands, including dynamic host completion for polycrate ssh.

Zsh

# Enable once
source <(polycrate completion zsh)

# Permanently in ~/.zshrc
echo 'source <(polycrate completion zsh)' >> ~/.zshrc

Bash

# Enable once
source <(polycrate completion bash)

# Permanently in ~/.bashrc
echo 'source <(polycrate completion bash)' >> ~/.bashrc

Fish

polycrate completion fish > ~/.config/fish/completions/polycrate.fish

After activation:

polycrate ssh <TAB>
# Shows all available hosts from the inventory

polycrate ssh web<TAB>
# Completes to matching hosts (e.g. web-1, web-2)

SSH passphrase support

Polycrate supports SSH keys with a passphrase via an integrated ssh-agent.

Activation

# On workspace init
polycrate --ssh-use-passphrase workspace init

# On action execution
polycrate --ssh-use-passphrase run my-block deploy

# For all commands
polycrate --ssh-use-passphrase ssh web-1

Passphrase sources (priority)

Priority Source Description
1 ssh-passphrase.poly File in the workspace root
2 Interactive prompt Manual input

ssh-passphrase.poly:

# Store the passphrase in a file (optional)
echo "my-passphrase" > ssh-passphrase.poly

# Set permissions
chmod 600 ssh-passphrase.poly

How it works

Polycrate automatically starts an ssh-agent, loads the workspace key with the passphrase and passes the agent socket to the respective SSH process. What this looks like in practice depends on whether it is a direct SSH operation or a container-based action:

Direct SSH operations (polycrate ssh, ssh exec, ssh tunnel, ssh copy):

  1. Polycrate starts an ssh-agent
  2. The SSH key is loaded with the passphrase
  3. The agent socket is passed as the SSH_AUTH_SOCK environment variable to the local ssh/scp process — no Docker container, no mount required
graph LR
    Polycrate[Polycrate CLI] -->|1. Start agent| Agent[ssh-agent]
    Polycrate -->|2. Load key| Agent
    Agent -->|3. SSH_AUTH_SOCK| SSH[ssh / scp]
    SSH -->|4. Connect| Host[Remote Host]

Container-based actions (polycrate run):

  1. Polycrate starts an ssh-agent
  2. The SSH key is loaded with the passphrase
  3. The agent socket is mounted into the Docker container (requires --ssh-agent-mount or config.sshagentmount: true)
  4. Ansible in the container uses the mounted agent
graph LR
    Polycrate[Polycrate CLI] -->|1. Start agent| Agent[ssh-agent]
    Polycrate -->|2. Load key| Agent
    Agent -->|3. Socket mount| Container[Docker Container]
    Container -->|4. SSH auth| Ansible[Ansible]
    Ansible -->|5. Connect| Host[Remote Host]

Troubleshooting

"Permission denied (publickey)":

# Check whether the agent is running
ssh-add -l

# Add the key to the agent manually
ssh-add artifacts/secrets/id_rsa

# Then try again
polycrate --ssh-use-passphrase run my-block deploy

"Agent refused connection":

# Check the agent socket
echo $SSH_AUTH_SOCK

# Start Polycrate with a new agent
polycrate --ssh-use-passphrase run my-block deploy

SSH agent socket mount

Since Polycrate CLI 0.37.0 the mount of the host SSH agent socket (SSH_AUTH_SOCK) into Polycrate containers is disabled by default. If you want to use keys from a running ssh-agent – e.g. for passphrase-protected keys or FIDO/U2F hardware keys – you must enable the mount explicitly.

Why the change?

  • Security: the agent socket gives containers full access to all keys loaded in the agent. For most Polycrate runs (workspace key without passphrase) this is not necessary.
  • Portability: on systems without a running agent (CI, fresh checkouts) the implicit mount failed with socket errors. Opt-in prevents that.
  • macOS/OrbStack: non-root containers on macOS could not reliably use the native agent socket. Opt-in makes problems visible instead of failing silently.

Enabling the mount

CLI flag (ad hoc):

polycrate --ssh-agent-mount run my-block install
polycrate --ssh-use-passphrase --ssh-agent-mount run my-block deploy

Workspace-permanent (workspace.poly):

name: my-workspace
config:
  sshagentmount: true

User-global (~/.polycrate/polycrate.yml):

config:
  sshagentmount: true

Then applies to all workspaces unless overridden in the workspace.

When is the agent mounted automatically?

Even without --ssh-agent-mount, Polycrate mounts the agent automatically in two scenarios:

Scenario Behavior
--ssh-use-passphrase with workspace key Internal agent (started by Polycrate) is mounted
config.sshkeysource: user + running host agent Host agent is mounted

In both cases the mount is purpose-bound and safe: without these flags the socket is not exposed.

Migration from ≤0.36.x

If you relied on the implicit mount under 0.36.x (typically: mixed host/workspace keys, FIDO sticks), add once:

# workspace.poly
config:
  sshagentmount: true

Alternatively --ssh-agent-mount per run. No downgrade notice appears – the old behavior is simply no longer the default.


SSH key source (sshkeysource)

By default Polycrate uses the workspace key (artifacts/secrets/id_rsa). With config.sshkeysource: user you can use your personal SSH key instead – ideal for uniform user authentication and use of the ssh-agent.

Activation

# workspace.poly
config:
  sshkeysource: user   # workspace (default) | user

Behavior with user

Condition Behavior
Host ssh-agent running (SSH_AUTH_SOCK) Agent is mounted into the container (automatically with sshkeysource: user, regardless of config.sshagentmount) – Ansible uses the keys in the agent
No agent ~/.ssh is mounted, the key file is used

Configuring the user key path

The path to the personal key is set in ~/.polycrate/polycrate.yml (not in the workspace, since it is user-specific):

# ~/.polycrate/polycrate.yml
ssh:
  user_key_path: "~/.ssh/work_ed25519"   # optional; default: ~/.ssh/id_rsa
  user_key_path_by_workspace:            # optional; workspace.name → key path
    laser24: "~/.ssh/laser_ed25519"
    archenemy: "~/.ssh/ayedo_work"

→ CLI configuration


Ansible become/sudo password

For Ansible tasks that use become: yes (sudo), you can have the password prompted interactively before the action is executed:

polycrate run my-block install --ask-become-pass
# or short:
polycrate run my-block install -K

The prompt appears before the container starts. The password is passed to Ansible as ANSIBLE_BECOME_PASSWORD and is not stored in the snapshot or in logs.


Notes

  • SSH key: by default Polycrate uses id_rsa from the workspace root or artifacts/secrets/id_rsa; with sshkeysource: user the personal key
  • Port: default is port 22, but it can be overridden via ansible_ssh_port in the inventory
  • User: read from ansible_user in the inventory (default: root)
  • Tunnel: no ControlMaster/ControlPersist — the local port is released immediately on exit
  • exec and copy: same key and agent logic as the interactive session; no additional setup required

Overview of all SSH commands

Command Description
polycrate ssh [hostname] Interactive shell session (TUI selection if no hostname)
polycrate ssh list List all hosts from the inventory
polycrate ssh exec <host> <cmd...> Run an ad-hoc command non-interactively
polycrate ssh tunnel <host> <lp>:<rh>:<rp> Establish an SSH local port forward
polycrate ssh copy <source> <destination> Copy files to/from a host

See also