☰
SSH登录自定义欢迎信息:从motd到动态脚本的完整指南
2026/10/6 16:20:06 网站建设 项目流程

每次打开终端甚至半夜爬起来连服务器,第一眼看到的那几行字,其实很有讲究。有人用它做合规告警,有人用它展示负载内存,有人用它标明"你在哪台机器上、是生产还是测试环境"。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启动用户的登录shell;shell启动过程中会读取系统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' =================================== 这台服务器仅供授权人员使用。 所有操作将被记录,请勿进行敏感操作。 如有问题请联系 ops@example.internal:12345 =================================== EOF

然后重新SSH登录一次就能看到效果。注意,如果你是在已登录的会话里,要重新连接才会生效,因为motd只在会话建立时读取一次。

不过这里有几个坑需要提前避开:

  • Ubuntu的update-motd会覆盖动态生成的内容,直接写/etc/motd有时候会被系统自动生成的"System Information"之类的行挤走,或者看起来没生效。处理办法是停用update-motd:
sudo 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

然后创建文件重启sshd:

sudo tee /etc/ssh/banner.txt <<'EOF' ========================================== 致所有访问者: 未经授权,请勿连接到本服务器。 连接即视为同意被监控和记录。 ========================================== EOF sudo systemctl restart sshd

重启后新建立的SSH连接会先看到Banner,然后才进入密码输入界面。这里有一个非常关键的运维细节:修改sshd_config前,建议先执行一次配置语法检查,然后再重启,否则一旦写错,容易把当前SSH会话搞断线。

sudo sshd -t sudo systemctl restart sshd

Banner和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 'NR==2 {print $3 "/" $2 " (" $5 ")"}')" echo "========================================"

脚本执行顺序取决于文件名前缀的数字,用01-开头保证它比较早执行。这里有一个关键判断:shopt -q login_shell用来确认当前bash是不是登录shell,如果不是就退出,这就避免了某些非登录场景(比如SFTP会话、rsync拉取)打印一堆干扰内容。$SSH_ORIGINAL_COMMAND是SSH服务器在用户执行远程命令时设置的环境变量,如果非空,说明用户实际上是ssh user@host 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}')" fi

3.3 update-motd:Ubuntu特有的动态方案

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 fi

4.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转义序列,推荐用tput:

GREEN=$(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-8,Windows上的老版本终端的编码设置不同,容易踩坑。如果你需要显示多语言内容,最稳妥的是在脚本里先输出一个中文名字的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.j2

playbook里定义两个任务,一个写静态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脚本里放实时状态和动态告警,两类内容互不掺和,接手的人一眼就能看懂结构。第二,每个动态脚本开头都要写注释,标明这个脚本是哪个项目、什么时候加的、回滚方法是什么,工程化思维能帮你避免很多原本可以靠"约定"解决的麻烦。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询