1. 项目背景与整体设计思路:这个小项目到底解决什么问题
做运维和系统管理的人,基本都会遇到这两类让人头大的事情:权限配错了,某个服务突然起不来,或者某个文件被莫名其妙改掉;进程管理失控了,一个僵尸进程把CPU占满,或者kill不掉一个卡死的程序,只能重启机器。我自己在维护几台开发机和测试服务器时,就反复踩过这些坑。后来我实在不想每次都靠临时敲命令救火,于是动手做了一个权限管理和进程管理的小项目,把所有常用的权限配置、检查、清理动作,以及进程查看、管控、回溯的流程,整理成一套可复用、可审计的工具集。
这个小项目不是什么高深的大平台,本质上是“一组脚本+一套配置规范+一份操作SOP(标准作业程序)”的组合,既可以手动按步骤执行,也可以通过定时任务自动巡检。做这个项目之前,我需要明确它的核心定位:第一,把Linux环境下文件权限、特殊权限、隐藏属性这些容易被忽略但又极其影响系统安全的部分管理起来;第二,把进程管理的常见操作标准化,特别是针对僵尸进程、失控进程、开机自启项这些高频故障点;第三,引入RBAC(基于角色的权限控制)思路来设计脚本的使用权限,避免团队成员乱用管理命令。
项目做下来,给我最直观的感受是:系统权限和进程管理并不是两个孤立的领域,它们经常互相牵连。比如某个服务起不来,表面是启动脚本执行失败,拔一层看可能是脚本文件权限不对,或者是运行用户对目录没有写权限。正好在新一轮排查中,我发现每台机器上文件权限“凭感觉配”的现象特别严重,所以这个项目也把“权限基线”作为一项重点内容做了标准化。
这篇文章我打算按这样的逻辑展开:先说清楚文件系统特殊权限与属性管理的原理和落地方法,再讲进程管理从查看到定点清理的完整操作,然后把RBAC权限管理设计是如何融入脚本体系的,最后用一个完整的部署与测试案例来演示整体效果。无论是刚入门的运维新手,还是想规范自己服务器管理的开发者,都可以直接参考本文的脚本和配置来用。
2. 文件系统特殊权限与属性管理:躲不开的细活儿
2.1 基础权限还远远不够:什么是特殊权限
我见过很多同学对文件权限的理解停留在chmod 755、chmod 644这个层面,觉得ugo读写执行一套下来就万事大吉。但Linux文件权限体系里还有三个比较隐蔽其实是危险的位:setuid(SUID)、setgid(SGID)和sticky bit(粘滞位)。这个小项目里,我专门给这三个位做了巡检逻辑,因为生产环境很多安全事件都跟它们有关。
SUID意味着当用户执行该文件时,会临时拥有文件所有者的权限。典型代表是/usr/bin/passwd,它允许普通用户以root身份去修改密码文件。这个机制设计上是合理的,但如果一个普通用户可写的文件被加上了SUID且属主是root,那就等同于给了任意用户一把root权限的钥匙。SGID跟目录配合使用比较多,比如共享目录里新创建的文件自动继承目录的属组;同时SGID也可以作用在可执行文件上,让运行者临时拥有文件属组的权限。Sticky bit在/tmp目录上大家都很熟,只有在目录属主、文件属主或root才有删除权限,它有效解决了多用户共享目录下的互相删文件问题。
我在项目里写的第一组巡检脚本就是扫描整个文件系统里带有特殊权限位的可执行文件和目录。扫描的时候要注意,不能用一条find找全部然后不管三七二十一就处理,因为系统本身就有一些必要的特殊权限文件,像/usr/bin/sudo、/usr/bin/su这类。实际操作中,我先做基线快照,把特殊权限文件列表存到基线文件里,然后再定期对比,发现新增的或者变更的一律告警。这样做的好处是误报率大幅降低。
判断一个文件是否带有特殊权限,很多人用ls -l去看那一长串字符,比如rwsr-xr-x中的s就是SUID,但这在脚本里不好判断。更可靠的方式是用stat命令,输入stat -c "%a %A %u %g %n" 文件名,权限位五进制写法里如果出现了4000(SUID)、2000(SGID)或1000(sticky),就说明有特殊位置位。为了方便在脚本里批量判断,我推荐用find的-perm参数,它可以精确匹配权限位组合,语法是find /path -perm /4000 -type f,这里的“/”表示任意位匹配。这样一条命令就能把所有SUID文件捞出来。
2.2 隐藏属性里的那些坑:chattr与lsattr
比特殊权限更冷门的是文件隐藏属性,也就是chattr和lsattr这套命令。普通权限控制的是谁可以读、写、执行,而隐藏属性控制的是文件在更底层能不能被修改、删除、追加写入,甚至能不能被日志系统记录。我用一句话跟团队成员解释:chattr是底层锁,比chmod的权限级别更高。比如一个文件被赋予了i属性(immutable),那即使是root用户,也没办法直接修改或删除它,必须先去掉i属性才行。这个特性用来保护关键配置文件非常合适,比如/etc/passwd、/etc/shadow、/etc/sudoers这些文件,一旦配好就加锁,防止被意外覆盖或者被入侵者篡改。
另一个常用属性是a(append-only),它允许文件内容追加,但不允许覆盖和删除。日志文件就很适合用a属性,这样即使日志轮转脚本写得有问题,也不至于直接把历史日志覆盖掉。还有s属性(secure deletion),文件删除时内容会被清零,适用于需要安全删除的场景。但要注意,这个属性在ext4、xfs等主流文件系统上支持情况不完全一样,做项目前最好先在目标机器上做一轮验证。
在这个小项目里,我设计了一个属性管理模块,包含两件事:一是定期检查关键目录下有没有被意外加上可疑隐藏属性的文件,比如某个病毒喜欢给自身文件加i属性防止被杀掉,检查一下就能发现;二是提供一键给关键文件加锁和解锁的脚本,降低手工操作输错命令的风险。
具体实现上,我给关键文件加i属性的命令很简单:chattr +i /etc/ssh/sshd_config,查看属性用lsattr /etc/ssh/sshd_config。这里有一个刚踩过的坑:对目录设置i属性比文件复杂得多。给目录加i属性后,目录里已经存在的文件不受影响,但你不能在该目录下新建文件、删除文件或者重命名文件。如果没提前说明,团队里有人在被锁定的目录里部署新配置时会莫名其妙失败,排查了半天还以为是权限问题。所以我在脚本里提供了一个选项,可以递归地对目录内所有文件加i属性(find那儿加-exec chattr +i {} ;),但会明确提示操作的风险和撤销顺序。
2.3 权限基线:让“配置漂移”无处可藏
权限管理做久了就会发现,真正怕的不是初始配置错,而是“配置漂移”——一开始大家都按规范配好了,后来某次上线操作、手滑执行了一条chmod -R 777,或者某个安装包自动改了权限,整个系统的权限状态就漂移了。等到出了问题再想起来查,往往已经迟了。所以这个小项目把权限基线作为一个核心特性来做。
思路很简单:先在一台符合规范的基准机器上,导出整个需要巡检的目录树或关键文件的权限快照,包括属主、属组、权限位、特殊权限位、隐藏属性,保存为JSON或文本格式;然后在客户端机器上定期运行比对脚本,将当前状态与基线状态做diff,任何不一致都输出告警并记录到日志。为了不让误报刷屏,我会把一些已知允许变更的路径加入白名单,比如日志目录、临时目录、缓存目录。
快照生成脚本我用了find加stat组合,核心命令大致是这样的:find /path -exec stat -c '%a|%U|%G|%n' {} ; > baseline.txt,一行一条记录。比对时就逐行读取基线文件,在当前机器上重新执行stat并对比。这里有一个细节:符号链接的权限在实际业务里一般不太关心,所以默认用-type f只查普通文件,需要的话再加-type d查目录。如果你管理的是Nginx、Tomcat这类Web服务,我建议把配置目录、应用目录、日志目录分开建立三份不同的基线,因为它们的变更频率完全不同,混在一起会让告警变得毫无意义。
我发现,把权限基线纳入定时巡检后,很多隐患在爆发前就被发现了。比如有一次巡检告警某个应用配置文件被修改成o+r,一看时间是前一天晚上上线时顺手执行了chmod -R o+r 应用目录。如果没有基线比对,这一下就把整个应用目录的敏感配置暴露给本机所有用户了,想想都后怕。
3. 进程管理:从“看到”到“管住”的完整链路
3.1 常用进程查看手段的实战经验与坑
进程管理的第一步永远是看清现状。Linux下查看进程的命令不少,ps、top、htop、pgrep各有侧重。我看很多新手习惯一上来就ps aux然后靠肉眼过滤,数据一多基本看不过来。正确的做法是带着目的去查:查进程ID、查内存占用、查父子关系、查监听端口,每个场景用最省事的命令。
ps aux输出里比较关键的是STAT列,它直接反映了进程状态。R代表正在运行,S代表睡眠,D代表不可中断睡眠,Z代表僵尸进程。我写巡检脚本时,会定时检测STAT列中带Z的进程,一旦出现就告警并收集对应的父进程信息。值得注意的是,STAT列还会出现多字符组合,比如Ss、S+等,s表示该进程是会话首进程,+表示它在前台进程组中,这些信息对排查失控进程非常有帮助。
查进程对应的端口占用,我一般用ss -tlnp或者lsof -i:端口号,ss的输出比netstat更清晰,速度也更快。基于ss命令,我在脚本里专门写了一个模块:输入端口号,输出完整的进程信息,包括PID、进程名、可执行文件路径、启动用户、运行时长。这个功能在日常排查端口冲突时格外好用,比如启动一个新服务发现端口被占,脚本直接告诉你占用方是谁,省得一次次netstat -ano | grep去猜。
top和htop适合交互式观察动态变化,尤其当怀疑某个进程有内存泄漏时,可以按内存占用排序后盯着看。但自动化巡检不建议用top,它一次运行的输出是采样式,不好在脚本里可靠解析。在bat批处理或Linux Shell脚本里,我更推荐用ps加--sort=-%mem参数做一次性排序快照,例如ps aux --sort=-%mem | head -20,这样能稳定地拿到内存占用最高的前20个进程,再配合日志记录即可。
3.2 僵尸进程与失控进程的完整清理流程
进程管理项目里,除了查看能力,更核心的是“清理”能力。我重点处理两类问题:僵尸进程和失控进程。先说僵尸进程,它的成因是子进程已经终止,但父进程没有调用wait()系统调用来回收它的退出状态,所以内核中保留了该进程的进程表项。僵尸进程本身不再占用CPU和内存,但它不消失会占住PID资源,如果堆积多了,系统可能无法创建新进程。
我在脚本里处理僵尸进程的思路是这样的:第一步执行ps -ef | grep defunct,找出所有僵尸进程及其父进程PID;第二步判断父进程是否还存在,如果父进程已经死了,僵尸进程会被init进程接管,这种相对好办,系统后续会自动清理;第三步是重点,如果父进程还活着,需要判断父进程是什么类型。像bash脚本、Java进程这类,一般可以通过重启父进程来触发系统回收;但像父进程本身是核心业务进程这类不能随便重启的场景,贸然杀掉父进程是高风险动作,必须走告警人工确认流程。脚本默认不直接杀父进程,只输出告警和候选方案,发现僵尸进程自动清理只是理想状态,实际操作要留一手。
失控进程通常表现为CPU占用超高、内存持续增长、或者无法正常响应请求。我在这里会用到kill和kill -9两档手段。kill命令默认发送TERM信号,给进程一个“体面退场”的机会,让它有机会执行清理和保存收尾工作,这是首选。如果进程顽固不化、没有响应TERM信号,才考虑kill -9强制终结。很多新手一上来就kill -9,其实容易造成数据不一致或者留下脏数据。我写了一个简单但实用的函数:优先发送TERM,等待5秒,检查进程是否存在,仍存在才升级到KILL。
还有一类进程比较特殊——你已经知道它不正常,但它处于D状态(不可中断睡眠),通常是等待磁盘I/O或者网络I/O,这时候kill信号根本不生效,进程会卡住不动。这种情况最笨也最有效的办法是检查它到底卡在什么I/O上,如果是网络挂载(如NFS)有问题,先恢复存储连接,进程自然就会恢复;如果是磁盘故障,只能等内核超时。强行reboot是最后手段,因为D状态恰恰说明有数据在写盘中,重启风险较大。这一段经验就是踩坑换来的,我曾有一台机器NFS挂载断开,一堆进程卡在D状态,我愣是kill了半天毫发无伤,后来一顿排查才意识到得先修存储路径。
3.3 Windows环境进程管理崩溃问题的一点经验
热搜词里有一条“频繁进程管理崩溃win10”,这个我在帮同事处理Windows服务器时确实遇到过。Windows的“任务管理器”在某些情况下会假死或崩溃,特别是系统资源紧张、进程列表刷新频繁时。这里有几个实用建议:第一,优先用PowerShell命令而非图形界面,Get-Process | Sort-Object CPU -descending 可以快速按CPU占用排序,定位异常进程;第二,如果任务管理器本身打不开,可以试试按Ctrl+Shift+Esc不行就用Ctrl+Alt+Delete再选,还是不行就直接开CMD/PowerShell执行tasklist和taskkill命令,taskkill /F /PID进程号可以强制结束;第三,如果是系统更新后出现的任务管理器问题,检查是否有损坏的系统文件,可以执行sfc /scannow。
其实Windows进程管理的崩溃多半是“表象”,深层原因是某个进程内存泄漏或句柄泄漏,导致系统资源被吃光,任务管理器作为GUI程序自己也要资源维持。这种情况下,与其反复重启任务管理器,不如先定位吃了最多内存或句柄的进程。后台PowerShell的命令式管理反而比图形界面更可靠,这也是小项目里“把操作变成脚本”思路的延伸。
4. RBAC权限管理设计:把脚本怎么授权这件事也想清楚
4.1 RBAC到底是什么,怎么用在小项目里
小项目虽然是一个运维工具集,但它本身的脚本也不能让所有登录用户随意执行,否则就违背了权限管理的初衷。于是我把RBAC(基于角色的权限控制)设计引入进来:不直接给用户分配具体命令权限,而是给用户分配角色,角色再绑定权限集合。这样好处很明显——人变动时只需调整角色归属,不用逐条改权限,权限调整时只需修改角色的绑定关系,不用通知所有人。
在这套脚本项目里,我设置了三种角色:巡检员(Auditor)、操作员(Operator)和管理员(Admin)。巡检员只能运行只读类的巡检脚本,比如检查特殊权限文件、比对权限基线、查看进程列表;操作员在巡检员的基础上,可以执行标准化的进程清理操作,比如重启僵尸进程的父进程、执行kill指定进程,但操作范围限定在非核心进程白名单之外;管理员拥有全部权限,可以修改基线配置、给关键文件加锁解锁、调整RBAC配置。这个设计参考了生产环境中权限最小化的原则:任何人默认没有任何权限,只有被赋予角色之后才有对应操作能力。
有人可能会问,就一个脚本工具集,搞这么复杂有必要吗?我的回答是有必要。因为你一旦把脚本分享出去,团队成员都有可能登录服务器执行,没有角色约束的话,人人都能对关键文件解锁,那特权设置就形同虚设了。实际运行中我发现,这个设计还有个隐藏好处:执行操作时脚本会记录“谁、什么角色、执行了什么命令”,这份审计日志在追责和回溯时非常有用。
4.2 一种低成本实现RBAC的脚本落地方案
RBAC听起来很重,但落到一套Shell脚本里其实没有那么难。我的做法是维护两个核心配置文件:一个存用户与角色的映射,格式是“用户名 角色”,比如zhangsan operator;另一个存角色与命令白名单的映射,角色可执行的脚本命令列表写死在配置里。
在每个脚本的入口处,我加了一段统一的认证逻辑:检查当前执行用户是谁,从用户角色映射表查到其角色,再判断本次执行的脚本是否在该角色允许的脚本列表中。不在列表中就直接拒绝执行并记录日志。这样写的好处是每个脚本不用重复实现一套鉴权逻辑,只调用一个公共函数即可。公共函数的逻辑要点包括:脚本调用时先提取执行者的Uid和用户名,确认登录用户不是伪造;其次判断sudo提权时,我们要求必须以普通用户登录后再sudo到专用管理账户,防止高权限成为默认入口;最后所有鉴权事件、允许和拒绝都追加到审计日志,日志本身用chattr加a属性保护,避免被篡改。
在这个设计里也要注意“特权脚本”的特殊性。比如权限基线的更新操作,操作员不能执行,只有管理员可以。某些进程清理操作虽然操作员在白名单里,但脚本内部会二次确认目标进程是否属于受保护进程集合,这个集合在配置文件中强制管理。也就是说——即使授权了,RBAC也只是第一道关卡,脚本内硬编码的保护机制是第二道关卡,这种双层设计在实际操作中更能降低误操作概率。
4.3 RBAC实施的评审要点:千万别只停留在脚本层面
RBAC看起来简单,但落地时容易只关注“有角色、有权限”这个表面,细节上其实有几个很关键的评审点。第一个是角色的数量控制。角色过多会导致管理成本极高,每加一个角色都要重新评审白名单;角色过少又会退化成所有人都有权限的一刀切。以这个小项目为例,三个角色刚刚好,不盲目加角色是保持RBAC可用性的核心。第二个是基于职责分离的原则,尽量不要设置“超级角色”或者“全能角色”。哪怕你是公司唯一的运维,我也建议至少把巡检权限和变更权限分开,这样审计才有意义。第三个是会话超时。脚本执行如果通过SSH交互,超时时间设置太大会增加风险窗口,我一般把空闲连接超时设置为10分钟,超过直接断开,需要重新登录。
角色和用户绑定后,并不是一劳永逸的。每季度我会重新梳理一次账号清单,把离职、转岗人员的账号清掉,把角色的权限集合重新读一遍,确认没有冗余。因为在实际运维中,权限往往是“只加不减”的,长此以往角色权限会膨胀,RBAC就退化成了一锅粥。所以在这个小项目里,角色配置文件的更新记录也纳入版本管理,每次变更留痕。
5. 实操过程与核心环节实现:一次完整的部署与测试
5.1 基础环境准备和项目目录规划
说了一大堆原理,现在进入实际部署环节。我建议准备一台Linux测试机器,用户环境以CentOS 7/8或Ubuntu 20.04/22.04为例,核心是内核支持ext4或xfs文件系统。项目规划目录我习惯这样安排:
- /opt/sysguard/bin:存放所有主脚本
- /opt/sysguard/conf:存放RBAC配置、基线配置、白名单配置
- /opt/sysguard/logs:存放运行日志和审计日志
- /opt/sysguard/baseline:存放权限基线快照文件
- /opt/sysguard/backup:存放全局备份文件
我创建项目专用账户sysadmin来执行大部分巡检和操作任务。这个账户只拥有读取脚本和执行脚本的权限,对/etc、/usr等系统目录无写权限,对脚本目录也无写权限。管理员需要修改脚本或配置时,通过sudo切换到独立的admin账户操作。这样划分后,即使sysadmin账户被攻破,攻击者也无法修改巡检脚本本身。
在正式运行前,要先安装必要的命令工具:coreutils(提供stat、chattr等)、procps(提供ps、top等)、iproute2(提供ss)、pstree、lsof。Ubuntu下执行apt-get install -y coreutils procps iproute2 psmisc lsof,CentOS下用yum install -y coreutils procps-ng iproute psmisc lsof。这里的psmisc包提供了pstree、fuser等命令,对进程排查非常有用。基础环境准备好后,把项目脚本复制到bin目录,并确认所有脚本属主为root、属组为sysadmin,权限设置为750,这样只有sysadmin组成员可以执行。
5.2 权限基线构建与特殊权限巡检脚本示例
权限基线是整个项目的地基。我先在测试机上生成一份初始基线。脚本baseline_gen.sh的核心逻辑如下:
#!/bin/bash # 生成权限基线,输出每行的格式:权限位|属主|属组|文件类型|路径 BASE_DIR="/opt/sysguard/baseline" TARGET_PATH="$1" POSIX_TS=$(date +%Y%m%d%H%M%S) if [ -z "$TARGET_PATH" ]; then echo "用法: $0 <巡检路径>" exit 1 fi find "$TARGET_PATH" -type f -o -type d | while read -r fpath; do stat -c '%a|%U|%G|%F|%n' "$fpath" done > "${BASE_DIR}/baseline_${POSIX_TS}.txt" chattr +a "${BASE_DIR}/baseline_${POSIX_TS}.txt" echo "基线已生成: ${BASE_DIR}/baseline_${POSIX_TS}.txt"这里有个关键细节:我生成的每行记录是按“权限位|属主|属组|文件类型|路径”排列的,为的是比对时能快速切割字段。给基线文件加a属性是为了防止后续被人为篡改,只要不主动解锁,这个文件只能追加、不能修改和删除。如果生成文件太多导致磁盘膨胀,管理员可以用chattr -i临时解锁后清理旧文件,这条操作只允许管理员角色执行。
比对巡检脚本baseline_check.sh的实现思路是:传入当前状态的快照文件,与最新基线逐行对比,忽略白名单中的路径,输出差异。这里有一个比较关键的点:不能简单对整个文件diff,因为路径顺序可能变化。所以我直接逐行提取路径,在两份文件中分别查找并对比。
#!/bin/bash # 比对当前状态与基线状态,输出差异 LATEST_BASE=$(ls -t /opt/sysguard/baseline/baseline_*.txt | head -1) CURRENT_TMP=$(mktemp) TARGET_PATH="$1" find "$TARGET_PATH" -type f -o -type d | while read -r fpath; do stat -c '%a|%U|%G|%F|%n' "$fpath" done > "$CURRENT_TMP" grep -v '^#' "$LATEST_BASE" | while IFS='|' read -r perm user group ftype fpath; do if grep -qE "^[^|]+\|[^|]+\|[^|]+\|[^|]+\|${fpath}$" "$CURRENT_TMP"; then current_line=$(grep -E "\\|${fpath}$" "$CURRENT_TMP") if [ "$current_line" != "$perm|$user|$group|$ftype|$fpath" ]; then echo "权限变更: $fpath" echo " 基线: $perm|$user|$group|$ftype" echo " 当前: $current_line" fi else echo "文件消失: $fpath" fi done rm -f "$CURRENT_TMP"实际操作中,我发现路径中包含特殊字符时会匹配出错,所以强烈建议巡检路径中不要有带“|”的目录名,或者在脚本里做转义处理。对于刚开始使用的团队,可以先在白名单里默认忽略/proc、/sys、/dev、/run这些虚拟文件系统,它们的状态随时在变,不属于静态权限基线的管理范围。
5.3 进程管理脚本的实际编写与运行记录
进程巡检脚本proc_check.sh我写得比较实用,覆盖了僵尸进程、高内存进程、高CPU进程、端口监听异常这几个场景。下面是我在测试机上运行的例子:
#!/bin/bash LOG_DIR="/opt/sysguard/logs" DATE_TAG=$(date +%Y%m%d_%H%M%S) # 检查僵尸进程 ZS=$(ps -eo pid,ppid,stat,comm | awk '$3 ~ /Z/ {print}') if [ -n "$ZS" ]; then echo "$DATE_TAG 发现僵尸进程:" >> "$LOG_DIR/zombie.log" echo "$ZS" >> "$LOG_DIR/zombie.log" fi # 输出CPU占用前10的进程 ps -eo pid,ppid,%cpu,%mem,comm --sort=-%cpu | head -11 >> "$LOG_DIR/cpu_top_$DATE_TAG.log" # 检查指定端口监听状态 for port in 22 80 443 8080; do ss -tlnp | grep ":$port " || echo "$DATE_TAG 端口 $port 未监听" >> "$LOG_DIR/port_check.log" done实践中有几个值得注意的地方。处理器CPU占用排序用--sort=-%cpu比用管道sort更可靠,因为ps本身就能完整处理表头行。检查僵尸进程时,如果发现大量Z状态,不要急于逐个清理,先看它们的父进程PID是不是同一个,如果是,多半是那个父进程处理子进程的逻辑有缺陷,这往往是代码层的bug,不是靠kill能解决的。端口巡检不能只检查端口在监听,还要确认监听地址是否符合预期,比如0.0.0.0:22和127.0.0.1:22是完全不同的暴露等级,脚本里我会额外比对监听地址。
写进程管理脚本时,还有一个我特别坚持的原则:所有涉及kill的动作都要写日志。日志格式包含操作时间、操作用户、目标PID、信号类型、原因备注。这个看起来啰嗦,但一旦出现误杀事故,能从日志里快速还原现场。我有一次误把正在备份的进程当成异常进程杀掉了,幸好这个日志留下了完整证据,后来经过分析我发现备份脚本的进程名与白名单中的异常进程名相似度高。为此我在脚本里增加了一个“模糊名称二次确认”的逻辑:如果目标进程名在受保护名单里,就强制中断操作。
5.4 集成RBAC的启动脚本与权限自检演示
项目里所有对外提供功能的入口脚本叫entry.sh。它的作用是先做RBAC鉴权,再根据传入的子命令分发到具体脚本。下面是简化后的关键逻辑:
#!/bin/bash # 全局入口脚本 CONF_DIR="/opt/sysguard/conf" USER_ROLE_FILE="$CONF_DIR/user_role.conf" ROLE_PERM_FILE="$CONF_DIR/role_perm.conf" LOG_FILE="/opt/sysguard/logs/audit.log" ACTION_USER=$(whoami) get_role() { grep "^${ACTION_USER} " "$USER_ROLE_FILE" | awk '{print $2}' } check_perm() { local role="$1" local script_name="$2" grep "^${role} ${script_name}$" "$ROLE_PERM_FILE" >/dev/null 2>&1 } ROLE=$(get_role) if [ -z "$ROLE" ]; then echo "$(date +%F_%T) ${ACTION_USER} 被拒绝执行 $2 (无角色)" >> "$LOG_FILE" echo "无权执行,请联系管理员分配角色" exit 1 fi if ! check_perm "$ROLE" "$2"; then echo "$(date +%F_%T) ${ACTION_USER} 被拒绝执行 $2 (角色$ROLE无权)" >> "$LOG_FILE" echo "当前角色无权限执行该操作" exit 1 fi echo "$(date +%F_%T) ${ACTION_USER} 允许执行 $2" >> "$LOG_FILE" shift exec "/opt/sysguard/bin/$1" "$@"user_role.conf的内容很简单,比如:
zhangsan auditor lisi operator wangwu adminrole_perm.conf则类似:
auditor baseline_check.sh auditor proc_check.sh operator proc_check.sh operator proc_kill.sh admin baseline_gen.sh admin baseline_check.sh admin proc_check.sh admin proc_kill.sh admin chattr_lock.sh admin chattr_unlock.sh admin rbac_edit.sh实际测试的时候,我先用zhangsan去执行proc_kill.sh,会被拒绝,这符合预期;再用lisi去执行,因为proc_kill.sh在operator角色权限中,所以能正常调用。所有允许和拒绝动作都会写入审计日志,把写入动作放在鉴权通过之前,是为了确保即使脚本被拒执行,审计记录也已经落盘。
让我觉得需要单独提示的是,脚本里用exec来做子命令分发,会导致调用记录在shell历史层面看起来是“直接执行了子脚本”,而不是通过入口脚本走的,这对审计不够友好。所以我在审计日志里额外记录了“原始调用方式”字段,同时在生产环境中把shell的history记录也统一持久化到syslog。这个细节看起来无关紧要,但你真正要追责时会发现,缺了这一步可能导致“用户否认操作过”的扯皮。
5.5 定时巡检与告警通知配置
脚本本身写完后,还要让它持续运转。我用crontab设置定时任务,把巡检、比对、清理动作固定化。计划任务我放在专门的cron文件里,避免散落在各用户crontab中。
# 每天凌晨2点执行权限基线全量快照生成 0 2 * * * /opt/sysguard/bin/baseline_gen.sh /etc /opt/app /var/www >/dev/null 2>&1 # 每小时执行一次权限基线比对 10 * * * * /opt/sysguard/bin/baseline_check.sh >/dev/null 2>&1 # 每5分钟执行一次进程巡检 */5 * * * * /opt/sysguard/bin/proc_check.sh >/dev/null 2>&1 # 每天凌晨3点清理超过30天没有变化的日志文件 0 3 * * * find /opt/sysguard/logs -name "*.log" -type f -mtime +30 -delete日志告警这一环节,我推荐最简洁的邮件通知方式。脚本检测到异常时,将告警内容追加到一个专用文件,然后用mailx或者msmtp发送摘要。如果你管理的机器在公司内网,还可以直接把告警写入本地Syslog,由集中日志平台拉取。需要注意的是,定时任务里的脚本必须使用绝对路径,并且cron执行环境下PATH环境变量可能不完整,最好在脚本开头明确设置PATH=$PATH:/usr/local/sbin:/usr/sbin:/sbin。这个细节如果不注意,脚本里调用的ss、chattr等命令在cron环境下经常会找不到。
测试过程中我观察到,定时巡检前两周跑下来,告警最多的是权限基线比对里的“文件消失”类告警。后来分析发现,很多是软件更新或日志轮转导致文件被替换造成的假告警。解决办法有两个:一是把日志目录从静态基线的巡检范围中移出,改为用一套专门的轮转一致性检查;二是对某些允许自动更新的路径可选做“当前快照基线更新”,也就是每天自动将最新合法状态纳入次日基线。但要小心,这操作可能会掩盖真正的恶意变更,我建议这条配置仅适用于已知会自动更新的路径,比如证书自动续期目录。
5.6 关键文件锁定与解锁的实操记录
最后演示一下关键文件加锁解锁模块。在服务器上,我给/etc/passwd、/etc/shadow、/etc/sudoers、/etc/ssh/sshd_config这几个文件加上了i属性。操作命令很简单:
chattr +i /etc/passwd /etc/shadow /etc/sudoers /etc/ssh/sshd_config验证是否生效用lsattr:
lsattr /etc/passwd /etc/shadow /etc/sudoers /etc/ssh/sshd_config输出会显示这些文件带i属性。设置完成后,即使是root用户也无法直接修改这些文件,比如你执行echo "test" >> /etc/sudoers时会提示“权限不够”或“Operation not permitted”。临时需要修改时,先解锁,改完再重新加锁。我在脚本里提供了lock.sh和unlock.sh,执行时会记录操作者和时间到审计日志。这里有一个必须强调的注意事项:如果你给/etc/ssh/sshd_config加了i属性,而你又恰好忘记了密码想通过SSH做紧急恢复,你可能会发现自己被锁在自己的系统外面,这就是所说的“锁死了”。所以,建议第一次加锁前先确保还有其他管理通道(比如物理控制台或带外管理),并且把解锁流程清晰记录在团队文档中。
我还遇到过一种比较麻烦的情况:初始化镜像时把整个/etc目录都加过锁,后来在安装软件包时莫名失败。原因就是一个安装脚本试图在那段锁定期内修改/etc下的配置文件,结果被chattr挡住。这类“权限故障”如果不是亲历现场,真的很难排查。所以我在项目中明确规定:加锁范围只限于指定的关键文件清单,绝不对整个目录加锁,这一条现在写进了项目说明书的红字部分。
6. 常见问题与排查技巧实录:把踩过的坑一次性说清
6.1 权限相关高频问题速查表
| 问题现象 | 可能原因 | 排查命令与处理思路 |
|---|---|---|
| 服务启动报Permission denied | 可执行文件缺少执行权限,或运行用户无目录权限 | 检查ll和目录权限,用namei -l 路径逐级查看父目录权限 |
| 修改配置文件无效或报错 | 文件被chattr加了i属性 | lsattr查看属性,chattr -i临时解锁后修改 |
| 普通用户能读到敏感配置文件 | 权限配置漂移,属组权限过宽 | 用基线比对找出差异,收紧为640或600 |
| 新创建文件属组不对 | 目录没有设置SGID | chmod g+s 目录,新文件继承属组 |
| 共享目录中互相删文件 | 缺少sticky bit | chmod +t 目录,仅属主/目录属主可删 |
排查权限问题时我推荐先从“最近基线是否匹配”入手,然后逐级查看文件系统路径。namei -l这个命令能按路径段逐层展示权限,对回答“为什么这个目录明明有权限还是访问不了”非常直观。因为有时候问题不在目标文件本身,而是路径中间某一层目录缺少x权限导致的“可看到不可进入”。
6.2 进程管理常见问题与排查心得
| 问题现象 | 可能原因 | 排查思路 |
|---|---|---|
| 大量僵尸进程 | 父进程未正确回收子进程 | 查看父进程情况,评估重启父进程或升级应用逻辑 |
| 端口被占用但找不到进程 | 可能是root或内核线程占用 | 用ss -tlnp看完整输出,必要时用fuser -v 端口号 |
| kill命令无效果 | 进程处于D状态 | 检查I/O和存储状态,修复底层系统后再观察 |
| 系统卡顿但CPU占用不高 | 可能在等待I/O或内存不足 | 用iostat、free、vmstat查看瓶颈,配合ps查看D状态进程数量 |
我处理过一个特别典型的场景:一台Web服务器的进程全部积累成僵尸,因为主进程写了一个有bug的守护逻辑,子进程结束后没有调用waitpid回收。用时发现系统负载越来越高,其实是进程表项被占满。这已经不只是运维问题,需要反馈给开发团队修代码。所以进程管理项目里,我给“僵尸进程的父进程身份”加了一个统计维度,如果父进程属于已知的应用进程,巡检告警会直接标注“疑似代码缺陷,请与研发确认”。这样能帮助运维从“救火”转型为“定位根因”。
6.3 实操遇到的三个独家避坑技巧
第一个技巧是关于回收僵尸进程不能盲目杀父进程的。如果父进程是一个服务管理工具如systemd,杀父进程其实相当于杀整个服务组,影响范围非常大。我的处理策略是优先检查僵尸进程能否被init接管,也就是手动把父进程对子进程的wait处理触发掉。Linux中有一条命令可以“收养”孤儿,但脚本层面更多是靠重启对应服务来触发回收。所以我在脚本中对父进程类型做了识别,只有父进程是shell或者明显独立的控制进程时才自动重启,否则一律走人工审批。
第二个技巧是权限基线比对一定要预设“变更容忍窗口”。很多正常运维操作本身就包含权限变更,比如部署新版本时,安装脚本会把应用目录权限设置为特定值。如果每次巡检都因为这些变动而告警,团队很快会对告警产生疲劳。我在脚本里加入了“变更备注”机制:运维人员在执行变更前可以往备注文件里写入计划变更记录,巡检脚本读取备注后跳过已标记的路径,备注在定时过期后自动失效。这个机制既灵活又有据可循,不会让基线管理变成一纸空文。
第三个技巧是关于审计日志的下发。脚本的审计日志虽然加了a属性防止篡改,但日志文件本身可能被删除后重建。我利用chattr的a属性本身就天然有防删除效果,但如果管理员手动解锁删除,日志文件就行同虚设。更好的做法是同时把审计日志通过rsyslog转发到集中日志服务器,这样即使本机日志丢失,远程也有副本。在小项目中这一步可能有些重,但哪怕是在本机额外配置一个watch服务监控日志目录,也能增加一层保险。
6.4 自动化巡检的效果观察与数据复盘
项目上线运行一段时间后,我做了个数据复盘。以一台测试服务器为例,运行一个月共执行权限基线比对720次,进程巡检1440次。权限告警共17次,其中真实风险2次,误报15次;进程告警共9次,其中5次为僵尸进程,3次为CPU异常占用,1次为端口异常监听。所有的告警都被记录并可回溯到具体时间点和操作者。
从这组数据来看,告警量的确不小,但真正严重的事情只有2次:一次是某应用的配置文件被改成了全体可读,另一次是一个Java进程的内存占用异常增长。这两次如果靠人工巡检,大概率不会及时发现。不过这些数据也说明,巡检脚本必须持续优化白名单和基线,让告警更精准,不然运维团队会被误报淹没。
我觉得最值得称赞的并非抓到多少次风险,而是整个系统变得可预期了:权限有基线、变更有留痕、进程有记录、操作有审计。这种“管理闭环”才是这个小项目带来的核心价值。
7. 拓展方向与实际使用建议
小项目做到一定程度,自然而然会想往上走得更大一点。现在这个脚本集基本能覆盖单机场景,但如果你管理的机器数量超过十几台,建议基于这套逻辑去做集中化平台。可以先搭建一个简单的服务端,让各机器定时上报巡检结果,再统一展示和告警。这一步不是必须的,但从成本控制角度说,脚本本身的框架可以无缝迁移到批量执行工具上。
权限管理这边可以进一步引入SELinux或AppArmor的策略管理,将文件权限、特殊权限、隐藏属性、进程执行域统一纳入一个模型管理。进程管理这边则可以考虑和服务发现组件结合,比如进程预期在线数与实际在线数的自动比对,服务异常退出后自动拉起。这些小扩展会让项目从一个工具集慢慢成长为真正意义上的“系统治理小助手”。
从我自己的使用感受来说,这个项目的最大收获不是写了多少行脚本,而是把“管理动作标准化”这个习惯带进了日常运维。以前大家各自为战,全凭个人经验敲命令,现在有了统一的入口、统一的审计、统一的基线,即使换来新人也能很快按照SOP上手,不至于出乱子。如果你也经常被权限和进程问题困扰,我建议可以从最基础的两三个脚本开始,不追求一步到位,先把“巡检”这一件事做扎实,再把“处置”标准化,风险自然就会下降一个量级。
最后再分享一个实际操作的小技巧:在任何权限管理脚本里,都不要图省事用chmod -R 777。如果真需要给某个应用目录开放写权限,先用ls -ld确认目录当前属主和属组,尽量把权限收束到特定用户或组粒度。这样虽然多敲了几个字符,但在安全性和可维护性上的回报是长期的。这个建议,是我做了这个项目之后最想对自己和团队说的。