修复Linux安装器目标检查与发布文件失败回退
This commit is contained in:
parent
9d3bd50251
commit
8c3d878680
@ -38,6 +38,10 @@ func TestLinuxInstallerPolicy(t *testing.T) {
|
||||
`/scert|/scert/*`,
|
||||
`chown -R -h "$SERVICE_USER:$SERVICE_GROUP"`,
|
||||
`write_install_metadata`,
|
||||
`begin_install_transaction`,
|
||||
`trap finish_install_transaction EXIT`,
|
||||
`mv -fT -- "$main_temporary" "$INSTALL_DIR/scert"`,
|
||||
`mv -T -- "$INSTALL_DIR/web" "$transaction_dir/web-replaced"`,
|
||||
`load_existing_certificate_dir`,
|
||||
`certificate_dir_explicit=true`,
|
||||
`file -b -- "$source_dir/$artifact"`,
|
||||
|
||||
@ -14,6 +14,9 @@ source_dir="$SCRIPT_DIR"
|
||||
certificate_dir="$DEFAULT_CERTIFICATE_DIR"
|
||||
certificate_dir_explicit=false
|
||||
nologin_shell=""
|
||||
transaction_dir=""
|
||||
transaction_active=false
|
||||
declare -a transaction_targets=()
|
||||
|
||||
print_usage() {
|
||||
cat <<'USAGE'
|
||||
@ -146,13 +149,28 @@ validate_managed_paths() {
|
||||
"$SERVICE_DROP_IN_DIR/10-certificate-path.conf"; do
|
||||
reject_symlink_path "$managed_path" "Scert 受管路径"
|
||||
done
|
||||
for managed_path in "$INSTALL_DIR" "$INSTALL_DIR/conf" "$INSTALL_DIR/data" \
|
||||
"$INSTALL_DIR/log" "$INSTALL_DIR/backups" "$INSTALL_DIR/docs" \
|
||||
"$INSTALL_DIR/web" "$SERVICE_DIR" "$SERVICE_DROP_IN_DIR"; do
|
||||
[[ ! -e "$managed_path" || -d "$managed_path" ]] || fail "Scert 目录路径不是普通目录:$managed_path"
|
||||
done
|
||||
for managed_path in "$INSTALL_DIR/scert" "$INSTALL_DIR/scert-admin" \
|
||||
"$INSTALL_METADATA_FILE" "$INSTALL_DIR/docs/Scert-service-management.md" \
|
||||
"$INSTALL_DIR/docs/Scert-admin-service-management.md" \
|
||||
"$SERVICE_DIR/Scert.service" "$SERVICE_DIR/Scert-admin.service" \
|
||||
"$SERVICE_DROP_IN_DIR/10-certificate-path.conf"; do
|
||||
[[ ! -e "$managed_path" || -f "$managed_path" ]] || fail "Scert 文件路径不是普通文件:$managed_path"
|
||||
done
|
||||
if [[ -d "$INSTALL_DIR/web" ]]; then
|
||||
[[ -z "$(find "$INSTALL_DIR/web" ! -type f ! -type d -print -quit)" ]] || fail "既有 Web 目录包含符号链接或特殊文件,拒绝覆盖"
|
||||
fi
|
||||
}
|
||||
|
||||
validate_environment() {
|
||||
[[ "$(uname -s)" == "Linux" ]] || fail "此安装器只支持 Linux"
|
||||
[[ "$(id -u)" -eq 0 ]] || fail "请使用 root 或 sudo 运行安装器"
|
||||
local command_name
|
||||
for command_name in install systemctl getent groupadd useradd id chmod chown cut grep readlink find file mv cp rm; do
|
||||
for command_name in install systemctl getent groupadd useradd id chmod chown cut grep readlink find file mv cp mktemp; do
|
||||
command -v "$command_name" >/dev/null 2>&1 || fail "找不到系统命令:$command_name"
|
||||
done
|
||||
nologin_shell="$(command -v nologin || true)"
|
||||
@ -165,6 +183,9 @@ validate_environment() {
|
||||
validate_artifacts() {
|
||||
[[ -d "$source_dir" ]] || fail "安装产物目录不存在:$source_dir"
|
||||
source_dir="$(cd -- "$source_dir" && pwd -P)"
|
||||
case "$source_dir/" in
|
||||
"$INSTALL_DIR/"|"$INSTALL_DIR/web/"*) fail "不能从正在替换的安装文件或 Web 目录安装" ;;
|
||||
esac
|
||||
local artifact
|
||||
for artifact in scert scert-admin; do
|
||||
[[ -f "$source_dir/$artifact" && ! -L "$source_dir/$artifact" ]] || fail "安装产物必须是普通文件:$source_dir/$artifact"
|
||||
@ -183,6 +204,65 @@ validate_artifacts() {
|
||||
[[ -f "$SCRIPT_DIR/systemd/Scert服务管理说明.md" && ! -L "$SCRIPT_DIR/systemd/Scert服务管理说明.md" ]] || fail "缺少普通 Scert 服务管理说明"
|
||||
}
|
||||
|
||||
begin_install_transaction() {
|
||||
install -d -o root -g "$SERVICE_GROUP" -m 0750 "$INSTALL_DIR"
|
||||
transaction_dir="$(mktemp -d "$INSTALL_DIR/.install-XXXXXXXX")"
|
||||
chmod 0700 "$transaction_dir"
|
||||
transaction_targets=(
|
||||
"$INSTALL_METADATA_FILE" "$INSTALL_DIR/scert" "$INSTALL_DIR/scert-admin"
|
||||
"$INSTALL_DIR/web" "$SERVICE_DIR/Scert.service" "$SERVICE_DIR/Scert-admin.service"
|
||||
"$SERVICE_DROP_IN_DIR/10-certificate-path.conf"
|
||||
"$INSTALL_DIR/docs/Scert-service-management.md"
|
||||
"$INSTALL_DIR/docs/Scert-admin-service-management.md"
|
||||
)
|
||||
local index target
|
||||
for index in "${!transaction_targets[@]}"; do
|
||||
target="${transaction_targets[$index]}"
|
||||
printf '%s\n' "$target" >> "$transaction_dir/targets.txt"
|
||||
if [[ -e "$target" ]]; then
|
||||
cp -a -- "$target" "$transaction_dir/before-$index"
|
||||
fi
|
||||
done
|
||||
transaction_active=true
|
||||
}
|
||||
|
||||
finish_install_transaction() {
|
||||
local status=$?
|
||||
trap - EXIT INT TERM
|
||||
if [[ "$transaction_active" != "true" ]]; then
|
||||
exit "$status"
|
||||
fi
|
||||
set +e
|
||||
local index target
|
||||
local rollback_failed=false
|
||||
for ((index=${#transaction_targets[@]}-1; index>=0; index--)); do
|
||||
target="${transaction_targets[$index]}"
|
||||
if [[ -e "$target" || -L "$target" ]]; then
|
||||
if ! mv -T -- "$target" "$transaction_dir/failed-$index"; then
|
||||
printf '无法保留失败文件,请人工恢复:%s\n' "$target" >&2
|
||||
rollback_failed=true
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
if [[ -e "$transaction_dir/before-$index" ]]; then
|
||||
if ! mv -T -- "$transaction_dir/before-$index" "$target"; then
|
||||
printf '无法恢复升级前文件,请人工恢复:%s\n' "$target" >&2
|
||||
rollback_failed=true
|
||||
fi
|
||||
fi
|
||||
done
|
||||
if ! reload_systemd; then
|
||||
rollback_failed=true
|
||||
fi
|
||||
if [[ "$rollback_failed" == "true" ]]; then
|
||||
printf '安装回退不完整,禁止启动服务。恢复记录:%s\n' "$transaction_dir" >&2
|
||||
else
|
||||
printf '安装失败,已恢复升级前发布文件,服务保持停止。失败现场:%s\n' "$transaction_dir" >&2
|
||||
fi
|
||||
[[ "$status" -ne 0 ]] || status=1
|
||||
exit "$status"
|
||||
}
|
||||
|
||||
ensure_services_stopped() {
|
||||
if systemctl is-active --quiet Scert.service; then
|
||||
fail "升级或安装前必须停止 Scert.service"
|
||||
@ -251,7 +331,7 @@ write_install_metadata() {
|
||||
printf '{\n "certificate_root": "%s"\n}\n' "$certificate_dir" > "$temporary_file"
|
||||
chown root:"$SERVICE_GROUP" "$temporary_file"
|
||||
chmod 0640 "$temporary_file"
|
||||
mv -f -- "$temporary_file" "$INSTALL_METADATA_FILE"
|
||||
mv -fT -- "$temporary_file" "$INSTALL_METADATA_FILE"
|
||||
}
|
||||
|
||||
install_binaries() {
|
||||
@ -259,16 +339,17 @@ install_binaries() {
|
||||
local admin_temporary="$INSTALL_DIR/.scert-admin.new.$$"
|
||||
install -o root -g root -m 0755 "$source_dir/scert" "$main_temporary"
|
||||
install -o root -g root -m 0755 "$source_dir/scert-admin" "$admin_temporary"
|
||||
mv -f -- "$main_temporary" "$INSTALL_DIR/scert"
|
||||
mv -f -- "$admin_temporary" "$INSTALL_DIR/scert-admin"
|
||||
mv -fT -- "$main_temporary" "$INSTALL_DIR/scert"
|
||||
mv -fT -- "$admin_temporary" "$INSTALL_DIR/scert-admin"
|
||||
}
|
||||
|
||||
install_web() {
|
||||
[[ -z "$(find "$INSTALL_DIR/web" -type l -print -quit)" ]] || fail "既有 Web 目录包含符号链接,拒绝覆盖"
|
||||
[[ -z "$(find "$INSTALL_DIR/web" ! -type f ! -type d -print -quit)" ]] || fail "既有 Web 目录包含不支持的特殊文件"
|
||||
cp -a "$source_dir/web/." "$INSTALL_DIR/web/"
|
||||
find "$INSTALL_DIR/web" -type d -exec chown root:root {} + -exec chmod 0755 {} +
|
||||
find "$INSTALL_DIR/web" -type f -exec chown root:root {} + -exec chmod 0644 {} +
|
||||
local staged_web="$transaction_dir/web-new"
|
||||
cp -a -- "$source_dir/web" "$staged_web"
|
||||
find "$staged_web" -type d -exec chown root:root {} + -exec chmod 0755 {} +
|
||||
find "$staged_web" -type f -exec chown root:root {} + -exec chmod 0644 {} +
|
||||
mv -T -- "$INSTALL_DIR/web" "$transaction_dir/web-replaced"
|
||||
mv -T -- "$staged_web" "$INSTALL_DIR/web"
|
||||
}
|
||||
|
||||
install_service_files() {
|
||||
@ -321,12 +402,18 @@ main() {
|
||||
validate_artifacts
|
||||
ensure_services_stopped
|
||||
ensure_service_account
|
||||
trap finish_install_transaction EXIT
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
begin_install_transaction
|
||||
create_directories
|
||||
write_install_metadata
|
||||
install_binaries
|
||||
install_web
|
||||
install_service_files
|
||||
reload_systemd
|
||||
transaction_active=false
|
||||
printf '升级前发布文件已保留:%s\n' "$transaction_dir"
|
||||
print_completion
|
||||
}
|
||||
|
||||
|
||||
191
deploy/tests/install-linux-test.py
Normal file
191
deploy/tests/install-linux-test.py
Normal file
@ -0,0 +1,191 @@
|
||||
"""在私有挂载命名空间中验证固定路径安装器,不操作正式服务。"""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def run(*args, **kwargs):
|
||||
return subprocess.run(args, check=True, text=True, **kwargs)
|
||||
|
||||
|
||||
def digest(path):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def snapshot(root):
|
||||
result = {}
|
||||
for path in sorted(root.rglob("*")):
|
||||
info = path.lstat()
|
||||
relative = str(path.relative_to(root))
|
||||
if path.is_symlink():
|
||||
result[relative] = ["link", os.readlink(path), info.st_uid, info.st_gid]
|
||||
elif path.is_file():
|
||||
result[relative] = [digest(path), info.st_mode & 0o777, info.st_uid, info.st_gid]
|
||||
else:
|
||||
result[relative] = ["directory", info.st_mode & 0o777, info.st_uid, info.st_gid]
|
||||
return result
|
||||
|
||||
|
||||
def child(args):
|
||||
base = Path(args.child).resolve(strict=True)
|
||||
marker = base / "namespace-marker"
|
||||
if marker.read_text() != "scert-installer-test\n":
|
||||
raise RuntimeError("隔离目录标识不正确")
|
||||
source = base / "source"
|
||||
run("mount", "--bind", str(Path(args.source).resolve(strict=True)), str(source))
|
||||
run("mount", "-o", "remount,bind,ro", str(source))
|
||||
for name, target in [("install", "/scert"), ("units", "/etc/systemd/system"), ("certificates", args.certificate_root)]:
|
||||
run("mount", "--bind", str(base / name), target)
|
||||
if os.stat(base / name).st_ino != os.stat(target).st_ino:
|
||||
raise RuntimeError("隔离挂载未生效")
|
||||
environment = dict(os.environ, PATH=str(base / "commands") + ":/usr/sbin:/usr/bin:/sbin:/bin")
|
||||
command = ["bash", str(source / "install-linux.sh"), "--source-dir", str(source)]
|
||||
if not (base / "install/conf/installation.json").exists():
|
||||
command.extend(["--certificate-dir", args.certificate_root])
|
||||
result = subprocess.run(
|
||||
command,
|
||||
env=environment, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
)
|
||||
(base / "output.txt").write_text(result.stdout)
|
||||
(base / "result.json").write_text(json.dumps({"exit_code": result.returncode}))
|
||||
|
||||
|
||||
def prepare(base, scenario, certificate_root):
|
||||
for name in ["install/conf", "install/data", "install/log", "install/backups", "install/docs", "install/web/assets", "units/Scert.service.d", "certificates", "commands", "source"]:
|
||||
(base / name).mkdir(parents=True, exist_ok=True)
|
||||
(base / "namespace-marker").write_text("scert-installer-test\n")
|
||||
for name in ["scert", "scert-admin", "docs/Scert-service-management.md", "docs/Scert-admin-service-management.md", "web/index.html", "web/assets/old.js"]:
|
||||
(base / "install" / name).write_text("old:" + name)
|
||||
(base / "install/conf/installation.json").write_text(json.dumps({"certificate_root": certificate_root}) + "\n")
|
||||
for name in ["conf/scert.json", "conf/master.key", "conf/secrets.json", "data/scert.db", "backups/keep"]:
|
||||
(base / "install" / name).write_text("preserve:" + name)
|
||||
(base / "certificates/keep.pem").write_text("preserve:certificate")
|
||||
for name in ["Scert.service", "Scert-admin.service", "Scert.service.d/10-certificate-path.conf"]:
|
||||
(base / "units" / name).write_text("old:" + name)
|
||||
command_log = str(base / "systemctl.log")
|
||||
counter = str(base / "reload-count")
|
||||
systemctl = base / "commands/systemctl"
|
||||
systemctl.write_text(
|
||||
'#!/usr/bin/env bash\nset -eu\n'
|
||||
f'printf "%s\\n" "$*" >> "{command_log}"\n'
|
||||
'if [[ "$1" == "is-active" ]]; then exit 3; fi\n'
|
||||
'[[ "$*" == "daemon-reload" ]] || exit 97\n'
|
||||
+ (f'if [[ ! -e "{counter}" ]]; then touch "{counter}"; exit 42; fi\n' if scenario == "reload-failure" else "")
|
||||
+ ('exit 42\n' if scenario == "rollback-reload-failure" else "")
|
||||
)
|
||||
systemctl.chmod(0o755)
|
||||
if scenario == "web-link":
|
||||
(base / "install/web/assets/link").symlink_to(base / "certificates/keep.pem")
|
||||
if scenario == "binary-directory":
|
||||
(base / "install/scert").rename(base / "old-scert")
|
||||
(base / "install/scert").mkdir()
|
||||
if scenario in ["unit-failure", "fresh-failure"]:
|
||||
command = base / "commands/install"
|
||||
command.write_text(
|
||||
'#!/usr/bin/env bash\nset -eu\n'
|
||||
'if [[ "${@: -1}" == "/etc/systemd/system/Scert-admin.service" ]]; then exit 43; fi\n'
|
||||
'exec /usr/bin/install "$@"\n'
|
||||
)
|
||||
command.chmod(0o755)
|
||||
if scenario in ["fresh-failure", "fresh-success"]:
|
||||
(base / "install").rename(base / "unused-install-fixture")
|
||||
(base / "install").mkdir()
|
||||
(base / "units").rename(base / "unused-units-fixture")
|
||||
(base / "units").mkdir()
|
||||
|
||||
|
||||
def parent(args):
|
||||
if sys.platform != "linux" or os.geteuid() != 0:
|
||||
raise RuntimeError("测试需要 Linux root 和私有挂载命名空间")
|
||||
for target in ["/scert", args.certificate_root, "/etc/systemd/system"]:
|
||||
if not Path(target).is_dir() or Path(target).is_symlink():
|
||||
raise RuntimeError("测试挂载目标必须是既有普通目录")
|
||||
run("getent", "passwd", "Scert", stdout=subprocess.DEVNULL)
|
||||
run("unshare", "--mount", "--propagation", "private", "true")
|
||||
output = Path(args.output).resolve(strict=True)
|
||||
for target in [Path("/scert"), Path(args.certificate_root), Path("/etc/systemd/system")]:
|
||||
if output == target or target in output.parents:
|
||||
raise RuntimeError("测试产物不能位于被隔离的挂载点内")
|
||||
source = Path(args.source).resolve(strict=True)
|
||||
failures = []
|
||||
for scenario in args.scenarios.split(","):
|
||||
if scenario not in ["web-link", "binary-directory", "unit-failure", "reload-failure", "rollback-reload-failure", "success", "fresh-failure", "fresh-success"]:
|
||||
raise RuntimeError("未知测试场景")
|
||||
base = Path(tempfile.mkdtemp(prefix=scenario + "-", dir=output))
|
||||
prepare(base, scenario, args.certificate_root)
|
||||
protected = ["conf/scert.json", "conf/master.key", "conf/secrets.json", "data/scert.db", "backups/keep"]
|
||||
if scenario.startswith("fresh-"):
|
||||
protected = []
|
||||
before_protected = {name: digest(base / "install" / name) for name in protected}
|
||||
before_install = snapshot(base / "install")
|
||||
before_units = snapshot(base / "units")
|
||||
before_certificates = snapshot(base / "certificates")
|
||||
run("unshare", "--mount", "--propagation", "private", sys.executable, str(Path(__file__).resolve()),
|
||||
"--source", str(source), "--child", str(base), "--certificate-root", args.certificate_root)
|
||||
code = json.loads((base / "result.json").read_text())["exit_code"]
|
||||
errors = []
|
||||
if (code == 0) != (scenario in ["success", "fresh-success"]):
|
||||
errors.append("退出码不符合预期")
|
||||
if {name: digest(base / "install" / name) for name in protected} != before_protected:
|
||||
errors.append("业务文件被修改")
|
||||
if snapshot(base / "certificates") != before_certificates:
|
||||
errors.append("证书文件被修改")
|
||||
if scenario in ["success", "fresh-success"]:
|
||||
for name in ["scert", "scert-admin", "web/index.html"]:
|
||||
if digest(base / "install" / name) != digest(source / name):
|
||||
errors.append("安装文件内容不正确:" + name)
|
||||
if (base / "install/web/assets/old.js").exists():
|
||||
errors.append("旧前端资源仍在生效目录")
|
||||
else:
|
||||
after = snapshot(base / "install")
|
||||
for name, expected in before_install.items():
|
||||
# 安装器会统一业务目录权限,仅比较发布文件与安装记录。
|
||||
if name in ["scert", "scert-admin", "conf/installation.json"] or name.startswith(("web/", "docs/")):
|
||||
if after.get(name) != expected:
|
||||
errors.append("失败后发布文件未保留:" + name)
|
||||
if scenario == "fresh-failure":
|
||||
for name in ["scert", "scert-admin", "web", "conf/installation.json"]:
|
||||
if (base / "install" / name).exists():
|
||||
errors.append("首次安装失败仍残留生效文件:" + name)
|
||||
if any(path.is_file() for path in (base / "units").rglob("*")):
|
||||
errors.append("首次安装失败仍残留服务文件")
|
||||
elif snapshot(base / "units") != before_units:
|
||||
errors.append("失败后服务文件未恢复")
|
||||
for path in (base / "install").glob(".install-*"):
|
||||
if path.stat().st_mode & 0o777 != 0o700 or path.stat().st_uid != 0:
|
||||
errors.append("升级副本目录权限不正确")
|
||||
text = (base / "output.txt").read_text()
|
||||
if scenario == "rollback-reload-failure" and "安装回退不完整,禁止启动服务" not in text:
|
||||
errors.append("回退失败缺少明确提示")
|
||||
if scenario in ["unit-failure", "reload-failure", "fresh-failure"] and "已恢复升级前发布文件" not in text:
|
||||
errors.append("缺少回退完成记录")
|
||||
command_log = base / "systemctl.log"
|
||||
calls = command_log.read_text().splitlines() if command_log.exists() else []
|
||||
allowed = {"is-active --quiet Scert.service", "is-active --quiet Scert-admin.service", "daemon-reload"}
|
||||
if any(call not in allowed for call in calls):
|
||||
errors.append("出现不允许的服务操作")
|
||||
report = {"scenario": scenario, "exit_code": code, "errors": errors, "artifacts": str(base)}
|
||||
(base / "report.json").write_text(json.dumps(report, ensure_ascii=False, indent=2))
|
||||
print(json.dumps(report, ensure_ascii=False), flush=True)
|
||||
failures.extend(errors)
|
||||
if failures:
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--source", required=True)
|
||||
parser.add_argument("--output")
|
||||
parser.add_argument("--child")
|
||||
parser.add_argument("--certificate-root", required=True)
|
||||
parser.add_argument("--scenarios", default="web-link,binary-directory,unit-failure,reload-failure,rollback-reload-failure,success,fresh-failure,fresh-success")
|
||||
arguments = parser.parse_args()
|
||||
if arguments.child:
|
||||
child(arguments)
|
||||
else:
|
||||
parent(arguments)
|
||||
@ -150,6 +150,14 @@ sudo systemctl status Scert --no-pager -l
|
||||
|
||||
没有显式传入 `--certificate-dir` 时,升级会读取 `/scert/conf/installation.json` 并保留既有证书根目录。升级不得覆盖 `/scert/conf/scert.json`、数据库、主密钥、敏感配置、证书和备份。
|
||||
|
||||
安装器在任何发布文件写入前检查既有目标类型;文件目标不能是目录,Web 目录不能包含符号链接或特殊文件。升级前发布文件会保留在 root 所有、权限 `0700` 的 `/scert/.install-XXXXXXXX` 中,`targets.txt` 按行记录目标,`before-0` 等编号与行号减一对应。不要删除该目录,确认新版本业务验收完成后再按保留策略人工处理。
|
||||
|
||||
Web 使用整目录替换,旧资源不会继续留在当前 Web 目录中;原目录和升级副本均保留。安装器捕获非零退出、INT 和 TERM 时会尝试恢复安装记录、两个程序、Web、两个 unit、证书目录 drop-in 和两份服务说明,并重新加载 systemd。失败现场保存在同一目录的 `failed-*` 中,不启动或启用服务。若提示“安装回退不完整,禁止启动服务”,必须先核查保留目录与 systemd 状态,不能继续执行启动命令。
|
||||
|
||||
此回退仅涵盖安装中的发布文件变更,不回退服务用户创建、既有业务目录权限规范化,不替代停服数据库备份;断电、SIGKILL、磁盘故障及安装成功后启动新程序造成的数据迁移,也不保证自动恢复。需要回到旧版本时,应保留现场,使用已校验的旧版完整发布包重新安装;若新程序已经改写数据库,应先按对应版本的备份恢复流程处理,不得仅覆盖可执行文件。
|
||||
|
||||
仓库提供 `deploy/tests/install-linux-test.py`,仅用于 Linux root 下的隔离文件安装测试。它以私有挂载命名空间映射固定安装路径,使用新建测试目录和 systemctl 故障注入桩,保留全部产物;要求指定既有普通证书挂载点及挂载点之外的产物目录。该测试不能代替真实 systemd 升级、启动、就绪检查和完整业务回退验收。
|
||||
|
||||
### 4.3 systemd 安全边界
|
||||
|
||||
主服务使用独立 `Scert` 用户、`UMask=0077`、`NoNewPrivileges`、私有设备和临时目录、内核与控制组保护、只读系统目录和受限地址族。安装后必须验证:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user