
刚接手一批服务器的时候我最怕看到的不是报错而是 SSH 上去第一屏弹出这样一行字Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-152-generic x86_64)后面跟着一长串系统更新提示、公网 IP、还有厂商塞的推广链接。这事儿看着小实际上暴露了两件事一是这台机器的登录欢迎界面没人打理二是它的输出内容把内核版本、发行版版本号全抖搂出来了。Linux 终端登录欢迎界面这个东西说简单点就是几个文本文件说复杂点它牵扯到 agetty、PAM、login、sshd、shell 启动流程整整一条链路。这篇就把我这些年改欢迎界面的经验完整捋一遍从字是谁打出来的讲到怎么搭一套既不泄露信息、又不拖慢登录、还能显示实时状态的面板中间踩过的坑一并放上。适合天天 SSH 到服务器上的运维、后端、嵌入式工程师也适合刚学 Linux 想搞明白登录流程的朋友——看完你至少能判断出你屏幕上那行字到底归谁管。1. 一条登录横幅背后其实有三拨人在抢着说话很多人第一次改欢迎界面是直接vi /etc/motd写完发现 SSH 上去后面还是多出来一大段厂商提示换个思路去改/etc/issue结果本地 tty 变了、SSH 一点没变。这类改了没用或者改了冒出两份的问题根源都在于没搞清楚打印顺序和责任人。1.1 从 agetty 到 shell 的完整打印链路一次典型的本地登录顺序大致是这样的内核启动完成后拉起gettytty1.service实际执行的是agettyagetty 打开 tty、设置波特率然后在提示 login: 之前把/etc/issue的内容打印到屏幕上。你输入用户名密码login程序通过 PAM 完成认证认证成功后 PAM 的 session 阶段里pam_motd负责打印/etc/motd或/run/motd.dynamic以及pam_lastlog打出的 Last login: ...。再往后login拉起你的 shell而且是以 login shell 的方式拉起的于是/etc/profile被读取/etc/profile.d/下面所有.sh被依次 source最后才轮到~/.bash_profile或~/.bashrc。远程 SSH 登录的链路是另一条sshd 完成认证如果sshd_config里配了Banner这个文件的内容会在认证之前就被发到客户端认证之后 sshd 走 PAM 的 session 阶段/etc/pam.d/sshd里的pam_motd干活随后 sshd 启动用户 shell同样会读/etc/profile和/etc/profile.d/。所以你在 SSH 上看到的那一屏字很可能是三四个不同组件在不同时刻打出来的拼接结果它们之间互不知情这也是重复显示的根本原因。1.2 本地登录和远程登录走的是两套完全不同的路径这个区别必须刻在脑子里否则你会一直困惑为什么改了 issue 远程没变化。/etc/issue只有 agetty 读而 agetty 只在本地虚拟控制台tty1-tty6、串口控制台ttyS0这类场景下运行。SSH 登录全程没有 agetty 参与/etc/issue对 SSH 完全无效。反过来/etc/motd是 PAM 打的本地和远程都会走前提是 PAM 配置里有pam_motd所以想在 SSH 上看到欢迎语改 motd 才是正路。还有一个容易被忽略的分叉/etc/profile.d/只对login shell生效。什么是 login shellssh登录进来的第一个 shell、su -、sudo -i、bash --login都是而在图形界面里打开的终端窗口、直接敲bash起的子 shell属于 non-login interactive shell它们只读~/.bashrcDebian 系还读/etc/bash.bashrcRHEL 系读/etc/bashrc。所以如果你把欢迎脚本放在/etc/profile.d/会发现SSH 有、桌面终端没有这是设计如此不是坏了。1.3 一张表看清每个文件是谁负责打印的文件/位置打印者生效场景时机转义字符/etc/issueagetty本地 tty、串口认证前支持如\n\l\d/etc/issue.nettelnetd 遗留 / sshd Banner认证前认证前sshd 下不解析原样输出/run/motd.dynamicpam_motdUbuntu本地 SSH认证后不支持/etc/motdpam_motd本地 SSH认证后不支持/etc/update-motd.d/*pam_motdDebian 系补丁本地 SSH认证后不支持脚本自己产内容/etc/profile.d/*.shshelllogin shell本地 SSHshell 启动时无自己产内容/etc/bash.bashrc、/etc/bashrcshell交互式本地 SSHshell 启动时无sshd_config的Bannersshd仅 SSH认证前不支持sshd_config的PrintMotdsshd仅 SSH认证后不支持这张表建议对着自己的机器核一遍。看一台机器到底是谁在打印最简单的办法是grep -rn motd /etc/pam.d/看看/etc/pam.d/login、/etc/pam.d/sshd、/etc/pam.d/su里有没有pam_motd.so那一行以及它带的motd参数指向哪个文件。2. /etc/issue 与 /etc/issue.net最早出场、也最容易踩坑的两个文件/etc/issue是整条链路上出场最早的文件agetty 在你还不能输入任何命令的时候就把它打出来了。它的内容天然适合放一些登录前的提示比如主机用途、归属团队、维护窗口提示。但它也是最容易翻车的一个因为它有一套自己的转义语法而且这套语法不是 shell 的转义语法两者长得很像含义完全不同。2.1 agetty 的转义序列到底能取到哪些信息agetty 的 issue 文件支持一批反斜杠转义常用的有这几个\n展开为主机名node name\l是当前 tty 的名字比如 tty1\d是当前日期\t是当前时间\s是操作系统名Linux\r是内核版本\m是机器架构x86_64 之类\u/\U是当前登录用户数\o是域名\4和\6分别是第一个全局 IPv4/IPv6 地址\b是当前线路波特率串口场景很有用\e是 ESC 字符\\输出一个反斜杠。这里有个反直觉的点\n在 issue 里不是换行是主机名。我见过不止一个人写了/etc/issue里一行\n想换行结果登录时看到主机名被打了两遍一脸懵。想要换行就直接敲回车文件里怎么写就怎么显示。另一个高频需求是在横幅里显示 IP。\4在较新的 agetty 上可用但老版本比如 CentOS 7 自带的 util-linux 2.23里没有这个转义写了就原样输出\4。判断版本可以agetty --version实在不确定就用/etc/profile.d/里的脚本自己取稳妥得多。2.2 转义字符不生效的三种典型原因第一种是权限和路径问题。/etc/issue必须是普通文件、全局可读644 足够如果被改成 600agetty 读不到就什么都不显示。第二种是 agetty 启动参数带了-i/--noissue这个参数的意思是不显示 issue 文件。要确认当前生效的命令行直接systemctl cat gettytty1.service看ExecStart那一行就行。第三种最常见你以为你在看/etc/issue其实看到的是 motd 的内容只是两段文本长得差不多。定位办法是临时把/etc/issue内容改成一个独一无二的字符串比如ISSUE_TEST_123重新登录看它出不出现。2.3 IssueFile 与 --noissue 的配置位置在 systemd 系统上想改本地登录的行为正确姿势是写 drop-in不要直接改/usr/lib/systemd/system/getty.service——那个文件会被包管理器覆盖。操作是这样sudo systemctl edit gettytty1编辑器里填[Service] ExecStart ExecStart-/sbin/agetty --noissue --noclear %I $TERM注意ExecStart那行空赋值是必须的作用是把原来的启动命令清空否则会变成两条 ExecStart 冲突。另外 agetty 的路径在不同发行版不一样/sbin/agetty、/usr/sbin/agetty都见过用command -v agetty确认一下再填。改完systemctl daemon-reload然后systemctl restart gettytty1——注意如果你正在这个 tty 上操作重启会把你踢出去所以换个 tty 或者在 SSH 里操作。串口控制台对应的单元是serial-gettyttyS0.service同样的改法。嵌入式设备上这套用得很多因为它往往是唯一的登录入口横幅里写清楚设备型号、固件版本、调试口用途比任何文档都管用。至于/etc/issue.net它现在唯一还有意义的用法是给 sshd 的Banner用。重点来了sshd 发送 Banner 文件时是原样字节输出不做任何转义展开。如果你把Banner指向/etc/issue用户会看到一行字面量\n \l \4非常难看。所以 Banner 用的文件里只能写死内容需要动态信息就只能靠脚本在别处生成、再让 sshd 读取——但那又变成认证后了做不到认证前显示。3. /etc/motd 的水比想象中深谁在打印会不会打印两遍motd 是 message of the day 的缩写绝大多数人的第一反应就是改/etc/motd呗。这个思路在 RHEL 系上基本正确在 Debian/Ubuntu 系上大概率会让你困惑好一阵——因为你改完发现屏幕上一点变化都没有或者变化只出现了一部分。3.1 Debian/Ubuntu 的 update-motd.d 动态生成机制Ubuntu 引入了一套 update-motd 机制/etc/update-motd.d/目录下放一堆可执行脚本文件名用两位数字前缀排序PAM 在登录时按顺序执行它们把输出拼起来当作欢迎信息显示。常见的脚本有00-header打印 Welcome to Ubuntu 那行、10-help-text、50-motd-news、90-updates-available、91-contract-ua-esm-status、98-reboot-required等等。这里有两个必须知道的前提。第一脚本必须有可执行位chmod x漏了它会被静默跳过连报错都没有这是我最常帮人排查的问题。第二文件名前缀决定顺序00开头的最先输出。所以想让自己的内容排在最上面用00-或01-前缀想让它压轴用99-。在 Ubuntu 上改欢迎界面的推荐做法是新建/etc/update-motd.d/99-my-welcome而不是去覆盖00-header。原因很简单00-header是软件包自带的文件系统升级时会问你配置文件冲突保留哪个处理起来烦而且它默认会打印发行版版本号本身就不是你想留的内容。真要改先把原文件备份或者直接dpkg -S查一下归属知道自己动的是什么。顺便说一句/etc/update-motd.d/里那些厂商脚本尤其是打印安全更新数量、订阅状态的那几个在某些环境里会做网络请求登录时能卡好几秒。要不要留看你的场景内网机器基本都可以删掉或者加个exit 0。3.2 SSH 登录看到两次 motd 的真正原因欢迎语打了两遍是社区里被问得最多的问题之一原因几乎总是同一个sshd 的PrintMotd和 PAM 的pam_motd同时在干活。sshd 有个配置项PrintMotd默认值各发行版不一致。在 Debian/Ubuntu 上因为 PAM 那边已经负责打印了所以发行版通常把PrintMotd关掉避免重复但如果你从别处复制了一份sshd_config或者用配置管理工具统一推了一份带PrintMotd yes的文件立刻就会出现两份。RHEL 系反过来历史上主要靠 sshd 自己打印/etc/motd。排查这条问题的完整路径是这样的先grep -i printmotd /etc/ssh/sshd_config看 sshd 侧怎么配的再grep -rn pam_motd /etc/pam.d/看 PAM 侧有几处然后确认/etc/pam.d/sshd里pam_motd.so那行带的motd参数指向哪个文件——Ubuntu 上通常是motd/run/motd.dynamic noupdatenoupdate表示不要重新跑 update-motd 脚本、直接用已经生成好的那份。改完sshd_config要sshd -t验证语法、再systemctl reload sshd。还有一种看起来打了两遍其实是两段不同内容一段是 pam_motd 打的 motd另一段是 shell 启动时/etc/profile.d/里的某个脚本打的。这种情况内容措辞往往不太一样仔细看就能分辨。3.3 一个被低估的小开关~/.hushlogin如果只是某个用户不想看这套东西比如自动化账号、跑批账号不要在系统层面删文件直接在他家目录里touch ~/.hushlogin就行。login 和 sshd 看到这个文件会跳过 motd 和 last login 那行输出。这个开关的好处是粒度细、可回退、不影响别人。sshd_config里的PrintLastLog控制认证后那行 Last login: ... from ...它来自pam_lastlog新版本叫pam_lastlog2。把这行关掉能少一次 wtmp 读取在 wtmp 特别大的老机器上能省下一点登录延迟。但我不建议关因为上次登录时间/IP是第一手的异常登录信号留着有用。4. 把欢迎界面做成运维仪表盘动态信息的取法与性能红线静态横幅解决的是知道自己在哪台机器上动态面板解决的是一眼看清这台机器现在什么状态。后者价值高得多但坑也集中在这里——很多看起来很美的欢迎脚本实际上让每次登录慢了两三秒甚至有卡死十分钟的。4.1 一个可以直接抄的 /etc/profile.d 版欢迎脚本先给一个我用了很久、改过好几轮的版本。放在/etc/profile.d/99-welcome.sh权限 644、属主 root:root#!/bin/bash # /etc/profile.d/99-welcome.sh # 只在交互式 shell 中执行scp/sftp/rsync 等非交互调用直接返回 case $- in *i*) ;; *) return ;; esac # 没有终端或哑终端直接不打印避免污染管道输出 [ -n $TERM ] || return [ $TERM dumb ] return [ -t 1 ] || return # 颜色先探测终端能力探测不到就退化成纯文本 if command -v tput /dev/null 21 [ $(tput colors 2/dev/null || echo 0) -ge 8 ]; then C_TITLE$(tput setaf 6) C_KEY$(tput setaf 2) C_VAL$(tput setaf 7) C_WARN$(tput setaf 3) C_OFF$(tput sgr0) else C_TITLE; C_KEY; C_VAL; C_WARN; C_OFF fi # 所有取数尽量读 /proc不调外部重命令 read -r L1 L2 L3 _ /proc/loadavg MEM_TOTAL$(awk /^MemTotal:/{print $2} /proc/meminfo) MEM_AVAIL$(awk /^MemAvailable:/{print $2} /proc/meminfo) MEM_USED_PCT$(( (MEM_TOTAL - MEM_AVAIL) * 100 / MEM_TOTAL )) UPTIME_SEC$(cut -d. -f1 /proc/uptime) UPTIME_D$(( UPTIME_SEC / 86400 )) UPTIME_H$(( (UPTIME_SEC % 86400) / 3600 )) ROOT_PCT$(timeout 2 df -h --outputpcent / 2/dev/null | tail -n1 | tr -dc 0-9) LAST_LOGIN$(timeout 2 last -n 1 $LOGNAME 2/dev/null | head -n1) printf \n%s%s%s\n $C_TITLE $C_OFF printf %s 主机%s : %s (%s)\n $C_KEY $C_OFF $(hostname -f 2/dev/null || hostname) $(uname -m) printf %s 运行%s : %s天%s小时 负载 %s %s %s\n $C_KEY $C_OFF $UPTIME_D $UPTIME_H $L1 $L2 $L3 printf %s 内存%s : 已用 %s%% 根分区 %s%%\n $C_KEY $C_OFF $MEM_USED_PCT ${ROOT_PCT:-?} printf %s 时间%s : %s\n $C_KEY $C_OFF $(date %Y-%m-%d %H:%M:%S %Z) [ -n $LAST_LOGIN ] printf %s 上次%s : %s\n $C_KEY $C_OFF $LAST_LOGIN printf %s%s%s\n\n $C_TITLE $C_OFF这段脚本里有几个刻意的设计值得单独说。case $- in *i*)判断交互式是最廉价的守卫比判断$PS1更可靠[ -t 1 ]保证输出目标是终端这样ssh host cat file local.txt这类调用不会被塞进横幅文字颜色能力先探测再使用tput colors返回小于 8 就退化成纯文本能兼容老终端和日志采集。内存和负载全部从/proc直接读不 fork 外部进程比free、uptime快一个数量级。4.2 为什么我从来不把慢命令直接写进 motd有一次接到反馈说某台机器登录要等十几秒查了半天是 motd 脚本里写了一行df -h而机器上挂着一个已经失联的 NFS 目录df在遍历挂载点时卡住了。类似的还有curl一个外网接口取公网 IP断网就等到超时、last在 wtmp 涨到几百 MB 的机器上扫全表、systemctl list-units在几百个服务的机器上跑一秒多、du -sh /var直接跑几十秒。我的原则是三条。第一能读/proc、/sys就不调外部命令。第二非要调外部命令一律套timeout取值 1 到 2 秒。第三绝不在登录路径上做网络请求。第三条尤其重要——欢迎界面是每次登录都会跑的代码任何一次网络抖动都会变成用户体验问题。还有一点如果这些脚本放在/etc/update-motd.d/它们是在每次 PAM session 建立时执行的也就是说su -、sudo -i甚至某些图形登录场景都会触发。曾经有个同事在里面放了个生成随机名言的命令结果批量运维脚本里嵌了sudo -i几百台机器同时跑把本地生成工具跑出了 CPU 尖峰。欢迎界面这东西代码量越小越安全。4.3 非交互式 shell 重复打印的坑再说一个几乎人人都踩的坑同一个会话里欢迎语出现两次。常见触发方式是ssh host进去看到一次然后sudo -i又看到一次——因为sudo -i会以 login shell 方式启动 root 的 shell重新读/etc/profile和/etc/profile.d/su -同理。另外tmux或screen里新开窗口一般不会重复因为它不启动 login shell。彻底解决有三种思路一是在脚本里用环境变量打标记比如[ -n $WELCOME_SHOWN ] return; export WELCOME_SHOWN1但要注意sudo默认不继承任意环境变量得配合env_keep复杂度上来了二是接受它把脚本写得轻量多跑一次也不心疼三是把欢迎内容交给 motdPAM 打的它在su -场景下同样会再打一次问题依旧。我个人的选择是方案二因为最省心而且从用户角度每次拿到一个新 shell 都看到一次环境概览并不是坏事。5. 配色、编码与终端兼容同样的脚本在别人机器上就花了欢迎界面最终是要显示在别人的终端里的而别人的终端这件事不确定性比你想的大得多。我见过同一份脚本在我的窗口里排版整齐在同事的 Xshell 里颜色全丢、在另一个人的 PuTTY 里中文变问号、在串口终端里直接刷屏。5.1 用 tput 而不是硬编码 ANSI 序列很多教程直接写echo -e \033[36m...这在大多数现代终端上没问题但有三个隐患。一是echo -e是 bash 的扩展如果脚本被/bin/sh有些系统上是 dash执行行为不一致二是 POSIX 的echo对反斜杠的处理是实现定义的用printf更稳三是\033[这套是 ANSI 序列遇到TERMdumb或者在把输出重定向进日志的场景下会留下一堆^[[36m这种垃圾字符。正确姿势是用tput setaf 6这类能力查询它会根据$TERM返回该终端真正支持的控制序列再配合tput sgr0复位。还有个必须做的动作每一行输出结尾都要复位颜色。忘了复位的结果是你后面所有的 shell 提示符、ls输出全变成一个颜色用户会以为终端坏了。我在自己脚本里的做法是每行printf都带上$C_OFF并且最后再来一次全局复位宁可多打几个字节。如果配色不是刚需最稳的方案其实是完全不用颜色靠排版字符、|、-和缩进做视觉分组。这样在日志采集、串口终端、老式客户端里都不会出问题。5.2 中文乱码与 locale 的两层坑中文乱码这件事要分两层看。第一层是编码脚本文件本身必须是 UTF-8 保存终端也要按 UTF-8 解码这两边一致才不会乱。如果 SSH 客户端的字符编码设置成了 GBK而服务器输出 UTF-8那看到的就是一串问号或方块。这个在 Windows 上的老客户端里特别常见。第二层更深本地虚拟控制台tty1-tty6默认没有中文字库。你在 SSH 里写的中文横幅漂漂亮亮切到本机 tty 一看全是方块——因为内核控制台用的是内嵌的点阵/矢量字体多数发行版默认只装了拉丁字符集。想让它显示中文需要额外装 CJK 控制台字体并加载操作量大、兼容性差性价比极低。所以我的结论很明确/etc/issue这种本地控制台专用的文件一律用英文/ASCII中文内容只放在 motd 和 profile.d 里也就是只在 SSH 场景出现的地方。这一条能省掉你后面 90% 的乱码投诉。5.3 不同终端工具对颜色和宽度的处理差异终端模拟器之间的差异集中在三点颜色支持8 色、16 色、256 色、真彩、字符宽度计算全角字符占两列、以及TERM变量的取值。现代的 Tabby、Windows Terminal、iTerm2 这些对 256 色和真彩支持都很好一些老式的 SSH 客户端可能只认 8 色甚至连setaf都不完全支持需要退化成tput setf老式写法。宽度问题更隐蔽如果横幅里有中文而某个终端把中文按 1 列计算右边框就会对不齐。解决办法是不做右侧封口的边框只用左侧分组或纯分隔线这样宽度算错也不影响观感。我在给团队做统一横幅时最后就是放弃了带右边框的方框排版——好看但太脆。判断能力可以直接在服务器上执行几条命令看结果echo $TERM、tput colors、tput cols。注意tput cols在非终端环境下会报错所以脚本里要做容错拿不到就默认按 80 列处理。6. 生产环境的边界哪些东西绝对不能写进欢迎界面欢迎界面是所有能登录的人都会看到的内容它的传播范围等于系统的访问范围。这一点决定了它和信息安全的关系比一般配置文件更近。6.1 信息泄露与合规声明第一件要删掉的就是内核版本、完整发行版版本号、内网 IP、主机名里的业务语义。很多默认横幅尤其是 Ubuntu 的00-header会把完整的内核版本打出来这在日常运维里很方便但对一个暴露在更大网络里的机器来说等于主动告诉别人我是什么版本、可能有哪些已知问题。虽然这不是唯一的信息来源但没必要的暴露就是没必要的风险。第二件是登录前声明。有些团队的内控要求登录前显示一条使用声明比如本系统仅限授权人员访问所有操作将被记录。这种内容放在 sshd 的Banner里认证前显示比放在 motd 里更合适因为它能覆盖输了密码但没输对的场景。写的时候只用纯文本别加转义字符原因前面说过了。第三件是要注意别在横幅里写运维细节比如数据库密码在 /xxx、联系某某手机号。横幅会被截图、会被贴到工单里、会进监控日志写在上面的东西等于公开。6.2 备份、灰度与回滚改登录相关文件之前备份是必须的但更重要的是留好后路。我习惯这样做sudo mkdir -p /root/banner-backup-$(date %F) sudo cp -a /etc/issue /etc/issue.net /etc/motd /root/banner-backup-$(date %F)/ 2/dev/null sudo cp -a /etc/update-motd.d /root/banner-backup-$(date %F)/ 2/dev/null sudo cp -a /etc/pam.d/sshd /root/banner-backup-$(date %F)/ 2/dev/nullcp -a保留权限和属主对/etc/update-motd.d这种依赖权限位的目录尤其重要。回滚就是把文件拷回去、恢复权限位、必要时重跑update-motd。灰度方面我的做法是先在一台跳板机或者一台自建测试机上全流程验证包括本地 tty 登录、SSH 登录、scp传文件、ssh host cmd执行单条命令这四种场景。特别要验证scp和rsync如果横幅污染了标准输出自动化脚本会被莫名其妙地打挂而且报错信息通常非常误导人。我用过的检查方式是ssh host echo OK的输出必须干干净净只有OK一行。6.3 容器、WSL、Termux 等特殊环境怎么办容器里没有 agetty也没有完整的 logindocker exec -it走的是另一条路径/etc/issue完全不会显示。能生效的只有 shell 启动时读取的那批文件/etc/profile、/etc/profile.d/、~/.bashrc。所以在容器镜像里加欢迎语正确位置是这些地方而且要注意镜像里可能根本没有tput尤其在精简镜像里脚本必须能优雅降级。WSL 的情况又不一样它通常会执行 login shell所以/etc/profile.d/和发行版自带的 update-motd 都会跑。在 WSL 里改 Ubuntu 的欢迎语还是那套update-motd.d的做法。但 WSL 的启动频率非常高每次开窗口都跑一遍所以脚本要更轻尤其别在里面做apt list --upgradable这类慢操作。Termux 是安卓上的环境没有/etc/issue也没有 PAM它的欢迎语来自$PREFIX/etc/motd由相关包生成和管理。想自定义我建议不去动那个文件会被工具覆盖而是在~/.bashrc或$PREFIX/etc/profile.d/里加自己的输出。Termux 的场景下还有个小细节屏幕窄横幅宽度最好控制在 40 列以内否则换行会乱七八糟。7. 从零搭一套完整落地步骤与验收清单把前面这些串起来给一套我自己在用的落地流程。目标是登录时看到一块干净的状态面板不含版本号不拖慢登录不影响自动化。7.1 目录结构与文件命名我的习惯是用一个目录集中放方便整体回滚sudo mkdir -p /etc/welcome.d sudo tee /etc/welcome.d/banner.txt /dev/null EOF ------------------------------------------ 本机为内部测试环境仅限授权人员访问 所有登录与操作均会被记录 ------------------------------------------ EOF sudo chmod 644 /etc/welcome.d/banner.txt再把状态面板脚本放到/etc/profile.d/99-welcome.sh上面第 4.1 节的版本权限 644。这样静态声明和动态面板分离改文案不用碰脚本逻辑。如果机器是 Debian/Ubuntu 并且希望 SSH 上也能看到就把面板脚本也放到/etc/update-motd.d/98-status记得sudo chmod 755。7.2 分场景验证方法改完之后不要只验证一种登录方式按这个清单走# 1. 语法与来源检查 bash -n /etc/profile.d/99-welcome.sh ls -l /etc/update-motd.d/98-status # 2. 非交互式调用不能被污染 ssh localhost echo MARKER | cat -A # 3. 模拟 login shell 看输出 env -i HOME$HOME TERMxterm-256color bash -lc true # 4. 检查执行耗时超过 0.3s 就要优化 time (env -i HOME$HOME TERMxterm-256color bash -lc true /dev/null)第 2 条的cat -A会显示不可见字符如果输出里出现了^[[36m这类东西说明颜色没有被正确复位或者脚本在不该打印的时候打印了。第 4 条是我最看重的登录路径上的每一毫秒都会被乘上登录次数0.3 秒已经算上限了。本地 tty 的验证必须切换到另一个虚拟控制台做按CtrlAltF3切过去登录看 effect再CtrlAltF1或F2切回来。不要在自己正在用的 tty 上重启 getty。7.3 常见故障速查表现象最可能的原因定位命令改/etc/issue后 SSH 无变化SSH 不走 agettygrep -rn motd /etc/pam.d/改/etc/motd后无变化被/run/motd.dynamic覆盖ls -l /etc/motd /run/motd.dynamic欢迎语出现两遍sshdPrintMotd与 pam_motd 冲突grep -i printmotd /etc/ssh/sshd_configupdate-motd.d 脚本不执行缺可执行位ls -l /etc/update-motd.d/横幅里出现\4、\l字面量该文件不支持转义如 Banner直接看文件内容颜色跑到提示符里去了未输出tput sgr0检查脚本每行结尾本地 tty 中文显示成方块控制台缺 CJK 字体改用 ASCII 文案登录明显变慢横幅脚本里有慢命令bash -x逐行计时排查这类问题时有个万能工具是script命令script -q /tmp/session.log会把整个终端会话包括所有控制字符录下来然后cat -v /tmp/session.log就能看到原始字节谁在什么时候打了什么一目了然。最后分享一个我用了很多年的小技巧在横幅里加一行上次登录时间和当前登录用户数就够了别塞太多。信息越多用户越会直接忽略整块内容这和监控告警刷屏是同一个道理。真正有用的欢迎界面是那种看一眼就能确认我连对机器了、机器状态正常的三秒钟面板而不是一份微型巡检报告。