Skip to content

Commit bf0f1da

Browse files
committed
feat(ssh): 新增跨平台SSH免密登录配置脚本
添加一键配置脚本 Setup-SshNoPasswd.ps1,支持 Windows/macOS/Linux 平台自动完成 SSH 免密登录配置。主要功能包括: - 自动生成 ed25519 或 RSA 密钥 - 安全追加公钥到远端主机 - 生成 VSCode 兼容的 SSH 配置 - 支持 DryRun 模式和详细日志 - 幂等操作设计 同时更新文档添加脚本使用说明和参数说明
1 parent e98bb9b commit bf0f1da

3 files changed

Lines changed: 297 additions & 0 deletions

File tree

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
## 目标
2+
- 在项目根目录新增 `Setup-SshNoPasswd.ps1`(PowerShell 7+ 跨平台),一键完成 Windows/macOS/Linux 到目标主机的 SSH 免密登录配置,并为 VSCode 提供可用的 `~/.ssh/config` 条目。
3+
4+
## 功能概览
5+
- 自动检测本机系统(`$IsWindows/$IsLinux/$IsMacOS`)。
6+
- 若不存在密钥则生成(默认 `ed25519`,可选 `rsa`),路径默认 `~/.ssh/id_ed25519`。
7+
- 将本机公钥安全追加到目标主机 `~/.ssh/authorized_keys` 并设置权限(`700 ~/.ssh`、`600 authorized_keys`)。
8+
- Linux 服务器可选开关:检查并启动 `sshd`,提示防火墙放行 22 端口(不强制安装)。
9+
- macOS 服务器可选开关:开启“远程登录”(如需,执行前提示并征求同意)。
10+
- 生成 VSCode 兼容的 `~/.ssh/config` 条目(`Host` 别名、`HostName`、`User`、`IdentityFile`、`Port`)。
11+
- 幂等:重复运行不重复写入、权限/配置只在缺失时修复。
12+
- 支持 `-DryRun`(仅展示预执行操作)、`-Verbose`(详细日志)。
13+
14+
## 参数设计
15+
- `-RemoteHost`:目标主机 IP/域名(必填)。
16+
- `-Username`:目标主机登录用户(必填)。
17+
- `-Port`:SSH 端口,默认 `22`。
18+
- `-KeyType`:`ed25519|rsa`,默认 `ed25519`。
19+
- `-KeyPath`:密钥路径,默认 `~/.ssh/id_ed25519` 或随 `-KeyType` 变更。
20+
- `-Alias`:`~/.ssh/config` 中的 `Host` 别名,默认 `ssh-{RemoteHost}`。
21+
- `-ManageServer`:是否执行服务器侧检查(权限、服务状态),默认 `true`(提示并可交互跳过)。
22+
- `-DryRun`:仅打印预执行计划。
23+
24+
## 与现有脚本的关系
25+
- 现有 `Setup-VSCodeSSH.ps1` 使用 `RSA` 并偏向 Windows;新脚本将统一为跨平台与 `ed25519` 优先的实现,保留旧脚本不动,并在文档中推荐使用新脚本。
26+
27+
## Plan
28+
- [ ] **Impact Analysis (影响面分析)**
29+
- 修改/新增文件:`Setup-SshNoPasswd.ps1`、`docs/cheatsheet/vscode/ssh_nopasswd.md`(追加“一键脚本”章节)
30+
- 潜在风险:
31+
- 目标服务器无 `sshd` 或防火墙拦截导致失败
32+
- 用户主机缺失 `ssh`/`ssh-keygen`(仅提示安装,不强制)
33+
- [ ] **Step 1: Context Gathering**
34+
- 确认现有 `Setup-VSCodeSSH.ps1` 功能与风格(已对齐)
35+
- 对照文档 `ssh_nopasswd.md` 的步骤与术语
36+
- [ ] **Step 2: Implementation**
37+
- 新增 `Setup-SshNoPasswd.ps1`:
38+
- `[CmdletBinding()]` + `param()`,中文 DocStrings
39+
- OS 识别与依赖检查(`Get-Command ssh/ssh-keygen`)
40+
- 密钥生成(默认 `ed25519`,无密码短语;可自定义路径/类型)
41+
- 公钥追加到远端:
42+
- 优先使用 `ssh-copy-id`(在 Linux/macOS 常见)
43+
- 回退:以 `ssh` 远程执行 `mkdir/chmod/echo >> authorized_keys`(确保转义与权限)
44+
- 修正服务器权限与(可选)服务状态
45+
- 写入 `~/.ssh/config` 条目(避免重复)
46+
- `-DryRun`/`-Verbose` 支持,错误时抛出可追踪上下文
47+
- [ ] **Step 3: Verification**
48+
- 冒烟测试:
49+
- 本地:若 `-DryRun`,检查所有路径与命令渲染是否正确
50+
- 远端:`ssh -o BatchMode=yes -p {Port} {User}@{Host} true` 验证免密是否可用
51+
- 文档更新:在 `ssh_nopasswd.md` 追加“使用脚本”小节与常见报错排查
52+
53+
## 交付物
54+
- 新增:`Setup-SshNoPasswd.ps1`(根目录)
55+
- 文档:在 `docs/cheatsheet/vscode/ssh_nopasswd.md` 增补脚本用法示例
56+
57+
## 后续可选增强
58+
- 支持多主机批量配置(从 JSON/CSV 读取)
59+
- 支持自动检测并提示远端防火墙开放
60+
61+
请确认以上方案,确认后我将实现脚本与文档更新,并进行本地可执行的冒烟验证。

