Linux登录审计:last与lastlog命令实战与排查流程

发布时间:2026/10/24 5:45:55
Linux登录审计:last与lastlog命令实战与排查流程

在 Linux 运维和服务器安全排查中,有一类问题非常典型:某个服务突然异常、某台机器上出现了陌生进程、某个配置被改过,排查到一半却卡在同一个问题上——这段时间到底有哪些账号登录过这台机器?如果平时没有做过登录记录留存和审计习惯,这个问题的答案往往是“不知道”,而last和lastlog就是把这个问题从“不知道”变成“可查证”的两个基础工具。

这篇文章围绕 Linux 用户管理中的登录信息查询展开,重点拆解last和lastlog两个命令的工作原理、参数用法、输出字段含义,以及在实际审计场景中怎么把它们组合成一套可落地的排查流程。文章面向有一定 Linux 命令行基础、需要做服务器运维或安全巡检的读者,也适合刚接触 Linux 用户管理、想搞明白/var/log/wtmp这类文件到底存了什么的新手。看完之后,你应该能够独立完成一次登录记录回溯,并且知道在哪些环节容易踩坑。

1. 登录审计的底层逻辑与命令定位

1.1 一个常见场景引出的需求

假设一台对外提供 SSH 服务的 Linux 服务器,某天运维同学发现/etc/ssh/sshd_config被人改动过,或者某个定时任务里多了一条陌生的 crontab 记录。此时第一反应通常是查history,但history只对当前登录的 shell 有效,而且默认只保留有限条数,用户退出后还可能被清空,甚至连不同会话之间都不共享。更要命的是,如果是通过scp、sftp或者非交互式 SSH 命令进来的,history里根本不会留下痕迹。

这时候真正可靠的信息源是系统层面的登录记录。Linux 从 Unix 继承了一套“记账”机制,把每次登录、登出、系统启动、关机这些事件以二进制结构写入固定文件,再配套提供读取工具。这套机制的特点是:写入由系统调用完成,普通用户很难在不被发现的情况下篡改(因为文件权限受控),而且它记录的是“事件”,不依赖用户是否开了 shell。last和lastlog就是这套机制的两个读取入口,前者看的是“登录事件流水”,后者看的是“每个账号最后一次登录的快照”。

1.2 三个二进制日志文件的分工

在讲命令之前,必须先把这几个文件的关系理清楚,否则后面的输出会看得一头雾水。Linux 登录审计相关的文件主要有三个,它们的职责边界很清晰:

文件路径记录内容对应读取命令典型权限
/var/log/wtmp成功的登录、登出、系统重启、运行级别切换last0664 root:utmp
/var/log/btmp失败的登录尝试lastb0600 root:utmp
/var/log/lastlog每个用户最后一次登录的时间、来源、终端lastlog0664 root:utmp

需要特别强调的是,这三个文件全部是二进制格式,不是文本日志。用cat、tail、grep去读它们,得到的会是一堆乱码或者看起来像乱码的字符,这不是文件损坏,而是设计如此。正因为是二进制结构体数组,读写效率才高,才能支撑高并发登录场景。要查看内容,必须用配套的命令。

这三个文件的权限设计也很有意思:btmp因为记录了失败登录(可能包含密码尝试的账号信息),默认只有 root 可读,所以lastb必须以 root 身份执行;而wtmp和lastlog允许utmp组读取,一些发行版会把普通用户也放进这个组,方便用户自己查看“我最近什么时候登录过”。这个权限差异在实际排查时经常被忽略,导致出现“命令没输出”却误以为是日志丢了的误判。

1.3 last 与 lastlog 的本质区别

很多人会把这两个命令混着用,实际上它们回答的是完全不同的问题。last回答的是“按时间顺序,这台机器上发生过哪些登录事件”,它读的是事件流水,所以同一用户可以出现很多次,而且能精确到“几点几分登录、几点几分登出、持续了多久”。lastlog回答的是“每个账号最近一次登录是什么时候”,它是一个用户维度的快照表,每个账号最多只有一行,没登录过的账号显示从未登录。

