# Openship SSH Executor Key-Based Authentication Remote Deployment: Implementation Guide

> Implement secure Openship SSH executor key-based authentication for remote deployment. This guide details how to set up and use this exclusive, passwordless method for robust security.

- Repository: [oblien/openship](https://github.com/oblien/openship)
- Tags: how-to-guide
- Published: 2026-08-19

---

**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`](https://github.com/oblien/openship/blob/main/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.

```typescript
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`](https://github.com/oblien/openship/blob/main/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`](https://github.com/oblien/openship/blob/main/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.

```typescript
// 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 SFTP `writeFile()` for atomic config deployment
- `readFile()` retrieves remote files via SFTP `readFile()`
- `exists()` checks file presence through SFTP `stat()`
- `rm()` removes files via SFTP `unlink()`

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`](https://github.com/oblien/openship/blob/main/packages/adapters/src/system/factory.ts) instantiates the appropriate implementation based on configuration presence:

```typescript
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

```typescript
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

```typescript
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

```typescript
// 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`](https://github.com/oblien/openship/blob/main/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`](https://github.com/oblien/openship/blob/main/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 `SshExecutor` in [`packages/adapters/src/system/ssh-executor.ts`](https://github.com/oblien/openship/blob/main/packages/adapters/src/system/ssh-executor.ts) rejects password authentication, requiring either `privateKey` or `sshAgent` configuration.
- **Unified interface**: `CommandExecutor` abstracts 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 `dockerode` directly 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`](https://github.com/oblien/openship/blob/main/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`](https://github.com/oblien/openship/blob/main/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.