ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

scp自动输密码的三种方案:SSH密钥、sshpass与expect实战

scp自动输密码的三种方案:SSH密钥、sshpass与expect实战 1. 每次敲密码都手疼scp交互式登录的日常困境1.1 scp为什么天生要求手动输入密码很多刚开始做服务器运维的朋友都有过这样的经历写完一条scp命令按下回车然后眼睁睁看着光标停在userhosts password:那一行等你手动敲一串密码。这其实不是scp故意给人添堵而是它的底层走的是SSH协议而SSH的密码认证从设计之初就是面向真人坐在终端前这种交互场景的。SSH之所以坚持从终端设备读取密码而不是像普通命令那样从标准输入读取是因为它要防一类典型的攻击脚本或恶意程序通过管道伪造输入来骗取认证。强制让密码来自/dev/tty相当于告诉系统这行输入必须来自真实键盘。这个安全决策在面对人类用户时很合理但一旦进入自动化领域就成了挡在脚本前面的第一道墙。如果你只是偶尔从本机拖一个配置文件到服务器手动输一次密码完全无所谓。但假设你有一个每天凌晨两点跑的备份任务或者要给几十台机器批量分发脚本scp的手动交互就会让整条自动化链路直接瘫掉。最典型的场景包括定时任务里执行scp同步因为没人去敲密码任务静默失败批量部署脚本跑到scp步骤就挂起后面所有机器都卡住CI/CD流水线中需要跨服务器拉取产物交互式密码输入让整个流程无法无人值守痛点就是这么来的。文章后面要讨论的正是围绕怎么让scp在不牺牲安全底线的前提下摆脱手动输密码这个核心问题展开。1.2 新手最常试的三种土办法为什么都不行先说结论直接往scp命令里管道传密码是行不通的。用echo mypassword | scp ...这种方式第一次看到它的人都会觉得应该能行因为管道可以覆盖很多命令的输入。但scp不走标准输入它走的是伪终端管道里的字符串根本到不了它要读取的位置命令最终还是会停下来等键盘输入。第二种常见的尝试是把密码写进命令行的参数比如scp -P 22 file userhost:/path然后试图用sshpass以外的工具拼接。但问题是scp本身没有提供--password这类的参数它在认证阶段直接把控制权交给了SSH客户端参数再怎么拼也绕不过交互这关。第三种是用别名或者写个shell函数比如alias scpscp -o BatchModeyes这个方向其实反而值得聊一下BatchModeyes确实能让scp不询问密码但结果不是自动通过认证而是直接认证失败。因为BatchMode的意思是如果密码输入不可用就不要交互直接报错退出。它适合用来检测密钥是否配好而不是用来自动输密码。理解了这些失败尝试背后的原因你就能明白真正靠谱的路径只有三条第一用SSH密钥认证从根上消除密码输入第二用一个程序替你把密码填进伪终端第三写一个完整的交互脚本模拟人工操作。下面三章分别把这三条路讲透。2. 治本方案优先SSH密钥配对是怎么干掉密码输入的2.1 密钥认证的底层逻辑为什么不建议一上来就装工具如果你问我在自动输入密码这个问题上最优的长期解是什么我的回答一定是SSH密钥认证而不是任何自动填密码的工具。原因很简单密钥方案让密码这个概念彻底消失。客户端持有一把私钥服务器存着对应的公钥连接时服务器发送一个只有私钥才能正确应答的挑战客户端解密并返回结果认证就此完成。整个过程不放任何明文密码到网络里也不存在密码泄露这个风险面。具体流程大概是这样的客户端生成一对密钥包含私钥和公钥公钥内容追加到服务器上目标用户的~/.ssh/authorized_keys文件中客户端发起ssh/scp连接时SSH协议自动完成密钥协商服务器验证应答后直接放行全程无交互这就像给服务器配了一把只有你有的钥匙而不是每次进门都报一串口令。凡是能用钥匙开的门你都不需要记得口令。2.2 从生成到分发一套完整的密钥配置实操下面这套流程我每隔一段时间就会在文档里重新写一遍因为真的太好用了。在客户端机器上执行ssh-keygen -t ed25519 -C deploy-key-t ed25519指定用Ed25519算法比传统的RSA 2048/4096更短更安全现代OpenSSH版本都默认支持。-C只是一个备注方便你日后在服务器的authorized_keys里区分这把公钥是给谁的。执行过程中会问你保存路径默认是~/.ssh/id_ed25519回车即可。然后会提示你设置passphrase也就是私钥本身的密码。这里有两个选择如果你希望完全无人值守直接留空回车跳过如果你希望私钥本身也被保护可以设置passphrase再配合ssh-agent在会话期间记住它对于纯自动化服务器之间的同步我通常建议留空passphrase因为一旦设了就又有一次交互输入的机会自动化程度又降回去了。不过如果你有得选用ssh-agent缓存passphrase是更好的平衡。接下来把公钥分发到目标服务器ssh-copy-id -i ~/.ssh/id_ed25519.pub userserver这条命令会提示你输入一次目标服务器的密码之后就把公钥自动追加到对应用户的authorized_keys里。分发完成后再执行scp /local/file.txt userserver:/remote/path/这时候你就会发现scp直接开始传文件全程没有再问密码。这就是密钥认证的效果。如果你有几十台机器要批量分发可以写个简单的for循环for host in node01 node02 node03; do ssh-copy-id -i ~/.ssh/id_ed25519.pub root$host done每次会让你输入一次目标机器密码逐台确认一次性搞定。2.3 配置顺了也会翻车权限和SELinux这两道暗门密钥认证配置流程本身不复杂但实际部署时翻车率非常高几乎都栽在文件和目录权限上。OpenSSH对~/.ssh目录和里面的文件有一套严格权限要求。~/.ssh目录的权限不建议超过700authorized_keys文件的权限不建议超过600。如果权限过于宽松比如~/.ssh是755很多sshd会认为文件不安全直接忽略你的公钥然后悄悄退回密码认证看起来就像密钥没配上。检查命令如下chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys在CentOS这类带SELinux的系统上问题又多一层。如果SELinux处于Enforcing模式从非标准位置拷贝过来的authorized_keys文件可能会被拒绝读取。排查的时候可以先临时用getenforce看状态再用restorecon -R -v ~/.ssh恢复正确的文件上下文。还有一个非常容易忽略的点目标用户的家目录本身权限也不能太开放。如果/home/deploy是777sshd一样会拒绝读取里面的.ssh配置。这个坑我印象很深刻当时排查了一下午最后发现是家目录被之前某个脚本改成了755以外的开放权限。从长期维护的角度来看密钥方案是最省心的。配置一次之后所有scp、ssh、rsync命令都自动免密脚本和定时任务不需要任何额外处理。3. 最省事的自动输密码利器sshpass的安装与用法3.1 sshpass干了一件什么事它和手工输密码的本质区别有些场景下你没办法用密钥方案。比如对方服务器属于第三方厂商管理员不允许你部署自己的公钥或者你是在一个临时环境里做演示不能改动任何配置。这时候就需要一把能自动把密码填进去的辅助工具主流的选型就是sshpass。sshpass的原理不算复杂它分配一个伪终端pty然后在这个伪终端上等待SSH输出password:提示一旦匹配到就把你预设的密码写进去。简单说它模拟了一个真人坐在终端前敲键盘这个动作只不过它敲得飞快而且永远不会输错。用一句话概括sshpass是一层给SSH喂密码的代理。它的存在只服务于密码认证不能交互执行这一个痛点没有密码以外的任何黑魔法。3.2 安装方法主流的包管理器基本都收录了由于sshpass太常见了主流发行版的软件源里基本都能直接安装。Debian/Ubuntu系的安装命令是apt update apt install -y sshpassCentOS/RHEL系需要EPEL源装好EPEL之后再安装yum install -y epel-release yum install -y sshpass如果你用的是Rocky Linux或者AlmaLinux 9dnf install sshpass也应该能从EPEL源拉到。macOS上可以用Homebrewbrew install sshpass不过Homebrew历史上曾因为安全原因下架过sshpass如果你装不了可以考虑源码编译从GitHub上把源码拉下来执行标准的./configure make make install。源码安装并不复杂但它依赖GCC和一些基础编译工具链所以我还是优先建议用包管理器。3.3 四种喂密码的姿势以及和scp组合的实战示例sshpass提供了几种不同的传密码方式我按实际使用频率分别说一下。方式一直接用-p参数sshpass -p MyPassword123 scp /data/app.tar.gz deploy192.168.1.10:/data/这是最直观的写法命令逻辑一目了然。但有一个不容忽视的隐患在Linux的进程列表里任何用户只要权限允许都可能看到-p后面的明文密码。虽然服务器上一般不是人人都有权限执行ps -ef但在多用户共享的开发机上这个风险不能无视。方式二从环境变量读取export SSHPASSMyPassword123 sshpass -e scp /data/app.tar.gz deploy192.168.1.10:/data/ unset SSHPASS-e让sshpass从环境变量SSHPASS里取密码这样进程列表里不会直接暴露明文。不过环境变量本身也能被同权限的进程读取所以这只是一个折中不是绝对安全。方式三从文件读取echo MyPassword123 ~/.ssh/.passwd chmod 600 ~/.ssh/.passwd sshpass -f ~/.ssh/.passwd scp /data/app.tar.gz deploy192.168.1.10:/data/这是我在脚本中比较推荐的一种方式密码放在一个权限为600的独立文件里脚本内容本身不包含密码查看脚本的人无法直接拿到密码。只要把那个文件的权限管好安全面就小很多。方式四从标准输入读取echo MyPassword123 | sshpass scp /data/app.tar.gz deploy192.168.1.10:/data/这种方式也常见但要注意管道传入的内容会被sshpass一次性读完。日常用它来快速测试没问题。实际使用中你完全可以把sshpass套在其他依赖SSH的传输命令前面比如rsyncsshpass -f ~/.ssh/.passwd rsync -avz /data/ deploy192.168.1.10:/backup/rsync本身也走SSH协议配上sshpass同样能实现全自动同步。3.4 用sshpass必须想清楚的三个安全代价sshpass用起来确实爽但有几个代价我建议每个使用者都想清楚。第一个是明文密码的暴露面。不管是命令行参数、环境变量还是文件密码始终以明文形式存在于某个地方这和密钥认证完全不同。一旦文件权限设置不当或者shell历史被记录就存在泄露风险。第二个是无法应对动态交互。sshpass只会简单等待password:提示然后填一次密码。如果对方服务器配置了一次性密码、需要应答验证码、或者有多重认证sshpass就无能为力了。这种复杂场景要交给下面说的expect。第三个是兼容性边界。如果你在Windows上通过OpenSSH的bash环境使用sshpass少数旧版本会行为诡异。好消息是Linux主流发行版上都很稳我用了几年没碰到过严重问题。4. expect脚本硬核实操能处理跳板机和复杂交互的老兵4.1 为什么总有一些场景sshpass也搞不定sshpass能解决的严格来说只有出现一次password提示填入密码这一种交互。但现实中的SSH登录往往不止这么简单首次连接到一台新服务器SSH会提示Are you sure you want to continue connecting (yes/no)?一旦出现这个提示sshpass直接傻眼有的运维平台开启了双因子认证需要先输密码再输短信验证码公司内网访问生产服务器要先跳板机认证可能发生在跳板机而不是目标机密码策略要求定期修改密码超时或密码过期后交互过程比单次输入复杂得多这种时候就需要一个能完整模拟人类键盘操作的方案这就是expect的用武之地。expect最早是Tcl生态里的一个自动化交互工具核心思路简单粗暴启动一个程序等待它输出特定的字符串然后给它发送预设的输入。三者循环往复直到程序退出。4.2 最小可用的expect脚本从spawn到expect eof下面是一个最小可用的expect脚本功能等价于一条自动化scp#!/usr/bin/expect set timeout 30 set password MyPassword123 spawn scp /data/app.tar.gz deploy192.168.1.10:/data/ expect { password: { send $password\r exp_continue } yes/no { send yes\r exp_continue } 100% { # 传输完成继续等待 } eof }脚本第一行声明了用expect作为解释器。spawn负责启动真正的scp进程。expect块里写的是匹配规则如果出现了password:就把密码发送过去\r表示回车如果出现yes/no就发送yes然后回车处理首次指纹确认。exp_continue的作用是循环等待因为scp可能先后出现多次交互提示发送完一次输入后不能立刻跳出匹配块而是要继续等下一轮输出。最后用expect eof收尾表示等到scp进程结束。总的来说expect脚本比sshpass多了一层程序对话的抽象你能控制的不止是密码还有所有可能的提示语。4.3 跳板机场景expect怎么处理多层认证跳板机场景是我认为expect不可替代的经典案例。假设你的目标机器是10.0.0.5但你得先登录jump.example.com这台跳板机再从那里跳到目标机两段都要输密码。用原生命令行的-J参数当然可以比如scp -J userjump ...但前提是你能在跳板机上完成密钥认证。如果跳板机密码就是需要自动输入的expect就可以派上用场。expect脚本可以分两步处理#!/usr/bin/expect set timeout 30 set jump_pass Jump123 set target_pass Target123 spawn ssh -J userjump.example.com user10.0.0.5 expect { password: { send $jump_pass\r exp_continue } yes/no { send yes\r exp_continue } } expect { password: { send $target_pass\r exp_continue } } expect ~ send scp /tmp/test.txt user10.0.0.5:/data/\r expect { password: { send $target_pass\r exp_continue } 100% } expect ~ send exit\r expect eof这段脚本本质上模拟了一个完整的人工操作过程先通过跳板机连接再在目标机上执行scp碰到密码提示就填入。你甚至可以把它扩展成一段交互式运维会话的自动化。这里要提醒一句如果你的SSH客户端版本足够新-J参数配合密钥认证已经能覆盖大多数跳板机场景expect更多是作为备选方案存在而不是优先手段。4.4 调试和细节别让脚本卡在一个等待上expect脚本写起来快调试起来也可能很让人头疼。几个实用技巧分享给你。exp_internal -f /tmp/expect.log 1可以在脚本里打开调试输出把spawn的进程收到的每一条输出都记到日志里。遇到匹配不上的情况看这份日志比盲猜高效得多。set timeout控制所有expect等待的超时秒数默认是10秒对于scp传大文件这个时间太短了建议根据文件大小调到30到60秒。如果是大文件传输expect的匹配模式里最好不要死等某个进度字符串直接用eof收尾更稳妥。密码尽量不要硬编码在脚本文件里可以通过环境变量传进来#!/usr/bin/expect set password $env(APP_PASS) spawn scp /data/app.tar.gz deploy192.168.1.10:/data/ expect password: { send $password\r } expect eof运行前先执行export APP_PASSMyPassword123脚本从环境变量里取密码。这样脚本本身不包含任何明文密码即使被别人看到也不至于泄露凭据。5. 三个方案怎么选场景、安全性和维护成本对比讲完了密钥、sshpass、expect三种方案很多读者可能会问我到底该用哪一个这个问题没有标准答案取决于你的场景、安全要求和长期维护意愿。先给一张对照表方便快速决策对比维度SSH密钥认证sshpassexpect脚本配置难度低一次性配置很低装完即用中等需要懂匹配逻辑密码执行时的安全面无明文密码最安全密码明文存储有泄露风险密码可由外部传入但脚本逻辑复杂应对首次连接交互自动处理无法处理yes/no可以处理yes/no应对双因子/跳板机配合ProxyJump可以实现基本无能为力能模拟完整交互可以覆盖批量自动化的稳定性高无交互即无等待高适合简单密码认证中匹配逻辑依赖输出稳定性适用场景长期、可自主控制配置的服务器临时、不允许改服务器配置的密码认证复杂认证链路、动态交互我能给出的个人建议是第一凡是你能控制对方服务器配置的场景一律优先用SSH密钥。它是唯一一个能让密码彻底消失的方案天然免疫密码泄露、密码过期、明文存储这些问题脚本里也干干净净没有任何敏感信息。定时任务、CI/CD流水线中的文件传输我都强烈推荐先配好密钥认证。第二临时场景、对方服务器不允许改配置、或者你只是偶尔手动执行一条scp用sshpass就够。它最大的价值是简单直接一装一用不需要写逻辑。安全方面只要把密码文件权限管好避免在命令行直接暴露日常使用问题不大。第三遇到跳板机、验证码、多重提示等复合交互才考虑用expect。expect是兜底方案它赋予你几乎无限的交互控制能力但同时也意味着你要维护一份脚本逻辑。脚本匹配规则一旦和服务器输出格式对不上就会卡住或超时所以非必要不轻易上expect上之前最好先把对方的提示输出结构摸清楚。6. 实战避坑记录自动化传输中最容易翻车的几个细节6.1 Permission denied 出现时按这个顺序排查自动化传文件最让人头大的错误就是Permission denied。很多人第一反应是密码错了但实际原因五花八门我建议按下面的顺序排查。先确认用户名和密码本身没问题手动执行一次scp试试排除最基础的原因。再检查目标目录是否有对应的写权限有时候你传文件到/data但目标用户对/data只有读权限也会报类似Permission denied的错误。如果用的是密钥认证重点看是否因为前面说的权限问题导致公钥被忽略。跑一条ssh -v userserver查看详细日志里有没有Authentications that can continue和Server accepts key这样的关键行。如果服务端根本没接受你的公钥日志里会明确写出来。还要留意sshd_config里的AllowUsers、DenyUsers和MaxAuthTries配置这些配置都可能直接拒绝特定用户的认证。我曾经排查过一个诡异问题密钥和密码都正确但一直认证失败最后发现是运维在sshd_config里限制了只能从特定IP登录。6.2 scp参数里那些容易踩的隐性坑scp的参数细节也埋着不少坑尤其对从cp命令过来的人很容易记混。端口参数是大写的-P不是小写的-p。小写的-p在scp里表示保留源文件的修改时间和访问时间这是从BSD家族的cp -p沿袭来的标志。如果你写scp -p 2222 file userhost:/pathscp并不会把2222解释成端口号而是会当成一个开关值然后连接失败。路径里有空格时本地文件路径可以加引号远程路径要额外小心。远程路径的空格在shell里需要转义比如userhost:/data/my file.txt。如果你是通过expect脚本发送命令引号嵌套会更麻烦建议在远程目录名里尽量避免空格。6.3 大批量文件同步我劝你换成rsync虽然我这篇文章主要讲scp但有一个务实建议必须说如果同步的不止一两个文件而是整个目录树scp其实不是最优选。rsync的增量传输和断点续传能力比scp的整文件复制高效得多。rsync也走SSH协议所以前面讲的sshpass和expect方案完全可以套用到rsync上只是把scp换成rsync -avz。比如sshpass -f ~/.ssh/.passwd rsync -avz --delete /data/ deploy192.168.1.10:/backup/-a表示归档模式保留权限、时间戳、软链接-v输出详细信息-z开启压缩传输--delete让目标端删除源端已经不存在的文件。唯一要注意的是源路径末尾的斜杠/data/表示同步目录内容/data不带斜杠则表示同步整个目录本身写错会导致目录层级多套一层。如果你维护的是一组持续更新的业务文件长期用rsync做准实时的镜像同步比反复执行scp更省带宽也更容易发现传输异常。6.4 让自动化脚本跑得更稳的几条实操经验不论是定时任务还是批量脚本自动化传文件这件事想做得靠谱光有免密还不够。几个我用了很久的小经验分享给你。脚本开头建议加上set -euxo pipefail。-e表示任何一条命令出错就退出-u防止使用未定义变量-x打印执行的每一条命令方便追踪pipefail让管道命令只要有一个环节失败就算整体失败。这个组合能避免很多脚本执行到一半还继续往下跑的隐患。每次传输后做个简单的完整性校验。可以传完后执行ssh host md5sum /remote/path和本地的md5sum比对确认文件字节一致。对重要的配置文件、数据库备份来说这一步省不得。日志方面把scp或rsync的输出重定向到日志文件加上时间戳方便事后排查。比如{ echo $(date) sshpass -f ~/.ssh/.passwd rsync -avz /data/ deploy192.168.1.10:/backup/ } /var/log/sync.log 21这样每次同步的时间、结果都清清楚楚记录在案哪次失败了一眼就能看到。最后如果你在多台机器上跑自动化任务建议统一用一个专用部署账号而不是直接用root。专用账号可以隐藏真实密码可以在authorized_keys里按主机隔离公钥配合sudo提权做到最小权限。这个习惯看似不起眼但遇到安全审计或故障排查时能省掉大量沟通成本。就我个人的使用习惯来说现在大部分机器已经换成密钥认证真正长期依赖sshpass的场景只剩第三方托管服务器和临时测试环境。expect则常驻在工具箱里专门对付那些奇怪的跳板机和交互流程。这套组合配合合理的日志和权限管理已经让我很久没有被自动传文件这件事困扰过了。
返回列表