这个区别决定了使用场景:怀疑有异常登录、需要还原时间线、要排查某个时间段谁在线,用last;做账号治理、找长期不登录的僵尸账号、确认某个账号是否被启用过,用lastlog。两者结合,才能既看到全局时间线,又看到账号维度的活跃度画像。

2. 环境准备与工具来源确认

2.1 系统环境与命令归属

last、lastb、lastlog这三个命令在主流发行版上都由util-linux这个基础包提供,它不是需要额外安装的第三方工具,而是系统自带的底层工具集之一。你可以用下面的命令确认它们是否存在以及来自哪个包:

# 确认命令路径 which last lastb lastlog # 查看命令由哪个软件包提供(Debian/Ubuntu 系) dpkg -S "$(readlink -f "$(which last)")" # 查看命令由哪个软件包提供(RHEL/CentOS/Rocky 系) rpm -qf "$(which last)"

输出大致会是/usr/bin/last、/usr/bin/lastb、/usr/bin/lastlog,分别指向util-linux的软链接或实体文件。如果提示command not found,说明系统极简安装时裁掉了这个包,需要重新安装util-linux。这里要注意,不同发行版的util-linux版本不同,last支持的部分参数(比如-F完整时间、-p呈现指定时间在线用户)在较老的版本上可能不存在,本文示例以常见发行版的现代版本为例,如果你的环境参数报invalid option,先跑一下last --help看实际支持范围。

本文不绑定具体的版本号,因为util-linux在不同发行版上差异较大,写死版本反而会误导。重点是掌握“读什么文件、参数控制什么行为”这套思路,具体参数以你机器上last --help的输出为准。

2.2 日志文件是否存在与权限检查

命令能不能跑,取决于目标文件在不在。第一件事是确认文件状态:

ls -l /var/log/wtmp /var/log/btmp /var/log/lastlog

典型输出如下:

-rw-rw-r--. 1 root utmp 26112 Mar 10 09:15 /var/log/btmp -rw-rw-r--. 1 root utmp 29184 Mar 10 09:20 /var/log/lastlog -rw-rw-r--. 1 root utmp 85248 Mar 10 09:20 /var/log/wtmp

如果某个文件不存在,last会直接提示wtmp begins ...之前什么都没有,或者报last: /var/log/wtmp: No such file or directory。文件不存在的常见原因有两个:一是极简镜像或容器环境本身没有启用这套记账机制;二是系统用了systemd的 journal 方案,某些发行版的 PAM 配置不再写wtmp。容器环境尤其要注意,容器内的/var/log/wtmp通常是空的或者不存在,因为登录事件发生在宿主机上,容器本身没有 tty 登录。

权限方面,用普通用户执行lastb大概率会看到lastb: /var/log/btmp: Permission denied,这不是命令坏了,而是设计上的最小可见原则。涉及失败登录的排查请使用sudo。

2.3 时区与语言环境的前置确认

这一点经常被忽略但影响很大。last输出里的时间是按系统本地时区渲染的,如果你的服务器时区是UTC而你在Asia/Shanghai,看到的时间会差 8 小时,很容易误判“这个登录发生在凌晨”其实只是时区问题。排查前先确认:

timedatectl 2>/dev/null || cat /etc/timezone date

同时,lastlog的输出在中文语言环境下会把表头和“从未登录”翻译成中文,这会影响后面的awk、grep文本处理。做脚本化分析时,建议统一加LC_ALL=C前缀,强制英文输出,保证字段位置稳定、匹配规则可靠。这是写审计脚本时最基本的一条经验,后面实战部分的脚本都会带上它。

3. last 命令核心用法拆解

3.1 默认输出的字段含义

先看一个最朴素的调用:

last -n 5

输出类似这样:

