暗色模式

Linux 文件隐藏属性:从 lsattr 到 chattr 不可变保护

技术教程
2026-09-14
3
0
本文要点
  • lsattr 输出的 --------------e------- 是固定 22 个字符的属性位,一位一个标志;ext4 上的文件默认只带一个 e(extent 格式),它只读,chattr 自己也改不动
  • chattr +i 之后连 root 都动不了:删除、覆盖、chmod、改名、建硬链接全部 Operation not permitted,唯一出路是先 chattr -i
  • chattr +a 只放开「追加」:>> 正常写、> 清空和 rm 都被拒,适合给审计日志上锁
  • 属性属于文件系统:tmpfs(/dev/shm)和 /proc 上直接报 Operation not supported,换文件系统这个能力就没了
  • ⚠️ cp -atarrsync -a 都不保留 i 标志——备份还原回来的文件是「解锁」状态,别以为锁还在
  • ⚠️ 给 /etc/usr 下的文件加 +i 会让 apt/dpkg 升级直接报错,脚本里务必先解锁

Linux 文件隐藏属性:从 lsattr 到 chattr 不可变保护

chmod 管的是「谁能读写执行」,chown 管的是「归谁」,ACL 管的是「某个人能不能」,但它们都有一个共同点:拿到 root 就全都不算数。而有一类属性比这些更硬——它绕过权限模型,直接落在文件系统的 inode 上,设上之后连 root 亲手敲 rm -rf 都会被内核挡住。

这就是 chattr 的领域。它不常出现在日常命令里,但在几个场景中几乎是唯一解:给日志文件上锁防止攻击者清痕迹、给关键配置上锁防止误改、给 Web 目录上锁防止挂马落地。本文在一台 Ubuntu 22.04 云服务器(KVM,1 核、内存 1.9G,内核 5.15.0-30-generic,ext4)上把常用属性逐个跑了一遍,包括几个不太直观的坑。

第一步:lsattr 看属性,22 个字符里大多数字符你都改不动

先看一台普通机器上的文件都带什么属性:

lsattr -d /root; lsattr /etc/passwd /etc/shadow /etc/hostname
--------------e------- /root
--------------e------- /etc/passwd
--------------e------- /etc/shadow
--------------e------- /etc/hostname

lsattr 的第一个字段是固定 22 个字符的属性位,一位对应一个标志,- 表示没设。上面这些文件全都只有一个 e

e 是 ext4 的 extent 标志(表示文件用 extent 树而不是传统的块映射表来记录磁盘块),它是只读属性——chattr 自己也去不掉:

chattr -e f.txt 2>&1; lsattr f.txt
chattr: Operation not supported while setting flags on f.txt
--------------e------- f.txt

所以看到 e 不用管它,它是这个文件系统的正常状态。真正能操作的是后面那几个字母,其中最有价值的是 ia

第二步:chattr +i,连 root 都删不掉的不可变

造一个配置文件,上锁,然后把它能想到的破坏手段挨个试一遍:

cd /root/chattr-demo; printf 'listen=8080\nmode=prod\n' > app.conf; lsattr app.conf; chattr +i app.conf; lsattr app.conf; cp /dev/null app.conf 2>&1; rm app.conf 2>&1; chmod 600 app.conf 2>&1; cat app.conf

lsattr 从只有 e 变成 i+e;随后覆盖、删除、chmod 全部报 Operation not permitted,而 cat 出来的内容原封不动

这张图的四行报错值得逐条看,它们各自代表一个不同的系统调用被内核拒绝:

尝试的操作报错被拦在哪一层
cp /dev/null app.conf(覆盖)cp: cannot create regular file 'app.conf': Operation not permittedopen(O_TRUNC) 被拒
rm app.conf(删除)rm: cannot remove 'app.conf': Operation not permittedunlink() 被拒
chmod 600 app.conf(改权限)chmod: changing permissions of 'app.conf': Operation not permittedchmod() 被拒
mv / ln(改名、建硬链接,另测)cannot move / failed to create hard linkrename() / link() 被拒

而最后那句 cat app.conf 依然正常输出 listen=8080 / mode=prod——+i 只禁止写,不禁止读,所以备份、校验、日志采集这些只读操作完全不受影响。

