尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SSH登录自定义欢迎信息:从motd到动态脚本的完整指南

SSH登录自定义欢迎信息:从motd到动态脚本的完整指南 每次打开终端甚至半夜爬起来连服务器第一眼看到的那几行字其实很有讲究。有人用它做合规告警有人用它展示负载内存有人用它标明你在哪台机器上、是生产还是测试环境。SSH登录后显示自定义信息这件事本质上是往登录会话里插入一段内容而插入的位置、时机和显示方式决定了你后续的操作体验。这篇文章想把整条链路讲清楚从/etc/motd到sshd_config的Banner从profile.d动态脚本到按用户分流的进阶玩法。适合刚开始折腾Linux服务器的新手也适合在运维团队里想统一管理登录提示的同行。我会把实际踩过的坑和调试思路一并写出来。1. SSH会话的信息注入点motd、Banner、Shell脚本到底谁在负责显示先说一个很多人搞混的地方SSH登录后能看到信息并不是只有一种机制。常见的是这四条链路它们的加载时机和触发条件完全不同机制显示时机配置位置典型用途Banner验证密码/密钥之前/etc/ssh/sshd_config 中的 Banner 参数指向一个文本文件合规免责、登录前提示/etc/motd登录会话建立后、shell 提示符出来之前/etc/motd 文件或由 pam_motd 模块读取发布公告、维护窗口提醒/etc/profile.d/ 脚本用户登录加载 shell 环境时/etc/profile.d/*.sh动态输出系统负载、IP、磁盘等实时信息~/.ssh/rc 或 /etc/ssh/sshrcSSH 认证成功后、用户 shell 启动前用户家目录或全局 sshd 配置特定于 SSH 的命令钩子一般不推荐直接输出内容理解这些时机的关键在于明白SSH会话建立的大致过程TCP连接建立后sshd先做协议和密钥交换认证通过后由sshd启动用户的登录shellshell启动过程中会读取系统profile文件然后打印motd内容最后才把提示符交给你。Banner则发生在认证之前所以哪怕密码输错了你也能先看到Banner的内容。这里有一个常见的认知误区很多人以为改了/etc/motd一定会生效结果发现登录后没反应或者只显示一半。原因往往是系统用了动态motd机制比如Ubuntu默认的update-motd它会把/run/motd.dynamic和/etc/motd拼在一起显示Debian 11之后还把motd拆成了/etc/motd.d目录。你直接写/etc/motd不是不行而是要先搞清楚你的发行版到底走哪条路。另外还要注意上面说的这些机制只有Banner和motd是登录层面的显示profile脚本其实是shell层面的它更像是一个登录后自动执行的脚本所以它除了能显示文字还可以影响环境变量和后续命令行为。这也是为什么我强烈建议如果你想展示动态信息放在profile.d里最灵活如果你想展示简单的静态公告直接写motd最省事如果你想在用户输密码前就告知他你正在进入受监控系统那得用Banner。2. 静态欢迎语实操motd文件与sshd_config的Banner配置2.1 /etc/motd基础但很多细节静态信息的经典入口是/etc/motd。这是老Unix遗留下来的传统文件作用是当日问候现在广泛被用作系统公告板。SSH登录后pam_motd模块会在会话创建时读取它并打印到终端所以你登录后看到的默认内容往往就是它。我第一次改这个文件时犯过一个错误直接往/etc/motd里写中文结果终端显示乱码。后来才意识到motd的显示和客户端终端的编码强相关纯文本加ASCII字符最稳妥。如果一定要放中文确认两端都是UTF-8多数现代发行版默认没问题但PuTTY老版本有时候需要手动设置。操作上很简单sudo tee /etc/motd EOF 这台服务器仅供授权人员使用。 所有操作将被记录请勿进行敏感操作。 如有问题请联系 opsexample.internal:12345 EOF然后重新SSH登录一次就能看到效果。注意如果你是在已登录的会话里要重新连接才会生效因为motd只在会话建立时读取一次。不过这里有几个坑需要提前避开Ubuntu的update-motd会覆盖动态生成的内容直接写/etc/motd有时候会被系统自动生成的System Information之类的行挤走或者看起来没生效。处理办法是停用update-motdsudo chmod -x /etc/update-motd.d/*或者把/etc/motd符号链接手动改成真实文件具体看发行版习惯。Debian 11及以上版本很多系统配置改成了/etc/motd.d/目录里面放多个小文件按文件名排序拼接显示。这种情况直接改/etc/motd可能正常但要养成检查的习惯。motd里不要放太多内容。终端宽度一般只有80列或120列超出部分会换行看起来非常乱。我见过有人往motd里贴一大段中文公告登录的时候糊满了整个屏幕。记住motd最合适的长度是撑满一屏但不超过一屏。2.2 Banner认证前提示的独立通道和motd不同Banner是sshd_config里配置的它会在用户输入密码之前就把内容显示出来常用于安全告警和合规声明。在/etc/ssh/sshd_config里添加Banner /etc/ssh/banner.txt然后创建文件重启sshdsudo tee /etc/ssh/banner.txt EOF 致所有访问者 未经授权请勿连接到本服务器。 连接即视为同意被监控和记录。 EOF sudo systemctl restart sshd重启后新建立的SSH连接会先看到Banner然后才进入密码输入界面。这里有一个非常关键的运维细节修改sshd_config前建议先执行一次配置语法检查然后再重启否则一旦写错容易把当前SSH会话搞断线。sudo sshd -t sudo systemctl restart sshdBanner和motd还有一个体验上的差别Banner在认证前就显示所以它更适合放硬性告警比如法律依据、监控声明、联系人信息起到一个事前告知的作用motd则更适合放操作层面的公告比如维护窗口、版本变更说明、当天注意事项。两者搭配使用时不要写重复的信息否则用户会觉得整个登录过程很啰嗦。2.3 静态方案的适用场景与取舍静态文本的好处是零依赖、加载快、基本不出错。它适合放那些短期不变的内容团队联系方式、服务器物理位置、采购编号、运维值班电话、网络策略提示等。但静态方案的问题是它无法反映服务器的实时状态。我电脑上有一台常年挂着5个Docker容器的小服务器静态motd根本看不出任何问题等发现的时候往往已经拆东墙补西墙了。所以静态信息只解决登录那一刻的告知真正要值班看板一样的信息得靠动态脚本。3. 动态系统状态展示在profile.d里注入实时信息3.1 脚本骨架动态展示的核心思路很简单在/etc/profile.d/下放一个bash脚本每次用户登录时脚本自动执行并输出系统状态。这个路径下的脚本在login shell启动时被读取因此天然适合登录后显示一段信息的需求。先给一个最小可用的骨架#!/bin/bash # /etc/profile.d/01-sysinfo.sh # 只对 bash 登录 shell 生效且不要在 SFTP/SCP 等非交互会话里打印 if ! shopt -q login_shell /dev/null; then exit 0 fi if [ -n $SSH_ORIGINAL_COMMAND ]; then exit 0 fi echo echo 主机名 : $(hostname -f 2/dev/null || hostname) echo 系统负载 : $(cat /proc/loadavg | awk {print $1, $2, $3}) echo 内存使用 : $(free -h 2/dev/null | awk /^Mem:/ {print $3 / $2}) echo 根分区 : $(df -h / 2/dev/null | awk NR2 {print $3 / $2 ( $5 )}) echo 脚本执行顺序取决于文件名前缀的数字用01-开头保证它比较早执行。这里有一个关键判断shopt -q login_shell用来确认当前bash是不是登录shell如果不是就退出这就避免了某些非登录场景比如SFTP会话、rsync拉取打印一堆干扰内容。$SSH_ORIGINAL_COMMAND是SSH服务器在用户执行远程命令时设置的环境变量如果非空说明用户实际上是ssh userhost some-command这种非交互式调用此时也不该打印横幅。3.2 常见系统指标的采集写法系统信息采集其实有很多现成工具但脚本里尽量用最基础的命令兼容性最好。我平时固定用这几个负载读/proc/loadavg取前三个数字代表1分钟、5分钟、15分钟平均负载。内存free -h解析Mem行取used/total数值。磁盘df -h /根分区使用率生产环境一般还会加/data或/opt等分区。最近登录last -n 3取最近三次登录记录这个对安全巡检很有用。当前登录用户whoami和$SSH_CONNECTION里的来源IP可以判断谁从哪里登进来的。还可以加一个上次失败登录次数的提示。注意很多系统日志文件需要root权限才能读所以脚本里最好用2/dev/null静默掉错误别因为权限问题导致整个登录过程出现警告。# 显示来源IP如果有SSH_CONNECTION环境变量 if [ -n $SSH_CONNECTION ]; then echo 来源IP : $(echo $SSH_CONNECTION | awk {print $1}) fi3.3 update-motdUbuntu特有的动态方案Ubuntu有一套独特的update-motd机制/etc/update-motd.d/目录下放可执行脚本系统每次登录时通过pam_exec或pam_motd动态生成内容写到/run/motd.dynamic。这套机制的好处是它严格走motd通道不破坏会话建立后显示这个语义坏处是脚本多、执行顺序复杂而且很多脚本会调用命令导致登录延迟。如果你想自定义有两个思路禁用不需要的脚本sudo chmod -x /etc/update-motd.d/*保留自己想要的。添加自己的脚本新建/etc/update-motd.d/97-custom-system-info内容就是一段输出加执行权限即可。#!/bin/bash # /etc/update-motd.d/97-custom-system-info uptime我个人用下来这个机制更适合发行版自带的默认体验真正的生产环境我反而更喜欢profile.d方案因为它的控制粒度更细而且脚本逻辑对运维来说更透明。3.4 容错和登录延迟动态脚本最大的隐患是登录延迟。之前在一台配置很差的低配云服务器上我加了一个脚本用ansible采集信息并输出版本号结果每次登录都慢了好几秒。排查后发现脚本里执行的命令太多了而且没有加超时限制。做动态脚本必须遵守三个原则每个命令都用2/dev/null处理错误找不到文件或者权限不足时直接跳过不能让错误输出跑到终端上。关键命令用timeout 3这类限制执行时间防止某个系统服务异常时卡住整个登录会话。脚本里尽量少用外部命令。比如要拿IP优先从hostname -I或$SSH_CONNECTION里取而不是ip addr再awk可以省下不少执行时间。实测下来一个精简的动态脚本执行时间最好控制在100毫秒以内这样对登录体验几乎没有影响。如果信息量大宁可分多个脚本也不要堆在一个脚本里。4. 按用户、按会话分流登录信息的权限控制4.1 只对交互式登录显示前面提到用SSH_ORIGINAL_COMMAND来判断这里再补充一个更彻底的办法在profile.d脚本里判断是否已经分配了TTY。if [ ! -t 0 ]; then exit 0 fi-t 0表示标准输入是否来自终端。交互式SSH登录一定会分到终端而执行远程命令、scp、rsync这类不会分配终端因此这个判断可以有效避免横幅信息破坏文件传输。我遇到过一个非常实际的问题rsync脚本因为远程shell在登录时打印了系统信息导致整个传输失败排查了很久才定位到是profile里多打了一行字。这个坑自动化运维的朋友一定要重视。4.2 区分用户与角色同样的服务器管理员和业务账号关心的信息完全不同。比如root用户登录时需要看到安全提醒普通开发者登录时需要看到服务状态而只执行部署命令的CI账号最好什么都别显示。在profile.d脚本里判断当前用户很方便if [ $LOGNAME root ]; then echo 警告当前为root用户请谨慎操作 fi还可以按用户组判断比如用id命令看是否属于ops组然后输出不同的提示。这套分流逻辑做好之后运维人员和生产账号的登录体验会清爽很多。另一个思路是把登录后动作做成用户级钩子。用户家目录下的~/.bash_profile、~/.profile、~/.bashrc都可以放自定义显示逻辑但注意这些文件的加载时机不同登录shell读取~/.profile或~/.bash_profile交互式非登录shell读取~/.bashrc。如果有人在~/.bashrc里放了一堆echo那么同一个用户在tmux里新建窗口都会看到非常烦人所以一定要把显示逻辑放在合适的文件里。4.3 hushlogin与个性化覆盖我不想看到motd的需求也很常见。SSH有一个古老的约定用户家目录下存在~/.hushlogin文件时登录时不显示motd、不显示上次登录时间、不输出邮件等待提示。这个文件几乎不占空间一行就能触发。touch ~/.hushlogin很多人以为hushlogin只影响motd其实它还会影响是否有Last login的时间提示。新员工入职配服务器时我一般建议他们把这个文件留着不然天天看那一行Last login from x.x.x.x确实没什么用。但对于profile.d脚本来说hushlogin不一定能拦住因为脚本本身不会自动去检查这个文件除非你在脚本里加逻辑if [ -f $HOME/.hushlogin ]; then exit 0 fi4.4 只在SSH会话中显示只在通过SSH登录时显示还有一个隐含条件不是所有登录都走SSH。服务器本地控制台登录、VMware控制台、串口登录、容器内exec进入的shell这些虽然也会加载profile脚本但如果没有SSH_CONNECTION这个环境变量它们和SSH登录是有区别的。如果你只想让SSH远程登录时显示信息判断变量是否非空即可if [ -z $SSH_CONNECTION ]; then exit 0 fi反过来如果希望无论本地还是远程都显示那就不用判断直接在脚本里输出。在我工作过的环境里团队更倾向本地登录显示同样的系统信息因为故障排查时本地控制台一样需要看到磁盘状态。5. 排版、颜色与编码别让你的信息变成一团乱码5.1 对齐和printf系统信息展示最怕的就是飘——字段长度不一样输出看起来歪歪扭扭。这里推荐用printf来对齐printf %-12s %s\n 主机名: $(hostname) printf %-12s %s\n 系统负载: $(cat /proc/loadavg) printf %-12s %s\n 内存使用: $(free -h | grep Mem | awk {print $3 / $2})%-12s表示左对齐固定宽度12字符。这样输出出来标签栏整齐划一肉眼扫过去就知道每行数据对应什么。我之前用echo加一堆空格在终端宽度变化时直接爆掉换成printf之后舒服很多。5.2 颜色与tput颜色能让信息区分优先级比如负载过高用红色正常用绿色。但不要直接写死ANSI转义序列推荐用tputGREEN$(tput setaf 2) YELLOW$(tput setaf 3) RED$(tput setaf 1) RESET$(tput sgr0) load$(cat /proc/loadavg | awk {print $1}) if [ $(echo $load 1.0 | bc) 1 ]; then echo ${RED}负载偏高: $load${RESET} else echo ${GREEN}负载正常: $load${RESET} fi注意tput sgr0是重置所有属性比\e[0m更规范。颜色不要用于纯文本motd文件因为motd经过部分工具读取时会把转义序列当普通文本处理反而产生一堆乱码。动态脚本里可以用颜色因为终端能解释它们。5.3 UTF-8、终端宽度和日志污染编码问题实际上贯穿整个登录提示motd、Banner、动态脚本里的中文只要终端编码不匹配必然乱码。多数现代SSH客户端默认UTF-8Windows上的老版本终端的编码设置不同容易踩坑。如果你需要显示多语言内容最稳妥的是在脚本里先输出一个中文名字的ASCII对照比如主机名(hostname)这种既保留可读性又避免纯中文在某些老终端里打翻。终端宽度是另一个容易被忽视的问题。要读取COLUMNS环境变量动态调整宽度比较麻烦。我一般采取折中策略横幅总宽不超过60列内容行不超过80字符。这样即使终端宽度只有80列xterm默认也不会自动换行观感稳定。最后是日志污染。动态脚本如果写得不严谨在非登录shell中被执行输出的内容会被一些系统工具当成命令结果处理。我写过一次大乌龙在profile里加了一句echo 欢迎结果cron的任务邮件和系统日志里到处都是这行字排查了半天才发现是这个原因。所以动态脚本务必用login_shell加上SSH_ORIGINAL_COMMAND双重判断从根源上拦截非交互会话。6. 多服务器批量管理的实践建议当你管理的机器从个位数变成几十上百台逐台手工改motd和脚本就完全不现实了。这里给出我的实践方案。6.1 Ansible推送模板以Ansible为例把motd和动态脚本做成不可变基础设施的一部分。项目结构长这样ops/ansible/playbooks/login_message.yml ops/ansible/files/motd.template ops/ansible/templates/sysinfo.sh.j2playbook里定义两个任务一个写静态motd一个分发动态脚本到profile.d- name: 部署静态 motd ansible.builtin.copy: src: files/motd.template dest: /etc/motd owner: root group: root mode: 0644 - name: 部署动态系统信息脚本 ansible.builtin.template: src: templates/sysinfo.sh.j2 dest: /etc/profile.d/01-sysinfo.sh owner: root group: root mode: 0755 notify: 无 # 脚本逻辑在下次登录时生效无需重启任何服务J2模板的好处是可以按主机变量动态生成内容比如在sysinfo.sh.j2里引用hostname、部署环境、业务标签一台机器一套信息。模板里还可以生成一份当前负责人的变量新同事接手的时候直接看motd就知道找谁。6.2 统一脚本维护与Git版本管理动态脚本一旦分散到多台机器你就必须有版本管理意识。我的做法是把这些脚本放进一个独立的Git仓库目录结构模拟/etc/profile.d的排序文件名前缀统一从01开始。每次修改后提交然后在Ansible里指定版本号或分支实现全平台一致。一个很重要的文件名约定不要用sysinfo.sh这种通用名用01-system-status.sh、02-security-banner.sh这种带排序和用途的名称。文件名就是执行顺序也是管理边界。后续加新功能只需要新增一个文件而不用改老文件减少了对现有状态的影响。6.3 测试与回滚批量改登录提示的坏消息是问题不会在改完的瞬间暴露而在你下次登录时才发现。所以必须有一套测试流程先挑一台非生产备用机登录验证motd和动态脚本都正常展示。检查非交互行为ssh host uptime、scp file host:/tmp/、sftp host确认没有附加输出。验证特殊终端宽度窗口拉窄到60列看看是否换行。回滚预案备份旧文件Ansible的回滚方式则是把playbook的版本回退到上一个tag后重跑。具体的检查命令可以写成一个小脚本放到仓库里作为发布前检查清单ssh -T localhost echo OK # 不应输出横幅 scp /etc/hosts localhost:/tmp/ # 不应出现横幅干扰 ssh localhost uptime # 正常输出命令结果这套流程跑下来登录提示这种看似小功能的改动就不会变成线上事故。最后分享两个我自己的习惯。第一motd和profile脚本分开管理motd里只放静态公告和维护窗口信息profile脚本里放实时状态和动态告警两类内容互不掺和接手的人一眼就能看懂结构。第二每个动态脚本开头都要写注释标明这个脚本是哪个项目、什么时候加的、回滚方法是什么工程化思维能帮你避免很多原本可以靠约定解决的麻烦。
返回列表