Skip to content

安全加固:配置最小权限服务与访问限制 ​

本篇以systemd 最小权限任务为例;前置是执行用户、只读程序目录与必要写入路径。源码与输入与下文配置共用同一份文件,示例未执行。

前置、依赖与输入 ​

安全加固完整示例是离线证据处理器,示例未执行。需要Python3.11以上标准库,第三方依赖为零,不需要服务、管理员或网络。输入JSON只含虚构记录,程序不调用系统命令、不建立连接、不修改主机。完整文件位于examples/m5-next-hardening,必须一起阅读README、lab.py和case.json。

完整源码与配置 ​

下面包括完整源码与全部输入;这不是产品配置验证器,也不执行协议、密码学或系统策略。判断只依据输入声明,不能用输出证明真实环境。

python
"""Offline teaching model. No network, subprocesses, or system mutations."""
import json
from pathlib import Path
import sys


def require(value, schema):
    if type(value) is not dict or set(value) != set(schema):
        raise ValueError("unexpected or missing fields")
    for key, expected in schema.items():
        if type(value[key]) is not expected:
            raise ValueError("wrong field type: " + key)


def decide(row):
    require(row, {"name": str, "service_user": str, "config_writable": bool, "no_new_privileges": bool, "approved_exception": bool, "exception_expires": str, "at": str})
    from datetime import date
    now = date.fromisoformat(row["at"])
    expiry = date.fromisoformat(row["exception_expires"])
    if row["service_user"] in ("root", "Administrator", ""):
        return "REVIEW_PRIVILEGED_USER"
    if row["config_writable"]:
        return "REVIEW_CONFIG_WRITE"
    if not row["no_new_privileges"]:
        if row["approved_exception"] and now <= expiry:
            return "TRACK_TEMPORARY_EXCEPTION"
        return "REVIEW_PRIVILEGE_BOUNDARY"
    return "DECLARED_BASELINE_MATCH"


def main(args):
    if len(args) != 1:
        print("usage: python lab.py case.json", file=sys.stderr)
        return 2
    try:
        source = Path(args[0]).read_text(encoding="utf-8")
        data = json.loads(source)
        if type(data) is not list or not data:
            raise ValueError("nonempty list required")
        # Validate every row before publishing any decisions.
        results = [(row, decide(row)) for row in data]
        for row, result in results:
            print(row["name"] + ": " + result)
        return 0
    except (OSError, ValueError, KeyError, TypeError, OverflowError):
        print("invalid input", file=sys.stderr)
        return 2


if __name__ == "__main__":
    raise SystemExit(main(sys.argv[1:]))
json
[
  {
    "name": "normal",
    "service_user": "reporter",
    "config_writable": false,
    "no_new_privileges": true,
    "approved_exception": false,
    "exception_expires": "2026-10-18",
    "at": "2026-10-17"
  },
  {
    "name": "root-user",
    "service_user": "root",
    "config_writable": false,
    "no_new_privileges": true,
    "approved_exception": false,
    "exception_expires": "2026-10-18",
    "at": "2026-10-17"
  },
  {
    "name": "expired-exception",
    "service_user": "reporter",
    "config_writable": false,
    "no_new_privileges": false,
    "approved_exception": true,
    "exception_expires": "2026-10-16",
    "at": "2026-10-17"
  }
]

读者自跑、成功和失败 ​

安全加固离线推演先复制三个文件到独立目录。Windows PowerShell使用python,Linux Bash使用python3调用lab.py case.json;命令在README完整列出。正常输入输出逐行判定并以0结束;缺字段、非法类型或JSON损坏输出invalid input并以2结束。预期拒绝是业务判定,不等同输入错误,仍以0结束。

恢复、清理与边界 ​

恢复时从仓库还原case.json,重新核对变更前后字段。程序不写结果文件,不产生系统资源,不需要关闭端口或停止服务。清理只删除读者自己复制的练习目录;不要删除仓库示例。修改输入不会改变真实授权、证书或路由。

综合练习与分开的解答 ​

