Scert/deploy/install-linux.sh

288 lines
11 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
set -Eeuo pipefail
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
readonly INSTALL_DIR="/scert"
readonly DEFAULT_CERTIFICATE_DIR="/ssl"
readonly SERVICE_USER="Scert"
readonly SERVICE_GROUP="Scert"
readonly SERVICE_DIR="/etc/systemd/system"
readonly SERVICE_DROP_IN_DIR="$SERVICE_DIR/Scert.service.d"
readonly INSTALL_METADATA_FILE="$INSTALL_DIR/conf/installation.json"
source_dir="$SCRIPT_DIR"
certificate_dir="$DEFAULT_CERTIFICATE_DIR"
nologin_shell=""
print_usage() {
cat <<'USAGE'
Scert Linux 安装器
用法:
sudo ./install-linux.sh [选项]
选项:
--source-dir DIR 安装产物目录,必须包含 scert 和 scert-admin
--certificate-dir DIR 证书根目录,默认 /ssl
-h, --help 显示帮助
Linux 安装目录固定为 /scert,证书业务目录可在首次初始化时继续配置。
安装器只负责安装文件、创建运行用户和目录、注册 systemd unit,并执行一次
systemctl daemon-reload。安装器不会启动或启用 Scert-admin,也不会创建公网防火墙规则。
USAGE
}
fail() {
printf '安装失败:%s\n' "$1" >&2
exit 1
}
parse_args() {
while (($# > 0)); do
case "$1" in
--source-dir)
(($# >= 2)) || fail "--source-dir 缺少参数"
source_dir="$2"
shift 2
;;
--certificate-dir)
(($# >= 2)) || fail "--certificate-dir 缺少参数"
certificate_dir="$2"
shift 2
;;
-h|--help)
print_usage
exit 0
;;
*)
fail "不支持的参数:$1"
;;
esac
done
}
validate_certificate_dir() {
[[ "$certificate_dir" == /* ]] || fail "证书目录必须是绝对路径"
[[ "$certificate_dir" != "/" ]] || fail "证书目录不能是根目录"
[[ "$certificate_dir" =~ ^/[A-Za-z0-9._/-]+$ ]] || fail "证书目录只能包含字母、数字、点、下划线、连字符和斜杠"
[[ ! "$certificate_dir" =~ (^|/)\.\.(/|$) ]] || fail "证书目录不能包含上级目录片段"
reject_symlink_path "$certificate_dir" "证书目录"
certificate_dir="$(readlink -m -- "$certificate_dir")"
[[ "$certificate_dir" != "/" ]] || fail "证书目录不能是根目录"
case "$certificate_dir" in
/bin|/bin/*|/boot|/boot/*|/dev|/dev/*|/etc|/etc/*|/home|/home/*|/lib|/lib/*|/lib64|/lib64/*|/proc|/proc/*|/root|/root/*|/run|/run/*|/sbin|/sbin/*|/sys|/sys/*|/tmp|/tmp/*|/usr|/usr/*)
fail "证书目录不能位于受保护的系统路径:$certificate_dir"
;;
/var|/var/*)
case "$certificate_dir" in
/var/lib/scert|/var/lib/scert/*) ;;
*) fail "证书目录只能使用 /var/lib/scert 下的专用路径:$certificate_dir" ;;
esac
;;
/scert|/scert/*)
fail "证书目录不能覆盖 Scert 的安装、配置、数据、日志、备份或文档目录:$certificate_dir"
;;
esac
}
reject_symlink_path() {
local target_path="$1"
local description="$2"
local current_path=""
local part
local -a path_parts
IFS='/' read -r -a path_parts <<< "${target_path#/}"
for part in "${path_parts[@]}"; do
[[ -n "$part" ]] || continue
current_path="$current_path/$part"
if [[ -L "$current_path" ]]; then
fail "$description 不能包含符号链接:$current_path"
fi
done
}
validate_managed_paths() {
local managed_path
for managed_path in \
"$INSTALL_DIR" \
"$INSTALL_DIR/conf" \
"$INSTALL_DIR/data" \
"$INSTALL_DIR/log" \
"$INSTALL_DIR/backups" \
"$INSTALL_DIR/docs" \
"$INSTALL_DIR/web" \
"$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" \
"$SERVICE_DROP_IN_DIR/10-certificate-path.conf"; do
reject_symlink_path "$managed_path" "Scert 受管路径"
done
}
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; do
command -v "$command_name" >/dev/null 2>&1 || fail "找不到系统命令:$command_name"
done
nologin_shell="$(command -v nologin || true)"
[[ -n "$nologin_shell" ]] || fail "找不到 nologin,无法创建禁止登录的服务用户"
validate_certificate_dir
validate_managed_paths
}
validate_artifacts() {
[[ -d "$source_dir" ]] || fail "安装产物目录不存在:$source_dir"
source_dir="$(cd -- "$source_dir" && pwd)"
local artifact
for artifact in scert scert-admin; do
[[ -f "$source_dir/$artifact" ]] || fail "安装产物不存在:$source_dir/$artifact"
[[ -r "$source_dir/$artifact" ]] || fail "安装产物不可执行:$source_dir/$artifact"
done
[[ -f "$source_dir/web/index.html" ]] || fail "缺少前端入口文件:$source_dir/web/index.html"
[[ -f "$SCRIPT_DIR/systemd/Scert.service" ]] || fail "缺少 Scert systemd unit"
[[ -f "$SCRIPT_DIR/systemd/Scert-admin.service" ]] || fail "缺少 Scert-admin systemd unit"
[[ -f "$SCRIPT_DIR/systemd/Scert-admin服务管理说明.md" ]] || fail "缺少 Scert-admin 服务管理说明"
[[ -f "$SCRIPT_DIR/systemd/Scert服务管理说明.md" ]] || fail "缺少 Scert 服务管理说明"
}
ensure_service_account() {
if ! getent group "$SERVICE_GROUP" >/dev/null 2>&1; then
if ! groupadd --system "$SERVICE_GROUP"; then
if groupadd --help 2>&1 | grep -q -- '--badname'; then
groupadd --system --badname "$SERVICE_GROUP"
else
fail "当前系统无法创建名称为 $SERVICE_GROUP 的系统用户组"
fi
fi
fi
if ! id "$SERVICE_USER" >/dev/null 2>&1; then
if ! useradd --system --gid "$SERVICE_GROUP" --home-dir "$INSTALL_DIR" --shell "$nologin_shell" "$SERVICE_USER"; then
if useradd --help 2>&1 | grep -q -- '--badname'; then
useradd --system --badname --gid "$SERVICE_GROUP" --home-dir "$INSTALL_DIR" --shell "$nologin_shell" "$SERVICE_USER"
else
fail "当前系统无法创建名称为 $SERVICE_USER 的系统用户"
fi
fi
fi
[[ "$(id -gn "$SERVICE_USER")" == "$SERVICE_GROUP" ]] || fail "用户 $SERVICE_USER 未使用预期用户组 $SERVICE_GROUP"
local current_shell
current_shell="$(getent passwd "$SERVICE_USER" | cut -d: -f7)"
case "$current_shell" in
*/nologin|*/false) ;;
*) fail "现有用户 $SERVICE_USER 允许交互式登录,请先修正其登录 Shell" ;;
esac
}
create_directories() {
install -d -o root -g "$SERVICE_GROUP" -m 0750 "$INSTALL_DIR"
install -d -o root -g root -m 0755 "$INSTALL_DIR/docs"
install -d -o root -g root -m 0755 "$INSTALL_DIR/web"
install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0750 \
"$INSTALL_DIR/conf" \
"$INSTALL_DIR/data" \
"$INSTALL_DIR/log" \
"$INSTALL_DIR/backups"
install -d -o "$SERVICE_USER" -g "$SERVICE_GROUP" -m 0750 "$certificate_dir"
chown -R -h "$SERVICE_USER:$SERVICE_GROUP" \
"$INSTALL_DIR/conf" \
"$INSTALL_DIR/data" \
"$INSTALL_DIR/log" \
"$INSTALL_DIR/backups"
chmod 0700 "$INSTALL_DIR/conf"
local sensitive_file
for sensitive_file in scert.json master.key secrets.json; do
if [[ -f "$INSTALL_DIR/conf/$sensitive_file" ]]; then
chown "$SERVICE_USER:$SERVICE_GROUP" "$INSTALL_DIR/conf/$sensitive_file"
chmod 0600 "$INSTALL_DIR/conf/$sensitive_file"
fi
done
}
write_install_metadata() {
printf '{\n "certificate_root": "%s"\n}\n' "$certificate_dir" > "$INSTALL_METADATA_FILE"
chown root:"$SERVICE_GROUP" "$INSTALL_METADATA_FILE"
chmod 0640 "$INSTALL_METADATA_FILE"
}
install_binaries() {
install -o root -g root -m 0755 "$source_dir/scert" "$INSTALL_DIR/scert"
install -o root -g root -m 0755 "$source_dir/scert-admin" "$INSTALL_DIR/scert-admin"
}
install_web() {
cp -a "$source_dir/web/." "$INSTALL_DIR/web/"
find "$INSTALL_DIR/web" -type d -exec chmod 0755 {} +
find "$INSTALL_DIR/web" -type f -exec chown root:root {} + -exec chmod 0644 {} +
}
install_service_files() {
install -o root -g root -m 0644 "$SCRIPT_DIR/systemd/Scert.service" "$SERVICE_DIR/Scert.service"
install -o root -g root -m 0644 "$SCRIPT_DIR/systemd/Scert-admin.service" "$SERVICE_DIR/Scert-admin.service"
install -d -o root -g root -m 0755 "$SERVICE_DROP_IN_DIR"
printf '[Service]\nReadWritePaths=\nReadWritePaths=%s /scert/conf /scert/data /scert/log /scert/backups\n' \
"$certificate_dir" > "$SERVICE_DROP_IN_DIR/10-certificate-path.conf"
chmod 0644 "$SERVICE_DROP_IN_DIR/10-certificate-path.conf"
install -o root -g root -m 0644 \
"$SCRIPT_DIR/systemd/Scert服务管理说明.md" \
"$INSTALL_DIR/docs/Scert-service-management.md"
install -o root -g root -m 0644 \
"$SCRIPT_DIR/systemd/Scert-admin服务管理说明.md" \
"$INSTALL_DIR/docs/Scert-admin-service-management.md"
}
reload_systemd() {
systemctl daemon-reload
}
print_completion() {
printf '%s\n' \
'Scert Linux 文件安装完成。' \
'' \
'已安装:' \
' 核心程序:/scert/scert' \
' 密码重置程序:/scert/scert-admin' \
' 配置目录:/scert/conf' \
' 数据目录:/scert/data' \
' 日志目录:/scert/log' \
' 备份目录:/scert/backups' \
" 证书目录:$certificate_dir" \
" 安装路径记录:$INSTALL_METADATA_FILE" \
'' \
'本次只加载了 systemd unit,没有启动任何服务,也没有设置开机启动。' \
'首次初始化会读取安装路径记录,并将上述证书目录作为默认值。' \
'请先完成初始化配置。Scert-admin 需要密码重置时,管理员仅使用:' \
' systemctl start Scert-admin' \
' systemctl stop Scert-admin' \
'' \
'Scert 主服务是否启用和启动,应在初始化配置及证书部署完成后按运维流程执行。' \
'本机防火墙和云安全组端口放行由初始化流程处理或提示,不由本安装器创建公网规则。'
}
main() {
parse_args "$@"
validate_environment
validate_artifacts
ensure_service_account
create_directories
write_install_metadata
install_binaries
install_web
install_service_files
reload_systemd
print_completion
}
main "$@"