# 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](../../../product/advisories/09-Jan-2026%20-%20Stella%20Ops%20Orchestrator%20Architecture.md) **Related Modules:** [Agents Module](../modules/agents.md), [Deploy Orchestrator](../modules/deploy-orchestrator.md) **Sprints:** [108_004 SSH Agent](../../../../implplan/SPRINT_20260110_108_004_AGENTS_ssh.md), [108_005 WinRM Agent](../../../../implplan/SPRINT_20260110_108_005_AGENTS_winrm.md) ## 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 ```typescript class SSHRemoteExecutor implements TargetExecutor { private ssh: SSHClient; async connect(config: SSHConnectionConfig): Promise { 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 ```typescript async deploy(task: DeployTaskPayload): Promise { 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 ```typescript private async exec( command: string, options?: ExecOptions ): Promise { 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 ```typescript private async uploadFile(content: Buffer | string, remotePath: string): Promise { 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 ```typescript async rollback(task: RollbackTaskPayload): Promise { 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 ```typescript class WinRMRemoteExecutor implements TargetExecutor { private winrm: WinRMClient; async connect(config: WinRMConnectionConfig): Promise { 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 ```typescript async deploy(task: DeployTaskPayload): Promise { 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 ```typescript private async execPowerShell( script: string, options?: ExecOptions ): Promise { const encoded = Buffer.from(script, "utf16le").toString("base64"); return this.winrm.runCommand( `powershell -EncodedCommand ${encoded}`, { timeout: options?.timeout || 60000 } ); } ``` ### File Upload ```typescript private async uploadFile(content: Buffer | string, remotePath: string): Promise { // 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 1. **Key-Based Authentication:** Always use SSH keys, never passwords 2. **Key Rotation:** Regularly rotate SSH keys 3. **Bastion Hosts:** Use jump hosts for network isolation 4. **Connection Timeouts:** Enforce strict connection timeouts 5. **Known Hosts:** Verify host fingerprints ### WinRM Security 1. **HTTPS Required:** Always use WinRM over HTTPS in production 2. **Certificate Validation:** Validate server certificates 3. **Kerberos Preferred:** Use Kerberos when available, NTLM as fallback 4. **Credential Protection:** Store credentials in vault 5. **Session Cleanup:** Always close sessions after use --- ## Configuration Examples ### SSH Target Configuration ```yaml 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 ```yaml 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 ``` --- ## See Also - [Agent-Based Deployment](agent-based.md) - [Agents Module](../modules/agents.md) - [Deployment Orchestrator](../modules/deploy-orchestrator.md) - [Security Overview](../security/overview.md)