练习:先不看参考答案,解释输入中每个正常和失败判定,修改一项条件让一个判定改变,再写出该变化仍不能证明的现实条件。

参考解答

normal使用非特权声明账户、配置不可写且no_new_privileges为真,输出DECLARED_BASELINE_MATCH。root-user先命中特权账户条件,REVIEW_PRIVILEGED_USER。expired-exception缺少NoNewPrivileges且例外已到期,REVIEW_PRIVILEGE_BOUNDARY;审批标志并不能永久覆盖到期条件。

具体变体:只把 normal 的 config_writable 改为 true,其他字段不变,将由 DECLARED_BASELINE_MATCH 变为 REVIEW_CONFIG_WRITE。只是离线声明触发复核,不会改变主机文件权限;不能因为unit文件存在就声称拒绝写入已经生效。

产品配置与操作契约 ​

以下为与离线模型分开的完整产品配置,示例未执行。产品二进制、真实密钥与账户均需读者按前置另行提供;静态解析不等于产品验证。

ini
[Unit]
Description=One-shot offline teaching evidence report
After=local-fs.target

[Service]
Type=oneshot
DynamicUser=yes
WorkingDirectory=/opt/m5-hardening
ExecStart=/usr/bin/python3 -B /opt/m5-hardening/lab.py /opt/m5-hardening/case.json
Environment=PYTHONDONTWRITEBYTECODE=1
NoNewPrivileges=yes
CapabilityBoundingSet=
AmbientCapabilities=
PrivateTmp=yes
PrivateDevices=yes
ProtectSystem=strict
ProtectHome=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictSUIDSGID=yes
RestrictNamespaces=yes
LockPersonality=yes
RestrictRealtime=yes
RestrictAddressFamilies=AF_UNIX
IPAddressDeny=any
UMask=0077
TimeoutStartSec=15s
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

完整systemd单次任务配置(未执行) ​

lesson.service面向systemd256/Linux,关联本目录lab.py与case.json,Type=oneshot运行后退出,不开启服务端口。读者已有Python3.11以上且实际路径为/usr/bin/python3时,才可使用此ExecStart;路径不同需同步替换。该配置不适用于Windows服务或macOS launchd。

管理员在另行批准的隔离主机把只读lab.py/case.json放到/opt/m5-hardening,目录root拥有且0755,文件0644;不能让临时DynamicUser修改程序。DynamicUser分配临时非特权身份;NoNewPrivileges禁止继承链通过exec增加权限;CapabilityBoundingSet与AmbientCapabilities为空。ProtectSystem=strict让系统层次只读,ProtectHome隐藏用户家目录,程序没有需要持久写入的路径,所以没有ReadWritePaths例外。-B防止写pycache。

RestrictAddressFamilies=AF_UNIX限制可创建套接字族,IPAddressDeny=any还依赖内核/cgroup支持,不能把文本声明当成网络隔离已生效。沙箱选项有平台能力前置,受限容器中可能失败。读者必须检查真实进程约束及拒绝证据,不得为了启动成功就批量移除隔离。

读者另行批准后才安装unit到/etc/systemd/system/lesson.service并执行以下维护步骤,示例未执行:

bash
sudo systemctl daemon-reload
sudo systemctl start lesson.service
sudo systemctl status lesson.service
sudo journalctl -u lesson.service --no-pager

预期任务退出0且日志出现三行教学判定,oneshot退出后inactive不代表失败;修改JSON缺字段预期退出2。路径、权限或隔离不支持导致失败时先查journal,最小修复前置,不改成root常驻。恢复还原之前的批准unit与输入;清理只删除本次安装的lesson.service,daemon-reload,并删除专用/opt/m5-hardening副本,不清理其他unit或日志。不要enable该教学任务。本项目没有执行安装、启动或清理。

完整项目说明与依赖声明 ​

以下同源文件完整列出读者前置、自跑命令、正常/失败预期和清理;正文只供阅读,示例未执行。

text
# Python >=3.11; standard library only; no third-party dependencies.
完整 README(与项目文件一致)
markdown
# hardening 离线证据推演

