
王.W
lzc-cli 是 懒猫微服 的官方命令行工具,用于应用的开发、构建、安装、调试和发布。
在使用 lzc-cli 连接懒猫微服设备时,需要将本机的 SSH 公钥添加到设备的信任列表。
lzc-cli 为此实现了一套 密钥扫描 → 交互选择 → 授权注册 的流程,核心逻辑分布在以下两个文件中:
| 文件 | 核心函数 | 职责 |
|---|---|---|
lib/utils.js | findSshPublicKey() | 扫描 ~/.ssh/ 下所有 .pub 公钥文件 |
lib/utils.js | selectSshPublicKey() | 交互式菜单让用户选择密钥 |
lib/debug_bridge.js | sshApplyGrant() | 串联扫描和选择,生成授权 URL |
findSshPublicKey()📍
lib/utils.js· 第 569-630 行
export function findSshPublicKey() {
// ① 根据操作系统获取 HOME 目录
let homeDir;
switch (process.platform) {
case 'win32':
homeDir = process.env.USERPROFILE || os.homedir();
break;
case 'darwin':
case 'linux':
homeDir = process.env.HOME || os.homedir();
break;
default:
throw new Error('Unsupported operating system');
}
// ② 定位 .ssh 目录
const sshDir = path.join(homeDir, '.ssh');
// ③ 校验 .ssh 目录是否存在且合法
const dirStat = fs.statSync(sshDir);
if (!dirStat.isDirectory()) {
throw new Error('.ssh 目前存在,但不是一个正常的目录');
}
// ④ 遍历目录,收集所有可读的 .pub 文件
let avaiableKeys = [];
const files = fs.readdirSync(sshDir);
files.forEach((keyName) => {
if (keyName.endsWith('.pub')) {
const keyPath = path.join(sshDir, keyName);
try {
fs.accessSync(keyPath, fs.constants.R_OK);
avaiableKeys.push({
keyName, // 文件名,如 "id_ed25519.pub"
path: keyPath, // 完整路径,如 "/root/.ssh/id_ed25519.pub"
});
} catch (error) {
console.warn(`Found ${keyName} but cannot read it:`, error.message);
}
}
});
// ⑤ 没有找到任何公钥则抛出异常
if (avaiableKeys.length == 0) {
throw new Error('.ssh 目录没有找到任何 .pub 公钥');
}
return avaiableKeys;
}
关键设计:
id_ed25519 或 id_rsa,而是扫描所有 .pub 后缀文件。fs.accessSync 确保文件可读,不可读的文件会输出警告并跳过。USERPROFILE)、macOS/Linux(HOME)。selectSshPublicKey()📍
lib/utils.js· 第 551-567 行
export async function selectSshPublicKey(avaiableKeys) {
const keyNames = avaiableKeys.map((k) => k.path);
// 使用 inquirer 弹出交互式列表
const selected = (
await inquirer.prompt([
{
name: 'type',
message: '选择使用的公钥',
type: 'list',
choices: keyNames,
},
])
)['type'];
return {
path: selected,
content: fs.readFileSync(selected, 'utf8'),
};
}
关键设计:
inquirer 的 list 类型,用户通过上下键选择、回车确认。path(路径)和 content(文件内容)。sshApplyGrant()📍
lib/debug_bridge.js· 第 131 行附近
async sshApplyGrant() {
// ① 扫描所有可用公钥
const keys = await findSshPublicKey();
logger.info(
'检测到您当前的环境还没有添加 ssh 公钥到 \'懒猫开发者工具\' 中,' +
'请选择您需要添加的公钥类型'
);
// ② 让用户交互式选择
const sshInfo = await selectSshPublicKey(keys);
logger.debug('ssh public key info', sshInfo);
// ③ Base64 编码公钥内容
const pk = Buffer.from(sshInfo.content.trimLeft()).toString('base64');
// ④ 生成授权 URL,引导管理员在浏览器中访问
logger.warn(
`您当前机器的公钥未添加到微服(${this.boxname})的信任列表中,\n` +
`请使用微服管理员账号在浏览器中访问以下地址:\n` +
`-> https://${this.domain}/auth?key=${pk}`
);
}
流程小结:
findSshPublicKey() 获取 ~/.ssh/ 下所有 .pub 文件。selectSshPublicKey() 让用户选择一个。key 参数。┌──────────────────────────────────────────────────────────────────┐
│ lzc-cli SSH 密钥选择完整流程 │
└──────────────────────────────────────────────────────────────────┘
用户执行 lzc-cli 命令(如 lzc-cli app install)
│
▼
┌──────────────────────────────┐
│ sshApplyGrant() 被触发 │
│ 检测到当前机器未授权 │
└──────────────────────────────┘
│
▼
┌──────────────────────────────┐
│ findSshPublicKey() │
│ 扫描 ~/.ssh/*.pub │
│ │
│ 找到: │
│ ├─ id_ed25519.pub │
│ ├─ id_rsa.pub │
│ └─ my_custom.pub │
└──────────────────────────────┘
│
▼
┌──────────────────────────────┐
│ selectSshPublicKey() │
│ │
│ ? 选择使用的公钥 │
│ ❯ /root/.ssh/id_ed25519.pub│
│ /root/.ssh/id_rsa.pub │
│ /root/.ssh/my_custom.pub │
└──────────────────────────────┘
│
用户按回车确认
│
▼
┌──────────────────────────────┐
│ Base64 编码公钥内容 │
│ 生成授权 URL: │
│ https://box.domain/auth │
│ ?key=c3NoLWVkMjU1MT... │
└──────────────────────────────┘
│
▼
┌──────────────────────────────┐
│ 管理员在浏览器中打开 URL │
│ 完成公钥授权 ✅ │
└──────────────────────────────┘
| 维度 | SSH 客户端(ssh / git) | lzc-cli |
|---|---|---|
| 密钥发现 | 按固定优先级搜索标准名称 | 扫描 ~/.ssh/*.pub 所有公钥 |
| 选择逻辑 | 自动匹配,无需干预 | inquirer 交互式菜单 |
| 支持自定义名称 | 需通过 -i 参数或 ~/.ssh/config 指定 | 天然支持,扫描即可发现 |
| 非交互环境 | ✅ 天然支持 | ❌ inquirer.prompt 会阻塞 |
SSH 客户端的默认搜索顺序(参考 ssh_config(5) 手册):
~/.ssh/id_ed25519
~/.ssh/id_ecdsa
~/.ssh/id_rsa
~/.ssh/id_dsa
在一个 Docker 化的部署流水线中,容器需要通过 lzc-cli 将应用部署到懒猫微服设备。
容器内没有终端(TTY),无法进行交互式操作。
# entrypoint.sh 中的关键配置
SSH_DATA_DIR="/data/ssh" # 挂载的持久化卷
SSH_HOME_DIR="/root/.ssh" # SSH 标准查找路径
# 软链接:让所有读取 ~/.ssh 的程序都实际访问持久化目录
ln -sf "${SSH_DATA_DIR}" "${SSH_HOME_DIR}"
容器首次启动时生成密钥:
if [ ! -f "/data/ssh/id_ed25519" ]; then
ssh-keygen -t ed25519 -f /data/ssh/id_ed25519 -N "" -q
echo "✓ SSH 密钥对已生成"
fi
生成后的目录结构:
/data/ssh/ # 持久化卷(重启不丢失)
├── id_ed25519 # 私钥
└── id_ed25519.pub # 公钥
/root/.ssh -> /data/ssh # 软链接
当后端通过 subprocess 调用 lzc-cli 时:
findSshPublicKey() 正常扫描到 id_ed25519.pub ✅selectSshPublicKey() 弹出 inquirer 交互菜单 → 容器无 TTY,程序卡死 ❌在 entrypoint.sh 中对 lzc-cli 源码做运行时 Patch:
# ==================== lzc-cli SSH 自动选择 Patch ====================
echo ""
echo "Patching lzc-cli SSH auto-select..."
LZC_UTILS="/usr/lib/node_modules/@lazycatcloud/lzc-cli/lib/utils.js"
if [ -f "${LZC_UTILS}" ]; then
if ! grep -q "auto-select single key" "${LZC_UTILS}" 2>/dev/null; then
python3 -c "
import pathlib, sys
p = pathlib.Path('${LZC_UTILS}')
c = p.read_text()
old = 'export async function selectSshPublicKey(avaiableKeys) {'
new = '''export async function selectSshPublicKey(avaiableKeys) {
\t// [patched by deploy-pipeline] auto-select single key
\tif (avaiableKeys.length === 1) {
\t\tconst key = avaiableKeys[0];
\t\tconsole.log(\"Auto-selecting the only available SSH key:\", key.path);
\t\treturn { path: key.path, content: require(\"fs\").readFileSync(key.path, \"utf8\") };
\t}'''
if old in c and 'auto-select single key' not in c:
p.write_text(c.replace(old, new))
print(' ✓ lzc-cli SSH auto-select patch 已应用')
else:
print(' ✓ lzc-cli SSH 无需 patch 或已 patch')
"
else
echo " ✓ lzc-cli SSH auto-select 已 patch"
fi
fi
Patch 后的 selectSshPublicKey 等价逻辑:
export async function selectSshPublicKey(avaiableKeys) {
// [patched] 单密钥时自动选择,跳过交互
if (avaiableKeys.length === 1) {
const key = avaiableKeys[0];
console.log('Auto-selecting the only available SSH key:', key.path);
return { path: key.path, content: fs.readFileSync(key.path, 'utf8') };
}
// 多密钥时仍走交互式选择
const keyNames = avaiableKeys.map((k) => k.path);
const selected = (
await inquirer.prompt([
{
name: 'type',
message: '选择使用的公钥',
type: 'list',
choices: keyNames,
},
])
)['type'];
return {
path: selected,
content: fs.readFileSync(selected, 'utf8'),
};
}
| 验证项 | 结果 | 说明 |
|---|---|---|
| SSH/Git 能找到密钥 | ✅ | 软链接 /root/.ssh → /data/ssh,标准名称 id_ed25519 自动匹配 |
| lzc-cli 能扫描到公钥 | ✅ | findSshPublicKey() 扫描 /root/.ssh/*.pub → 实际读取 /data/ssh/*.pub |
| 非交互环境不阻塞 | ✅ | Patch 后单密钥自动选择,不触发 inquirer.prompt |
| 容器重启后密钥持久化 | ✅ | /data 为持久化卷,密钥不丢失 |
| 多密钥场景兼容 | ✅ | 多密钥时仍走交互式选择(适用于开发者本地终端) |
key 参数仅为 Base64 编码,而非加密。公钥本身是公开信息,但应避免在不安全的网络中传输该 URL。node_modules 中的源码,npm update 后需要重新应用。| 问题 | 原因 | 解决方案 |
|---|---|---|
.ssh 目录没有找到任何 .pub 公钥 | 未生成密钥对 | 运行 ssh-keygen -t ed25519 |
inquirer 交互卡死 | 在无 TTY 的环境中运行 | 应用自动选择 Patch |
| 重启容器后密钥丢失 | ~/.ssh 未持久化 | 使用持久化卷 + 软链接方案 |
| 问题 | 答案 |
|---|---|
| lzc-cli 如何发现 SSH 密钥? | 扫描 ~/.ssh/*.pub,收集所有可读的公钥文件 |
| 是否硬编码特定密钥名称? | 否,支持任意 .pub 后缀文件 |
| 只有一个密钥会自动选择吗? | 原版 不会,仍需交互确认;可通过 Patch 实现自动选择 |
| 软链接方案是否对 lzc-cli 透明? | 是,findSshPublicKey() 通过标准文件系统 API 访问,软链接完全透明 |
评论
0暂无评论