几个必须记住的性质:

  • root 也不例外。 上面所有命令都是以 root 身份执行的。Linux 的权限检查在 +i 面前直接短路,cap_dac_override 之类的特权绕不过去。
  • 但不是攻不破的墙。 同一个 root 只要敲一句 chattr -i app.conf,锁立刻就开了。+i 挡的是误删、误改、以及拿到普通 shell 的攻击者,挡不住已经拿到 root 并且知道这个机制的人。想往「让 root 也改不了」的方向走,那是 Linux capabilities 能力机制:从 setuid 特权到 setcap 最小授权AppArmor 强制访问控制:从内核集成到自定义限制策略 的地盘,+i 只是其中成本最低的第一道门槛。
  • 需要 CAP_LINUX_IMMUTABLE 本机 root 的 capability 集合里带着 cap_linux_immutable,所以能设;在受限容器里如果这个能力被裁掉,chattr +i 同样会报 Operation not permitted

真实运维里最典型的用法是给关键配置和凭证上锁,例如 /etc/ssh/sshd_config/etc/sudoers~/.ssh/authorized_keys(防止有人塞进自己的公钥后门)。改的时候先 chattr -i,改完再 chattr +i,这两个动作应该写进变更流程里,而不是靠记性。

第三步:chattr +a,只准追加的日志锁

+i 太绝对了——日志文件需要「能写,但不能改」。这就是 +a(append only)的用途:

cd /root/chattr-demo; printf '08:00 login ok\n' > audit.log; chattr +a audit.log; lsattr audit.log; echo '08:01 login FAIL' >> audit.log; echo '08:02 sudo ok' >> audit.log; cat audit.log; cp /dev/null audit.log 2>&1; rm audit.log 2>&1

chattr +a 之后两条日志正常追加,而清空与删除操作被内核拒绝

结果非常干净:两次 >> 追加都成功,日志积到三行;而 cp /dev/null audit.log(实质是 O_TRUNC 清空)和 rm audit.log 都被拒绝。+a 的语义可以概括成一句话:文件只能往长了长,不能变短,也不能消失

对一个正在被写入的日志文件来说,这正是想要的形态。它和 Linux 安全审计:从 auditd 规则监控到 ausearch 操作溯源 那套是互补的:auditd 记录「谁做了什么」,+a 保证「记录下来的东西删不掉」。两者都不难配,但很多人只做了前一半——日志采集得很全,攻击者进来一句 rm -f /var/log/auth.log 就全没了。

需要注意 +a 的两个边界:

一是它拦的不只是删除,改名和建硬链接一样被拒——也就是说攻击者没法把日志文件「挪走」再慢慢处理:

mv app.log app2.log 2>&1; ln app.log app.link 2>&1; echo line2 >> app.log; cat app.log
mv: cannot move 'app.log' to 'app2.log': Operation not permitted
ln: failed to create hard link 'app.link' => 'app.log': Operation not permitted
line1
line2

前两条被拒绝,第三条追加照常成功——「只能变长」这个语义执行得很彻底。

二是日志轮转logrotate 默认用 rename + 新建的方式轮转,遇到 +a 的文件会失败——如果你的日志既要上锁又要轮转,得把 logrotate 改成 copytruncate 模式,但 copytruncate 依赖的 truncate()+a 文件上同样会被拒,所以 +a 和常规 logrotate 本质上是不兼容的,正确做法是让日志直接走远程 syslog 或把锁加在只写不轮的归档副本上。轮转本身的细节可以看 Linux logrotate 日志轮转:从轮转策略到防止磁盘写满

第四步:chattr -R 递归加锁,以及目录上的坑

目录也能加 +i,而且效果比文件更彻底——它把整个子树封死:

cd /root/chattr-demo; mkdir -p site/static; echo hello > site/index.html; echo css > site/static/a.css; chattr -R +i site; lsattr -R site; cp /dev/null site/index.html 2>&1; mkdir site/newdir 2>&1

chattr -R +i 之后 lsattr -R 递归列出子目录与文件,改文件与在目录内新建都被拒

两点说明:

