文件属性这一层,很多人从来没用过
大部分站长改文件权限就停在 chmod 755 和 chown www-data 这两招。但 Linux 的权限模型其实有三层:普通的 rwx 位、ACL 扩展权限,以及最容易被忽略的文件属性(file attributes)。第三层由 chattr 控制,它不属于 POSIX 权限体系,连 root 都会受它约束——这正是它最有价值的地方。
这个命令小到只有一百多行 man 手册,但用对了能解决几个很具体的问题:防止误删重要配置文件、防止程序往日志里写入时被意外截断、让备份文件不被同目录下的 rm -rf 带走、以及在共享目录里阻止某人修改别人的文件。下面按实际场景讲,每条都给出验证方法。
先看清当前有哪些属性
查看属性用 lsattr,注意它和 ls -l 是两个东西,别搞混:
lsattr -d /etc/nginx/nginx.conf
lsattr -a /var/www/html
lsattr -R /etc/nginx 2>/dev/null | head -20输出形如 ----------------e----- /etc/hosts,那一串短横线加字母就是属性位。最右边那个 e 在 ext4 上表示 extent 格式,几乎所有文件都有,不用管。真正要关心的是下面这几个字母:
- i(immutable):不可变。不能修改、不能删除、不能重命名、不能建硬链接,连 root 也不行。
- a(append only):只追加。可以往文件末尾写,但不能覆盖或截断已有内容。
- A(no atime):不更新访问时间,减少一次写入 I/O。
- d(no dump):被 dump 工具忽略。
- j(data journalling):数据也走日志,写入更安全但更慢,ext4 上默认不推荐开。
- S(synchronous):写入立即同步落盘,等价于每次 write 都 fsync。
- c(compressed):只在 btrfs 等支持透明压缩的文件系统上有效。
目录上的属性含义略有不同:给目录加 i 意味着目录里不能增删文件,但已有文件的内容仍可修改。这个区别在防误删场景里非常关键,很多人以为加了 i 目录整棵树都锁死了,其实里面的文件照样能改。
场景一:锁住关键配置,防自己和脚本误改
最常见的痛点是:某个自动部署脚本每次跑都会覆盖 nginx.conf,或者有人手快 rm 了证书目录。给最关键的几个文件加 i,能让破坏性操作直接失败并留下报错:
chattr +i /etc/nginx/nginx.conf
chattr +i /etc/ssh/sshd_config
rm -f /etc/nginx/nginx.conf
# rm: cannot remove '/etc/nginx/nginx.conf': Operation not permitted但这里立刻有一个坑:加锁会影响正常的运维流程。包管理器升级 nginx 时会尝试写 nginx.conf,遇到 immutable 会报错中断,甚至留下半装的包状态。所以不要一上来就锁一堆,只锁那些「确实不该被任何自动化碰」的文件,并且在变更流程里明确写出解锁步骤:
# 标准变更流程
chattr -i /etc/nginx/nginx.conf
vim /etc/nginx/nginx.conf
nginx -t
systemctl reload nginx
chattr +i /etc/nginx/nginx.conf
lsattr -d /etc/nginx/nginx.conf # 确认已回到 ----i---------把这个四步写成脚本,比靠记忆可靠。另外注意 dpkg 有 --force-* 之类的绕过参数,但用它们绕过 immutable 是不推荐的,等于把安全网剪了。正确做法是升级前统一解锁:
cat >/usr/local/bin/unlock-etc <<'SH'
#!/bin/bash
find /etc -maxdepth 3 -type f -perm -u+w -exec lsattr {} \; 2>/dev/null \
| grep -- '----i' | awk '{print $2}' | while read -r f; do chattr -i "$f"; done
SH
chmod +x /usr/local/bin/unlock-etc先列出来再解锁,比盲目 chattr -R -i /etc 安全——后者会顺带清掉别的安全设置,还会在某些文件系统上跑很久。
场景二:让日志只能追加,防止被截断
Web 站点最怕的一件事是日志被清空。有些 CMS 插件或者老旧脚本会 file_put_contents($path, $data) 覆盖整个日志文件,一跑就把历史记录抹掉。chattr +a 能让这种覆盖操作直接失败,同时正常的追加写入依然工作:
chattr +a /var/log/nginx/access.log
echo hello > /var/log/nginx/access.log
# bash: /var/log/nginx/access.log: Operation not permitted
echo hello >> /var/log/nginx/access.log # 成功但 append-only 属性和日志轮转是天然冲突的:logrotate 默认先 mv 再建新文件,或者用 copytruncate 原地截断,两种方式都会失败。这是这个技巧最常踩的坑。正确做法是把该文件的轮转配置改成 create 模式:
# /etc/logrotate.d/nginx-append
/var/log/nginx/access.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 0640 www-data adm
postrotate
chattr -a /var/log/nginx/access.log 2>/dev/null || true
chattr +a /var/log/nginx/access.log 2>/dev/null || true
endscript
}create 会新建文件,但新建出来的文件没有 a 属性,所以 postrotate 里要重新加回去。同时必须不要用 copytruncate:截断被 append-only 挡住,logrotate 会在邮件里报错,但很多人不看 /var/log/logrotate.status,结果日志悄悄停止轮转,最后把磁盘写满。
更稳妥的替代方案是让应用自己按大小切分,Nginx 有内置的 access_log ... buffer=32k flush=5s,配合外部按日期切割的目录结构,就不需要碰 append 属性。所以这个技巧适合的场景是「日志是被动写入、轮转由别人负责」的情况,自己完全掌控写入链路时,用轮转配置解决更干净。
场景三:备份目录里的文件不被误删
给备份文件加 i 是低成本的勒索病毒与误操作防线。典型的 rm -rf /data/backup/* 会失败,攻击者要删掉还得先跑 chattr -i,门槛提高了不少:
for f in /data/backup/*.tar.gz; do chattr +i "$f"; done
# 恢复前先解锁
find /data/backup -name '*.tar.gz' -exec chattr -i {} \;有一个容易忽略的细节:chattr 也支持 +R 递归,但对目录递归在千万级文件上非常慢,而且会把属性加到不该加的文件上。备份目录用 find -exec 按类型精确匹配更可控。另外,属性是存在 inode 里的,复制文件不会保留属性,cp -a 和 rsync -a 默认都不带 immutable 位。所以备份到另一台机器时,需要在目标端单独再跑一次加锁脚本。
还有一处容易踩的:如果备份目标是 NFS 或 CIFS 挂载,chattr 会返回 Operation not supported。这不是权限问题,是远端文件系统不支持 Linux 扩展属性。脚本里必须判断失败并记录,否则会出现「以为加锁了实际没有」的假安全。
场景四:多人共享目录,阻止越权修改
团队共用一个上传目录时,常见需求是「每个人只能改自己的文件」。这一条 chattr 做不到,应该用 ACL 加粘滞位。但如果需求是「已归档的文件任何人不能再改」,chattr +i 就很合适,因为 ACL 只能限制写权限,而文件属主自己仍可以 chmod 回来,immutable 位只有 root 能解:
# 归档目录:文件一旦移入即冻结
chattr +i /srv/archive/2025/*.pdf
# 需要更正时由 root 统一解锁,留下操作记录
logger -t chattr-audit "unlock $f by $(whoami)"再配合目录的粘滞位 chmod 1777 /srv/upload,普通用户只能删自己的文件,root 冻结过的文件谁也删不了。这一组合在不引入额外权限系统的前提下,能把「误删/误改」的概率压到很低。
场景五:防止配置模板被自动部署覆盖
站点搬迁或者换机器时,最烦的一种事故是:新配置改好上线了,第二天有个老定时任务或者某个面板插件把文件又写回模板版本,问题重现但日志里查不到是谁干的。除了给文件加 i 之外,还有一层更轻的保护:把模板目录整体设为不可变,让所有写入都必须走「先解锁」这条路径,这样任何一次覆盖尝试都会在日志里留下痕迹。
配合 auditd 可以把「谁尝试写过」记下来,这对多人协作的环境尤其有用:
# 监控对关键目录的写入尝试
auditctl -w /etc/nginx -p wa -k nginx_conf
# 事后查看
ausearch -k nginx_conf -ts recent | aureport -f -i即使写入被 immutable 挡住了,auditd 仍然会记下那次失败的 open 调用,进程名和 PID 都在。这是我认为 chattr 最被低估的用法——它不只是「拦住修改」,还是一个天然的操作触发器,让你知道系统里有谁在试图动这些文件。
场景六:给系统关键文件兜住最后一层
除了 nginx 和 sshd,还有几类文件的损坏会导致整机无法启动或无法登录,值得单独加锁:/etc/passwd、/etc/shadow、/etc/sudoers、/etc/fstab、/boot/grub/grub.cfg。这些文件被错误编辑是极难恢复的故障,尤其 fstab 写错会让机器进不去系统,只能靠救援模式。
for f in /etc/passwd /etc/shadow /etc/sudoers /etc/fstab; do
chattr +i "$f" 2>/dev/null || echo "skip $f"
done
lsattr /etc/passwd /etc/shadow /etc/sudoers /etc/fstab但要非常清楚代价:adduser、passwd、userdel 这些命令全部会失败,因为它们需要写 /etc/passwd 和 /etc/shadow。如果你给这两者加 +i,那么以后建用户的标准流程就变成三步:解锁、建用户、加锁。这在小团队、人员流动快的环境里很容易忘,最后演变为「加连锁了之后有人直接 chattr -R -i /etc 解锁并且忘记加回」。所以我的建议是:只锁那些真正不需要日常变更的文件,passwd/shadow 用别的机制(备份 + 文件完整性监控)保护更合适。
真正值得锁的通常是:/etc/nginx/nginx.conf、证书私钥文件、/etc/systemd/system 下的关键 unit、以及备份文件。判断标准很简单——问一句「这个文件正常情况下多久改一次」。半年才改一次,锁;每周都要改,别锁。
几个必须知道的限制
这些限制不看会浪费很多时间,逐条列出来:
- 文件系统支持有限:ext2/3/4、XFS(部分属性)、btrfs、f2fs 支持;tmpfs、overlayfs、NFS、多数容器挂载不支持或部分支持。Docker 里给容器内文件加属性经常无效果,因为 rootfs 是 overlayfs。
- 容器里加锁要小心:在容器内
chattr +i成功,但镜像层是只读的,重建容器后属性全部丢失。要持久,必须写在 Dockerfile 的 RUN 里或者由 entrypoint 每次启动时加。 - root 也有边界:
i和a属性对 root 同样生效,这是特性不是 bug。但 root 可以chattr -i解开,所以它防的是误操作和低权限攻击者,不防已经拿到 root 的人。 - 属性不影响已打开的文件句柄:一个进程已经
open()了文件,你之后加+a,该进程的写入依然按原始的打开模式进行。想让属性生效,必须重启持有句柄的进程。 - tar 备份会丢属性:需要
--acls --xattrs,但 immutable 位仍不一定保留,恢复后要重新跑加锁步骤。
最后一条最实际:任何「加锁」动作都要有一个配套的「加锁状态巡检」,否则半年后没人知道哪里被锁了,升级失败时排查半天。写个每日检查,把带属性位的文件列表落盘:
find /etc /srv /data -xdev -type f -exec lsattr {} + 2>/dev/null \
| grep -E -- '^----[ia-]{2}' | sort > /var/log/chattr_state_$(date +%F).txt
diff <(sed 's/^[^ ]*//' /var/log/chattr_state_$(date -d yesterday +%F).txt) \
<(sed 's/^[^ ]*//' /var/log/chattr_state_$(date +%F).txt)这样每天早上就能看到「昨天到今天有没有人动了属性位」,包括自己。安全措施如果没有可观测性,很快就会变成负债。