commit ac8b7cc17134557ba218ac0c0453ca8eff33e9ac Author: kairee <1+kairee@users.noreply.github.com> Date: Mon Oct 12 10:10:47 2026 +0800 Debian 初始化脚本与数据包 - init-debian.sh:用户创建、数据包下载与校验(防路径穿越)、/data 权限归一化(私钥、ACME 存储、.env 收紧为 600)、apt/docker/sshd 配置、sudoers、SSH 密钥、证书同步定时任务,新增任务 14 首次同步证书 - data.tar.gz:不含任何证书、私钥和密码,证书由 scripts/certs.sh 从证书机拉取;gateway 面板密码改从 .env 读取,并移除含 EAB 凭据的 zerossl 解析器 - 历史已压缩,旧历史中含敏感文件 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..9b6c6a3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,100 @@ +*.psd filter=lfs diff=lfs merge=lfs -text + +# videos +*.mp4 filter=lfs diff=lfs merge=lfs -text +*.avi filter=lfs diff=lfs merge=lfs -text +*.mkv filter=lfs diff=lfs merge=lfs -text +*.wmv filter=lfs diff=lfs merge=lfs -text +*.asf filter=lfs diff=lfs merge=lfs -text +*.asx filter=lfs diff=lfs merge=lfs -text +*.rm filter=lfs diff=lfs merge=lfs -text +*.rmvb filter=lfs diff=lfs merge=lfs -text +*.3gp filter=lfs diff=lfs merge=lfs -text +*.mov filter=lfs diff=lfs merge=lfs -text +*.m4v filter=lfs diff=lfs merge=lfs -text +*.dat filter=lfs diff=lfs merge=lfs -text +*.vob filter=lfs diff=lfs merge=lfs -text +*.dv filter=lfs diff=lfs merge=lfs -text +*.mpeg filter=lfs diff=lfs merge=lfs -text +*.mpg filter=lfs diff=lfs merge=lfs -text +*.mpe filter=lfs diff=lfs merge=lfs -text +*.m2v filter=lfs diff=lfs merge=lfs -text +*.webm filter=lfs diff=lfs merge=lfs -text +*.flv filter=lfs diff=lfs merge=lfs -text +*.vob filter=lfs diff=lfs merge=lfs -text +*.swf filter=lfs diff=lfs merge=lfs -text +*.avc filter=lfs diff=lfs merge=lfs -text +*.arf filter=lfs diff=lfs merge=lfs -text +*.vcr filter=lfs diff=lfs merge=lfs -text + +# images +*.psd filter=lfs diff=lfs merge=lfs -text +*.ai filter=lfs diff=lfs merge=lfs -text +*.cdr filter=lfs diff=lfs merge=lfs -text +*.raw filter=lfs diff=lfs merge=lfs -text +*.hdr filter=lfs diff=lfs merge=lfs -text +*.fbx filter=lfs diff=lfs merge=lfs -text +*.3ds filter=lfs diff=lfs merge=lfs -text +*.prj filter=lfs diff=lfs merge=lfs -text +*.dae filter=lfs diff=lfs merge=lfs -text +*.dwg filter=lfs diff=lfs merge=lfs -text +*.flt filter=lfs diff=lfs merge=lfs -text +*.htr filter=lfs diff=lfs merge=lfs -text +*.iges filter=lfs diff=lfs merge=lfs -text +*.igs filter=lfs diff=lfs merge=lfs -text +*.ige filter=lfs diff=lfs merge=lfs -text +*.ipt filter=lfs diff=lfs merge=lfs -text +*.iam filter=lfs diff=lfs merge=lfs -text +*.lp filter=lfs diff=lfs merge=lfs -text +*.ls filter=lfs diff=lfs merge=lfs -text +*.shp filter=lfs diff=lfs merge=lfs -text +*.aep filter=lfs diff=lfs merge=lfs -text +*.psb filter=lfs diff=lfs merge=lfs -text +*.edx filter=lfs diff=lfs merge=lfs -text +*.cds filter=lfs diff=lfs merge=lfs -text + +# audios +*.ape filter=lfs diff=lfs merge=lfs -text +*.wav filter=lfs diff=lfs merge=lfs -text +*.flac filter=lfs diff=lfs merge=lfs -text +*.aiff filter=lfs diff=lfs merge=lfs -text +*.aac filter=lfs diff=lfs merge=lfs -text +*.mp3 filter=lfs diff=lfs merge=lfs -text +*.cpa filter=lfs diff=lfs merge=lfs -text +*.swa filter=lfs diff=lfs merge=lfs -text +*.sesx filter=lfs diff=lfs merge=lfs -text +*.ses filter=lfs diff=lfs merge=lfs -text + +# libraries +*.a filter=lfs diff=lfs merge=lfs -text +*.o filter=lfs diff=lfs merge=lfs -text +*.so filter=lfs diff=lfs merge=lfs -text +*.lib filter=lfs diff=lfs merge=lfs -text + +# executable files +*.msi filter=lfs diff=lfs merge=lfs -text +*.exe filter=lfs diff=lfs merge=lfs -text +*.ipa filter=lfs diff=lfs merge=lfs -text +*.apk filter=lfs diff=lfs merge=lfs -text + +# compressed files +*.7z filter=lfs diff=lfs merge=lfs -text +*.zip filter=lfs diff=lfs merge=lfs -text +*.tar filter=lfs diff=lfs merge=lfs -text +*.gz filter=lfs diff=lfs merge=lfs -text +*.tgz filter=lfs diff=lfs merge=lfs -text +*.rar filter=lfs diff=lfs merge=lfs -text +*.bz2 filter=lfs diff=lfs merge=lfs -text +*.Z filter=lfs diff=lfs merge=lfs -text + + +# disk images +*.iso filter=lfs diff=lfs merge=lfs -text +*.bin filter=lfs diff=lfs merge=lfs -text +*.dmg filter=lfs diff=lfs merge=lfs -text +*.cue filter=lfs diff=lfs merge=lfs -text +*.raw filter=lfs diff=lfs merge=lfs -text + +# linux packages +*.rpm filter=lfs diff=lfs merge=lfs -text +*.deb filter=lfs diff=lfs merge=lfs -text \ No newline at end of file diff --git a/.ssh/authorized_keys b/.ssh/authorized_keys new file mode 100644 index 0000000..48a0697 --- /dev/null +++ b/.ssh/authorized_keys @@ -0,0 +1,3 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEUu8VEtNg0HmkTrfAPNmKS5Z2RybxOhMlVme+Yh3YDF kaireewu@win +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII2mSS17QHO+v41rCf9X8VczMP4CgK+AmKKiiNRxgZNt kairee@mac +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGbtaNu05don3+OC/1n94AEU+15OxzGaiVUgpdcdKjaq kairee@omen \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..045b288 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# debian + +Debian init scripts + +``` +curl -O https://git.ofcss.com/kairee/debian/raw/branch/main/init-debian.sh +chmod +x init-debian.sh +sudo bash init-debian.sh https://git.ofcss.com/kairee/debian/media/branch/main/data.tar.gz +``` + +## 说明 + +- `data.tar.gz` 不包含任何证书、私钥或密码。初始化时,任务 14 会用任务 12 配好的 SSH 密钥执行 `/data/scripts/certs.sh`,从证书机拉取证书,之后由定时任务每天同步。 +- `apps/gateway` 的面板密码哈希放在本机的 `/data/apps/gateway/.env`,格式见 `.env.example`,这个文件不进仓库。 +- 凭据只放在目标机器上,不要再提交到本仓库。 diff --git a/data.tar.gz b/data.tar.gz new file mode 100644 index 0000000..18cd82d --- /dev/null +++ b/data.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d56c5c48c0f5cc023ed40862fc8473b32fbca17ef90b1af250ad9a54713a4614 +size 5543 diff --git a/etc/apt/sources-bookworm.list b/etc/apt/sources-bookworm.list new file mode 100644 index 0000000..dcb4f20 --- /dev/null +++ b/etc/apt/sources-bookworm.list @@ -0,0 +1,12 @@ +deb http://deb.debian.org/debian bookworm main contrib non-free non-free-firmware +deb http://deb.debian.org/debian bookworm-updates main contrib non-free non-free-firmware +deb http://deb.debian.org/debian bookworm-backports main contrib non-free non-free-firmware +deb http://deb.debian.org/debian bookworm-proposed-updates main contrib non-free non-free-firmware + +deb-src http://deb.debian.org/debian bookworm main contrib non-free non-free-firmware +deb-src http://deb.debian.org/debian bookworm-updates main contrib non-free non-free-firmware +deb-src http://deb.debian.org/debian bookworm-backports main contrib non-free non-free-firmware +deb-src http://deb.debian.org/debian bookworm-proposed-updates main contrib non-free non-free-firmware + +deb http://security.debian.org/debian-security bookworm-security main contrib non-free non-free-firmware +deb-src http://security.debian.org/debian-security bookworm-security main contrib non-free non-free-firmware \ No newline at end of file diff --git a/etc/apt/sources-bullseye.list b/etc/apt/sources-bullseye.list new file mode 100644 index 0000000..e8b36ac --- /dev/null +++ b/etc/apt/sources-bullseye.list @@ -0,0 +1,12 @@ +deb http://deb.debian.org/debian bullseye main contrib non-free +deb http://deb.debian.org/debian bullseye-updates main contrib non-free +deb http://deb.debian.org/debian bullseye-backports main contrib non-free +deb http://deb.debian.org/debian bullseye-proposed-updates main contrib non-free + +deb-src http://deb.debian.org/debian bullseye main contrib non-free +deb-src http://deb.debian.org/debian bullseye-updates main contrib non-free +deb-src http://deb.debian.org/debian bullseye-backports main contrib non-free +deb-src http://deb.debian.org/debian bullseye-proposed-updates main contrib non-free + +deb http://security.debian.org/debian-security bullseye-security main contrib non-free non-free-firmware +deb-src http://security.debian.org/debian-security bullseye-security main contrib non-free non-free-firmware diff --git a/etc/profile b/etc/profile new file mode 100644 index 0000000..b43800c --- /dev/null +++ b/etc/profile @@ -0,0 +1,36 @@ +# /etc/profile: system-wide .profile file for the Bourne shell (sh(1)) +# and Bourne compatible shells (bash(1), ksh(1), ash(1), ...). + +if [ "`id -u`" -eq 0 ]; then + PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" +else + PATH="/usr/local/bin:/usr/bin:/bin" +fi +export PATH + +if [ "${PS1-}" ]; then + if [ "${BASH-}" ] && [ "$BASH" != "/bin/sh" ]; then + # The file bash.bashrc already sets the default PS1. + # PS1='\h:\w\$ ' + if [ -f /etc/bash.bashrc ]; then + . /etc/bash.bashrc + fi + else + if [ "`id -u`" -eq 0 ]; then + PS1='# ' + else + PS1='$ ' + fi + fi +fi + +if [ -d /etc/profile.d ]; then + for i in /etc/profile.d/*.sh; do + if [ -r $i ]; then + . $i + fi + done + unset i +fi + +ulimit -SHn 51200 diff --git a/etc/security/limits.conf b/etc/security/limits.conf new file mode 100644 index 0000000..119c516 --- /dev/null +++ b/etc/security/limits.conf @@ -0,0 +1,2 @@ +* soft nofile 51200 +* hard nofile 51200 \ No newline at end of file diff --git a/etc/ssh/sshd_config b/etc/ssh/sshd_config new file mode 100644 index 0000000..57a238e --- /dev/null +++ b/etc/ssh/sshd_config @@ -0,0 +1,23 @@ +Port 46571 +AddressFamily any +Protocol 2 +HostKey /etc/ssh/ssh_host_ed25519_key +PermitRootLogin prohibit-password +AuthorizedKeysFile .ssh/authorized_keys +PasswordAuthentication no +ChallengeResponseAuthentication no +GSSAPIAuthentication no +GSSAPICleanupCredentials no +GSSAPIStrictAcceptorCheck no +GSSAPIKeyExchange no +UsePAM yes +X11Forwarding yes +TCPKeepAlive yes +ClientAliveInterval 60 +UseDNS no +PrintMotd no +AcceptEnv LANG LC_* +Subsystem sftp /usr/lib/openssh/sftp-server + +Match User kairee + PasswordAuthentication yes \ No newline at end of file diff --git a/etc/sysctl.conf b/etc/sysctl.conf new file mode 100644 index 0000000..55cf66c --- /dev/null +++ b/etc/sysctl.conf @@ -0,0 +1,36 @@ +kernel.sysrq = 1 +kernel.printk = 3 4 1 3 +vm.swappiness = 10 +vm.overcommit_memory=1 + +fs.file-max = 1491124 +net.core.rmem_max = 67108864 +net.core.wmem_max = 67108864 +net.core.rmem_default = 65536 +net.core.wmem_default = 65536 +net.ipv4.tcp_max_syn_backlog = 1048576 +net.ipv4.tcp_tw_reuse = 1 +net.ipv4.tcp_max_tw_buckets = 55000 +net.core.netdev_max_backlog = 1048576 +net.core.somaxconn = 1048576 +net.ipv4.tcp_syncookies = 1 + +net.ipv4.tcp_fin_timeout = 30 +net.ipv4.tcp_keepalive_time = 1200 +net.ipv4.ip_local_port_range = 1024 65000 + +net.ipv4.tcp_fastopen = 3 +#net.ipv4.tcp_mem = 25600 51200 102400 +#net.ipv4.tcp_rmem = 4096 87380 67108864 +#net.ipv4.tcp_wmem = 4096 87380 67108864 +net.ipv4.tcp_mtu_probing = 1 +net.ipv4.tcp_ecn = 1 +net.ipv4.tcp_ecn_fallback = 1 +net.core.default_qdisc = fq +net.ipv4.tcp_congestion_control = bbr +net.ipv4.tcp_retries2 = 8 + +net.ipv4.ip_forward = 1 + +net.ipv6.conf.default.autoconf = 0 +net.ipv6.conf.all.autoconf = 0 \ No newline at end of file diff --git a/init-debian.sh b/init-debian.sh new file mode 100644 index 0000000..5fc2de6 --- /dev/null +++ b/init-debian.sh @@ -0,0 +1,707 @@ +#!/bin/bash + +# 注意:不使用 set -e,以便更好地处理错误和重试 + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# 状态文件路径 +STATUS_FILE="/root/.init-debian-status" + +# 日志函数 +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# 检查任务是否已完成 +task_completed() { + local task_id="$1" + if [ -f "$STATUS_FILE" ]; then + grep -q "^${task_id}$" "$STATUS_FILE" + return $? + fi + return 1 +} + +# 标记任务为已完成 +mark_task_completed() { + local task_id="$1" + mkdir -p "$(dirname "$STATUS_FILE")" + if ! task_completed "$task_id"; then + echo "$task_id" >> "$STATUS_FILE" + fi +} + +# 执行任务(带状态检查) +execute_task() { + local task_id="$1" + local task_name="$2" + local task_func="$3" + + if task_completed "$task_id"; then + log_info "任务 $task_id: $task_name - 已完成,跳过" + return 0 + fi + + log_info "执行任务 $task_id: $task_name..." + if $task_func; then + mark_task_completed "$task_id" + log_success "任务 $task_id 完成" + return 0 + else + log_error "任务 $task_id 失败" + return 1 + fi +} + +# 自动接受 SSH host key +accept_ssh_host_key() { + local host="$1" + local port="$2" + local user_home="$3" # 用户家目录路径 + local known_hosts_file="${user_home}/.ssh/known_hosts" + + # 确保 .ssh 目录存在并设置正确的权限 + mkdir -p "${user_home}/.ssh" + chmod 700 "${user_home}/.ssh" + chown kairee:kairee "${user_home}/.ssh" + + # 检查是否已存在该 host 的 key + if ssh-keygen -F "[${host}]:${port}" -f "$known_hosts_file" &>/dev/null; then + log_info "SSH host key 已存在于 known_hosts" + return 0 + fi + + log_info "自动接受 SSH host key..." + # 使用 ssh-keyscan 获取并添加 host key + if ssh-keyscan -p "$port" "$host" >> "$known_hosts_file" 2>/dev/null; then + chown kairee:kairee "$known_hosts_file" + chmod 600 "$known_hosts_file" + log_success "SSH host key 已添加" + return 0 + else + log_warning "无法自动获取 SSH host key,将在连接时手动确认" + return 1 + fi +} + +# 检查是否为 root +if [[ $EUID -ne 0 ]]; then + log_error "此脚本必须以 root 权限运行" + exit 1 +fi + +# 输出同时写入日志文件,便于远程运行失败时回溯 +LOG_FILE="/var/log/init-debian.log" +exec > >(tee -a "$LOG_FILE") 2>&1 +log_info "日志文件: $LOG_FILE" + +# 提示输入 kairee 用户密码 +echo "========================================" +echo "Debian 系统初始化脚本" +echo "========================================" +echo +echo "请输入要为用户 'kairee' 设置的密码:" +read -rs -p "密码: " USER_PASSWORD +echo +read -rs -p "确认密码: " USER_PASSWORD_CONFIRM +echo + +# 验证密码 +if [ -z "$USER_PASSWORD" ]; then + log_error "密码不能为空" + exit 1 +fi + +if [ "$USER_PASSWORD" != "$USER_PASSWORD_CONFIRM" ]; then + log_error "两次输入的密码不匹配" + exit 1 +fi + +log_success "密码验证通过" +echo + +# 任务函数定义 +task_1_create_user() { + if id "kairee" &>/dev/null; then + log_warning "用户 'kairee' 已存在,跳过创建" + return 0 + fi + + mkdir -p /data/home + if ! useradd -m -d /data/home/kairee -s /bin/bash -u 1000 -U kairee; then + log_error "创建用户 'kairee' 失败" + return 1 + fi + + if ! chpasswd <<< "kairee:$USER_PASSWORD"; then + log_error "设置密码失败" + return 1 + fi + + chmod 700 /data/home/kairee + log_success "用户 'kairee' 创建成功 (uid=1000, 家目录: /data/home/kairee)" + return 0 +} + +task_2_download_data() { + DATA_URL="https://example.com/path/to/data.tar.gz" + + if [ -n "$1" ]; then + DATA_URL="$1" + log_info "使用提供的 URL: $DATA_URL" + fi + + if [ "$DATA_URL" = "https://example.com/path/to/data.tar.gz" ]; then + log_warning "未设置 DATA_URL,跳过下载步骤" + log_warning "你可以手动将 data 目录复制到 /data" + return 2 + fi + + TEMP_FILE=$(mktemp) + log_info "正在下载..." + if ! curl -L -o "$TEMP_FILE" "$DATA_URL"; then + log_error "下载失败" + rm -f "$TEMP_FILE" + return 1 + fi + + # 路径穿越防护:拒绝包含绝对路径或 .. 路径的归档 + if tar -tzf "$TEMP_FILE" | grep -qE '(^|/)\.\.(/|$)|^/'; then + log_error "归档包含绝对路径或 .. 路径,已拒绝解压" + rm -f "$TEMP_FILE" + return 1 + fi + + log_info "解压到 /data..." + mkdir -p /data + if ! tar -xzf "$TEMP_FILE" -C /; then + log_error "解压失败" + rm -f "$TEMP_FILE" + return 1 + fi + + rm -f "$TEMP_FILE" + + # 归一化权限,修复归档中携带的 777/666 等异常权限 + fix_data_permissions + + log_success "数据包下载并解压完成" + return 0 +} + +# 归一化 /data 权限:目录 755、普通文件 644、脚本 755;并修正 kairee 家目录与 SSH 权限 +fix_data_permissions() { + if [ ! -d /data ]; then + return 0 + fi + + log_info "归一化 /data 权限(目录 755,文件 644,脚本 755)..." + + find /data -type d -exec chmod 755 {} + + find /data -type f -exec chmod 644 {} + + find /data -type f \( -path "/data/scripts/*" -o -name "*.sh" \) -exec chmod 755 {} + + # 敏感文件收紧为 600:TLS 私钥、ACME 存储(Traefik 要求 600)、各应用 .env + find /data/certs -type f \( -name "key.pem" -o -name "*.key" \) -exec chmod 600 {} + 2>/dev/null + find /data/apps -type f \( -path "*/vols/certs/*.json" -o -name ".env" \) -exec chmod 600 {} + 2>/dev/null + + if id "kairee" &>/dev/null && [ -d /data/home/kairee ]; then + log_info "修正 kairee 家目录与 SSH 权限..." + chown -R kairee:kairee /data/home/kairee + chmod 700 /data/home/kairee + chmod 700 /data/home/kairee/.ssh 2>/dev/null + chmod 600 /data/home/kairee/.ssh/id_ed25519 2>/dev/null + chmod 600 /data/home/kairee/.ssh/known_hosts 2>/dev/null + chmod 644 /data/home/kairee/.ssh/id_ed25519.pub 2>/dev/null + fi + + log_success "/data 权限归一化完成" +} + +task_3_install_packages() { + apt-get update || return 1 + apt-get install --no-install-recommends -y sudo vim rsync curl sshpass cron || return 1 + apt-get install -y --reinstall openssl openssh-server || return 1 + log_success "基础软件包安装完成" + return 0 +} + +task_4_install_docker() { + if command -v docker &> /dev/null; then + log_warning "Docker 已安装,跳过安装步骤" + return 0 + fi + + if curl -Lo- https://get.docker.com | bash; then + log_success "Docker CE 安装完成" + return 0 + else + log_error "Docker 安装失败" + return 1 + fi +} + +task_5_configure_apt() { + if [ ! -d /data/etc/apt/sources.list.d ]; then + log_warning "未找到 /data/etc/apt/sources.list.d 目录,跳过 apt 源配置" + return 0 + fi + + if [ -f /etc/apt/sources.list ]; then + mv /etc/apt/sources.list /etc/apt/sources.list.bak + log_info "备份 /etc/apt/sources.list -> /etc/apt/sources.list.bak" + fi + + if [ -f /etc/apt/sources.list.d/docker.list ]; then + mv /etc/apt/sources.list.d/docker.list /etc/apt/sources.list.d/docker.list.bak + log_info "备份 /etc/apt/sources.list.d/docker.list -> docker.list.bak" + fi + + mkdir -p /etc/apt/sources.list.d + + for src_file in /data/etc/apt/sources.list.d/*; do + if [ -f "$src_file" ]; then + filename=$(basename "$src_file") + target_file="/etc/apt/sources.list.d/$filename" + + ln -sf "$src_file" "$target_file" + log_success "创建软链接: $src_file -> $target_file" + fi + done + return 0 +} + +task_6_configure_docker() { + if [ ! -f /data/etc/docker/daemon.json ]; then + log_warning "未找到 /data/etc/docker/daemon.json,跳过 Docker 配置" + return 0 + fi + + mkdir -p /etc/docker + + if [ -f /etc/docker/daemon.json ]; then + cp /etc/docker/daemon.json /etc/docker/daemon.json.bak + log_info "备份 /etc/docker/daemon.json -> daemon.json.bak" + fi + + ln -sf /data/etc/docker/daemon.json /etc/docker/daemon.json + log_success "创建 Docker daemon 配置软链接" + + if command -v docker >/dev/null 2>&1 && systemctl restart docker 2>/dev/null; then + log_success "Docker 已重启,新配置已生效" + else + log_warning "Docker 重启失败,配置将在 Docker 下次启动时生效" + fi + return 0 +} + +task_7_configure_ssh() { + if [ ! -f /data/etc/ssh/sshd_config ]; then + log_warning "未找到 /data/etc/ssh/sshd_config,跳过 SSH 配置" + return 0 + fi + + if [ -f /etc/ssh/sshd_config ]; then + cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak + log_info "备份 /etc/ssh/sshd_config -> sshd_config.bak" + fi + + ln -sf /data/etc/ssh/sshd_config /etc/ssh/sshd_config + log_success "创建 SSH 配置软链接" + + if ! sshd -t; then + log_error "sshd 配置语法检查失败,已跳过重启" + return 1 + fi + + systemctl restart sshd || systemctl restart ssh + log_success "SSH 服务已重启" + return 0 +} + +task_8_configure_sudoers() { + echo "kairee ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/kairee + chmod 0440 /etc/sudoers.d/kairee + log_success "创建 /etc/sudoers.d/kairee" + return 0 +} + +task_9_add_docker_group() { + if getent group docker > /dev/null; then + usermod -aG docker kairee + log_success "已将 kairee 添加到 docker 组" + return 0 + else + log_warning "docker 组不存在,请检查 Docker 是否正确安装" + return 0 # 不算错误,只是警告 + fi +} + +task_10_create_home_link() { + if [ ! -d /data/home/kairee ]; then + log_warning "未找到 /data/home/kairee,无法创建软链接" + return 0 + fi + + if [ -e "/home/kairee" ] && [ ! -L "/home/kairee" ]; then + log_warning "/home/kairee 已存在但不是软链接,将被移动到 /home/kairee.old 备份" + mv /home/kairee /home/kairee.old + fi + + if [ -L "/home/kairee" ] && [ "$(readlink /home/kairee)" != "/data/home/kairee" ]; then + log_warning "/home/kairee 软链接指向错误的位置,将被重新创建" + rm -f /home/kairee + fi + + if [ ! -e "/home/kairee" ]; then + ln -s /data/home/kairee /home/kairee + log_success "创建家目录软链接: /data/home/kairee -> /home/kairee" + else + log_success "家目录软链接已存在且正确" + fi + return 0 +} + +task_11_generate_ssh_key() { + # 使用 kairee 用户的家目录 + KAIREE_HOME="/data/home/kairee" + SSH_KEY_DIR="${KAIREE_HOME}/.ssh" + SSH_PRIVATE_KEY="$SSH_KEY_DIR/id_ed25519" + SSH_PUBLIC_KEY="$SSH_KEY_DIR/id_ed25519.pub" + + # 确保 kairee 用户存在 + if ! id "kairee" &>/dev/null; then + log_error "用户 kairee 不存在,请先执行任务1" + return 1 + fi + + # 创建 .ssh 目录并设置权限 + mkdir -p "$SSH_KEY_DIR" + chmod 700 "$SSH_KEY_DIR" + chown kairee:kairee "$SSH_KEY_DIR" + + if [ -f "$SSH_PRIVATE_KEY" ]; then + log_warning "SSH 私钥已存在,跳过生成: $SSH_PRIVATE_KEY" + return 0 + fi + + # 使用 kairee 用户身份生成密钥 + if sudo -u kairee ssh-keygen -t ed25519 -f "$SSH_PRIVATE_KEY" -N ""; then + log_success "生成 SSH 密钥对完成(kairee 用户)" + log_info "私钥: $SSH_PRIVATE_KEY" + log_info "公钥: $SSH_PUBLIC_KEY" + return 0 + else + log_error "SSH 密钥生成失败" + return 1 + fi +} + +task_12_copy_ssh_key() { + REMOTE_HOST="qc5.ofcdn.cn" + REMOTE_PORT="46571" + REMOTE_SSH_COPY_ID_USER="kairee" + + # 使用 kairee 用户的家目录 + KAIREE_HOME="/data/home/kairee" + SSH_PUBLIC_KEY="${KAIREE_HOME}/.ssh/id_ed25519.pub" + SSH_KNOWN_HOSTS="${KAIREE_HOME}/.ssh/known_hosts" + + if [ ! -f "$SSH_PUBLIC_KEY" ]; then + log_error "公钥文件不存在: $SSH_PUBLIC_KEY" + return 1 + fi + + # 自动接受 SSH host key(使用 kairee 用户的 known_hosts) + accept_ssh_host_key "$REMOTE_HOST" "$REMOTE_PORT" "$KAIREE_HOME" + + # 密码重试机制 + MAX_RETRIES=3 + RETRY_COUNT=0 + TEMP_SCRIPT="${KAIREE_HOME}/.ssh/temp_ssh_copy_$$.sh" + # 无论成功失败或脚本被中断,都清理含密码的临时脚本并清除密码变量 + trap 'rm -f "$TEMP_SCRIPT" 2>/dev/null; unset REMOTE_SSH_PASSWORD SSHPASS_B64' EXIT + + while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do + echo + echo "========================================" + echo "输入远程服务器 SSH 密码" + echo "========================================" + echo "目标: $REMOTE_SSH_COPY_ID_USER@${REMOTE_HOST}:${REMOTE_PORT}" + if [ $RETRY_COUNT -gt 0 ]; then + log_warning "这是第 $((RETRY_COUNT + 1)) 次尝试(最多 $MAX_RETRIES 次)" + fi + echo + echo "请输入 $REMOTE_SSH_COPY_ID_USER 在 $REMOTE_HOST 上的 SSH 密码:" + read -rs -p "SSH 密码: " REMOTE_SSH_PASSWORD + echo + + if [ -z "$REMOTE_SSH_PASSWORD" ]; then + log_warning "未输入 SSH 密码" + if [ $RETRY_COUNT -lt $((MAX_RETRIES - 1)) ]; then + read -rp "是否继续重试?(y/n): " CONTINUE + if [ "$CONTINUE" != "y" ] && [ "$CONTINUE" != "Y" ]; then + log_warning "用户取消,跳过 ssh-copy-id" + return 0 + fi + fi + RETRY_COUNT=$((RETRY_COUNT + 1)) + continue + fi + + # 使用 sshpass 执行 ssh-copy-id,添加 SSH 选项以处理 host key + log_info "正在尝试复制公钥到远程服务器(使用 kairee 用户的密钥)..." + + # 在 kairee 用户目录下创建临时脚本文件(避免 /tmp 权限问题) + mkdir -p "${KAIREE_HOME}/.ssh" + chown kairee:kairee "${KAIREE_HOME}/.ssh" + + # 密码经 base64 传入避免特殊字符转义问题,用 sshpass -e 避免密码出现在进程列表 + SSHPASS_B64=$(printf '%s' "$REMOTE_SSH_PASSWORD" | base64 -w0) + + # 先测试连接并接受 host key(这会自动将 host key 添加到 kairee 用户的 known_hosts) + # 使用 sudo -u kairee tee 来创建文件,确保文件所有者是 kairee + sudo -u kairee tee "$TEMP_SCRIPT" > /dev/null << EOF +#!/bin/bash +export SSHPASS=\$(printf '%s' "$SSHPASS_B64" | base64 -d) +sshpass -e ssh -p "$REMOTE_PORT" \ + -o StrictHostKeyChecking=accept-new \ + -o PasswordAuthentication=yes \ + -o UserKnownHostsFile="$SSH_KNOWN_HOSTS" \ + "$REMOTE_SSH_COPY_ID_USER@$REMOTE_HOST" exit +EOF + chmod 700 "$TEMP_SCRIPT" + + # 执行测试连接,捕获错误信息 + TEST_OUTPUT=$(sudo -u kairee bash "$TEMP_SCRIPT" 2>&1) + TEST_EXIT_CODE=$? + + if [ $TEST_EXIT_CODE -eq 0 ]; then + # 连接成功,现在复制公钥(此时 host key 已确认,使用 StrictHostKeyChecking=no) + sudo -u kairee tee "$TEMP_SCRIPT" > /dev/null << EOF +#!/bin/bash +export SSHPASS=\$(printf '%s' "$SSHPASS_B64" | base64 -d) +sshpass -e ssh-copy-id -p "$REMOTE_PORT" \ + -o StrictHostKeyChecking=no \ + -o UserKnownHostsFile="$SSH_KNOWN_HOSTS" \ + "$REMOTE_SSH_COPY_ID_USER@$REMOTE_HOST" +EOF + chmod 700 "$TEMP_SCRIPT" + + # 执行 ssh-copy-id,捕获错误信息 + COPY_OUTPUT=$(sudo -u kairee bash "$TEMP_SCRIPT" 2>&1) + COPY_EXIT_CODE=$? + + if [ $COPY_EXIT_CODE -eq 0 ]; then + sudo -u kairee rm -f "$TEMP_SCRIPT" + log_success "公钥已复制到远程服务器(kairee 用户)" + unset REMOTE_SSH_PASSWORD + return 0 + else + sudo -u kairee rm -f "$TEMP_SCRIPT" + RETRY_COUNT=$((RETRY_COUNT + 1)) + if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then + log_error "公钥复制失败,请重试" + log_info "错误信息: ${COPY_OUTPUT:0:200}" # 只显示前200个字符 + unset REMOTE_SSH_PASSWORD + else + log_error "已达到最大重试次数,跳过 ssh-copy-id" + log_info "最后错误信息: ${COPY_OUTPUT:0:200}" + unset REMOTE_SSH_PASSWORD + return 1 + fi + fi + else + sudo -u kairee rm -f "$TEMP_SCRIPT" + RETRY_COUNT=$((RETRY_COUNT + 1)) + if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then + log_error "密码错误或连接失败,请重试" + log_info "错误信息: ${TEST_OUTPUT:0:200}" # 只显示前200个字符 + unset REMOTE_SSH_PASSWORD + else + log_error "已达到最大重试次数,跳过 ssh-copy-id" + log_info "最后错误信息: ${TEST_OUTPUT:0:200}" + unset REMOTE_SSH_PASSWORD + return 1 + fi + fi + done + + return 1 +} + +task_13_add_cron() { + if [ ! -f /data/scripts/certs.sh ]; then + log_warning "未找到 /data/scripts/certs.sh,跳过定时任务配置" + return 0 + fi + + # 确保 kairee 用户存在 + if ! id "kairee" &>/dev/null; then + log_error "用户 kairee 不存在,请先执行任务1" + return 1 + fi + + # 确保 /data/certs 目录存在且权限正确 + mkdir -p /data/certs + chown kairee:kairee /data/certs + + RANDOM_HOUR=$((1 + RANDOM % 7)) + RANDOM_MINUTE=$((RANDOM % 60)) + + CRON_CMD="$RANDOM_MINUTE $RANDOM_HOUR * * * /data/scripts/certs.sh >> /data/certs/sync.log 2>&1" + + # 检查是否已存在相同的定时任务(kairee 用户的 crontab) + if sudo -u kairee crontab -l 2>/dev/null | grep -q "/data/scripts/certs.sh"; then + log_warning "定时任务已存在于 kairee 用户的 crontab,跳过添加" + return 0 + fi + + # 添加到 kairee 用户的 crontab + if (sudo -u kairee crontab -l 2>/dev/null; echo "$CRON_CMD") | sudo -u kairee crontab -; then + log_success "定时任务已添加到 kairee 用户的 crontab" + log_info "执行时间: 每天 $RANDOM_HOUR:$RANDOM_MINUTE" + log_info "命令: /data/scripts/certs.sh >> /data/certs/sync.log 2>&1" + return 0 + else + log_error "添加定时任务失败" + return 1 + fi +} + +task_14_sync_certs() { + if [ ! -x /data/scripts/certs.sh ]; then + log_warning "未找到 /data/scripts/certs.sh,跳过首次证书同步" + return 0 + fi + # 依赖任务12:kairee 的 SSH 公钥已复制到证书机 + mkdir -p /data/certs + chown kairee:kairee /data/certs + if sudo -u kairee -H /data/scripts/certs.sh >> /data/certs/sync.log 2>&1; then + log_success "首次证书同步完成(来源见 /data/certs/sync.log)" + return 0 + else + log_error "首次证书同步失败,请检查任务12是否成功,或手动执行 sudo -u kairee /data/scripts/certs.sh" + return 1 + fi +} + +# 执行所有任务 +echo +log_info "开始执行初始化任务..." +echo + +# 记录失败任务,结束时汇总 +FAILED_TASKS=() + +# 任务1: 创建用户 kairee +execute_task "task_1" "创建用户 kairee" task_1_create_user || { log_warning "任务1失败,继续执行后续任务"; FAILED_TASKS+=("task_1"); } + +# 任务2: 下载并解压数据包(需要特殊处理参数) +if task_completed "task_2"; then + log_info "任务 task_2: 下载并解压数据包 - 已完成,跳过" +else + log_info "执行任务 task_2: 下载并解压数据包..." + task_2_download_data "$@" + case $? in + 0) + mark_task_completed "task_2" + log_success "任务 task_2 完成" + ;; + 2) + log_warning "未提供数据包 URL,跳过下载(未标记完成,可在重跑时传入 URL 参数)" + ;; + *) + log_warning "任务2失败,继续执行后续任务" + FAILED_TASKS+=("task_2") + ;; + esac +fi + +# 任务3: 安装软件包 +execute_task "task_3" "安装基础软件包" task_3_install_packages || { log_warning "任务3失败,继续执行后续任务"; FAILED_TASKS+=("task_3"); } + +# 任务4: 安装 Docker +execute_task "task_4" "安装 Docker CE" task_4_install_docker || { log_warning "任务4失败,继续执行后续任务"; FAILED_TASKS+=("task_4"); } + +# 任务5: 配置 apt 源 +execute_task "task_5" "配置 apt 源" task_5_configure_apt || { log_warning "任务5失败,继续执行后续任务"; FAILED_TASKS+=("task_5"); } + +# 任务6: 配置 Docker daemon +execute_task "task_6" "配置 Docker daemon" task_6_configure_docker || { log_warning "任务6失败,继续执行后续任务"; FAILED_TASKS+=("task_6"); } + +# 任务7: 配置 SSH +execute_task "task_7" "配置 SSH" task_7_configure_ssh || { log_warning "任务7失败,继续执行后续任务"; FAILED_TASKS+=("task_7"); } + +# 任务8: 配置 sudoers +execute_task "task_8" "配置 sudoers" task_8_configure_sudoers || { log_warning "任务8失败,继续执行后续任务"; FAILED_TASKS+=("task_8"); } + +# 任务9: 添加 kairee 到 docker 组 +execute_task "task_9" "添加 kairee 到 docker 组" task_9_add_docker_group || { log_warning "任务9失败,继续执行后续任务"; FAILED_TASKS+=("task_9"); } + +# 任务10: 创建家目录软链接 +execute_task "task_10" "创建家目录软链接" task_10_create_home_link || { log_warning "任务10失败,继续执行后续任务"; FAILED_TASKS+=("task_10"); } + +# 任务11: 生成 SSH 密钥对 +execute_task "task_11" "生成 SSH 密钥对" task_11_generate_ssh_key || { log_warning "任务11失败,继续执行后续任务"; FAILED_TASKS+=("task_11"); } + +# 任务12: 复制公钥到远程服务器 +execute_task "task_12" "复制公钥到远程服务器" task_12_copy_ssh_key || { log_warning "任务12失败,继续执行后续任务"; FAILED_TASKS+=("task_12"); } + +# 任务13: 添加定时任务 +execute_task "task_13" "添加定时任务" task_13_add_cron || { log_warning "任务13失败,继续执行后续任务"; FAILED_TASKS+=("task_13"); } + +# 任务14: 首次同步证书(之后由任务13的定时任务每天同步) +execute_task "task_14" "首次同步 TLS 证书" task_14_sync_certs || { log_warning "任务14失败,继续执行后续任务"; FAILED_TASKS+=("task_14"); } + +# 失败任务汇总 +if [ ${#FAILED_TASKS[@]} -gt 0 ]; then + log_warning "以下任务执行失败: ${FAILED_TASKS[*]}" + log_warning "可通过删除 $STATUS_FILE 中对应任务 ID 后重跑,或检查日志 $LOG_FILE" +else + log_success "所有任务均执行成功" +fi + +echo +echo "========================================" +echo -e "${GREEN}初始化完成!${NC}" +echo "========================================" +echo +echo "重要提示:" +echo "1. 请重新登录或执行 'su - kairee' 以使 Docker 组权限生效" +echo "2. 如果下载了数据包,请检查 /data 目录内容是否正确" +echo "3. SSH 配置已更新,请测试远程连接是否正常" +echo "4. 可以使用 'sudo -u kairee -i' 切换到 kairee 用户" +echo "5. SSH 密钥已生成(kairee 用户): /data/home/kairee/.ssh/id_ed25519" +if sudo -u kairee crontab -l 2>/dev/null | grep -q "/data/scripts/certs.sh"; then + CRON_TIME=$(sudo -u kairee crontab -l 2>/dev/null | grep "/data/scripts/certs.sh" | awk '{print $2":"$1}') + echo "6. 定时任务(kairee 用户): 每天 $CRON_TIME 执行 certs.sh" +else + echo "6. 定时任务未配置(可能因为 /data/scripts/certs.sh 不存在)" +fi +echo +echo "状态文件: $STATUS_FILE" +echo "日志文件: $LOG_FILE" +echo "如需重新执行某个任务,可以编辑状态文件删除对应的任务ID" +echo + +# 清理密码变量 +unset USER_PASSWORD +unset USER_PASSWORD_CONFIRM +unset REMOTE_SSH_PASSWORD