示例未执行。前半的 lab.py 审阅运行身份、配置可写性、权限提升限制与例外到期声明;后半才是 Linux systemd 单次任务的隔离配置。静态声明符合基线不能证明主机上限制已生效。

## 文件与前置

离线部分需要 Python 3.11 以上和标准库,无主机读取、服务或管理员权限。从 [case.json](case.json) 的普通身份、root 身份和过期例外三例开始,对照 [lab.py](lab.py);先核对例外批准来源及日期,再解释判定。systemd 版本、程序路径和管理员安装条件仅适用于后半节 unit 示例;macOS 未做原生运行验收。

## 读者自跑

先复制全部文件到自己的隔离练习目录并进入该目录。以下仅是读者步骤,本轮未执行。

Windows PowerShell:

```powershell
python .\lab.py .\case.json
```

Linux Bash(macOS若已有Python3也使用此形式):

```bash
python3 ./lab.py ./case.json
```

预期标准输出如下,进程退出0;明确拒绝是正常决策而不是输入错误。

```text
normal: DECLARED_BASELINE_MATCH
root-user: REVIEW_PRIVILEGED_USER
expired-exception: REVIEW_PRIVILEGE_BOUNDARY
```

## 失败路径

把case.json复制为broken.json,再人工删除第一条记录的name字段。使用同一命令把参数换成broken.json,预期标准错误invalid input、退出2,不发布部分判定。破损JSON、非法字段类型、未知字段、空列表或不存在的文件同样退出2。不打印异常原文,防止输入内容意外进入日志。程序严格匹配固定教学字段,不接受任意扩展。

## 恢复与清理

从原仓库重新复制case.json恢复,或撤销练习副本编辑。程序没有写文件、创建用户或启动连接,无需服务清理;只删除自己复制的目录和broken.json,不删除仓库原文件。

## 解释边界

只核对声明,不读取账户、ACL、服务配置或内核状态。approved_exception假定由管理流程批准,程序不验证批准人。基线匹配不等于主机安全。

输入由读者提供,不具有独立真实性。需要真实运行验证时,另行取得授权并采集环境证据,不能用本程序作产品验收工具。

## 完整systemd单次任务配置(未执行)

lesson.service面向systemd256/Linux,关联本目录lab.py与case.json,Type=oneshot运行后退出,不开启服务端口。读者已有Python3.11以上且实际路径为/usr/bin/python3时,才可使用此ExecStart;路径不同需同步替换。该配置不适用于Windows服务或macOS launchd。

管理员在另行批准的隔离主机把只读lab.py/case.json放到/opt/m5-hardening,目录root拥有且0755,文件0644;不能让临时DynamicUser修改程序。DynamicUser分配临时非特权身份;NoNewPrivileges禁止继承链通过exec增加权限;CapabilityBoundingSet与AmbientCapabilities为空。ProtectSystem=strict让系统层次只读,ProtectHome隐藏用户家目录,程序没有需要持久写入的路径,所以没有ReadWritePaths例外。-B防止写pycache。

RestrictAddressFamilies=AF_UNIX限制可创建套接字族,IPAddressDeny=any还依赖内核/cgroup支持,不能把文本声明当成网络隔离已生效。沙箱选项有平台能力前置,受限容器中可能失败。读者必须检查真实进程约束及拒绝证据,不得为了启动成功就批量移除隔离。

读者另行批准后才安装unit到/etc/systemd/system/lesson.service并执行以下维护步骤,本批未执行:

```bash
sudo systemctl daemon-reload
sudo systemctl start lesson.service
sudo systemctl status lesson.service
sudo journalctl -u lesson.service --no-pager
```

预期任务退出0且日志出现三行教学判定,oneshot退出后inactive不代表失败;修改JSON缺字段预期退出2。路径、权限或隔离不支持导致失败时先查journal,最小修复前置,不改成root常驻。恢复还原之前的批准unit与输入;清理只删除本次安装的lesson.service,daemon-reload,并删除专用/opt/m5-hardening副本,不清理其他unit或日志。不要enable该教学任务。本项目没有执行安装、启动或清理。

继续阅读