Remote Execution over SSH
March 17, 2026 · View on GitHub
TTPForge supports executing steps on remote hosts over SSH. You first establish
a named connection with a connect step, then reference that connection by name
with remote: on subsequent steps. Steps without remote: continue to run on
the local machine, allowing you to freely mix local and remote actions within a
single TTP.
Basic Usage
Use a connect step to establish a named SSH connection, then reference it with
remote: <connection_name> on any step:
steps:
- name: setup-connection
connect:
host: my-server.example.com
user: root
auth: agent
connection_name: my-server
- name: enumerate_remote
remote: my-server
inline: whoami && hostname
# This step runs locally (no remote: field)
- name: local_step
inline: echo "running on local machine"
The connect Step
The connect step establishes a named SSH connection that can be reused across
multiple steps. It eagerly connects at execution time so that authentication
failures surface immediately rather than on the first step that uses the
connection.
Fields
The connect block supports the following fields:
host:(type:string, required) the hostname or IP address of the remote target.connection_name:(type:string, required) the name used to reference this connection fromremote:fields on subsequent steps.protocol:(type:string, default:ssh) the protocol to use. Currently onlysshis supported.port:(type:int, default:22) the SSH port on the remote host.user:(type:string, default: current user) the SSH username.auth:(type:string, default:agent) the authentication method. See Authentication Methods below.key_file:(type:string) path to the SSH private key file. Required whenauth: key.password:(type:string) the SSH password. Required whenauth: password. Use with{{ .Args.x }}to avoid hardcoding passwords in YAML files.password_env:(type:string) name of the environment variable containing the SSH password. Required whenauth: password_env.known_hosts:(type:string) path to a known_hosts file for host key verification. If omitted, host key checking is disabled.jump_host:(type:string) a bastion/jump host to connect through, inhost:portformat.shell:(type:string, default:posix) the shell type on the remote host. Controls how environment variables, directory changes, and argument quoting are constructed. Supported values:posix— POSIX-compatible shells (bash, sh, zsh). Usesexport, single-quote escaping, and&&chaining.powershell— Windows PowerShell. Uses$env:, backtick escaping, and;chaining. Process management usesStop-Process/Get-Process.cmd— Windows cmd.exe. Usesset, double-quote escaping, and&chaining. Process management usestaskkill/tasklist.
All string fields support {{ .Args.x }} template variables, so connection
parameters can be passed as TTP arguments.
Referencing Connections with remote:
Once a connection is established with a connect step, reference it by name
on any subsequent step:
steps:
- name: setup-connection
connect:
host: "{{ .Args.target_host }}"
user: "{{ .Args.target_user }}"
auth: key
key_file: "{{ .Args.key_file }}"
connection_name: target
- name: step_one
remote: target
inline: whoami
- name: step_two
remote: target
inline: hostname
- name: drop_file
remote: target
create_file: /tmp/payload.sh
contents: |
#!/bin/bash
echo "hello from $(hostname)"
mode: 0755
cleanup: default
Multiple Connections
You can establish multiple named connections to different hosts:
steps:
- name: connect-web
connect:
host: web-server.example.com
user: deploy
auth: key
key_file: ~/.ssh/deploy_key
connection_name: web
- name: connect-db
connect:
host: db-server.example.com
user: admin
auth: agent
connection_name: db
- name: check-web
remote: web
inline: curl -s localhost:8080/health
- name: check-db
remote: db
inline: psql -c "SELECT 1"
Authentication Methods
agent (default)
Uses the SSH agent (via SSH_AUTH_SOCK) for authentication. The agent must be
running and have the appropriate key loaded:
connect:
host: target.example.com
auth: agent
connection_name: target
# Ensure your key is in the agent
ssh-add /path/to/your/key
key
Reads a private key directly from disk. If a corresponding certificate file
exists (following the OpenSSH convention of <key_file>-cert.pub), it is
automatically loaded for certificate-based authentication:
connect:
host: target.example.com
auth: key
key_file: /path/to/id_rsa
connection_name: target
password
Uses a password provided directly in the YAML. Combine with {{ .Args.x }}
to accept the password as a TTP argument:
args:
- name: ssh_password
type: string
steps:
- name: setup
connect:
host: target.example.com
auth: password
password: "{{ .Args.ssh_password }}"
connection_name: target
- name: run
remote: target
inline: whoami
ttpforge run my-ttp.yaml --arg ssh_password=secret
password_env
Reads the password from an environment variable. This avoids hardcoding passwords in YAML files:
connect:
host: target.example.com
auth: password_env
password_env: SSH_PASSWORD
connection_name: target
export SSH_PASSWORD="my-secret-password"
ttpforge run my-ttp.yaml
Connection Pooling
TTPForge automatically reuses SSH connections across steps that reference the
same named connection. Connections are created when the connect step executes
and closed when the TTP finishes. If two connect steps point to the same
host/port/user combination, they share the underlying SSH connection.
Supported Actions
The following action types support remote execution:
inline:— shell commands run on the remote hostfile:— scripts/binaries execute on the remote hostcreate_file:— files are created on the remote filesystemremove_path:— files/directories are removed from the remote filesystemcopy_path:— files are copied on the remote filesystem (or transferred between local and remote withdirection:)edit_file:— files are edited on the remote filesystemfetch_uri:— fetched content is written to the remote filesystemchange_directory:— working directory is changed on the remote filesystemkill_process:— processes are killed on the remote host
Output from remote inline: and file: steps is streamed line-by-line in
real time, matching the behavior of local execution.
How Remote Actions Work
Remote actions use one of two mechanisms:
inline:,file:,kill_process:execute shell commands via SSH sessions, producing the same process telemetry as running the command directly on the host.- All other actions (
create_file,remove_path,copy_path,edit_file,fetch_uri,change_directory) operate through SFTP — no shell process is spawned and no shell history is written on the target. This simulates C2-style file operations.
Unsupported Actions
The following action types do not support remote execution and will return
an error if used with a remote: block:
expect:— requires a local PTY for interactive console automation, which cannot be forwarded over SSH.http_request:— makes HTTP calls from the local machine. To make HTTP requests from the remote host, useinline:withcurlor similar.
Checks
By default, success checks defined on a remote step inherit the step's
remote: field — file existence checks use the remote filesystem, and command
checks execute on the remote host.
Each check can optionally specify its own remote: field to override this
behavior:
| Check YAML | Behavior |
|---|---|
No remote: field | Inherits the step's remote: (backwards compatible) |
remote: <connection_name> | Runs against that named connection |
remote: local | Forces local execution even if the step is remote |
steps:
- name: deploy_payload
remote: target
inline: cp payload.bin /opt/payload.bin
checks:
- msg: Payload exists on remote
path_exists: /opt/payload.bin
# no remote: → inherits step's "target"
- msg: Local log updated
remote: local
path_exists: /var/log/deploy.log
- msg: C2 callback received
remote: c2_server
command: grep -q "callback" /var/log/c2.log
Cleanup
Cleanup actions support independent remote targeting:
cleanup: defaultinherits the step'sremote:field, so cleanup runs on the same host as the step. For example,cleanup: defaulton a remotecreate_filestep removes the file from the remote filesystem.- Custom cleanup (a YAML mapping) runs locally by default. Add
remote: <connection_name>inside the cleanup block to run it on a specific remote host.
steps:
- name: drop-payload
remote: target
create_file: /tmp/payload.sh
contents: echo "hello"
cleanup: default # inherits remote: target
- name: run-payload
remote: target
inline: /tmp/payload.sh
cleanup:
remote: target # explicit remote for cleanup
inline: rm -f /tmp/payload.sh
- name: exfil
remote: target
inline: cat /etc/passwd
cleanup:
inline: rm -f /tmp/local-evidence.log # no remote: → runs locally
Cross-Platform Support
By default, TTPForge assumes the remote host uses a POSIX-compatible shell. To
target Windows hosts, set the shell: field in the connect step:
steps:
- name: connect-windows
connect:
host: windows-server.example.com
user: Administrator
auth: key
key_file: ~/.ssh/windows_key
shell: powershell
connection_name: winbox
- name: enumerate_windows
remote: winbox
inline: Get-ComputerInfo | Select-Object CsName, OsName
The shell setting affects:
- Environment variables:
export K='V'vs$env:K = "V"vsset K=V - Directory changes:
cd 'path'vsSet-Location "path"vscd /d "path" - Argument quoting: single-quote vs backtick-escaped double-quote vs double-quote
- Command chaining:
&&vs;vs& - Process management:
kill/pgrepvsStop-Process/Get-Processvstaskkill/tasklist - Verification checks:
sh -cvspowershell -Commandvscmd /c
Example
See the full example TTP at
example-ttps/actions/inline/remote-ssh.yaml:
ttpforge run examples//actions/inline/remote-ssh.yaml \
--arg target_host=my-server.example.com \
--arg target_user=root \
--arg target_key_file=/path/to/id_rsa