root pts/0 192.168.10.21 Mon Mar 10 09:12 still logged in deploy pts/1 10.0.3.55 Mon Mar 10 08:47 - 09:05 (00:18) root tty1 Mon Mar 10 08:30 - 08:45 (00:15) reboot system boot 5.10.0-28-amd64 Mon Mar 10 08:20 still running root pts/0 192.168.10.21 Sun Mar 9 22:10 - 22:35 (00:25)

从左边数起,各列含义分别是:用户名、终端设备、来源主机(IP 或主机名)、登录时间、登出时间、持续时长。有几个细节值得单独说明。

终端列里的pts/N表示伪终端,通常对应 SSH 或终端模拟器会话;ttyN表示本地虚拟控制台,也就是直接在机器前面接显示器键盘登录的那种。所以当你看到一个tty1的登录记录,而服务器放在机房里根本没人碰过,这就是一个需要重点核查的信号。

来源主机列如果为空,一般表示本地登录(不走网络)。如果是:0这类形式,通常是图形界面本地会话。如果是 IP,可以用-i参数强制以数字形式显示,避免 DNS 反解带来的延迟和不确定性。

最后一列的still logged in表示该会话当前仍然在线,没有对应的登出记录;(00:18)这种括号里的值是本次会话持续的时长;still running出现在reboot条目上,表示系统自那次启动后一直运行至今。

3.2 常用参数与场景对照

last的参数不少,但真正高频的就那么几个。下面按使用场景整理:

场景命令写法说明
只看最近 N 条last -n 20等价于last -20,控制输出行数
显示完整日期时间last -F输出年月日时分秒,适合精确对时
IP 强制数字显示last -i避免 DNS 反解导致的卡顿
关闭主机名解析last -R只输出 IP 或用空值,速度更快
只看某时间之后last -s "2025-03-01 00:00:00"起点过滤
只看某时间之前last -t "2025-03-05 23:59:59"终点过滤,可与-s组合
查看关机重启记录last -x增加shutdown、runlevel等系统级条目
只看重启记录last reboot快速确认机器是否异常重启过
读取历史归档last -f /var/log/wtmp.1日志轮转后的旧文件回溯
查看指定时刻在线用户last -p "2025-03-10 09:00"还原某时刻的在线状态

其中-F和-i在排查中建议默认加上,前者避免“同一天多次登录分不清先后”,后者避免内网 DNS 反解失败时命令卡住几十秒。-x则是在怀疑“机器是不是被重启过”时的第一选择,因为默认输出里reboot记录只在特定条件下出现,加上-x才能稳定看到系统级事件。

关于时间参数的格式,-s和-t支持两种写法:YYYYMMDDHHMMSS紧凑格式,或者"YYYY-MM-DD HH:MM:SS"带引号的格式。带空格的写法必须加引号,否则 shell 会把时间拆成两个参数传给命令,导致解析失败。这个错误非常常见,报错信息却往往只提示“无效的时间格式”,不太好定位。

3.3 特殊条目:reboot、shutdown 与 wtmp begins

last的输出里除了普通用户,还有几类特殊条目,读懂它们对还原时间线很关键。

reboot system boot <内核版本> <时间>表示一次系统启动。如果有对应的still running,说明这是当前这次运行;如果有- 08:45 (00:15)这样的区间,说明那次启动只持续了 15 分钟就又重启了,这通常意味着异常崩溃、内核 panic 或者有人手动重启。

shutdown system down <内核版本> <时间>表示一次正常关机,需要-x才会显示。

runlevel (to lvl 5)和runlevel (to lvl 3)是运行级别切换记录,同样需要-x。运行级别发生变化,往往对应着有人手动切了图形界面或者调整了启动模式。

位于输出末尾的wtmp begins Mon Mar 03 10:22:15 2025是整份日志的时间起点。这个值非常重要:它告诉你“这份日志最早只能追溯到什么时候”。如果它显示的是三天前,而你正在排查一周前的事件,那就说明日志已经被轮转覆盖或者被截断过,继续在wtmp里找是没有意义的,必须去翻归档文件或者别的日志源。