1. lsattr -R 的输出顺序不是先目录后内容。 它会像 find 一样边走边打印:先列出 site 里的第一个条目 site/static(它自己带 i),再展开 site/static: 的内容,最后才回头列 site/index.html。看起来有点乱,但每一行都是「22 字符属性位 + 空格 + 路径」。

另外注意,不加参数时 lsattr <目录> 列的是目录里的内容,不是目录自己。要看去掉 -R 之前、加锁之前的目录本体属性,得用 -d

lsattr -d site; lsattr site
--------------e------- site
--------------e------- site/static
--------------e------- site/index.html

-d 那行给的是 site 自己,不带 -d 的那次把两个条目都列了出来。加锁之后同一个位置会变成 ----i---------e------- site——目录的 i 和里面文件的 i 是两套独立的标志-R 只是帮你递归地一次性设上。

2. 目录加 +i 之后,连在里面新建文件都不行。 截图最后一行 mkdir: cannot create directory 'site/newdir': Operation not permitted 就是这个意思——+i 的目录不能被增删条目。这解释了它最实用的场景:给静态资源目录、上传目录上锁。Web 目录被写进一个 webshell 是入侵后最常见的动作之一,而一个 chattr -R +i /var/www/html 就能让「落地」这一步失败(代价是正常发布流程也要先解锁,所以更适合发布频率低的站点)。

反过来,递归上锁之后要删掉整个目录,必须用 -R 解锁。实测 rm -rf 会直接失败:

chattr -R +i /root/cd2; rm -rf /root/cd2 2>&1 | head -1; ls -R /root/cd2
rm: cannot remove '/root/cd2/sub/f.txt': Operation not permitted
/root/cd2:
sub

/root/cd2/sub:
f.txt

那句 rm -rf 只失败了一条就停了,目录结构完好无损。解锁用 chattr -R -i /root/cd2 即可。这个「锁」和「解锁」必须成对出现在运维脚本里——脚本异常中断导致锁没解开,是这类属性最常见的自伤方式。

第五步:两个顺手能用的属性,+A 和 +S

除了 ia,还有几个实际有用的:

+A:访问文件时不更新 atime。 每次 cat 一个文件,内核默认都要回写它的 atime(访问时间),这是一次额外的元数据写。+A 让这个写入不再发生:

echo data > n.txt; echo data > y.txt; chattr +A y.txt; lsattr n.txt y.txt; cat n.txt > /dev/null; cat y.txt > /dev/null; stat -c '%n atime=%x mtime=%y' n.txt y.txt
--------------e------- n.txt
-------A------e------- y.txt
n.txt atime=2026-09-13 21:02:59.043729794 +0000 mtime=2026-09-13 21:02:59.039729745 +0000
y.txt atime=2026-09-13 21:02:59.039729745 +0000 mtime=2026-09-13 21:02:59.039729745 +0000

对比很直接:没加 +An.txt,atime(...043)已经跑到了 mtime(...039)后面,说明读操作确实回写了;加了 +Ay.txt,atime 和 mtime 一秒不差地相等,说明 atime 根本没被更新。这个属性的价值在写放大敏感的场景(闪存、大量小文件被频繁读取)。它和挂载选项 noatime / relatime 是同一件事的两种粒度:挂载选项管整个文件系统,+A 管单个文件,后者更精细——毕竟「哪些文件的 atime 有业务价值」通常只是少数。atime/mtime/ctime 三者的区别和排查用法,见 Linux 文件时间戳:atime、mtime、ctime 与 find 时间筛选

+S:写入立即同步落盘。 正常写文件是先进页缓存、由内核择机刷盘,+S 让每次 write() 都同步等待落盘(等价于对该文件始终开着 O_SYNC)。它适合那种「掉电丢一行都不能接受」的小文件,代价是写入速度会明显下降——别给日志文件或者数据库文件加这个,那是 fsync 策略该管的事。

其余标志里,d(不被 dump 备份)、j(data journaling)、s(安全删除,删除时用 0 覆盖磁盘块)、u(可恢复删除)在 e2fsprogs 的 chattr 里都能写,但 s/u 在现代 ext4 上支持有限,实际项目里极少使用,知道它们存在即可。

第六步:三个必须知道的边界

1. 属性依赖文件系统,不是所有地方都有。 在 tmpfs(比如 /dev/shm)和 /proc 这类文件系统上,命令连读都读不了:

