在日常 Linux 运维里,排查“谁在什么时候登录过这台机器”是一个高频需求,而系统自带的last和lastlog往往是被低估的两条命令。它们不需要额外安装任何软件包,几乎所有发行版开箱即用,读取的是系统在用户登录、注销、重启这些动作发生时自动写入的二进制记账文件。也就是说,只要机器启动过、有人登录过,证据就已经落在磁盘上了,你要做的只是把记录读出来。
这两个命令解决的是完全不同层面的问题。last关心的是“时间轴”,它按时间倒序把每一次登录会话、每一次关机重启、每一次运行级别切换都列出来;lastlog关心的是“最后状态”,它为系统里每一个用户维护一条记录,告诉你这个账号最后一次登录是什么时间、从哪里登录的,如果这个账号从建立之日起就没被使用过,它会明确告诉你从未登录。前者适合做事件追溯,后者适合做账号清点。
本文会围绕几个实操方向展开:先给一条命令速览表,说明last、lastlog以及配套的lastb、who、w各自负责什么;然后拆解命令背后的四个关键文件wtmp、btmp、lastlog、utmp,讲清楚它们为什么是二进制、为什么普通人读不了、为什么有的机器上last一执行就是空白;接着用大量可直接复制的命令演示时间过滤、IP 显示、完整时间戳、指定历史日志文件、只查某个用户等常见操作;最后补上日志轮转配置、自动化巡检脚本、常见故障排查表和审计合规提醒。
适合读这篇的包括:需要做服务器安全巡检的运维人员、要在故障复盘时确认“谁在几点动了机器”的后端开发、准备考 RHCE 或做 Linux 基础教学的同学,以及正在写堡垒机、审计系统或资产盘点脚本的工程师。所有命令都在标准 Linux 环境下通用,不依赖特定发行版,涉及文件路径的地方我会同时标注 Debian 系和 RHEL 系的差异。
1. last 与 lastlog 核心能力速览
开始敲命令之前,先把几条容易混淆的工具放到一张表里对比。很多人第一次接触last时会问:它和who、w有什么区别?答案藏在“数据来源”和“时间范围”这两个维度上。who和w读取的是/run/utmp,只反映当前还在线的会话,机器一重启这个文件就被清空;last读取的是/var/log/wtmp,这是一个只追加不删除的历史账本,重启也不会丢。理解这个区别,后面的所有行为就都说得通了。
| 能力项 | 说明 |
|---|---|
| 命令归属 | last、lastlog、lastb均来自util-linux包,主流发行版默认预装 |
| 数据来源 | last读/var/log/wtmp;lastb读/var/log/btmp;lastlog读/var/log/lastlog;who、w读/run/utmp |
| 文件格式 | 前三者中 wtmp/btmp/utmp 为二进制 utmp 结构记录;lastlog 为按 UID 索引的定长稀疏文件 |
| 默认排序 | last与lastb均为时间倒序,最新记录在最上面;lastlog按 UID 升序 |
| 权限要求 | 普通用户可以执行,但能否读到完整内容取决于文件权限,btmp通常仅 root 可读 |
| 时间过滤 | 支持-t指定截止时间,支持-s指定起始时间,两者可组合成时间窗口 |
| 输出控制 | 支持-n限制条数、-F显示完整时间戳、-i显示 IP、-R隐藏主机名、-a末列显示主机 |
| 特殊记录 | -x可额外显示关机、重启、运行级别切换等非登录事件 |
| 历史文件 | 支持-f指定轮转后的历史文件,如/var/log/wtmp.1 |
| API 能力 | 无非网络接口,需通过 shell 脚本或解析工具二次封装 |
| 批量能力 | 本身不支持批量,但输出为稳定文本格式,极易被脚本消费 |
| 典型场景 | 登录审计、异常排查、账号清理、合规取证、资产使用率统计 |
| 不适合场景 | 容器内部(多数容器不写 wtmp)、已清理日志的追查、实时监控 |
需要特别说明的一点是,lastlog的“从未登录”判断和账号是否被禁用是两回事。一个账号可能被usermod -L锁定,此时它仍然会保留最后一次历史登录记录;而一个刚创建但还没派发出去的账号,lastlog会显示**Never logged in**,这恰恰是账号清理时最有价值的信号。把这两类账号混为一谈,是很多巡检脚本误报的根源。
另外要提醒的是,这些命令都只能看到“本机记录的登录行为”,无法覆盖 SSH 之外的认证路径。例如通过su切换用户、通过sudo提权、通过 Web 控制台登录,行为轨迹会落在secure、auth.log或sudo日志里,last是看不到的。做完整审计时必须把last、lastlog、journalctl、auditd几路信息交叉比对,单靠一条命令下结论并不可靠。
2. 适用场景与使用边界
先说清楚这个工具组合“能干什么”。最常见的场景是故障复盘:某台生产机在凌晨三点挂了,你需要确认那个时间段有没有人登录、是不是有人手动改了配置。last -F -i -n 50能把最近 50 条会话的时间精确到秒、来源精确到 IP,配合-x还能看到系统在故障前后的关机重启动作,一条时间线就拼出来了。这比翻history靠谱得多,因为history只记录当前 shell 的命令,换个人、换个会话就断片了。
第二个高频场景是账号治理。很多公司的服务器上堆积着几十个历史账号,离职员工的、临时外包的、测试用的,谁也不敢删。用lastlog拉一遍全量,把“从未登录”和“最后登录在一年以前”的账号筛出来,再结合chage -l看密码有效期,就能形成一份可执行的清理清单。这个过程不需要登录每个账号,也不会对业务造成任何干扰,安全性很高。
第三个场景是安全巡检。lastb专门统计失败登录,如果你看到某个 IP 在短时间内连续几十条失败记录,基本可以判定是密码爆破尝试。配合last -i看同一 IP 是否最终成功登录,就能快速判断事件等级。这里要注意,lastb依赖/var/log/btmp文件存在且有写入权限,部分发行版默认不创建该文件,需要手动touch并设置600权限,否则lastb会直接报错而不是返回空结果。
2.1 能力边界与合规提醒
必须强调的是,登录审计属于敏感操作。last输出里包含用户名、来源 IP、登录终端等个人信息,在多用户或跨团队环境中,这些数据只应在授权的运维、安全、合规场景下使用。企业内部做审计时,应确保有明确的管理制度支撑,审计结果仅在必要范围内流转,不要随手把完整的登录记录贴到公开渠道或工单系统里。
另一个边界是“证据时效性”。wtmp和btmp会随日志轮转被压缩、被清理,很多发行版默认只保留四周左右。如果你在事件发生一个月后才去查,很可能什么都查不到。因此对关键服务器,建议单独配置轮转保留策略,或者定期把last输出归档到独立的日志平台。指望系统默认配置长期保存登录历史,是一个很常见的认知误区。
最后是“不能作为唯一证据”。wtmp是普通文件,root 用户可以改写甚至删除,恶意入侵者清理痕迹的第一步往往就是动这些文件。因此真正的取证场景需要结合文件完整性校验、远程日志服务器、auditd审计规则等多重手段。last的定位是“日常运维的快速视图”,不是“司法级取证工具”,把它放在正确的位置上,才不会在关键时刻误判。
3. 环境准备与前置条件
last系列命令的部署成本几乎为零,但要让它输出有意义的结果,需要先确认几件事。第一是命令是否存在,第二是数据文件是否存在,第三是当前用户有没有读取权限。这三步检查加起来不超过一分钟,却能避免后面大量“命令执行了但没输出”的困惑。
3.1 确认命令与软件包
绝大多数发行版把last、lastlog、lastb打包在util-linux里,该包是基础系统组件,通常不会缺失。如果确实找不到,可以用下面的命令确认包管理器的归属:
# Debian / Ubuntu 系 which last lastlog lastb dpkg -S $(which last) 2>/dev/null # RHEL / CentOS / Rocky 系 which last lastlog lastb rpm -qf $(which last) 2>/dev/null # 如果确实缺失,再考虑安装 # Debian 系 sudo apt update && sudo apt install -y util-linux # RHEL 系 sudo dnf install -y util-linux需要注意,部分精简镜像(比如某些 Alpine 或容器基础镜像)使用的是busybox提供的简化版last,参数支持不完整,-F、-i、-x可能直接报错。判断方法是执行last --help,观察输出里是否列出了完整参数列表。如果只有寥寥几行,说明用的是精简实现,建议安装完整版util-linux再继续。
3.2 检查四个关键数据文件
登录记录全部落在/var/log和/run目录下,动手之前先看一眼它们的状态:
# 查看文件是否存在、大小、权限、最后修改时间 ls -lh /var/log/wtmp /var/log/btmp /var/log/lastlog /run/utmp # 更精细地看权限位(wtmp 通常 664,btmp 通常 600,lastlog 通常 660) stat -c '%n %a %U:%G %s bytes' /var/log/wtmp /var/log/btmp /var/log/lastlog /run/utmp这里有两个典型现象值得记住。其一,btmp文件在很多系统上默认不存在,因为只有配置了 PAM 的pam_tally或faillock模块后才会写入,此时lastb会提示lastb: /var/log/btmp: No such file or directory,这不是报错故障,而是从未记录过失败登录。其二,lastlog文件的大小看起来可能非常夸张,动辄几百 MB 甚至上 GB,但它实际占用的磁盘块可能只有几十 KB,因为它是按 UID 直接寻址的稀疏文件,UID 越大文件逻辑体积越大。用du -h和ls -lh对比一下就能看出差别,不要被ls的数字吓到。
3.3 权限与提权说明
普通用户执行last通常可以正常看到自己的和部分他人的登录记录,因为wtmp权限一般是664 root:utmp。但lastb读取的btmp是600 root:root,普通用户执行会得到空结果或权限错误提示,这属于设计上的安全考量——失败登录记录可能暴露账号是否存在,不适合所有人可见。同理,lastlog在部分发行版上是660 root:utmp,普通用户可能读不到完整内容。
因此本文后续所有涉及lastb和全量lastlog的命令,都默认以具备sudo权限的账号执行。如果你的环境不允许提权,那就把范围收窄到last本身,它已经能覆盖大部分日常排查需求。
4. last 命令详解与实战用法
last的输出格式是固定的六列,从左到右依次是:用户名、登录终端、来源主机(或 IP)、登录时间、注销时间、会话时长。如果会话还在进行中,注销时间列会显示still logged in;如果是系统启动记录,用户名会显示为reboot、shutdown或runlevel。这个格式稳定了二十多年,正因如此才特别适合被脚本解析。
4.1 基础用法与输出解读
最朴素的执行方式就是不带任何参数:
last输出通常会以wtmp begins ...结尾,这一行非常关键,它告诉你当前wtmp文件里最早的一条记录是什么时间。如果这行显示的时间就是今天,说明日志刚刚被轮转过或者系统刚重装,你不可能查到更早的历史。很多人抱怨“查不到上周的登录”,问题往往就出在这一行。
限制输出条数是日常使用频率最高的参数,-n后面跟数字即可:
# 只看最近 10 条 last -n 10 # 等价写法,-10 是老式简写,仍然可用 last -10 # 只看某个用户,用户名支持多个,空格分隔 last root last alice bob指定用户查询这个能力经常被忽略,但它在实际场景中价值很高。比如怀疑某个账号被盗用,直接last 该账号,看登录 IP 是否出现了从未见过的网段;或者在做离职账号清理时,用last 该账号确认最后一次使用时间,再决定是禁用还是删除。
4.2 时间过滤:-t 与 -s 的组合用法
last的时间过滤参数设计得比较“反直觉”:-t表示“显示到这个时间为止的记录”,也就是取一个时间点之前的所有记录;-s表示“从这个时间开始显示”。两者组合起来才能构成一个时间段窗口。时间格式是YYYYMMDDHHMMSS,可以省略后面的部分,比如20240315表示当天零点。
# 查看 2024 年 3 月 15 日当天及之前的全部记录 last -t 20240315 # 查看 2024 年 3 月 15 日之后(含)的记录 last -s 20240315 # 组合成时间窗口:3 月 1 日到 3 月 15 日之间 last -s 20240301000000 -t 20240315235959 # 精确到分钟,排查某个凌晨时段的登录行为 last -s 202403150200 -t 202403150400 -F -i这套组合在故障复盘中特别好用。举个典型流程:某服务在 2024-03-15 02:10 报错,你先用-s 202403150200 -t 202403150300 -x -F -i拉出这个时段的所有事件,包括登录、注销、重启,再看有没有人在这个窗口里执行过操作,时间线立刻清晰。相比全量last再肉眼筛选,效率提升非常明显。
4.3 输出格式控制:-F、-i、-R、-a
默认输出里时间是被压缩过的,比如Mon Mar 15 02:10,没有年份也没有秒。做精确比对时这个精度完全不够,-F参数可以把完整时间戳打出来,包含年份、日期、时分秒,部分版本还会带上时区信息。
# 完整时间戳 last -F -n 20 # 来源显示为 IP 而不是主机名,避免 DNS 反解失败导致卡顿或显示异常 last -i -n 20 # 隐藏主机名列,只保留用户、终端、时间 last -R -n 20 # 把主机名放到最后一列,方便 awk 取值 last -a -n 20-i这个参数要重点说一下。默认情况下last会尝试把来源 IP 反解成主机名,如果 DNS 配置有问题或者网络不通,命令会明显变慢,甚至显示成乱七八糟的名字。在生产环境排查时,建议固定加上-i,直接看 IP 更可靠也更符合排查直觉。-R则是把主机名那一列整个去掉,适合只关心“谁在什么时候登的”而不关心“从哪来”的场景,输出更清爽。
4.4 -x 扩展事件与 -f 读取历史文件
-x是让last从“登录列表”升级为“系统事件列表”的关键参数。加上它之后,关机、重启、运行级别切换、内核版本变更这些记录都会出现在输出里,用户名列分别是shutdown、reboot、runlevel。
# 查看登录 + 系统事件的完整时间线 last -x -F -n 40 # 只看关机和重启记录 last -x | grep -E '^(reboot|shutdown)' # 只看运行级别切换 last -x | grep '^runlevel'-f参数用来读取轮转后的历史文件,这是很多人不知道的实用能力。wtmp被logrotate轮转后会变成wtmp.1、wtmp.2.gz等,直接last是看不到的。如果文件未被压缩,可以直接用-f指定:
# 读取轮转后的上一份 wtmp last -f /var/log/wtmp.1 -n 30 # 读取 btmp 的轮转文件(需要 root) sudo last -f /var/log/btmp.1 -n 30如果历史文件已经被 gzip 压缩,last无法直接读取,需要先解压到临时目录再查,或者用后面章节介绍的utmpdump配合zcat来处理。这个限制在跨月排查时经常会碰到,提前把保留策略调好能省不少事。
4.5 组合实战:拼一条完整的排查命令
把上面的参数组合起来,可以得到几条覆盖不同场景的“成品命令”,建议直接存到自己的排查手册里:
# 场景一:最近 50 条会话,完整时间 + IP,隐藏冗余列 last -n 50 -F -i -R # 场景二:某日凌晨窗口内的全部事件,含关机重启 last -x -F -i -s 202403150000 -t 202403150600 # 场景三:指定用户最近 30 次登录,完整时间 last alice -n 30 -F -i # 场景四:从历史轮转文件中查上个月的登录 last -f /var/log/wtmp.1 -F -i -n 100 # 场景五:只统计登录次数,按用户聚合(脚本化思路) last -i -R | awk 'NF>=3 && $1!="" {print $1}' | sort | uniq -c | sort -rn | head -20最后一条虽然超出了last本身的范围,但它代表了一个很重要的思路:last的输出是高度结构化的,六列以空格或制表符分隔,用awk取第一列统计用户名、用grep过滤特定 IP、用sort | uniq -c做计数都非常方便。把这些拼在一条管道里,就能快速回答“哪个账号登录最频繁”“哪个 IP 出现次数最多”这类问题,比手工数要靠谱得多。
5. lastb 与失败登录审计
lastb的用法和last几乎完全一致,参数通用,唯一区别是数据来源换成了/var/log/btmp,记录的是失败登录尝试。它的输出会在用户名位置显示尝试登录的账号名,来源列显示发起方的 IP 或主机名,时长列固定为00:00,因为失败尝试不会产生会话时长。
# 查看最近 20 次失败登录 sudo lastb -n 20 -F -i # 统计失败次数最多的账号 sudo lastb -i | awk 'NF>=3 {print $1}' | sort | uniq -c | sort -rn | head # 统计失败次数最多的来源 IP sudo lastb -i | awk 'NF>=3 {print $3}' | sort | uniq -c | sort -rn | head # 查看某个时间段的失败记录 sudo lastb -F -i -s 202403010000 -t 202403080000这里的价值在于“聚合”。单看一条失败记录说明不了什么,用户输错密码是常事;但如果同一个 IP 在一小时内产生了几百条失败记录,账号名还在root、admin、test、oracle之间轮换,那就是典型的字典爆破特征。把上面的聚合结果和last -i | grep 该IP交叉验证一下,如果失败几百次之后出现了一条成功登录,优先按账号被盗处理,立刻改密码、查authorized_keys、看历史命令。
5.1 btmp 文件缺失的处理
前面提过,btmp不是所有系统都默认创建。如果执行lastb时报No such file or directory,可以先确认 PAM 配置,再决定是否手动创建:
# 确认 faillock / pam_tally2 是否启用 grep -rE 'pam_(faillock|tally)' /etc/pam.d/ 2>/dev/null # 手动创建 btmp(仅 root,权限必须 600) sudo touch /var/log/btmp sudo chown root:root /var/log/btmp sudo chmod 600 /var/log/btmp需要提醒的是,创建空文件之后lastb只会显示btmp begins ...这样的空记录,不会凭空产生历史数据。失败登录从此开始被记录,之前的尝试无法追溯。这也是为什么关键服务器应该在部署阶段就把这部分配置确认好,而不是等出了事才想起查。此外,如果系统用的是systemd-journald统一收集,失败登录也可能只体现在journalctl -u sshd里,需要两路都看。
6. lastlog 命令详解与实战用法
理解了last再看lastlog,会发现两者的设计哲学完全不同。last是“事件流水账”,记录每一次动作;lastlog是“状态快照”,每个用户只保留一行。这种差异来自底层文件结构:/var/log/lastlog是一个按 UID 直接寻址的定长记录文件,读取某个用户时直接seek到对应偏移量,效率极高,但也意味着它天生只能保存“最后一次”。
6.1 基础输出与字段含义
lastlog输出共四列:用户名、登录终端、来源主机、最后登录时间。终端列显示pts/0、tty1这类伪终端或物理终端编号;来源列对于本地登录会显示为空或:0,对于 SSH 登录会显示 IP 或主机名;时间列如果显示**Never logged in**,说明这个账号自创建以来从未成功登录过。
# 查看指定用户 lastlog -u alice lastlog -u alice -u bob # 查看 UID 大于等于 1000 的普通用户(过滤掉系统账号) lastlog -u 1000 # 设置“最后登录时间”的显示格式为完整时间戳 lastlog -t 30-u参数支持用户名也支持 UID,这个细节在脚本里很有用:直接传-u 1000就能把 UID 1000 及以上的账号全部列出来,天然过滤掉bin、daemon、nobody这些永不登录的系统账号,输出干净很多。-t参数表示“只显示最近 N 天内有登录的账号”,注意它是按天数过滤,不是按具体时间点。
6.2 用 lastlog 做账号清点
账号清理是lastlog最有价值的落地场景,完整的操作流程大致如下。先拉全量并过滤系统账号,把结果按最后登录时间排序,人工确认后再执行禁用或删除:
# 第一步:导出 UID>=1000 的账号最后登录情况 sudo lastlog -u 1000 > /tmp/lastlog_report.txt # 第二步:筛出从未登录的账号 sudo lastlog | grep -i 'Never logged in' # 第三步:找出超过 90 天未登录的账号(借 date 计算阈值后手工比对) date -d '90 days ago' '+%a %b %e' # 第四步:对候选账号先锁定而不是直接删除 sudo usermod -L -s /sbin/nologin <username> # 第五步:观察一段时间无异常后再删除 sudo userdel -r <username>这套流程里最关键的判断是“先锁后删”。lastlog显示从未登录,可能有很多原因:账号是刚建的还在走流程、账号是给某个自动化任务用的但任务还没上线、账号被禁用后lastlog记录被重置过。直接删除风险很高,万一有服务依赖这个账号的 UID 或家目录,删了就是事故。锁定账号既能立即止损,又保留了回滚空间,是更稳妥的工程做法。
6.3 lastlog 文件的稀疏特性
/var/log/lastlog有一个容易让人误解的特性:它的文件大小等于“最大 UID × 每条记录长度”,而不是“实际用户数 × 记录长度”。如果你的系统上出现过 UID 50000 甚至更大的账号(常见于 LDAP 同步、容器映射、Kubernetes 场景),文件逻辑大小会膨胀到几十 MB 甚至更多,但du看到的实际占用可能只有几十 KB。
# 对比逻辑大小和实际占用 ls -lh /var/log/lastlog du -h /var/log/lastlog # 查看文件系统的稀疏文件支持情况(ext4/xfs 均支持) stat -f -c '%T' /var/log这个特性带来两个实践影响。第一,备份lastlog时不要被大小吓到,tar会自动处理稀疏文件,实际传输量很小。第二,如果你的监控系统按文件大小做告警,lastlog会产生持续误报,建议把它加入白名单,或者改用du口径判断。理解了稀疏文件的机制,这类困惑基本就消失了。
7. wtmp/btmp/lastlog 文件格式与底层解析
前面反复提到这些文件是二进制的,那么它们到底长什么样?wtmp和btmp使用同一套struct utmp结构,每条记录定长,字段包括记录类型、PID、终端名、用户名、来源主机、时间戳等。类型字段决定了这条记录是“用户登录”“用户注销”“系统启动”还是“运行级别切换”,last就是靠这个字段区分reboot和普通用户的。
最直接的查看工具是utmpdump,它会把二进制记录转成可读文本:
# 把 wtmp 转成文本格式,便于 grep 和比对 sudo utmpdump /var/log/wtmp | head -20 # 转 btmp sudo utmpdump /var/log/btmp | head -20 # 结合 grep 找特定用户的所有记录类型 sudo utmpdump /var/log/wtmp | grep 'alice'utmpdump的输出格式是方括号包裹的记录块,字段顺序固定,能清楚看到登录和注销是两条独立记录,通过终端名和 PID 配对。这种原始视角在排查“登录记录缺失”时特别有用,因为你能确认到底是文件里没有记录,还是last的解析逻辑把它当成无效记录过滤掉了。
如果需要在自己写的监控脚本里解析这些数据,用 Python 读取也是常见做法。下面是按utmp结构解析wtmp的最小示例,仅供学习结构之用,生产环境建议直接用utmpdump或调用last的文本输出更稳妥:
import struct import datetime # struct utmp 在 64 位 Linux 上的常见布局,字段顺序随内核版本略有差异 UTMP_FORMAT = "h2x32s4s4s4s4s2xh4s32s256shh4x" UTMP_SIZE = struct.calcsize(UTMP_FORMAT) def parse_wtmp(path="/var/log/wtmp", limit=10): rows = [] with open(path, "rb") as f: while len(rows) < limit: data = f.read(UTMP_SIZE) if len(data) < UTMP_SIZE: break fields = struct.unpack(UTMP_FORMAT, data) ut_type = fields[0] user = fields[1].split(b"\x00")[0].decode("utf-8", "ignore") line = fields[2].split(b"\x00")[0].decode("utf-8", "ignore") host = fields[3].split(b"\x00")[0].decode("utf-8", "ignore") ts = fields[4] if ts == 0: continue rows.append({ "type": ut_type, "user": user, "tty": line, "host": host, "time": datetime.datetime.fromtimestamp(ts).isoformat(), }) return rows for row in parse_wtmp(limit=5): print(row)这段代码要强调的是“仅供理解结构”,因为不同发行版的struct utmp字段长度、填充字节、记录大小并不完全一致,硬编码格式字符串很容易在跨平台时读到乱码。真要写工具,正确做法是用ctypes绑定 libc 的getutent系列函数,或者直接解析utmpdump的文本输出。这一点在实际项目里踩过坑的人应该深有体会。
至于lastlog,它的结构简单得多,每条记录定长,只包含终端、主机和时间戳三个字段,按 UID 索引。因为格式简单,用struct解析相对安全,但在没有明确需求的情况下,还是建议直接用命令输出。
8. 日志轮转、保留策略与自动化巡检
wtmp和btmp的保留时长完全由logrotate决定,这是很多“查不到历史记录”问题的真正根源。大部分发行版的轮转配置里,wtmp的保留份数是 1 到 4 周,btmp通常保留 1 个月。如果公司有更长的审计要求,就需要主动调整。
8.1 查看与调整轮转配置
# 查看默认轮转配置 cat /etc/logrotate.conf # 查看针对 wtmp/btmp 的具体规则(不同发行版位置不同) grep -rn 'wtmp\|btmp' /etc/logrotate.conf /etc/logrotate.d/ 2>/dev/null典型的wtmp轮转规则长这样:每月轮转一次、保留 1 份、创建新文件时权限0664 root utmp。如果要保留更长,可以把rotate改成 12,表示保留 12 份月度归档,相当于一年。修改后建议用logrotate -d做一次 dry-run 验证语法:
# 只检查配置语法,不实际执行 sudo logrotate -d /etc/logrotate.conf 2>&1 | grep -A3 wtmp # 强制立即轮转一次(谨慎操作,会移动当前 wtmp) sudo logrotate -f /etc/logrotate.conf第二类配置是btmp的权限。轮转规则里必须确保新文件仍是600 root:root,否则普通用户就能读到失败登录记录,存在信息泄露风险。这一点在自建规则时特别容易漏掉,建议每次改完都用stat复核一遍。
8.2 每日巡检脚本示例
把last、lastb、lastlog组合成一个每日巡检脚本,输出到独立目录,是成本最低的审计增强手段。下面是一个可运行的骨架,逻辑是:抓取昨日登录记录、汇总失败登录 TOP、列出从未登录账号,全部落到文件里供人工复核:
#!/usr/bin/env bash set -euo pipefail REPORT_DIR="/var/log/login-audit" DATE_TAG="$(date +%Y%m%d)" mkdir -p "$REPORT_DIR" # 1. 昨日 00:00 到今日 00:00 的成功登录 YESTERDAY="$(date -d 'yesterday' +%Y%m%d)" TODAY="$(date +%Y%m%d)" last -F -i -s "${YESTERDAY}000000" -t "${TODAY}000000" \ > "${REPORT_DIR}/login-${DATE_TAG}.log" 2>&1 || true # 2. 同时间窗内的失败登录 TOP50 lastb -F -i -s "${YESTERDAY}000000" -t "${TODAY}000000" 2>/dev/null \ | awk 'NF>=3 {print $1, $3}' | sort | uniq -c | sort -rn | head -50 \ > "${REPORT_DIR}/failed-${DATE_TAG}.log" || true # 3. 从未登录的普通账号 lastlog -u 1000 2>/dev/null | grep -i 'Never logged in' \ > "${REPORT_DIR}/never-login-${DATE_TAG}.log" || true # 4. 清理 90 天前的报告 find "$REPORT_DIR" -name '*.log' -mtime +90 -delete这个脚本有几个细节值得说明。set -euo pipefail保证任何一步出错都能被发现;每条命令后面加|| true是为了避免某条命令返回非零导致整个脚本中断;报告按日期命名并保留 90 天,既满足基本追溯需求又不会把磁盘塞满。实际部署时可以挂到 cron 里,每天凌晨跑一次。这类脚本的价值不在于多复杂,而在于把“想起来才查”变成“每天都有记录”。
9. 常见问题与排查方法
这一节把实际使用中最常遇到的坑集中列出来,配合命令直接给出排查路径。很多问题看似是命令坏了,实际是数据文件缺失、权限不足或者轮转配置导致的历史被清空。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
last只输出wtmp begins ...一行 | wtmp 刚被轮转或系统刚重装 | ls -l /var/log/wtmp*看文件时间 | 查轮转后的wtmp.1,或用last -f指定历史文件 |
last卡住十几秒才出结果 | DNS 反解超时 | 加-i观察是否变快 | 固定使用last -i,或在/etc/hosts补齐解析 |
lastb报文件不存在 | 系统未创建 btmp | ls -l /var/log/btmp | 手动创建并设chmod 600 root:root |
lastb输出为空但确实有失败登录 | 普通用户无读权限 | id、stat /var/log/btmp | 用sudo执行 |
lastlog显示大量**Never logged in** | 账号创建后未使用,或 UID 复用 | 结合chage -l、passwd -S看账号状态 | 按流程锁定或删除,先锁后删 |
lastlog文件几百 MB | 稀疏文件按最大 UID 计算体积 | du -h对比ls -lh | 无需处理,属正常现象;监控口径改用 du |
用户登录了但last查不到 | 使用su切换而非直接登录 | last -x、journalctl -u sshd交叉验证 | 查/var/log/secure或auth.log |
容器内last无任何记录 | 容器不写 wtmp,utmp 为空 | ls -l /run/utmp | 属预期行为,容器审计走宿主机或日志平台 |
历史文件是.gz无法-f读取 | logrotate 启用了压缩 | ls /var/log/wtmp* | zcat解压到临时目录后再last -f |
last输出中文用户名乱码 | 编码不一致 | locale查看当前字符集 | 统一使用 UTF-8,或用utmpdump原始查看 |
| 脚本解析结果错位 | 用户名或主机名含空格 | 抽样检查原始输出 | 用-R或-a固定列位置后再解析 |
表格之外还有两个细节值得单独提。第一个是“用户名含空格”这个坑,虽然少见,但一旦出现,所有基于awk按空格切列的脚本都会错位。稳妥做法是先确认环境里没有这种账号,或者改用utmpdump的结构化输出解析。第二个是“时间跨时区”的问题,如果服务器时钟是 UTC,而你在东八区看记录,会觉得时间对不上,排查前先date确认一下系统时区。
10. 最佳实践与安全审计建议
把上面所有内容收敛成几条可以直接执行的建议,供日常运维参考。
第一,把last -F -i作为默认写法而不是可选写法。完整时间戳和 IP 直显能消除大部分歧义,多敲两个字符换来的是排查时不用二次确认。养成习惯之后,回头看默认格式的输出反而会不适应。
第二,关键服务器务必调整wtmp和btmp的保留策略,并在变更后做一次验证。默认四周的保留期对于事后审计来说太短,尤其是发生过安全事件之后才发现日志已经轮转掉,那种后悔是很难弥补的。调整时注意同步检查新文件的权限,别在延长保留期的同时把权限放宽了。
第三,账号治理采用“先锁后删”的流程,lastlog只是初筛工具,不能作为唯一决策依据。一个账号从未登录,可能是因为它服务于某个尚未上线的定时任务,也可能是因为它绑定了某个服务的运行身份,直接删掉家目录和 UID 会引发连锁问题。锁定、观察、再删除,这个节奏看起来慢,实际上省下的返工时间更多。
第四,不要指望单一数据源。last看登录,lastlog看状态,lastb看失败,journalctl看服务层日志,auditd看文件级操作,每一样都有盲区。真正的安全审计是把几路信息按时间轴对齐后交叉印证,任何一条孤立的记录都不足以定性。这一点在写自动化告警规则时尤其重要,规则设计得越依赖单一指标,误报和漏报就越多。
第五,涉及登录记录的所有操作都要放在合规框架内。登录信息包含账号、IP、时间等个人相关数据,采集、存储、共享都应有明确的授权依据和保留期限。做巡检脚本时把输出目录权限收紧到750以内,报告按周期清理,不要把完整登录流水长期堆在公开可读的位置。
最后补一句实操层面的提醒:改完logrotate配置之后,别只做 dry-run 就完事,找一个低峰时段实际触发一次轮转,确认新的wtmp能被正常创建、last能正常读取、历史文件能正常用-f打开。这一步验证花不了五分钟,但能避免在真正需要查日志的那天才发现配置写错了。登录审计这套东西,平时看着没用,用上的时候就是关键证据,值得提前把它收拾利索。