3.4 时间窗口过滤与历史文件回溯

真实排查中很少需要看全部记录,更多是锁定一个时间窗口。假设怀疑问题发生在 3 月 10 日上午 8 点到 10 点之间,可以这样查:

last -F -i -s "2025-03-10 08:00:00" -t "2025-03-10 10:00:00"

如果wtmp已经被轮转,旧数据会落到/var/log/wtmp.1这样的归档文件里。很多发行版默认配置了 logrotate 对wtmp做按月轮转,保留若干份。回溯时可以直接指定文件:

# 列出所有归档文件 ls -l /var/log/wtmp* # 读取上一份归档 last -F -i -f /var/log/wtmp.1 # 读取第二份归档 last -F -i -f /var/log/wtmp.2

这里有一个容易被忽略的点:logrotate 对wtmp的处理方式和普通文本日志不同。普通文本日志通常是“重命名 + 新建”,而wtmp这类二进制记账文件如果只是重命名,正在运行的进程仍然持有原文件句柄,新事件会继续写进被重命名的旧文件里,导致归档文件“越写越新”、新文件“一直是空的”。所以wtmp的标准轮转方式是copytruncate或者配合postrotate脚本,具体配置在/etc/logrotate.conf里能看到。如果你发现wtmp.1的修改时间比wtmp还新,基本就是这个原因,排查时需要额外注意数据的完整性问题。

4. lastlog 命令核心用法拆解

4.1 输出格式与“从未登录”的含义

lastlog的默认输出是一个用户列表:

LC_ALL=C lastlog

输出类似:

Username Port From Latest root pts/0 192.168.10.21 Mon Mar 10 09:12:33 +0800 2025 daemon **Never logged in** bin **Never logged in** deploy pts/1 10.0.3.55 Mon Mar 10 08:47:11 +0800 2025 nginx **Never logged in**

四列分别是用户名、终端、来源、最后登录时间。表头那行Username Port From Latest需要跳过。凡是显示**Never logged in**的账号,说明自lastlog文件创建以来从未成功登录过。

这里必须提醒一个认知上的坑:lastlog记录的是“系统账号最后一次登录”的快照,但它不记录登录事件本身,所以它没法告诉你“这个账号一共登录过几次”。而且它的更新时机是在登录会话建立时由 PAM 模块写入的,如果某个服务(比如某些 FTP 配置、部分数据库的内置认证)不走 PAM,它的登录行为就不会反映到lastlog里。所以看到某个服务账号显示“从未登录”时,不能直接断定它没有被使用过,只能说“没有通过 PAM 会话登录过”。

另一个高频现象是:在某些较新的发行版上,lastlog输出为空,或者直接报The specified file /var/log/lastlog does not exist。这不是命令坏了。部分发行版出于精简考虑,默认不再生成/var/log/lastlog,或者 PAM 配置里没有启用对应的记录模块。遇到这种情况,先确认文件是否存在(见 2.2 节),再检查 PAM 配置里是否有pam_lastlog相关的行。如果确实没有,lastlog就无法工作,此时应改用last或者集中式日志方案,不要在这上面死磕。

4.2 参数详解与典型组合

lastlog的参数比last少,但有几个非常实用:

参数作用典型场景
-u LOGIN只查指定用户核查单个账号
-t DAYS只显示最近 DAYS 天内登录过的用户看活跃账号
-b DAYS只显示 DAYS 天之前登录过的用户找长期不活跃账号
-C清除某用户的最后登录记录慎用,修改日志
-S将某用户的最后登录记录设为当前时间慎用,修改日志
--help查看本机实际支持的参数版本差异确认

最常用的组合是查单个账号和查找长期未登录账号:

# 查看 root 的最后登录信息 LC_ALL=C lastlog -u root # 查看最近 7 天内登录过的用户 LC_ALL=C lastlog -t 7 # 查看 90 天之前登录过(含从未登录)的用户 LC_ALL=C lastlog -b 90