‎Setup-SshNoPasswd.ps1‎

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
[CmdletBinding()]
2+
param(
3+
[Parameter(Mandatory = $true)][string]$RemoteHost,
4+
[Parameter(Mandatory = $true)][string]$Username,
5+
[int]$Port = 22,
6+
[ValidateSet('ed25519', 'rsa')][string]$KeyType = 'ed25519',
7+
[string]$KeyPath,
8+
[string]$HostAlias,
9+
[bool]$ManageServer = $true,
10+
[switch]$DryRun
11+
)
12+
13+
$ErrorActionPreference = 'Stop'
14+
15+
# 解析主目录与 ~/.ssh 目录
16+
$userHome = [Environment]::GetFolderPath('UserProfile')
17+
if ([string]::IsNullOrWhiteSpace($userHome)) {
18+
if ($env:USERPROFILE -and $env:USERPROFILE.Trim().Length -gt 0) { $userHome = $env:USERPROFILE }
19+
elseif ($env:HOME -and $env:HOME.Trim().Length -gt 0) { $userHome = $env:HOME }
20+
else { try { $userHome = (Resolve-Path ~).Path } catch { $userHome = $pwd.Path } }
21+
}
22+
$sshDir = $null
23+
if ($userHome -and $userHome.Trim().Length -gt 0) {
24+
$sshDir = [System.IO.Path]::Combine($userHome, '.ssh')
25+
}
26+
else {
27+
$sshDir = [System.IO.Path]::Combine($pwd.Path, '.ssh')
28+
}
29+
if (-not $DryRun) { New-Item -ItemType Directory -Path $sshDir -Force | Out-Null }
30+
if ($DryRun) {
31+
Write-Output ("HOME=" + $userHome);
32+
Write-Output ("SSHDir=" + $sshDir);
33+
Write-Output ("CombineTest=" + [System.IO.Path]::Combine('C:\\test', 'id_ed25519'));
34+
Write-Output ("KeyType=" + $KeyType)
35+
Write-Output ("CombineHomeSsh=" + [System.IO.Path]::Combine($userHome, '.ssh'))
36+
Write-Output ("CombineDefaultKey=" + [System.IO.Path]::Combine($userHome, '.ssh', ('id_' + $KeyType)))
37+
Write-Output ("RemoteHost=" + $RemoteHost)
38+
}
39+
40+
# 依赖检测(ssh / ssh-keygen)
41+
if (-not (Get-Command ssh -ErrorAction SilentlyContinue)) { throw 'ssh not found. Please install OpenSSH client.' }
42+
if (-not (Get-Command ssh-keygen -ErrorAction SilentlyContinue)) { throw 'ssh-keygen not found. Please install OpenSSH client.' }
43+
44+
# 计算密钥路径与公钥路径
45+
$defaultKeyPath = [System.IO.Path]::Combine($userHome, '.ssh', ('id_' + $KeyType))
46+
if ([string]::IsNullOrWhiteSpace($KeyPath)) { $KeyPath = [System.IO.Path]::Combine($userHome, '.ssh', ('id_' + $KeyType)) }
47+
$pubPath = "${KeyPath}.pub"
48+
if ($DryRun) { Write-Output ("DefaultKeyPath=" + $defaultKeyPath); Write-Output ("KeyPath=" + $KeyPath); Write-Output ("PubPath=" + $pubPath) }
49+
50+
# 生成密钥(若不存在)
51+
if (-not (Test-Path $KeyPath)) {
52+
$userLocal = if ($env:USER) { $env:USER } else { $env:USERNAME }
53+
$machineLocal = [Environment]::MachineName
54+
$comment = "$userLocal@$machineLocal"
55+
if ($DryRun) {
56+
Write-Output ("Generate key: " + $KeyPath + " type: " + $KeyType)
57+
}
58+
else {
59+
& ssh-keygen -t $KeyType -f $KeyPath -N "" -C $comment | Out-Null
60+
}
61+
}
62+
63+
# 公钥准备(DryRun 时容忍不存在)
64+
$pubKeyOutput = $null
65+
if (Test-Path $pubPath) {
66+
$pubKeyOutput = (Get-Content -Path $pubPath -Raw).Trim()
67+
}
68+
else {
69+
if ($DryRun) {
70+
Write-Output ("Missing public key: " + $pubPath + " (will be created)")
71+
$pubKeyOutput = 'PUBKEY_PLACEHOLDER'
72+
}
73+
else {
74+
throw ("Public key not found: " + $pubPath)
75+
}
76+
}
77+
78+
# Host 别名(用于 VSCode 配置)
79+
$aliasToWrite = $HostAlias
80+
if ([string]::IsNullOrWhiteSpace($aliasToWrite)) { $aliasToWrite = "ssh-" + $RemoteHost }
81+
if ($DryRun) { Write-Output ("HostAlias=" + $aliasToWrite) }
82+
83+
# 优先使用 ssh-copy-id(Linux/macOS 常见),回退到 ssh 远程命令
84+
$hasCopyId = [bool](Get-Command ssh-copy-id -ErrorAction SilentlyContinue)
85+
$login = $Username + '@' + $RemoteHost
86+
if ($hasCopyId) {
87+
if ($DryRun) {
88+
Write-Output ("ssh-copy-id -i '" + $pubPath + "' -p " + $Port + " " + $login)
89+
}
90+
else {
91+
& ssh-copy-id -i $pubPath -p $Port $login
92+
}
93+
}
94+
else {
95+
$hasScp = [bool](Get-Command scp -ErrorAction SilentlyContinue)
96+
if ($hasScp) {
97+
$tmpRemote = "~/.__tmp_id_key.pub"
98+
if ($DryRun) {
99+
Write-Output ("scp -P " + $Port + " '" + $pubPath + "' " + $login + ":" + $tmpRemote)
100+
Write-Output ("ssh -p " + $Port + " " + $login + " mkdir -p ~/.ssh; chmod 700 ~/.ssh; cat " + $tmpRemote + " >> ~/.ssh/authorized_keys; chmod 600 ~/.ssh/authorized_keys; rm -f " + $tmpRemote)
101+
}
102+
else {
103+
& scp -P $Port $pubPath ($login + ":" + $tmpRemote)
104+
& ssh -p $Port $login ("mkdir -p ~/.ssh; chmod 700 ~/.ssh; cat " + $tmpRemote + " >> ~/.ssh/authorized_keys; chmod 600 ~/.ssh/authorized_keys; rm -f " + $tmpRemote)
105+
}
106+
}
107+
else {
108+
$remoteCmd = "mkdir -p ~/.ssh; chmod 700 ~/.ssh; echo '" + $pubKeyOutput + "' >> ~/.ssh/authorized_keys; chmod 600 ~/.ssh/authorized_keys"
109+
if ($DryRun) {
110+
Write-Output ("ssh -p " + $Port + " " + $login + " " + $remoteCmd)
111+
}
112+
else {
113+
& ssh -p $Port $login $remoteCmd
114+
}
115+
}
116+
}
117+
118+
# 可选:远端服务/权限检查(不强制变更)
119+
if ($ManageServer) {
120+
$svcCheck = "if command -v systemctl >/dev/null 2>&1; then systemctl is-active ssh; systemctl is-active sshd; fi"
121+
if ($DryRun) {
122+
Write-Output ("ssh -p " + $Port + " " + $login + " " + $svcCheck)
123+
}
124+
else {
125+
try { & ssh -p $Port $login $svcCheck } catch { }
126+
}
127+
$macEnable = "if command -v systemsetup >/dev/null 2>&1; then sudo -n systemsetup -setremotelogin on; fi"
128+
if ($DryRun) {
129+
Write-Output ("ssh -p " + $Port + " " + $login + " " + $macEnable)
130+
}
131+
else {
132+
try { & ssh -p $Port $login $macEnable } catch { }
133+
}
134+
}
135+
136+
# 验证已追加公钥
137+
$verifyCmd = "grep -F '" + $pubKeyOutput + "' ~/.ssh/authorized_keys >/dev/null 2>&1 && echo OK || echo MISSING"
138+
if ($DryRun) {
139+
Write-Output ("ssh -p " + $Port + " " + $login + " " + $verifyCmd)
140+
}
141+
else {
142+
try {
143+
$verify = & ssh -p $Port $login $verifyCmd
144+
if (-not ($verify -match 'OK')) { throw "Failed to append public key to remote authorized_keys" }
145+
}
146+
catch { throw $_ }
147+
}
148+
149+
# 写入 ~/.ssh/config(避免重复追加)
150+
$configFile = [System.IO.Path]::Combine($sshDir, 'config')
151+
if ($DryRun) {
152+
Write-Output ("Write ~/.ssh/config alias: " + $aliasToWrite)
153+
Write-Output ("ConfigFile=" + $configFile)
154+
}
155+
else {
156+
if (-not (Test-Path $configFile)) { New-Item -ItemType File -Path $configFile | Out-Null }
157+
$exists = $false
158+
if (Test-Path $configFile) {
159+
$exists = Select-String -Path $configFile -Pattern ("^Host\s+" + [regex]::Escape($Alias)) -Quiet
160+
}
161+
if (-not $exists) {
162+
$entry = "Host " + $aliasToWrite + "`n HostName " + $RemoteHost + "`n User " + $Username + "`n IdentityFile " + $KeyPath + "`n Port " + $Port + "`n PreferredAuthentications publickey"
163+
Add-Content -Path $configFile -Value $entry
164+
}
165+
}
166+
167+
# 完成提示
168+
Write-Output ("Done: use alias '" + $aliasToWrite + "'")
169+
<#
170+
.SYNOPSIS
171+
跨平台 SSH 免密登录配置脚本
172+
173+
.DESCRIPTION
174+
自动化完成免密 SSH 的关键步骤:
175+
1. 生成本机 SSH 密钥(若不存在,默认 ed25519)
176+
2. 将公钥追加到远端用户的 ~/.ssh/authorized_keys 并修正权限
177+
3. 写入本机 ~/.ssh/config(VSCode 兼容的 Host 别名)
178+
支持 DryRun 预览,将不会修改系统状态。
179+
180+
.PARAMETER RemoteHost
181+
目标主机的 IP 或域名,例如 192.168.1.10 或 server.local。
182+
183+
.PARAMETER Username
184+
目标主机登录用户名,例如 ubuntu、mac_user。
185+
186+
.PARAMETER Port
187+
SSH 端口,默认 22。
188+
189+
.PARAMETER KeyType
190+
密钥类型:ed25519 或 rsa,默认 ed25519。
191+
192+
.PARAMETER KeyPath
193+
密钥路径,默认 ~/.ssh/id_{KeyType}。
194+
195+
.PARAMETER HostAlias
196+
写入 ~/.ssh/config 的 Host 别名,默认 ssh-{RemoteHost}。
197+
198+
.PARAMETER ManageServer
199+
是否执行远端服务/权限检查,默认 true(非强制变更)。
200+
201+
.PARAMETER DryRun
202+
仅输出将执行的操作,不进行实际更改。
203+
204+
.EXAMPLE
205+
.\Setup-SshNoPasswd.ps1 -RemoteHost 192.168.1.10 -Username user
206+
使用默认端口 22 与 ed25519,在本机生成密钥、将公钥追加到远端并写入 ~/.ssh/config。
207+
208+
.EXAMPLE
209+
.\Setup-SshNoPasswd.ps1 -RemoteHost server.example.com -Username ubuntu -Port 2222 -Alias ssh-server -KeyType rsa
210+
使用自定义端口与 rsa 密钥类型,并指定连接别名。
211+
212+
.NOTES
213+
- 本机需安装 OpenSSH(ssh、ssh-keygen)。
214+
- 远端需开启 SSH 登录且防火墙放行对应端口。
215+
#>

‎docs/cheatsheet/vscode/ssh_nopasswd.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
11
## VSCode (Win11) SSH 连接 Unix 系统免密登录速查表
22

3+
### 使用一键脚本(推荐)
4+
5+
**在项目根目录执行:**
6+
7+
```powershell
8+
./Setup-SshNoPasswd.ps1 -RemoteHost 192.168.x.x -Username your_user
9+
```
10+
11+
- 默认生成 `ed25519` 密钥,路径 `~/.ssh/id_ed25519`。
12+
- 自动将公钥追加到远端 `~/.ssh/authorized_keys` 并设置权限。
13+
- 自动写入本地 `~/.ssh/config`,使用别名 `ssh-192.168.x.x` 连接。
14+
- 可选参数:
15+
- `-Port 22` 端口
16+
- `-KeyType ed25519|rsa`
17+
- `-KeyPath ~/.ssh/id_ed25519`
18+
- `-Alias ssh-your-host`
19+
- `-ManageServer $true|$false`
20+
- `-DryRun` 仅展示将执行的操作
21+
22+
---
23+
324
### 第 1 步:在 Windows 11 (PowerShell / 终端)
425

526
**1. 生成 SSH 密钥对**

0 commit comments

Comments
 (0)