2. 基于规则的密码复杂度策略审计
审计对象与基线规则
密码复杂度策略审计的核心是读取目标系统的策略配置,逐项与安全基线比对。以 Linux PAM 体系为例,策略分散在 /etc/login.defs、/etc/security/pwquality.conf 和 /etc/pam.d/system-auth(或 password-auth)三处,审计时需要把它们合并成一份"有效策略",再逐条判定。
本案例采用如下基线(可按等保/内控要求调整):
| 规则项 | 基线要求 | 配置来源 |
|---|---|---|
| 最小长度 minlen | ≥ 12 | pwquality.conf |
| 字符类别 credit | 至少 3 类(大写/小写/数字/符号) | pwquality.conf |
| 历史密码 remember | ≥ 5 | system-auth / pwquality.conf |
| 失败锁定 deny | ≤ 5 次 | system-auth (pam_faillock) |
| 锁定时间 unlock_time | ≥ 300 秒 | system-auth (pam_faillock) |
| 密码有效期 PASS_MAX_DAYS | ≤ 90 天 | login.defs |
策略读取与合并逻辑
审计脚本需要处理三类解析难点:pwquality.conf 是 key = value 格式;login.defs 是 KEY value 空格分隔;system-auth 中的参数嵌在 PAM 模块行里,形如 pam_pwquality.so ... remember=5 或 pam_faillock.so deny=5 unlock_time=300。下面给出可直接运行的审计脚本。
#!/usr/bin/env python3
# policy_audit.py —— 密码复杂度策略审计
import re, sys, os
BASELINE = {
"minlen": {"op": ">=", "val": 12, "src": "pwquality"},
"credit_classes":{"op": ">=", "val": 3, "src": "pwquality"},
"remember": {"op": ">=", "val": 5, "src": "pam"},
"deny": {"op": "<=", "val": 5, "src": "pam"},
"unlock_time": {"op": ">=", "val": 300, "src": "pam"},
"PASS_MAX_DAYS": {"op": "<=", "val": 90, "src": "login.defs"},
}
def parse_kv(path):
"""解析 key = value 或 KEY value 两种格式"""
cfg = {}
if not os.path.exists(path):
return cfg
for line in open(path, encoding="utf-8", errors="ignore"):
line = line.split("#", 1)[0].strip()
if not line:
continue
m = re.match(r"^([A-Za-z_]+)\s*[=\s]\s*(\S+)", line)
if m:
cfg[m.group(1)] = m.group(2)
return cfg
def parse_pam(path):
"""从 PAM 配置行中提取模块参数"""
cfg = {}
if not os.path.exists(path):
return cfg
for line in open(path, encoding="utf-8", errors="ignore"):
line = line.split("#", 1)[0].strip()
if not line or "pam_" not in line:
continue
for k, v in re.findall(r"(\w+)=(\w+)", line):
cfg[k] = v
return cfg
def count_credit_classes(cfg):
"""统计启用的字符类别数(credit 参数 > 0 视为启用)"""
keys = ["ucredit", "lcredit", "dcredit", "ocredit"]
return sum(1 for k in keys if int(cfg.get(k, 0)) > 0)
def audit(root="/etc"):
pwq = parse_kv(os.path.join(root, "security/pwquality.conf"))
ld = parse_kv(os.path.join(root, "login.defs"))
pam = parse_pam(os.path.join(root, "pam.d/system-auth"))
if not pam:
pam = parse_pam(os.path.join(root, "pam.d/password-auth"))
# 合并有效策略:PAM 行内参数优先于独立配置文件
eff = {}
eff.update(pwq)
eff.update(ld)
eff.update(pam)
eff["credit_classes"] = count_credit_classes(pwq)
findings = []
for rule, spec in BASELINE.items():
raw = eff.get(rule)
if raw is None:
findings.append((rule, "MISSING", "-", spec["val"], spec["op"]))
continue
try:
actual = int(raw)
except ValueError:
findings.append((rule, "INVALID", raw, spec["val"], spec["op"]))
continue
ok = actual >= spec["val"] if spec["op"] == ">=" else actual <= spec["val"]
if not ok:
findings.append((rule, "FAIL", actual, spec["val"], spec["op"]))
return findings
if __name__ == "__main__":
root = sys.argv[1] if len(sys.argv) > 1 else "/etc"
rows = audit(root)
if not rows:
print("[PASS] 所有策略项均满足基线")
else:
print(f"{'规则':<16}{'状态':<9}{'实际值':<10}{'基线':<10}")
print("-" * 46)
for r, st, a, b, op in rows:
print(f"{r:<16}{st:<9}{str(a):<10}{op}{b}")
审计结果判读
脚本对每个规则项输出三种状态:PASS(未出现在清单中)、FAIL(值不满足基线)、MISSING(配置项缺失,等效于未启用该策略)、INVALID(值无法解析为整数)。MISSING 和 INVALID 在合规审计中通常按不通过处理,因为缺失意味着系统采用默认值,而默认值往往弱于基线。
下面这张图展示了从三处配置文件到有效策略合并、再到逐项判定的完整数据流,以及各规则项的判定分支。
运行与验证
在目标主机上以只读权限运行:
python3 policy_audit.py /etc
若需在离线样本目录上测试,可准备一个模拟根目录,例如 ./sample/etc/security/pwquality.conf、./sample/etc/login.defs、./sample/etc/pam.d/system-auth,然后执行 python3 policy_audit.py ./sample/etc。预期输出示例(假设 minlen=8、remember=3、deny=10,其余合规):
规则 状态 实际值 基线
----------------------------------------------
minlen FAIL 8 >=12
remember FAIL 3 >=5
deny FAIL 10 <=5
验证要点:PASS 项不出现在清单中;MISSING 项说明该配置未显式设置,需人工确认系统默认值是否满足基线;INVALID 项说明配置值格式异常,应优先修复。
pam_faillock 而非 pam_tally2,参数名可能为 deny 和 unlock_time;若使用 pam_tally2,则对应 deny 和 unlock_time 语义相同但模块名不同,解析逻辑无需改动。MISSING 直接当作通过。很多发行版默认 minlen 为 8 甚至更短,缺失配置项意味着系统实际策略弱于基线,必须列入不符合项清单。