-b 90的语义需要特别说清楚:它是“最后一次登录发生在 90 天之前的用户”,并不包含“从未登录”的用户。从未登录的账号在lastlog里时间是空的,不会被时间条件命中。所以在做账号清理时,必须把“从未登录”和“长期未登录”两类分别捞出来,才能覆盖完整。

-C和-S属于写入操作,会实际修改/var/log/lastlog的内容。这两个参数在生产环境应该被视为高危操作:-C会把某个用户的最后登录记录抹掉,导致后续审计失去依据;-S会把时间改成当前,制造出“刚登录过”的假象。除非你在做测试环境的数据构造,或者有明确的合规要求并已获得授权、做过文件备份,否则不要碰这两个参数。

4.3 数据来源与刷新机制

lastlog的数据来自/var/log/lastlog,这个文件的结构比较特殊:它按 UID 偏移存储,每个 UID 对应固定长度的一条记录。这意味着文件大小通常等于“系统里最大 UID × 每条记录大小”,所以你会看到一个几十 MB 甚至更大的lastlog文件,但里面绝大部分是空洞(稀疏文件)。这也是为什么ls -l看到的大小和du -h看到的大小差异巨大,不用惊讶,这是正常现象。

记录写入发生在登录成功的那一刻。以 SSH 登录为例,sshd完成认证后会通过 PAM 调用pam_lastlog模块,把登录信息写进去。如果认证方式绕过了 PAM,或者 PAM 配置里把这一行注释掉了,就不会有记录。有些加固规范会出于“避免泄露上次登录信息”的考虑,主动关闭pam_lastlog的显示功能,这时候要区分“不显示”和“不记录”是两回事,需要看具体配置。

还有一个容易误解的点:如果用户通过su或sudo切换到别的账号,lastlog里目标账号的记录是否更新,取决于具体配置和 PAM 栈。一般不建议把这类切换行为当作“登录”来看待,审计时应该结合last的pts会话记录和sudo日志一起判断。

5. 完整实战:一次登录审计的落地流程

5.1 需求拆解与执行顺序

把前面的知识串起来,做一次完整的登录审计。假设的需求是:核查一台 SSH 服务器近期的登录情况,找出长期未登录的可疑账号、异常时间段的登录记录,以及是否存在暴力破解迹象。执行顺序建议如下:先做整体概览,再按时间窗口细分,然后做账号维度统计,最后交叉验证失败登录。每一步都用命令留下可复现的输出,方便后续对比。

第一步,整体概览,确认日志覆盖范围和数据完整性:

# 确认日志起点和最新状态 LC_ALL=C last -F -i | head -30 LC_ALL=C last -F -i | tail -5

head看最近的登录事件,tail看日志的起点wtmp begins。这两条命令先跑,能快速判断“我手上这份数据能不能支撑排查”。

第二步,按时间窗口锁定可疑区间:

# 假设关注 3 月 9 日到 3 月 10 日 LC_ALL=C last -F -i -s "2025-03-09 00:00:00" -t "2025-03-10 23:59:59"

第三步,账号维度扫描:

# 从未登录的账号 LC_ALL=C lastlog | awk 'NR > 1 && $2 == "**Never" {print $1}' # 超过 90 天未登录的账号 LC_ALL=C lastlog -b 90

第四步,失败登录排查:

# 需要 root 权限 sudo LC_ALL=C lastb -F -i -n 50

这四步跑完,基本的画像就有了。下面把常用的统计逻辑整理成脚本,方便重复执行。

5.2 登录审计脚本实现

把前面零散的命令固化成脚本,是让审计可重复的关键。下面这个脚本覆盖了“统计每个用户登录次数”“列出从未登录账号”“列出长期未登录账号”三个能力,可以直接保存后执行:

#!/bin/bash # 文件路径:/usr/local/sbin/login_audit.sh # 用途:Linux 登录情况审计快照 # 注意:只读取日志,不做任何写入操作 set -euo pipefail export LC_ALL=C echo "==============================================" echo "登录审计报告 生成时间: $(date '+%Y-%m-%d %H:%M:%S')" echo "==============================================" echo echo "[1] wtmp 日志覆盖范围" last -F | tail -1 echo echo "[2] 最近 20 条登录事件" last -F -i -n 20 echo echo "[3] 各用户成功登录次数统计(Top 15)" last -i | awk 'NF > 0 && $1 != "reboot" && $1 != "shutdown" && $1 != "wtmp" {count[$1]++} END {for (u in count) printf "%-20s %d\n", u, count[u]}' \ | sort -k2 -nr | head -15 echo echo "[4] 从未登录过的账号" lastlog | awk 'NR > 1 && $2 == "**Never" {print $1}' echo echo "[5] 超过 90 天未登录的账号" lastlog -b 90 echo echo "[6] 最近 20 条失败登录记录" if [ "$(id -u)" -eq 0 ]; then lastb -F -i -n 20 || echo "btmp 为空或不可读" else echo "当前非 root 用户,跳过 btmp 检查(请使用 sudo 执行)" fi echo echo "==============================================" echo "报告结束" echo "=============================================="

赋权并执行:

sudo chmod 750 /usr/local/sbin/login_audit.sh sudo /usr/local/sbin/login_audit.sh

脚本里有几个处理细节值得说明。set -euo pipefail保证任何一步失败都能及时暴露,而不是带着空数据往下跑;export LC_ALL=C保证awk的字段切分符合预期;第 [3] 段用awk做计数后按次数倒序排,能直观看出哪个账号登录最频繁——如果某个平时不用的服务账号排在第一,那就要重点核查了;第 [6] 段做了 root 判断,避免普通用户执行时因为权限不足中断整个脚本。

需要提醒的是,第 [3] 段的过滤条件里排除了reboot、shutdown、wtmp这几个特殊条目的首字段。这是因为last的输出中这些行也会占一行,不排除会污染统计结果。如果你的系统上还有其他特殊条目,比如runlevel(需要-x才出现),也要一并排除。这种过滤逻辑没有绝对通用的写法,建议先跑一遍last看看本机实际有哪些特殊行,再调整脚本。

5.3 异常登录的交叉验证

单看last或lastlog都不够,真正的判断需要交叉验证。举几个具体的组合思路。

如果last里出现了陌生的来源 IP,先确认这个 IP 是否属于公司出口或者跳板机范围,再去看这个 IP 对应的会话持续了多久、有没有对应的登出记录。如果一个会话持续了十几个小时且没有登出(still logged in),而账号本人说早就下班了,那就需要进一步核查。

如果lastlog -u someuser显示的最后登录时间和last | grep someuser的第一条记录对不上,比如lastlog显示的是三天前,但last里有今天的记录,这说明记录链路有问题——可能是pam_lastlog被改动过,也可能是有人动过日志文件。这种“对不上”本身就是重要线索。

如果lastb里出现大量针对同一个账号的失败记录,且来源 IP 集中,这就是典型的暴力破解特征。此时应该结合sshd日志(journalctl -u sshd或者/var/log/secure、/var/log/auth.log)确认是否有成功登录紧随其后。只看lastb只能知道“被尝试了”,不能判断“是否被攻破”,这个边界必须守住。

如果发现某次reboot记录的间隔异常短,比如十分钟内重启了三次,那可能是有人在反复尝试修改配置并重启服务,也可能是硬件或内核问题。这时候应该去看journalctl -b -1(上一次启动的日志)确认原因,而不是停留在last层面。

5.4 结果解读与结论输出

审计的最终产物应该是一份能让别人看懂的报告,而不是一堆命令输出。建议至少包含四个部分:日志覆盖范围(起止时间,说明这次审计的结论只在这个范围内有效)、登录事件摘要(按时间线列出关键节点)、异常项清单(每条都附上对应的证据命令和输出片段)、待确认事项(需要业务方或账号负责人确认的内容)。

