Openship SSH Executor Key-Based Authentication Remote Deployment: Implementation Guide
Openship enforces key-based SSH authentication exclusively, rejecting password-based login to ensure secure remote deployments through its SshExecutor class.
The oblien/openship repository abstracts all command execution behind a unified CommandExecutor interface, enabling seamless deployment to remote servers using cryptographic keys while maintaining identical APIs for local and remote operations.
CommandExecutor Interface Design
Openship defines all system interactions through a strict TypeScript interface located in packages/adapters/docs/EXECUTOR.md. This contract ensures that higher-level services remain transport-agnostic, whether executing commands on the local host or a remote SSH target.
interface CommandExecutor {
exec(command: string, opts?: { timeout?: number }): Promise<string>;
streamExec(command: string, onLog: (log: LogEntry) => void): Promise<{ code: number; output: string }>;
writeFile(path: string, content: string): Promise<void>;
readFile(path: string): Promise<string>;
exists(path: string): Promise<boolean>;
mkdir(path: string): Promise<void>;
rm(path: string): Promise<void>;
dispose(): Promise<void>;
}
Two concrete classes implement this contract: LocalExecutor for host-native execution and SshExecutor for remote deployment.
Key-Based Authentication Security Model
The SshExecutor implementation in packages/adapters/src/system/ssh-executor.ts mandates cryptographic authentication by design. Password authentication is explicitly disabled to prevent credential exposure.
When constructing an SshExecutor, the configuration must provide either a PEM-encoded privateKey string or an sshAgent socket path. If neither is supplied, the constructor throws the error: SSH requires either privateKey or sshAgent.
This security-first approach ensures that:
- No plaintext passwords traverse the network or persist in memory
- Private keys remain encrypted at rest in the database (schema defined in
packages/db/src/schema/servers.ts) - SSH agent forwarding is supported for environments using hardware security modules or agent-based workflows
SshExecutor Implementation Details
Connection Lifecycle and Reuse
The SSH implementation utilizes lazy connection establishment to minimize authentication overhead. The ssh2.Client connection initializes upon the first command execution and persists for subsequent operations within the same session.
// Connection established here on first use
const output = await executor.exec("uptime");
// Reuses existing connection
await executor.exec("docker ps");
The dispose() method explicitly terminates the connection by invoking client.end(), preventing resource leaks during long-running deployment processes.
File Operations via SFTP
File system abstraction maps directly to SFTP methods in the ssh2 library:
writeFile()uses SFTPwriteFile()for atomic config deploymentreadFile()retrieves remote files via SFTPreadFile()exists()checks file presence through SFTPstat()rm()removes files via SFTPunlink()
Directory creation (mkdir) executes through SSH exec("mkdir -p ...") rather than SFTP to ensure parent directory creation semantics match local behavior.
Process Management and Cleanup
Remote process termination implements process-group semantics using kill -- -PID via the underlying shell. This ensures entire process trees terminate cleanly, mirroring the local implementation that spawns processes with detached: true. This prevents orphaned processes when cancelling long-running deployments or build operations.
Local vs Remote Execution
The createExecutor factory in packages/adapters/src/system/factory.ts instantiates the appropriate implementation based on configuration presence:
import { createExecutor } from "@repo/adapters";
// Local execution using child_process
const local = createExecutor();
// Remote deployment via SSH key authentication
const remote = createExecutor({
host: "203.0.113.45",
username: "deploy",
privateKey: decryptedPemString, // Never hardcode in production
// sshAgent: process.env.SSH_AUTH_SOCK // Alternative authentication
});
Both implementations expose identical methods, allowing services like BareRuntime, TraefikProvider, and SystemManager to operate without transport-specific logic.
Practical Usage Examples
Creating a Remote Executor with Key Authentication
import { createExecutor } from "@repo/adapters";
async function initializeRemoteExecutor() {
// Retrieve key from secure vault per schema in packages/db/src/schema/servers.ts
const privateKey = await vault.getKey("production-deploy-key");
const executor = createExecutor({
host: "server.openship.example",
username: "openship",
privateKey, // PEM-encoded RSA or Ed25519 key
timeout: 30000 // Optional command timeout in milliseconds
});
return executor;
}
Executing Commands and Streaming Logs
const exec = await initializeRemoteExecutor();
// One-off command execution
const uname = await exec.exec("uname -a");
console.log("Remote kernel:", uname);
// Real-time build log streaming
await exec.streamExec(
"npm ci && npm run build",
(log) => {
// LogEntry interface: { level: "info" | "warn", message: string }
console[log.level === "warn" ? "error" : "log"](`[Remote] ${log.message}`);
}
);
Managing Configuration Files
// Deploy Traefik configuration
await exec.writeFile(
"/etc/openship/traefik/dynamic.yaml",
traefikConfigYaml
);
// Verify deployment
const configExists = await exec.exists("/etc/openship/traefik/dynamic.yaml");
if (configExists) {
const content = await exec.readFile("/etc/openship/traefik/dynamic.yaml");
console.log("Deployed config checksum:", content.length);
}
// Cleanup
await exec.dispose();
Integration with Openship Architecture
While the CommandExecutor handles system-level operations, Docker container management bypasses this abstraction. The DockerRuntime class in packages/adapters/src/runtime/docker.ts uses dockerode directly to interact with the Docker socket, preserving native streaming, multiplexing, and image-pull capabilities. The executor remains responsible for ancillary tasks such as writing Traefik YAML configurations and installing system packages.
Host validation logic in packages/core/src/host-profile.ts enforces SSH security policies, checking for forced commands and restricted login shells before establishing connections.
Summary
- Key-based only: The
SshExecutorinpackages/adapters/src/system/ssh-executor.tsrejects password authentication, requiring eitherprivateKeyorsshAgentconfiguration. - Unified interface:
CommandExecutorabstracts local and remote execution, enabling the same deployment code to run on either target. - Connection reuse: SSH connections initialize lazily and persist across multiple operations, closing only upon explicit
dispose()calls. - SFTP operations: File methods map directly to SFTP primitives for atomic remote file management.
- Process cleanup: Remote executions use process-group termination to prevent orphaned processes during deployment cancellations.
- Docker separation: Container operations use
dockerodedirectly rather than the SSH executor to preserve Docker API features.
Frequently Asked Questions
How does Openship handle SSH private key storage?
Openship stores encrypted private keys in the database according to the schema defined in packages/db/src/schema/servers.ts. Keys decrypt only at runtime when constructing the SshExecutor, ensuring credentials never exist as plaintext in version control or configuration files. The system supports both direct PEM string injection and SSH agent socket forwarding.
Can I use password authentication with the Openship SSH executor?
No. The SshExecutor constructor explicitly prohibits password authentication. If you attempt to create an executor without providing either privateKey or sshAgent, the code throws the error SSH requires either privateKey or sshAgent. This design decision eliminates password-based attack vectors from remote deployments.
What happens if the SSH connection drops during a long deployment?
The SshExecutor maintains persistent connections that reuse the authenticated ssh2.Client session across multiple commands. If the network connection fails, subsequent operations will fail with connection errors. For resilience, wrap critical deployments in retry logic at the application level, or ensure stable network connectivity to the target host. The dispose() method should always be called in finally blocks to clean up resources.
Does the SSH executor support Docker operations on remote hosts?
No. Docker container management bypasses the CommandExecutor abstraction entirely. According to the implementation in packages/adapters/src/runtime/docker.ts, Openship uses dockerode to communicate directly with the Docker daemon socket. The SSH executor handles only system-level tasks like file writes, package installation, and shell command execution on the remote host.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →