11 KiB
11 KiB
Agentless Deployment (SSH/WinRM)
Agentless deployment using SSH and WinRM for remote execution without installing agents.
Status: Planned (not yet implemented) Source: Architecture Advisory Section 10.4 Related Modules: Agents Module, Deploy Orchestrator Sprints: 108_004 SSH Agent, 108_005 WinRM Agent
Overview
Agentless deployment enables deployment to targets without requiring a pre-installed agent. The orchestrator connects directly to targets using SSH (Linux/Unix) or WinRM (Windows) to execute deployment commands.
SSH Remote Executor
Capabilities
- SSH key-based authentication
- File transfer via SFTP
- Remote command execution
- Docker operations over SSH
- Script execution
- Backup and rollback
Connection Management
class SSHRemoteExecutor implements TargetExecutor {
private ssh: SSHClient;
async connect(config: SSHConnectionConfig): Promise<void> {
const privateKey = await this.secrets.getSecret(config.privateKeyRef);
this.ssh = new SSHClient();
await this.ssh.connect({
host: config.host,
port: config.port || 22,
username: config.username,
privateKey: privateKey.value,
readyTimeout: config.connectionTimeout || 30000,
keepaliveInterval: 10000,
});
}
}
Deploy Task Flow
async deploy(task: DeployTaskPayload): Promise<DeployResult> {
const { artifacts, config } = task;
const deployDir = config.deploymentDirectory;
try {
// 1. Ensure deployment directory exists
await this.exec(`mkdir -p ${deployDir}`);
await this.exec(`mkdir -p ${deployDir}/.stella-backup`);
// 2. Backup current deployment
await this.exec(`cp -r ${deployDir}/* ${deployDir}/.stella-backup/ 2>/dev/null || true`);
// 3. Upload artifacts
for (const artifact of artifacts) {
const content = await this.fetchArtifact(artifact);
const remotePath = path.join(deployDir, artifact.name);
await this.uploadFile(content, remotePath);
}
// 4. Run pre-deploy hook
if (task.hooks?.preDeploy) {
await this.runRemoteHook(task.hooks.preDeploy, deployDir);
}
// 5. Execute deployment script
const deployScript = artifacts.find(a => a.type === "deploy_script");
if (deployScript) {
const scriptPath = path.join(deployDir, deployScript.name);
await this.exec(`chmod +x ${scriptPath}`);
const result = await this.exec(scriptPath, {
cwd: deployDir,
timeout: config.deploymentTimeout,
env: config.environment,
});
if (result.exitCode !== 0) {
throw new DeploymentError(`Deploy script failed: ${result.stderr}`);
}
}
// 6. Run post-deploy hook
if (task.hooks?.postDeploy) {
await this.runRemoteHook(task.hooks.postDeploy, deployDir);
}
// 7. Health check
if (config.healthCheck) {
const healthy = await this.runHealthCheck(config.healthCheck);
if (!healthy) {
await this.rollback(task);
throw new HealthCheckFailedError("Health check failed");
}
}
// 8. Write version sticker
await this.writeSticker(config.sticker, deployDir);
// 9. Cleanup backup
await this.exec(`rm -rf ${deployDir}/.stella-backup`);
return {
success: true,
logs: this.getLogs(),
durationMs: this.getDuration(),
};
} finally {
this.ssh.end();
}
}
Command Execution
private async exec(
command: string,
options?: ExecOptions
): Promise<CommandResult> {
return new Promise((resolve, reject) => {
const timeout = options?.timeout || 60000;
let stdout = "";
let stderr = "";
this.ssh.exec(command, { cwd: options?.cwd }, (err, stream) => {
if (err) {
reject(err);
return;
}
const timer = setTimeout(() => {
stream.close();
reject(new TimeoutError(`Command timed out after ${timeout}ms`));
}, timeout);
stream.on("data", (data: Buffer) => {
stdout += data.toString();
this.log(data.toString());
});
stream.stderr.on("data", (data: Buffer) => {
stderr += data.toString();
this.log(`[stderr] ${data.toString()}`);
});
stream.on("close", (code: number) => {
clearTimeout(timer);
resolve({ exitCode: code, stdout, stderr });
});
});
});
}
File Upload via SFTP
private async uploadFile(content: Buffer | string, remotePath: string): Promise<void> {
return new Promise((resolve, reject) => {
this.ssh.sftp((err, sftp) => {
if (err) {
reject(err);
return;
}
const writeStream = sftp.createWriteStream(remotePath);
writeStream.on("close", () => resolve());
writeStream.on("error", reject);
writeStream.end(content);
});
});
}
Rollback
async rollback(task: RollbackTaskPayload): Promise<DeployResult> {
const deployDir = task.config.deploymentDirectory;
// Restore from backup
await this.exec(`rm -rf ${deployDir}/*`);
await this.exec(`cp -r ${deployDir}/.stella-backup/* ${deployDir}/`);
// Re-run deployment from backup
const deployScript = path.join(deployDir, "deploy.sh");
await this.exec(deployScript, { cwd: deployDir });
return {
success: true,
logs: this.getLogs(),
durationMs: this.getDuration(),
};
}
WinRM Remote Executor
Capabilities
- NTLM/Kerberos authentication
- PowerShell script execution
- File transfer via base64 encoding
- Windows container operations
- Windows service management
Connection Management
class WinRMRemoteExecutor implements TargetExecutor {
private winrm: WinRMClient;
async connect(config: WinRMConnectionConfig): Promise<void> {
const credential = await this.secrets.getSecret(config.credentialRef);
this.winrm = new WinRMClient({
host: config.host,
port: config.port || 5986,
username: credential.username,
password: credential.password,
protocol: config.useHttps ? "https" : "http",
authentication: config.authType || "ntlm", // ntlm, kerberos, basic
});
await this.winrm.openShell();
}
}
Deploy Task Flow
async deploy(task: DeployTaskPayload): Promise<DeployResult> {
const { artifacts, config } = task;
const deployDir = config.deploymentDirectory;
try {
// 1. Ensure deployment directory exists
await this.execPowerShell(`
if (-not (Test-Path "${deployDir}")) {
New-Item -ItemType Directory -Path "${deployDir}" -Force
}
if (-not (Test-Path "${deployDir}\\.stella-backup")) {
New-Item -ItemType Directory -Path "${deployDir}\\.stella-backup" -Force
}
`);
// 2. Backup current deployment
await this.execPowerShell(`
Get-ChildItem "${deployDir}" -Exclude ".stella-backup" |
Copy-Item -Destination "${deployDir}\\.stella-backup" -Recurse -Force
`);
// 3. Upload artifacts
for (const artifact of artifacts) {
const content = await this.fetchArtifact(artifact);
const remotePath = `${deployDir}\\${artifact.name}`;
await this.uploadFile(content, remotePath);
}
// 4. Run pre-deploy hook
if (task.hooks?.preDeploy) {
await this.runRemoteHook(task.hooks.preDeploy, deployDir);
}
// 5. Execute deployment script
const deployScript = artifacts.find(a => a.type === "deploy_script");
if (deployScript) {
const scriptPath = `${deployDir}\\${deployScript.name}`;
const result = await this.execPowerShell(`
Set-Location "${deployDir}"
& "${scriptPath}"
exit $LASTEXITCODE
`, { timeout: config.deploymentTimeout });
if (result.exitCode !== 0) {
throw new DeploymentError(`Deploy script failed: ${result.stderr}`);
}
}
// 6. Run post-deploy hook
if (task.hooks?.postDeploy) {
await this.runRemoteHook(task.hooks.postDeploy, deployDir);
}
// 7. Health check
if (config.healthCheck) {
const healthy = await this.runHealthCheck(config.healthCheck);
if (!healthy) {
await this.rollback(task);
throw new HealthCheckFailedError("Health check failed");
}
}
// 8. Write version sticker
await this.writeSticker(config.sticker, deployDir);
// 9. Cleanup backup
await this.execPowerShell(`
Remove-Item -Path "${deployDir}\\.stella-backup" -Recurse -Force
`);
return {
success: true,
logs: this.getLogs(),
durationMs: this.getDuration(),
};
} finally {
this.winrm.closeShell();
}
}
PowerShell Execution
private async execPowerShell(
script: string,
options?: ExecOptions
): Promise<CommandResult> {
const encoded = Buffer.from(script, "utf16le").toString("base64");
return this.winrm.runCommand(
`powershell -EncodedCommand ${encoded}`,
{ timeout: options?.timeout || 60000 }
);
}
File Upload
private async uploadFile(content: Buffer | string, remotePath: string): Promise<void> {
// Use PowerShell to write file content
const base64Content = Buffer.from(content).toString("base64");
await this.execPowerShell(`
$bytes = [Convert]::FromBase64String("${base64Content}")
[IO.File]::WriteAllBytes("${remotePath}", $bytes)
`);
}
Security Considerations
SSH Security
- Key-Based Authentication: Always use SSH keys, never passwords
- Key Rotation: Regularly rotate SSH keys
- Bastion Hosts: Use jump hosts for network isolation
- Connection Timeouts: Enforce strict connection timeouts
- Known Hosts: Verify host fingerprints
WinRM Security
- HTTPS Required: Always use WinRM over HTTPS in production
- Certificate Validation: Validate server certificates
- Kerberos Preferred: Use Kerberos when available, NTLM as fallback
- Credential Protection: Store credentials in vault
- Session Cleanup: Always close sessions after use
Configuration Examples
SSH Target Configuration
target:
name: web-server-01
type: ssh
connection:
host: 192.168.1.100
port: 22
username: deploy
privateKeyRef: vault://ssh-keys/deploy-key
deployment:
directory: /opt/myapp
healthCheck:
command: curl -f http://localhost:8080/health
timeout: 30
WinRM Target Configuration
target:
name: windows-server-01
type: winrm
connection:
host: 192.168.1.200
port: 5986
useHttps: true
authType: kerberos
credentialRef: vault://windows-creds/deploy-user
deployment:
directory: C:\Apps\MyApp
healthCheck:
command: Invoke-WebRequest -Uri http://localhost:8080/health -UseBasicParsing
timeout: 30