这里有个原则值得坚持:只陈述事实,不下过度结论。比如“账号 deploy 于 3 月 10 日 08:47 从 10.0.3.55 登录,09:05 登出,持续 18 分钟”是事实;“这个登录很可疑”是判断,需要额外的上下文支撑。把事实和判断分开写,报告才经得起复核。

6. 常见问题与排查思路

实际使用中遇到的问题,大多集中在“命令没输出”“输出看不懂”“记录缺失”这三类。下面把高频问题整理成表,每条都给出从现象到根因的排查路径。

问题现象常见原因解决思路
last无任何输出,只报wtmp beginswtmp为空或刚被轮转清空;容器环境无登录事件用ls -l /var/log/wtmp*查归档;容器内改用宿主机的记录
last: /var/log/wtmp: No such file or directory文件被删除;极简系统未启用记账手动创建空文件并设权限touch /var/log/wtmp && chmod 664 /var/log/wtmp && chgrp utmp /var/log/wtmp
lastb: Permission denied/var/log/btmp权限为 0600,仅 root 可读使用sudo lastb,不要为了图方便放宽文件权限
输出的时间是“错误”的,和实际差几小时服务器时区与本地时区不一致timedatectl确认时区,必要时统一调整或换算
命令卡住几十秒才出结果来源 IP 触发了 DNS 反解且超时加-i参数强制数字显示,或加-R关闭主机名解析
lastlog报文件不存在发行版默认不生成/var/log/lastlog确认 PAM 是否启用pam_lastlog,或改用last作为替代方案
lastlog输出全为**Never logged in**登录未经过 PAM;或账号确实从未登录结合last交叉验证;检查服务的认证方式
脚本里awk匹配不到预期字段中文 locale 导致表头和提示被翻译脚本开头加export LC_ALL=C
-s参数报时间格式无效时间含空格但没加引号写成-s "2025-03-10 08:00:00"
wtmp.1比wtmp还新logrotate 对二进制记账文件处理方式不当检查/etc/logrotate.conf中 wtmp 的轮转配置

表格里最后一条值得展开说。很多人在排查“日志去哪了”时发现归档文件的时间戳很新,以为是文件系统出了问题,实际上是因为进程持有的文件句柄指向了被重命名的 inode,新事件继续写入旧文件。要根治这个问题,轮转策略应该改成copytruncate,或者配置postrotate在轮转后重新打开文件。改logrotate配置属于生产变更,务必先在测试环境验证,并确认改动后轮转周期内数据不丢。

另一个需要反复强调的点是:不要因为排查方便就去放宽/var/log/btmp的权限。把0600改成0644看起来能让普通用户也执行lastb,但失败登录记录里包含账号名和尝试信息,属于敏感数据,放宽权限会让低权限账号获得攻击面的情报。正确做法是通过sudo授权特定运维人员执行,而不是改文件权限。

7. 最佳实践与工程建议

7.1 日志保留周期与轮转策略

wtmp和btmp默认的保留周期通常不长,很多发行版只有几周。对于有审计要求的场景,这个周期明显不够。建议显式检查并调整/etc/logrotate.conf中相关段落的rotate和monthly/weekly配置,把保留份数调整到满足合规要求的水平,比如保留 12 份月度归档,覆盖一年。

调整时要注意两点:一是磁盘空间,wtmp在高并发登录的机器上增长很快,保留过多份会占空间,建议配合du -sh /var/log/wtmp*定期评估;二是轮转方式,前面已经说过copytruncate的必要性,改配置后一定要等一个完整周期验证新文件确实在增长,而不是想当然地认为改完就好了。

7.2 权限设计与最小可见原则

wtmp涉及全员登录信息,btmp涉及失败尝试,lastlog涉及个人登录历史,三者敏感度不同,权限也应该分级。建议的做法是:btmp严格保持0600 root:utmp;wtmp和lastlog按需授予utmp组读取,但不要把普通业务账号加进这个组;审计执行统一走sudo白名单,并在sudo日志里留下“谁在什么时候执行了审计命令”的记录。