echo x > /dev/shm/cattest; chattr +i /dev/shm/cattest 2>&1; chattr +i /proc/version 2>&1
chattr: Operation not supported while reading flags on /dev/shm/cattest
chattr: Operation not supported while reading flags on /proc/version

注意报错措辞是 while reading flags——内核连「读属性」这个 ioctl 都不支持,更别说设置。ext4、XFS、btrfs 支持 i/a 这些基本标志(XFS 的实现细节与 ext4 并不完全一致),但 tmpfs、overlayfs、NFS、FAT 系大多不支持。所以这个方案能不能用,取决于目标目录落在哪种文件系统上findmnt -no FSTYPE /path 先确认。文件系统选型的整体比较见 Linux 文件系统:从 ext4 到 XFS 的选型与挂载优化

2. ⚠️ 备份和同步都不保留这个属性。 这是最容易被忽略的一条。实测四种搬运方式,结果一致:

mkdir -p /root/at/src && cd /root/at/src && echo important > locked.conf && chattr +i locked.conf && lsattr locked.conf && cp -a locked.conf /root/at/cp_a.conf && tar czf /root/at/bak.tgz locked.conf && mkdir -p /root/at/restore && tar xzf /root/at/bak.tgz -C /root/at/restore && rsync -a locked.conf /root/at/rsync_copy.conf && lsattr /root/at/cp_a.conf /root/at/restore/locked.conf /root/at/rsync_copy.conf
--------------e------- /root/at/cp_a.conf
--------------e------- /root/at/restore/locked.conf
--------------e------- /root/at/rsync_copy.conf

cp -atar 打包再解包、rsync -a,拿到的副本全都只有 ei 不见了。也就是说:从备份恢复回来的文件是解锁状态。如果上锁是你安全设计的一部分,恢复流程里必须补一句 chattr +i,否则会出现「源机上锁了、灾备机裸奔」这种没人会立刻发现的缺口。备份工具本身的用法见 rsync 数据同步与备份:从增量原理到定时快照Linux tar 打包与压缩:从归档原理到增量备份

3. ⚠️ 给系统文件上锁会让包管理炸掉。 这是最常踩的坑:apt upgrade 需要覆盖 /usr/bin/etc 下的文件,一旦目标文件是 +i 的,dpkg 会报 Operation not permitted 并中断,留下的可能是半升级状态。所以:

  • 不要给 /usr/lib/var/lib/dpkg 下的文件上锁;
  • 如果确实锁了 /etc 里的某个配置,升级前必须先 chattr -R -i
  • 上锁清单最好落成文件(lsattr -R /etc 2>/dev/null | grep -- '----i' 可以随时扫出当前哪些文件是锁着的——本机扫 /etc 下 1555 个条目、命中 0 个,耗时几十毫秒,放进 cron 做漂移巡检也不重)。

包管理的完整流程见 Linux 软件包管理:从 apt 软件源到 dpkg 包管理

小结

chattr 的属性按用途排一下,选型就很清楚了:

  • +i(immutable):最硬的锁,读写分离——读随便,写全禁。适合静态资源目录、关键配置、凭证文件。代价是包管理和发布流程都要先解锁;
  • +a(append only):只增不减,给审计日志用。注意它和 logrotate 天生不兼容,日志轮转要靠 copytruncate 之外的方案;
  • +A(no atime):省掉读操作带来的元数据写,读多写少的目录可以按文件粒度开;
  • +S(sync):写入即落盘,只给「丢一行都不可接受」的小文件用,别给大文件或日志加。

最后回到一句实话:chattr 是一道门槛,不是一堵墙。它拦得住手滑的 rm -rf、拦得住拿到普通账户的入侵者、拦得住自动化脚本的误写;但对已经拿到 root 的人来说,chattr -i 只需一条命令。它的正确定位是「成本极低的纵深防御第一层」,配合权限(Linux 文件权限:从 chmod 到 ACL 的完整指南)、完整性校验(Linux 文件完整性校验:从 sha256sum 到 dpkg -V 篡改排查)和操作审计一起用,才是一套完整的闭环。

发表评论

暂无评论,快来抢沙发吧!