diff --git a/.github/workflows/run_reinstall.yml b/.github/workflows/run_reinstall.yml index 6d5067c..c62eb68 100644 --- a/.github/workflows/run_reinstall.yml +++ b/.github/workflows/run_reinstall.yml @@ -12,9 +12,11 @@ jobs: os: [ubuntu-latest, windows-latest] include: - os: ubuntu-latest - command: sudo bash reinstall.sh --debug --password 123@@@ + command: sudo bash reinstall.sh --debug --username x --password x + command_no_username_option: sudo bash reinstall.sh --debug - os: windows-latest - command: ./reinstall.bat --debug --password 123@@@ + command: ./reinstall.bat --debug --username x --password x + command_no_username_option: ./reinstall.bat --debug runs-on: ${{ matrix.os }} steps: - run: | @@ -35,9 +37,9 @@ jobs: # ${{ matrix.command }} arch # ${{ matrix.command }} gentoo - ${{ matrix.command }} netboot.xyz - ${{ matrix.command }} dd --img=https://download.opensuse.org/tumbleweed/appliances/openSUSE-MicroOS.x86_64-SelfInstall.raw.xz - ${{ matrix.command }} windows --image-name='Windows Server blah' --iso https://aka.ms/HCIReleaseImage --username administrator + ${{ matrix.command_no_username_option }} netboot.xyz + ${{ matrix.command }} dd --img=https://cloud.debian.org/images/cloud/sid/daily/latest/debian-sid-nocloud-amd64-daily.tar.xz + ${{ matrix.command }} windows --image-name='Windows Server blah' --iso https://aka.ms/HCIReleaseImage ${{ matrix.command }} reset diff --git a/debian.cfg b/debian.cfg index eda5853..db7f9cd 100644 --- a/debian.cfg +++ b/debian.cfg @@ -24,13 +24,9 @@ d-i mirror/country string manual # d-i mirror/http/hostname string deb.debian.org # B.4.5. 帐号设置 -d-i passwd/make-user boolean false -# 注意如果用 ssh key 后面还要删除密码 -# d-i passwd/root-password password '' -# d-i passwd/root-password-again password '' -# d-i passwd/root-password-crypted password '' -# kali 需要下面这行,否则会提示输入用户名 -d-i passwd/root-login boolean true +# kali 需要设置这行,否则会提示输入用户名 +# 因为 kali installer initrd 内置了一个 preseed.cfg,设置了 passwd/root-login false +# d-i passwd/root-login boolean true # B.4.6. 时钟与时区设置 d-i time/zone string Asia/Shanghai @@ -121,6 +117,19 @@ d-i grub-installer/force-efi-extra-removable boolean true # debian 11+ 才有 websocketd +# debian 9 sshd_config 不支持 Include + +# di 环境下 /etc/passwd 设置了 root 的家目录是 /,不是常见的 /root +# 我们不修改它,防止出问题 + +# di 环境下 锁定用户将无法登录 ssh + +# passwd/root-password-crypted 是 ! 开头时会提示输入密码 +# 因此这个值设成 * +# https://salsa.debian.org/installer-team/user-setup/-/blob/1.109/user-setup-ask?ref_type=tags#L35 + +# screen 需要设置 +s 权限,否则普通用户无法 attach 到 root 的 screen 会话 + # 有 /cdrom/simple-cdd 才安装 simple-cdd-profiles # 不然安装时 control 脚本会报错: # Loading simple-cdd-profiles failed for unknown reasons @@ -133,6 +142,9 @@ d-i grub-installer/force-efi-extra-removable boolean true # 此时还没有配置源,anna-install 会在配置完源后再安装 d-i preseed/early_command string true; \ for str in $(grep -wo "extra_[^ ]*" /proc/cmdline | sed 's/^extra_//'); do eval "$str"; done; \ + username=${username:-root}; \ + ssh_port=${ssh_port:-22}; \ + web_port=${web_port:-80}; \ di(){ \ echo "d-i $*" >/tmp/selections.cfg; \ @@ -150,6 +162,11 @@ d-i preseed/early_command string true; \ cp -f /etc/screenrc.bak /etc/screenrc; \ }; \ + chmod +s /usr/bin/screen; \ + + screen -x root/ -X multiuser on; \ + screen -x root/ -X acladd "$username"; \ + if [ "$hold" = 1 ]; then \ di auto-install/enable boolean false; \ di debconf/priority select low; \ @@ -160,7 +177,9 @@ d-i preseed/early_command string true; \ echo 'Option 1. View logs:'; \ echo ' tail -fn+1 /var/log/syslog'; \ echo 'Option 2. Attach to the installer:'; \ - echo ' TERM=screen screen -xp1'; \ + echo ' TERM=screen screen -x root/ -p 1'; \ + echo 'Option 3. Attach to the root shell if you are not root:'; \ + echo ' TERM=screen screen -x root/ -p 2'; \ } >>/etc/motd; \ mem=$(grep ^MemTotal: /proc/meminfo | { read -r _ y _; echo "$((y / 1024))"; }); \ if command -v websocketd && [ "$mem" -ge 400 ]; then \ @@ -170,10 +189,7 @@ d-i preseed/early_command string true; \ fi; \ sleep 5; \ done; \ - if [ -z "$web_port" ]; then \ - web_port=80; \ - fi; \ - run_as_service_with_screen websocketd --port 80 --loglevel=fatal --staticdir=/tmp \ + run_as_service_with_screen websocketd --port "$web_port" --loglevel=fatal --staticdir=/tmp \ sh -c "tail -fn+0 /var/log/syslog | tr '\r' '\n' | grep -Fiv -e password -e token" ; \ fi; \ fi; \ @@ -182,25 +198,64 @@ d-i preseed/early_command string true; \ di finish-install/reboot_in_progress note; \ fi; \ - if [ -s /configs/ssh_keys ]; then \ - di passwd/root-password-crypted password "''"; \ + mkdir -p /home; \ + chmod 755 /home; \ + + if [ "$username" = "root" ]; then \ + uid=0; \ + scope=root; \ + user_home=/; \ + di passwd/root-login boolean true; \ + di passwd/make-user boolean false; \ else \ - di passwd/root-password-crypted password "$(cat /configs/password-linux-sha512)"; \ + uid=1000; \ + scope=user; \ + user_home=/home/$username; \ + di passwd/root-login boolean false; \ + di passwd/make-user boolean true; \ + di passwd/user-fullname string "$username"; \ + di passwd/username string "$username"; \ fi; \ - mkdir -p /etc/ssh; \ - true >/etc/ssh/sshd_config; \ if [ -s /configs/ssh_keys ]; then \ - (umask 077; mkdir -p /.ssh; cat /configs/ssh_keys >/.ssh/authorized_keys); \ + password_hash_for_initrd=''; \ + password_hash_for_preseed='*'; \ else \ - echo "PermitRootLogin yes" >>/etc/ssh/sshd_config; \ + password_hash_for_initrd=$(cat /configs/password-linux-sha512); \ + password_hash_for_preseed=$(cat /configs/password-linux-sha512); \ fi; \ - if [ -n "$ssh_port" ] && ! [ "$ssh_port" = 22 ]; then \ - echo "Port $ssh_port" >>/etc/ssh/sshd_config; \ - fi; \ - grep -qs ^root: /etc/shadow || echo "root:$(cat /configs/password-linux-sha512):1:0:99999:7:::" >>/etc/shadow; \ + + di passwd/$scope-password-crypted password "$password_hash_for_preseed"; \ + + grep -qs ^$username: /etc/passwd || echo "$username:*:$uid:$uid:$username:$user_home:/bin/sh" >>/etc/passwd; \ + grep -qs ^$username: /etc/group || echo "$username:*:$uid:" >>/etc/group; \ + grep -qs ^$username: /etc/shadow || echo "$username:$password_hash_for_initrd:1:0:99999:7:::" >>/etc/shadow; \ grep -qs ^nogroup: /etc/group || echo "nogroup:*:65534:" >>/etc/group; \ grep -qs ^sshd: /etc/passwd || echo "sshd:*:100:65534::/run/sshd:/bin/false" >>/etc/passwd; \ + + mkdir -p /etc/ssh/; \ + true >/etc/ssh/sshd_config; \ + + if [ -s /configs/ssh_keys ]; then \ + ( \ + umask 077; \ + mkdir -p "$user_home/.ssh"; \ + cat /configs/ssh_keys >"$user_home/.ssh/authorized_keys"; \ + ); \ + chown "$username:$username" "$user_home"; \ + chown "$username:$username" "$user_home/.ssh"; \ + chown "$username:$username" "$user_home/.ssh/authorized_keys"; \ + echo "PasswordAuthentication no" >>/etc/ssh/sshd_config; \ + else \ + if [ "$username" = root ]; then \ + echo "PermitRootLogin yes" >>/etc/ssh/sshd_config; \ + fi; \ + fi; \ + + if ! [ "$ssh_port" = 22 ]; then \ + echo "Port $ssh_port" >>/etc/ssh/sshd_config; \ + fi; \ + mkdir -p /run/sshd; \ chmod 0755 /run/sshd; \ ssh-keygen -A; \ @@ -230,7 +285,11 @@ d-i preseed/early_command string true; \ # efi 分区大小未改变时,不会被格式化,因此需要手动删除旧系统的 efi 文件 # os-prober 卡太久,因此跳过 d-i partman/early_command string true; \ - eval "$(grep -o 'extra_confhome=[^ ]*' /proc/cmdline | sed 's/^extra_//')"; \ + for str in $(grep -wo "extra_[^ ]*" /proc/cmdline | sed 's/^extra_//'); do eval "$str"; done; \ + username=${username:-root}; \ + ssh_port=${ssh_port:-22}; \ + web_port=${web_port:-80}; \ + postinst=/var/lib/dpkg/info/bootstrap-base.postinst; \ cp $postinst $postinst.orig; \ @@ -276,6 +335,9 @@ d-i partman/early_command string true; \ # debian 9 tar 不支持 --strip-components d-i preseed/late_command string true; \ for str in $(grep -wo "extra_[^ ]*" /proc/cmdline | sed 's/^extra_//'); do eval "$str"; done; \ + username=${username:-root}; \ + ssh_port=${ssh_port:-22}; \ + web_port=${web_port:-80}; \ if [ "$elts" = 1 ]; then sed -i "s|deb\.freexian\.com/extended-lts|$deb_mirror|" /target/etc/apt/sources.list; fi; \ @@ -283,19 +345,43 @@ d-i preseed/late_command string true; \ in-target systemctl enable ssh; \ - if [ -s /configs/ssh_keys ]; then \ - (umask 077; mkdir -p /target/root/.ssh; cat /configs/ssh_keys >/target/root/.ssh/authorized_keys); \ - in-target passwd -d root; \ + if [ "$username" = root ]; then \ + user_home=/root; \ else \ - echo "PermitRootLogin yes" >/target/etc/ssh/sshd_config.d/01-permitrootlogin.conf || \ - echo "PermitRootLogin yes" >>/target/etc/ssh/sshd_config; \ + user_home=/home/$username; \ fi; \ - if [ -n "$ssh_port" ] && ! [ "$ssh_port" = 22 ]; then \ - echo "Port $ssh_port" >/target/etc/ssh/sshd_config.d/01-change-ssh-port.conf || \ + if [ -s /configs/ssh_keys ]; then \ + ( \ + umask 077; \ + mkdir -p "/target/$user_home/.ssh"; \ + cat /configs/ssh_keys >"/target/$user_home/.ssh/authorized_keys"; \ + ); \ + in-target passwd -d -l "$username"; \ + in-target chown "$username:$username" "$user_home"; \ + in-target chown "$username:$username" "$user_home/.ssh"; \ + in-target chown "$username:$username" "$user_home/.ssh/authorized_keys"; \ + + echo "PasswordAuthentication no" >/target/etc/ssh/sshd_config.d/01-passwordauthentication.conf || \ + echo "PasswordAuthentication no" >>/target/etc/ssh/sshd_config; \ + + else \ + if [ "$username" = root ]; then \ + echo "PermitRootLogin yes" >/target/etc/ssh/sshd_config.d/01-permitrootlogin.conf || \ + echo "PermitRootLogin yes" >>/target/etc/ssh/sshd_config; \ + fi; \ + fi; \ + + if ! [ "$ssh_port" = 22 ]; then \ + echo "Port $ssh_port" >/target/etc/ssh/sshd_config.d/01-port.conf || \ echo "Port $ssh_port" >>/target/etc/ssh/sshd_config; \ fi; \ + if ! [ "$username" = root ]; then \ + printf '%s\n' "$username ALL=(ALL) NOPASSWD:ALL" >"/target/etc/sudoers.d/99-$username"; \ + chmod 0440 "/target/etc/sudoers.d/99-$username"; \ + fi; \ + if ls /configs/frpc.* >/dev/null 2>&1; then \ mkdir -p /target/usr/local/bin; \ mkdir -p /target/usr/local/etc/frpc; \ diff --git a/reinstall.sh b/reinstall.sh index 79d562e..ed8f361 100644 --- a/reinstall.sh +++ b/reinstall.sh @@ -102,6 +102,7 @@ Usage: $reinstall_____ anolis 7|8|23 reset Options: For Linux/Windows: + [--username USERNAME] [--password PASSWORD] [--ssh-key KEY] [--ssh-port PORT] @@ -1942,15 +1943,29 @@ verify_os_name() { } verify_os_args() { + # 必备参数 case "$distro" in - dd) [ -n "$img" ] || error_and_exit "dd need --img" ;; - redhat) [ -n "$img" ] || error_and_exit "redhat need --img" ;; + dd) [ -n "$img" ] || error_and_exit "dd need --img." ;; + redhat) [ -n "$img" ] || error_and_exit "redhat need --img." ;; windows) [ -n "$image_name" ] || error_and_exit "Install Windows need --image-name." ;; esac + # 用户名/密码/证书相关 case "$distro" in - netboot.xyz | windows) [ -z "$ssh_keys" ] || error_and_exit "not support ssh key for $distro" ;; + netboot.xyz) + [ -z "$username" ] || error_and_exit "not support set username for $distro." + [ -z "$password" ] || error_and_exit "not support set password for $distro." + [ -z "$ssh_keys" ] || error_and_exit "not support set ssh key for $distro." + ;; + windows) + [ -z "$ssh_keys" ] || error_and_exit "not support set ssh key for $distro." + ;; esac + + # 不能同时使用证书和密码 + if [ -n "$password" ] && [ -n "$ssh_keys" ]; then + error_and_exit "Cannot set both password and ssh key." + fi } get_cmd_path() { @@ -2341,30 +2356,28 @@ trim() { } assert_username_valid() { - if ! msg=$(is_username_valid); then - error_and_exit "$msg" - fi -} - -is_username_valid() { # https://learn.microsoft.com/windows-hardware/customize/desktop/unattend/microsoft-windows-shell-setup-useraccounts-localaccounts-localaccount-name # 不能为 none [ ] / \ : | < > + = ; , ? * % @ - # 账号为空,则使用 Administrator + # 账号为空 if [ -z "$username" ]; then - echo "Username: Will use the built-in Administrator account in ISO language." - return 0 + error_and_exit "Username: Can not be empty." fi + # 账号为 none if [ "$(to_lower <<<"$username")" = none ]; then - echo "Username: Do not use the name \"NONE\", this is a restricted username." - return 1 + error_and_exit "Username: Can not be 'none'." fi + # 账号包含非法字符 if grep -q '[][/\:|<>+=;,?*%@]' <<<"$username"; then - echo "Username: Do not use any of the following characters: / \ [ ] : | < > + = ; , ? * % @" - return 1 + error_and_exit "Username: Do not use any of the following characters: / \ [ ] : | < > + = ; , ? * % @" fi +} + +# trans.sh 有同名方法 +is_administrator_username() { + username_in_lower=$(to_lower <<<"$1") # 如果输入以下用户名则忽略,并使用系统内置的 Administrator 账号 # 防止系统有两个不同语言的 Administrator 账号而造成困扰 @@ -2376,27 +2389,38 @@ is_username_valid() { администратор \ järjestelmänvalvoja \ rendszergazda; do - if [ "$(to_lower <<<"$username")" = "$builtin_username" ]; then - echo "Username: Will use the built-in Administrator account in ISO language." - unset username + if [ "$username_in_lower" = "$builtin_username" ]; then return 0 fi done + + return 1 } prompt_username() { info "prompt username" - warn false "Leave blank to use Administrator" - warn false "不填写则使用 Administrator" + + if [ "$distro" = windows ]; then + default_username=administrator + else + default_username=root + fi + + warn false "Set username, leave blank to use $default_username" + warn false "设置用户名,不填写则使用 $default_username" IFS= read -r -p "Username: " username username="$(printf "%s" "$username" | trim)" + + if [ -z "$username" ]; then + username=$default_username + fi assert_username_valid } prompt_password() { info "prompt password" - warn false "Leave blank to use a random password." - warn false "不填写则使用随机密码" + warn false "Set password, leave blank to use a random password." + warn false "设置密码,不填写则使用随机密码" while true; do IFS= read -r -p "Password: " password if [ -n "$password" ]; then @@ -3626,7 +3650,7 @@ EOF # 2. 删除 debian busybox 无法识别的语法 # 3. 删除 apk 语句 # 4. debian 11/12 initrd 无法识别 > > - # 5. debian 11/12 initrd 无法识别 < < + # 5. debian 11/12 initrd 无法识别 < < ,注意可能分两行写 # 6. debian 11 initrd 无法识别 set -E # 7. debian 11 initrd 无法识别 trap ERR # 8. debian 9 initrd 无法识别 ${string//find/replace} @@ -3637,11 +3661,16 @@ EOF -e "s/> >/$replace/" \ -e "s/< &2 - echo "Run '/trans.sh alpine' to install Alpine Linux instead." >&2 + + if is_have_cmd sudo; then + sudo_='sudo ' + elif is_have_cmd doas; then + sudo_='doas ' + else + sudo_= + fi + + echo "Run '$sudo_/trans.sh' to retry." >&2 + echo "Run '$sudo_/trans.sh alpine' to install Alpine Linux instead." >&2 + + # 解除锁定,允许用户登录处理故障 + # passwd -u "$username" >/dev/null + + # 用不着,因为 alpine 锁定账户后无法登录 ssh + # 因此不会锁定 + exit 1 } @@ -292,9 +308,6 @@ setup_nginx() { wget $confhome/logviewer.html -O /logviewer.html wget $confhome/logviewer-nginx.conf -O /etc/nginx/http.d/default.conf - if [ -z "$web_port" ]; then - web_port=80 - fi sed -i "s/@WEB_PORT@/$web_port/gi" /etc/nginx/http.d/default.conf # rc-service -q nginx start @@ -310,10 +323,6 @@ setup_websocketd() { wget $confhome/logviewer.html -O /tmp/index.html apk add coreutils - if [ -z "$web_port" ]; then - web_port=80 - fi - pkill websocketd || true # websocketd 遇到 \n 才推送,因此要转换 \r 为 \n websocketd --port "$web_port" --loglevel=fatal --staticdir=/tmp \ @@ -424,6 +433,16 @@ extract_env_from_cmdline() { fi done < <(xargs -n1 /dev/null 2>&1; then nix_frpc=$( if false; then @@ -1856,9 +1924,8 @@ $nix_bootloader $nix_swap $nix_substituters boot.kernelParams = [ $(get_ttys console= | quote_word) ]; -services.openssh.enable = true; -$nix_ssh_keys_or_PermitRootLogin -$nix_ssh_ports +$nix_users +$nix_openssh $nix_frpc $(cat /tmp/nixos_network_config.nix) ################################################### @@ -1906,7 +1973,7 @@ EOF # 设置密码 if ! is_need_set_ssh_keys; then - echo "root:$(get_password_linux_sha512)" | nixos-enter --root /os -- \ + printf '%s\n' "$username:$(get_password_linux_sha512)" | nixos-enter --root /os -- \ /run/current-system/sw/bin/chpasswd -e fi @@ -2042,12 +2109,13 @@ basic_init() { fi # 公钥/密码 + add_user_if_need "$os_dir" if is_need_set_ssh_keys; then set_ssh_keys_and_del_password $os_dir - change_ssh_conf_for_root_key_login $os_dir + change_ssh_conf_for_key_login $os_dir else - change_root_password $os_dir - change_ssh_conf_for_root_password_login $os_dir + change_user_password $os_dir + change_ssh_conf_for_password_login $os_dir fi # 下载 fix-eth-name.service @@ -2119,6 +2187,10 @@ EOF if [ "$(uname -m)" = aarch64 ]; then pkgs="$pkgs archlinuxarm-keyring" fi + if ! [ "$username" = root ]; then + pkgs="$pkgs sudo" + fi + pacstrap -K $os_dir $pkgs # dns @@ -2270,6 +2342,10 @@ EOF chroot $os_dir emerge sys-fs/dosfstools fi + if ! [ "$username" = root ]; then + chroot $os_dir emerge app-admin/sudo + fi + # firmware + microcode if fw_pkgs=$(get_ucode_firmware_pkgs) && [ -n "$fw_pkgs" ]; then chroot $os_dir emerge $fw_pkgs @@ -3179,7 +3255,7 @@ modify_windows() { # 5. 设置用户密码永不过期 # Azure 的 Windows 实例,初始用户的密码也是永不过期的 # 管理员账号默认不会过期 - if [ -n "$username" ]; then + if ! is_administrator_username "$username"; then cat <$os_dir/windows-set-user-password-never-expires.bat wmic useraccount where name="$username" set passwordexpires=false || ^ powershell -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "Set-LocalUser -Name '$username' -PasswordNeverExpires \$true" @@ -3451,6 +3527,10 @@ remove_or_disable_cloud_init() { rm -f $os_dir/etc/cloud/cloud.cfg.rpmsave ;; zypper) + # 防止删除 cloud-init 时自动删除 sudo + if ! [ "$username" = root ]; then + sed -i '/^sudo$/d' "$os_dir/var/lib/zypp/AutoInstalled" + fi # 加上 -u 才会删除依赖 chroot $os_dir zypper remove -y -u cloud-init cloud-init-config-suse ;; @@ -3562,6 +3642,7 @@ EOF # find_and_mount /boot # find_and_mount /boot/efi # fedora 的 fstab 还有 /home /var,因此用 mount -a + # 不然无法往 /home/$username 写入 ssh 公钥 chroot $os_dir mount -a cp_resolv_conf $os_dir @@ -3823,7 +3904,7 @@ EOF chroot $os_dir zypper remove -y --force-resolution $origin_kernel fi if $need_password_workaround; then - chroot $os_dir passwd -d root + chroot $os_dir passwd -d -l root fi fi @@ -3860,7 +3941,7 @@ EOF # 在这里修改密码,而不是用cloud-init,因为我们的默认密码太弱 is_password_plaintext && sed -i 's/enforce=everyone/enforce=none/' $os_dir/etc/security/passwdqc.conf - change_root_password $os_dir + change_user_password $os_dir is_password_plaintext && sed -i 's/enforce=none/enforce=everyone/' $os_dir/etc/security/passwdqc.conf # 下载仓库,选择 profile @@ -4045,19 +4126,125 @@ create_swap() { fi } -set_ssh_keys_and_del_password() { - os_dir=$1 - info 'set ssh keys' +del_user_password_and_lock() { + local os_dir=$1 + local username=$2 - # 添加公钥 - ( - umask 077 - mkdir -p $os_dir/root/.ssh - cat /configs/ssh_keys >$os_dir/root/.ssh/authorized_keys - ) + # 锁定用户后 ssh 能否登录 + # alpine × + # 其它系统 √ + + # root 空密码,不锁定 root,其它用户用 su - root 能否切换到 root + # alpine × + # 其它系统 √ + + # centos 7 不支持一行命令同时 -d -l + # passwd: Only one of -l, -u, -d, -S may be specified. # 删除密码 - chroot $os_dir passwd -d root + chroot "$os_dir" passwd -d "$username" + + # 锁定用户 + if ! [ -e "$os_dir/etc/alpine-release" ]; then + chroot "$os_dir" passwd -l "$username" + fi + + # alpine 锁定用户无法登录 ssh + # 因为 alpine 默认不开启 pam + # 其他系统默认开启 + + # 不开启 pam 的话,锁定用户无法登录 ssh + # 开启 pam 后可以 + + # alpine 是通过安装 openssh-server-pam 开启 pam + # 不需要设置 UsePAM yes 也无法识别 UsePAM yes + # localhost:~# sshd -G | grep -i pam + # /etc/ssh/sshd_config line 88: Unsupported option UsePAM +} + +set_ssh_keys_and_del_password() { + local os_dir=$1 + + info 'set ssh keys' + + if [ "$username" = root ]; then + local user_home="/root" + else + local user_home="/home/$username" + fi + + # 添加公钥 + if true; then + ( + umask 077 + mkdir -p "$os_dir/$user_home/.ssh" + cat /configs/ssh_keys >"$os_dir/$user_home/.ssh/authorized_keys" + ) + # 注意要用 chroot,否则 uid/gid 是 alpine live os 下的 uid/gid + chroot "$os_dir" chown "$username:$username" "$user_home" + chroot "$os_dir" chown "$username:$username" "$user_home/.ssh" + chroot "$os_dir" chown "$username:$username" "$user_home/.ssh/authorized_keys" + else + ( + # 如果日后添加 bsd 无法 chroot 时可以这样 + umask 077 + read -r owner group < \ + <(awk -F: -v user="$username" '$1==user {print $3,$4}' "$os_dir/etc/passwd") + install -D \ + -m 600 \ + -o "$owner" \ + -g "$group" \ + /configs/ssh_keys \ + "$os_dir/$user_home/.ssh/authorized_keys" + ) + fi + + # 删除密码/锁定用户 + del_user_password_and_lock "$os_dir" "$username" + + # debian 云镜像 /etc/shadow 的 root 条目为 + # root:!unprovisioned:20591:0:99999:7::: + # 首次开机会停在设置 root 密码界面,且阻塞 ssh 服务 + # 因此这里手动清空 root 密码并锁定 + if ! [ "$username" = root ] && is_have_cmd_on_disk "$os_dir" systemd-firstboot; then + del_user_password_and_lock "$os_dir" root + fi +} + +_is_ssh_kv_effective() { + local os_dir=$1 + local key=$2 + local value=$3 + + # centos 7 不支持 -G + if res=$(chroot "$os_dir" sshd -G 2>/dev/null || chroot "$os_dir" sshd -T 2>/dev/null); then + printf "%s\n" "$res" | grep -Fxiq "$key $value" + else + error_and_exit "Failed to verify sshd config." + fi +} + +is_ssh_kv_effective() { + local os_dir=$1 + local key=$2 + local value=$3 + + if _is_ssh_kv_effective "$os_dir" "$key" "$value"; then + return 0 + fi + + # centos 7 设置 prohibit-password ,sshd -T 会显示成 without-password + if [ "$(echo "$key" | to_lower)" = "permitrootlogin" ] && { + [ "$(echo "$value" | to_lower)" = "prohibit-password" ] || + [ "$(echo "$value" | to_lower)" = "without-password" ] + }; then + if _is_ssh_kv_effective "$os_dir" "permitrootlogin" "prohibit-password" || + _is_ssh_kv_effective "$os_dir" "permitrootlogin" "without-password"; then + return 0 + fi + fi + + return 1 } change_ssh_conf_if_different() { @@ -4079,7 +4266,7 @@ change_ssh_conf_if_different() { # PasswordAuthentication no # 0. 如果已经有这个配置,则不修改,避免不必要的改动 - if chroot "$os_dir" sshd -G | grep -Fxiq "$key $value"; then + if is_ssh_kv_effective "$os_dir" "$key" "$value"; then return fi @@ -4109,19 +4296,25 @@ change_ssh_conf_if_different() { echo "$key $value" >>$os_dir/etc/ssh/sshd_config fi fi + + # 验证是否成功 + if ! is_ssh_kv_effective "$os_dir" "$key" "$value"; then + error_and_exit "Failed to set sshd config $key $value." + fi } -change_ssh_conf_for_root_key_login() { +change_ssh_conf_for_key_login() { local os_dir=$1 - # 目前脚本只用 root ,不需要设置这个 - # change_ssh_conf_if_different "$os_dir" PasswordAuthentication no + change_ssh_conf_if_different "$os_dir" PasswordAuthentication no - # 这个也不需要设置,默认就是 prohibit-password - # change_ssh_conf_if_different "$os_dir" PermitRootLogin prohibit-password + # centos 7 PermitRootLogin 默认是 yes,而不是 prohibit-password + if [ "$username" = root ]; then + change_ssh_conf_if_different "$os_dir" PermitRootLogin prohibit-password + fi } -change_ssh_conf_for_root_password_login() { +change_ssh_conf_for_password_login() { local os_dir=$1 # opensuse 16/tumbleweed 安装 openssh-server-config-rootlogin @@ -4137,7 +4330,10 @@ change_ssh_conf_for_root_password_login() { # PasswordAuthentication 默认是 yes # 但某些发行版会在 sshd_config.d 里设置 PasswordAuthentication no change_ssh_conf_if_different "$os_dir" PasswordAuthentication yes - change_ssh_conf_if_different "$os_dir" PermitRootLogin yes + + if [ "$username" = root ]; then + change_ssh_conf_if_different "$os_dir" PermitRootLogin yes + fi } change_ssh_port() { @@ -4147,10 +4343,117 @@ change_ssh_port() { change_ssh_conf_if_different "$os_dir" Port "$ssh_port" } -change_root_password() { - os_dir=$1 +# 暂时用不着 +add_user_if_need_for_alpine() { + local os_dir=$1 - info 'change root password' + if ! grep -q "^$username:" "$os_dir/etc/passwd"; then + # -a Create admin user. Add to wheel group and set up doas + # -u Unlock the user automatically (eg. creating the user non-interactively + # with an ssh key for login) + if is_need_set_ssh_keys; then + chroot "$os_dir" setup-user -a -u -k "$(cat /configs/ssh_keys)" "$username" + else + chroot "$os_dir" setup-user -a -u "$username" + change_user_password $os_dir + fi + fi +} + +add_user_if_need() { + local os_dir=$1 + + # 添加用户 + if ! grep -q "^$username:" "$os_dir/etc/passwd"; then + # debian 推荐使用 adduser 而不是 useradd + # https://manpages.debian.org/trixie/passwd/useradd.8.en.html + # useradd is a low level utility for adding users. + # On Debian, administrators should usually use adduser(8) instead. + + # adduser 会从 /etc/adduser.conf 读取默认要添加的组 + # 然而通常这个值是空白 + + # alpine + if is_have_cmd_on_disk "$os_dir" adduser && + chroot "$os_dir" adduser --help 2>&1 | grep -Fq -- BusyBox; then + chroot "$os_dir" adduser --disabled-password "$username" + + # debian/ubuntu + elif is_have_cmd_on_disk "$os_dir" adduser && + chroot "$os_dir" adduser --help 2>&1 | grep -Fq -- '--disabled-password'; then + chroot "$os_dir" adduser --disabled-password --comment '' "$username" + + # el + elif is_have_cmd_on_disk "$os_dir" adduser && + chroot "$os_dir" adduser --help 2>&1 | grep -Fq -- '--password'; then + chroot "$os_dir" adduser --password ! "$username" + + # arch/gentoo 默认没有 adduser + else + chroot "$os_dir" useradd -m "$username" + fi + fi + + # 添加到 wheel/sudo 组 + if ! [ "$username" = root ]; then + if [ -e "$os_dir/etc/alpine-release" ]; then + # alpine + # https://github.com/alpinelinux/alpine-conf/blob/master/setup-user.in#L168 + + # 安装 doas + chroot "$os_dir" apk add doas doas-sudo-shim + mkdir -p "$os_dir/etc/doas.d" + + # 添加用户到组 + chroot "$os_dir" addgroup "$username" wheel + + # doas: 添加 wheel 组 + local file="$os_dir/etc/doas.d/20-wheel.conf" + local content="permit persist :wheel" + if ! grep -q "^$content" "$file" 2>/dev/null; then + echo "$content" >>"$file" + fi + + # doas: 添加单个用户 nopass + echo "permit nopass $username" >"$os_dir/etc/doas.d/99-$username.conf" + else + # 通常用 wheel 组 + # debian/ubuntu 没有 wheel 组,只有 sudo 组 + + # aws lightsail 上测试默认用户加入了哪些组 + # debian admin : admin adm dialout cdrom floppy sudo audio dip video plugdev + # ubuntu ubuntu : ubuntu adm cdrom sudo dip lxd + # almalinux ec2-user : ec2-user adm systemd-journal + # opensuse ec2-user : ec2-user + + # 添加用户到组 + for group in \ + wheel sudo \ + adm dialout cdrom floppy audio dip video plugdev lxd systemd-journal; do + if grep -q "^$group:" "$os_dir/etc/group"; then + # chroot "$os_dir" addgroup "$username" "$group" + chroot "$os_dir" usermod -aG "$group" "$username" + fi + done + + # sudo: gentoo 安装 sudo 后也没有 /etc/sudoers.d + if ! [ -d "$os_dir/etc/sudoers.d" ]; then + install -d -m 0750 "$os_dir/etc/sudoers.d" + fi + + # sudo: 添加单个用户 NOPASSWD + # https://wiki.archlinux.org/title/Sudo#Sudoers_default_file_permissions + local file="$os_dir/etc/sudoers.d/99-$username" + printf '%s\n' "$username ALL=(ALL) NOPASSWD:ALL" >"$file" + chmod 0440 "$file" + fi + fi +} + +change_user_password() { + local os_dir=$1 + + info 'change user password' if is_password_plaintext; then pam_d=$os_dir/etc/pam.d @@ -4184,13 +4487,13 @@ change_root_password() { # 分两行写,不然遇到错误不会终止 plaintext=$(get_password_plaintext) - echo "root:$plaintext" | chroot $os_dir chpasswd + printf '%s\n' "$username:$plaintext" | chroot $os_dir chpasswd if $has_pamd_chpasswd; then mv $pam_d/chpasswd.orig $pam_d/chpasswd fi else - echo "root:$(get_password_linux_sha512)" | chroot $os_dir chpasswd -e + printf '%s\n' "$username:$(get_password_linux_sha512)" | chroot $os_dir chpasswd -e fi } @@ -4460,6 +4763,7 @@ chroot_apt_autoremove() { del_default_user() { os_dir=$1 + local user while read -r user; do if grep ^$user':\$' "$os_dir/etc/shadow"; then echo "Deleting user $user" @@ -4593,18 +4897,20 @@ install_fnos() { mount_pseudo_fs /os # 更改密码 - if is_need_set_ssh_keys; then - set_ssh_keys_and_del_password $os_dir - else - change_root_password $os_dir + if false; then + if is_need_set_ssh_keys; then + set_ssh_keys_and_del_password $os_dir + else + change_user_password $os_dir + fi fi # ssh root 登录,测试用 if false; then if is_need_set_ssh_keys; then - change_ssh_conf_for_root_key_login $os_dir + change_ssh_conf_for_key_login $os_dir else - change_ssh_conf_for_root_password_login $os_dir + change_ssh_conf_for_password_login $os_dir fi chroot $os_dir systemctl enable ssh fi @@ -5756,6 +6062,26 @@ is_nt_ver_ge() { [ "$orig" = "$sorted" ] } +# reinstall.sh 有同名方法 +is_administrator_username() { + username_in_lower=$(printf "%s" "$1" | to_lower) + + for builtin_username in \ + administrator \ + administrador \ + administrateur \ + administratör \ + администратор \ + järjestelmänvalvoja \ + rendszergazda; do + if [ "$username_in_lower" = "$builtin_username" ]; then + return 0 + fi + done + + return 1 +} + get_cloud_vendor() { # busybox blkid 不显示 sr0 的 UUID apk add lsblk @@ -7218,26 +7544,26 @@ EOF /tmp/autounattend.xml # 账号密码 - if [ -n "$username" ]; then - # 普通账号 - password_base64=$(get_password_windows_user_base64) - xmlstarlet ed -L -N x="urn:schemas-microsoft-com:unattend" \ - -d "//x:AdministratorPassword" \ - /tmp/autounattend.xml - sed -i \ - -e "s|%enable_administrator%|0|" \ - -e "s|%user_username%|$username|" \ - -e "s|%user_password%|$password_base64|" \ - /tmp/autounattend.xml - else + if is_administrator_username "$username"; then # Administrator password_base64=$(get_password_windows_administrator_base64) xmlstarlet ed -L -N x="urn:schemas-microsoft-com:unattend" \ -d "//x:LocalAccounts" \ /tmp/autounattend.xml sed -i \ - -e "s|%enable_administrator%|1|" \ - -e "s|%administrator_password%|$password_base64|" \ + -e "s|%enable_administrator%|1|gi" \ + -e "s|%administrator_password%|$password_base64|gi" \ + /tmp/autounattend.xml + else + # 普通账号 + password_base64=$(get_password_windows_user_base64) + xmlstarlet ed -L -N x="urn:schemas-microsoft-com:unattend" \ + -d "//x:AdministratorPassword" \ + /tmp/autounattend.xml + sed -i \ + -e "s|%enable_administrator%|0|gi" \ + -e "s|%user_username%|$username|gi" \ + -e "s|%user_password%|$password_base64|gi" \ /tmp/autounattend.xml fi @@ -7838,13 +8164,14 @@ if is_need_change_ssh_port; then fi # 设置密码,添加开机启动 + 开启 ssh 服务 +add_user_if_need / if is_need_set_ssh_keys; then set_ssh_keys_and_del_password / - # 目前脚本只用 root,不需要设置这个 - # change_ssh_conf_if_different / PasswordAuthentication no + change_ssh_conf_for_key_login / printf '\n' | setup-sshd else - change_root_password / + change_user_password / + change_ssh_conf_for_password_login / printf '\nyes' | setup-sshd fi