这套设计的意义在于,审计本身也应该被审计。如果任何人都能随意读取登录记录,那记录的价值和可信度都会打折扣。在合规场景下,还需要考虑把审计命令的执行记录纳入集中日志,形成闭环。

7.3 与集中式日志和审计框架的配合

单机日志的天然缺陷是“可以被本地修改”。如果攻击者拿到了 root,理论上可以篡改wtmp。所以对于安全要求较高的环境,本地的last/lastlog只能作为快速排查的第一手材料,真正的证据链应该依赖集中式日志:把sshd的认证日志、sudo日志实时转发到独立的日志服务器,同时把wtmp、btmp定期归档同步过去。这样即使本机被清理,远端仍有副本。

更进一步的做法是引入auditd,对关键文件(包括/var/log/wtmp、/var/log/btmp、/etc/passwd、/etc/ssh/sshd_config)配置写入监控,记录“谁在什么时间用哪个进程改了这些文件”。auditd记录的是系统调用级别的行为,和last的登录事件形成互补:一个看“人什么时候进来”,一个看“进来之后动了什么”。

7.4 账号生命周期治理

lastlog最大的价值不在应急排查,而在日常治理。建议建立定期巡检机制,比如每月执行一次lastlog -b 90,把结果和账号清单比对:系统账号(如daemon、bin、nobody)显示从未登录是正常的,不用管;业务账号超过 90 天未登录,就应该走确认流程,判断是禁用、锁定还是删除。

执行账号清理时,务必遵守三条底线:先备份再操作、先锁定再删除(usermod -L观察一段时间再userdel)、删除前确认该账号没有被定时任务、服务配置、文件属主引用。userdel默认不删除家目录,如果确认要清理,需要显式加-r,但这会连带删除该用户的所有文件,属于不可逆操作,必须慎之又慎。

8. 继续深入的方向与常用命令速查

把last和lastlog吃透之后,往下走有几个自然的方向。一是补齐同族命令,who看当前在线、w看在线的同时看负载、users看登录用户名列表、lastb看失败尝试,这几个配合起来能覆盖大部分日常场景。二是深入PAM配置,搞清楚登录信息是在认证流程的哪一步被写入的,这样在遇到“记录缺失”时才有能力定位到具体模块。三是接触集中式日志方案,理解日志从产生、传输、存储到检索的完整链路,这是从单机运维走向体系化运维的必经一步。

最后附一份高频命令速查,方便贴到自己的笔记里:

# ---- last 相关 ---- last -n 20 # 最近 20 条登录事件 last -F -i # 完整时间 + 数字 IP last -x # 包含关机、运行级别等系统事件 last reboot # 只看重启记录 last -s "2025-03-01 00:00:00" -t "2025-03-10 23:59:59" # 时间窗口 last -f /var/log/wtmp.1 # 读取归档文件 last -p "2025-03-10 09:00" # 指定时刻在线用户 # ---- lastlog 相关 ---- LC_ALL=C lastlog # 全量快照 LC_ALL=C lastlog -u root # 指定用户 LC_ALL=C lastlog -t 7 # 最近 7 天登录过 LC_ALL=C lastlog -b 90 # 90 天前最后登录 # ---- 失败登录 ---- sudo lastb -F -i -n 50 # 最近 50 条失败尝试 # ---- 配套排查 ---- who # 当前在线用户 w # 在线用户 + 负载 journalctl -u sshd --since "2025-03-10" # SSH 服务日志

这些命令看起来简单,但真正决定排查质量的是两件事:一是提前确认数据覆盖范围,每次排查先看wtmp begins,避免在已经失效的数据上耗时间;二是交叉验证,任何单一命令的输出都只是线索,登录时间线要和 SSH 日志对齐、失败尝试要和成功登录对齐、账号活跃度要和业务归属对齐。把这两点变成习惯,last和lastlog才不只是两个查历史的命令,而是一套可复用的排查方法论。