使用专用证书默认目录并拒绝接管其他账户目录
This commit is contained in:
parent
4dfcecfd21
commit
2d84f16554
@ -73,6 +73,66 @@ systemctl() {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinuxInstallerCertificateOwnership(t *testing.T) {
|
||||
bash, err := exec.LookPath("bash")
|
||||
if err != nil {
|
||||
t.Skip(err)
|
||||
}
|
||||
data, err := os.ReadFile(filepath.Join("..", "..", "..", "deploy", "install-linux.sh"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
content := string(data)
|
||||
start := strings.Index(content, "validate_existing_certificate_dir() {")
|
||||
if start < 0 {
|
||||
t.Fatal("validate_existing_certificate_dir")
|
||||
}
|
||||
end := strings.Index(content[start:], "\n}\n")
|
||||
if end < 0 {
|
||||
t.Fatal("validate_existing_certificate_dir")
|
||||
}
|
||||
guard := content[start : start+end+3]
|
||||
base := t.TempDir()
|
||||
file := filepath.Join(base, "keep.pem")
|
||||
if err := os.WriteFile(file, []byte("preserve"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
path string
|
||||
owner string
|
||||
allowed bool
|
||||
}{
|
||||
{"new-directory", filepath.Join(base, "new"), "root:root", true},
|
||||
{"owned-directory", base, "Scert:Scert", true},
|
||||
{"shared-directory", base, "root:root", false},
|
||||
{"other-service", base, "nginx:nginx", false},
|
||||
{"wrong-group", base, "Scert:root", false},
|
||||
{"missing-owner", base, "", false},
|
||||
{"existing-file", file, "Scert:Scert", false},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
script := `set -Eeuo pipefail
|
||||
SERVICE_USER=Scert
|
||||
SERVICE_GROUP=Scert
|
||||
certificate_dir="$TEST_PATH"
|
||||
fail() { printf '%s\n' "$1" >&2; exit 1; }
|
||||
stat() { printf '%s\n' "$TEST_OWNER"; }
|
||||
` + guard + "\nvalidate_existing_certificate_dir\n"
|
||||
command := exec.Command(bash, "--noprofile", "--norc", "-c", script)
|
||||
command.Env = append(os.Environ(), "TEST_PATH="+filepath.ToSlash(tc.path), "TEST_OWNER="+tc.owner)
|
||||
output, err := command.CombinedOutput()
|
||||
if (err == nil) != tc.allowed {
|
||||
t.Fatalf("allowed=%t, err=%v, output=%s", tc.allowed, err, output)
|
||||
}
|
||||
preserved, err := os.ReadFile(file)
|
||||
if err != nil || string(preserved) != "preserve" {
|
||||
t.Fatalf("preserved=%q, err=%v", preserved, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinuxInstallerPolicy(t *testing.T) {
|
||||
installerPath := filepath.Join("..", "..", "..", "deploy", "install-linux.sh")
|
||||
data, err := os.ReadFile(installerPath)
|
||||
@ -83,7 +143,8 @@ func TestLinuxInstallerPolicy(t *testing.T) {
|
||||
|
||||
required := []string{
|
||||
`readonly INSTALL_DIR="/scert"`,
|
||||
`readonly DEFAULT_CERTIFICATE_DIR="/ssl"`,
|
||||
`readonly DEFAULT_CERTIFICATE_DIR="/ssl/scert"`,
|
||||
`validate_existing_certificate_dir`,
|
||||
`readonly SERVICE_USER="Scert"`,
|
||||
`readonly INSTALL_METADATA_FILE="$INSTALL_DIR/conf/installation.json"`,
|
||||
`useradd --system`,
|
||||
|
||||
@ -38,7 +38,7 @@ func TestScertSystemdUnitPolicy(t *testing.T) {
|
||||
"RestrictSUIDSGID=yes",
|
||||
"RemoveIPC=yes",
|
||||
"SystemCallArchitectures=native",
|
||||
"ReadWritePaths=/ssl /scert/conf /scert/data /scert/log /scert/backups",
|
||||
"ReadWritePaths=/ssl/scert /scert/conf /scert/data /scert/log /scert/backups",
|
||||
"[Install]",
|
||||
"WantedBy=multi-user.target",
|
||||
} {
|
||||
|
||||
@ -168,7 +168,7 @@ func Default(baseDir string) Config {
|
||||
|
||||
func DefaultPaths(baseDir string) PathsConfig {
|
||||
if runtime.GOOS == "linux" {
|
||||
certificateRoot := "/ssl"
|
||||
certificateRoot := "/ssl/scert"
|
||||
if strings.TrimSpace(baseDir) == "" {
|
||||
if installedRoot, err := readInstalledCertificateRoot(LinuxInstallationMetadataPath); err == nil {
|
||||
certificateRoot = installedRoot
|
||||
|
||||
@ -11,7 +11,7 @@ import (
|
||||
func TestDefaultPaths(t *testing.T) {
|
||||
paths := DefaultPaths(t.TempDir())
|
||||
if runtime.GOOS == "linux" {
|
||||
if paths.Install != "/scert" || paths.Certificate != "/ssl" {
|
||||
if paths.Install != "/scert" || paths.Certificate != "/ssl/scert" {
|
||||
t.Fatalf("Linux 默认路径不正确:%+v", paths)
|
||||
}
|
||||
return
|
||||
|
||||
@ -158,7 +158,7 @@ SCERT_WEB_ENABLED=true
|
||||
## 参数说明
|
||||
|
||||
- `--source-dir`:包含两个 Linux 可执行文件和 `web/index.html` 的目录,默认是安装器所在目录。
|
||||
- `--certificate-dir`:证书根目录参数,默认 `/ssl`。安装器会创建该目录,写入 `/scert/conf/installation.json`,并生成对应的 `Scert.service.d/10-certificate-path.conf`。首次初始化会读取该记录作为证书目录默认值。
|
||||
- `--certificate-dir`:证书根目录参数,默认 `/ssl/scert`,不接管共用的 `/ssl`。安装器会创建专用目录,写入 `/scert/conf/installation.json`,并生成对应的 `Scert.service.d/10-certificate-path.conf`。首次初始化会读取该记录作为证书目录默认值;既有安装仍沿用安装记录,不自动迁移证书。已存在且不属于 Scert 的目录会被拒绝,不修改其所有者或内容,请选择尚不存在的专用子目录。
|
||||
|
||||
Linux 安装目录当前固定为 `/scert`。自定义证书根目录必须是规范化的安全绝对路径,只允许字母、数字、点、下划线、连字符和斜杠,不允许空格、`..` 路径片段、符号链接、受保护系统目录或 `/scert` 下的路径;`/var` 下仅允许 `/var/lib/scert` 专用目录。
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ set -Eeuo pipefail
|
||||
|
||||
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly INSTALL_DIR="/scert"
|
||||
readonly DEFAULT_CERTIFICATE_DIR="/ssl"
|
||||
readonly DEFAULT_CERTIFICATE_DIR="/ssl/scert"
|
||||
readonly SERVICE_USER="Scert"
|
||||
readonly SERVICE_GROUP="Scert"
|
||||
readonly SERVICE_DIR="/etc/systemd/system"
|
||||
@ -27,7 +27,7 @@ Scert Linux 安装器
|
||||
|
||||
选项:
|
||||
--source-dir DIR 安装产物目录,必须包含 scert 和 scert-admin
|
||||
--certificate-dir DIR 证书根目录,默认 /ssl
|
||||
--certificate-dir DIR 证书根目录,默认 /ssl/scert
|
||||
-h, --help 显示帮助
|
||||
|
||||
Linux 安装目录固定为 /scert,证书业务目录可在首次初始化时继续配置。
|
||||
@ -112,6 +112,15 @@ validate_certificate_dir() {
|
||||
|
||||
}
|
||||
|
||||
validate_existing_certificate_dir() {
|
||||
if [[ ! -e "$certificate_dir" ]]; then
|
||||
return
|
||||
fi
|
||||
[[ -d "$certificate_dir" ]] || fail "证书路径不是目录:$certificate_dir"
|
||||
[[ "$(stat -c '%U:%G' -- "$certificate_dir")" == "$SERVICE_USER:$SERVICE_GROUP" ]] || \
|
||||
fail "现有证书目录不属于 Scert,拒绝接管;请填写尚不存在的专用子目录:$certificate_dir"
|
||||
}
|
||||
|
||||
reject_symlink_path() {
|
||||
local target_path="$1"
|
||||
local description="$2"
|
||||
@ -173,13 +182,14 @@ 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 mktemp; do
|
||||
for command_name in install systemctl getent groupadd useradd id chmod chown cut grep readlink find file mv cp mktemp stat; do
|
||||
command -v "$command_name" >/dev/null 2>&1 || fail "找不到系统命令:$command_name"
|
||||
done
|
||||
nologin_shell="$(command -v nologin || true)"
|
||||
[[ -n "$nologin_shell" ]] || fail "找不到 nologin,无法创建禁止登录的服务用户"
|
||||
load_existing_certificate_dir
|
||||
validate_certificate_dir
|
||||
validate_existing_certificate_dir
|
||||
validate_managed_paths
|
||||
}
|
||||
|
||||
@ -337,7 +347,7 @@ create_directories() {
|
||||
"$INSTALL_DIR/data" \
|
||||
"$INSTALL_DIR/log" \
|
||||
"$INSTALL_DIR/backups"
|
||||
install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0750 "$certificate_dir"
|
||||
install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0700 "$certificate_dir"
|
||||
chown -R -h "$SERVICE_USER:$SERVICE_GROUP" \
|
||||
"$INSTALL_DIR/conf" \
|
||||
"$INSTALL_DIR/data" \
|
||||
|
||||
@ -39,7 +39,7 @@ RestrictRealtime=yes
|
||||
RestrictSUIDSGID=yes
|
||||
RemoveIPC=yes
|
||||
SystemCallArchitectures=native
|
||||
ReadWritePaths=/ssl /scert/conf /scert/data /scert/log /scert/backups
|
||||
ReadWritePaths=/ssl/scert /scert/conf /scert/data /scert/log /scert/backups
|
||||
# 初始化临时证书读取本机网卡地址需要 NETLINK_ROUTE,不授予网络管理能力。
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
|
||||
|
||||
|
||||
@ -31,7 +31,7 @@ systemctl daemon-reload
|
||||
/scert/data 数据目录
|
||||
/scert/log 日志目录
|
||||
/scert/backups 备份目录
|
||||
/ssl 证书根目录
|
||||
/ssl/scert 默认证书根目录
|
||||
```
|
||||
|
||||
## 安全要求
|
||||
|
||||
@ -4,6 +4,7 @@ import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import pwd
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
@ -92,6 +93,9 @@ def prepare(base, scenario, certificate_root):
|
||||
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")
|
||||
account = pwd.getpwnam("Scert")
|
||||
os.chown(base / "certificates", account.pw_uid, account.pw_gid)
|
||||
(base / "certificates").chmod(0o700)
|
||||
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")
|
||||
@ -117,6 +121,8 @@ def prepare(base, scenario, certificate_root):
|
||||
+ ('exit 0; fi\n' if scenario == "service-active" else 'exit 3; fi\n')
|
||||
+ 'if [[ "$1" == "show" ]]; then\n'
|
||||
+ f'if [[ "$2" == "{service_name}" ]]; then\n' + state_command + 'fi\n'
|
||||
+ 'if [[ "$2" == "Scert-storage.socket" ]]; then\n'
|
||||
+ "printf '%s\\n' 'LoadState=loaded' 'ActiveState=inactive' 'SubState=dead' 'ControlPID=0' 'Job='; exit 0\nfi\n"
|
||||
+ "printf '%s\\n' 'LoadState=loaded' 'ActiveState=inactive' 'SubState=dead' 'MainPID=0' 'ControlPID=0' 'Job='; exit 0\nfi\n"
|
||||
+ '[[ "$*" == "daemon-reload" ]] || exit 97\n'
|
||||
+ (f'if [[ ! -e "{counter}" ]]; then touch "{counter}"; exit 42; fi\n' if scenario == "reload-failure" else "")
|
||||
@ -213,7 +219,7 @@ def parent(args):
|
||||
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"}
|
||||
allowed.update("show " + name + " -p LoadState -p ActiveState -p SubState -p MainPID -p ControlPID -p Job" for name in ["Scert.service", "Scert-admin.service"])
|
||||
allowed.update("show " + name + " -p LoadState -p ActiveState -p SubState -p MainPID -p ControlPID -p Job" for name in ["Scert.service", "Scert-admin.service", "Scert-storage.service", "Scert-storage.socket"])
|
||||
if any(call not in allowed for call in calls):
|
||||
errors.append("出现不允许的服务操作")
|
||||
if scenario in SERVICE_GUARDS and "daemon-reload" in calls:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user