#!/bin/sh # # Initialize an Alpine Linux server: # - Set root password # - Create a new user # - Configure timezone (UTC+8, Asia/Shanghai) # - Install and configure OpenSSH # - Set up SSH key-based login from a fixed authorized_keys URL # - Configure sshd keepalive # - Install and initialize zsh for the new user set -u SSH_AUTH_KEYS_URL="https://webdav-syncthing.ivanli.cc/Ivan-Personal/Credentials/Public/authorized_keys-uys8y1bkrxi55v0gOJWtrKJ2uM9TLsUq" info() { printf '[INFO] %s\n' "$*" } warn() { printf '[WARN] %s\n' "$*" >&2 } error() { printf '[ERROR] %s\n' "$*" >&2 } require_root() { if [ "$(id -u)" -ne 0 ]; then error "This script must be run as root." exit 1 fi } check_alpine() { if ! grep -q '^ID=alpine' /etc/os-release 2>/dev/null; then warn "This script is designed for Alpine Linux, but this system does not report ID=alpine." warn "Continuing anyway, but commands may fail." fi } install_base_packages() { info "Installing base packages (tzdata, openssh, zsh, git, curl, zoxide)..." if ! command -v apk >/dev/null 2>&1; then error "apk command not found. Are you sure this is Alpine Linux?" exit 1 fi apk update || { error "apk update failed; check network or APK repositories." exit 1 } apk add --no-cache tzdata openssh zsh git curl zoxide || { error "Failed to install base packages." exit 1 } } configure_timezone() { local zone="Asia/Shanghai" if [ ! -f "/usr/share/zoneinfo/$zone" ]; then warn "Timezone data for $zone not found; skipping timezone configuration." return 1 fi info "Setting timezone to $zone (UTC+8)..." ln -sf "/usr/share/zoneinfo/$zone" /etc/localtime || { warn "Failed to update /etc/localtime." return 1 } echo "$zone" > /etc/timezone || { warn "Failed to write /etc/timezone." return 1 } } set_root_password() { info "Now setting root password (you will be prompted by passwd)." passwd root || { error "Failed to set root password." exit 1 } } prompt_for_username() { local username while :; do printf 'Enter username to create: ' read -r username case "$username" in ""|root) warn "Username cannot be empty or 'root'." ;; *[!a-z0-9_-]*) warn "Username should only contain lowercase letters, digits, '-', '_' ." ;; *) echo "$username" return 0 ;; esac done } create_user_if_needed() { local username="$1" if id "$username" >/dev/null 2>&1; then info "User '$username' already exists; will reuse it." return 0 fi info "Creating user '$username' with default shell /bin/zsh..." adduser -D -s /bin/zsh "$username" || { error "Failed to create user '$username'." exit 1 } info "Now setting password for user '$username' (you will be prompted by passwd)." passwd "$username" || { error "Failed to set password for user '$username'." exit 1 } } setup_zsh_for_user() { local username="$1" local home="/home/$username" local zshrc="$home/.zshrc" if [ ! -d "$home" ]; then warn "Home directory $home does not exist; skipping zsh configuration." return 1 fi if [ -f "$zshrc" ]; then warn "$zshrc already exists; leaving it unchanged." else info "Creating $zshrc with zsh plugins and prompt configuration..." cat > "$zshrc" <<'EOF' # Enable Powerlevel10k instant prompt. Should stay close to the top of ~/.zshrc. if [[ -r "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" ]]; then source "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" fi # Zinit plugin manager if [[ ! -f $HOME/.local/share/zinit/zinit.git/zinit.zsh ]]; then print -P "%F{33} %F{220}Installing %F{33}Zinit%F{220} plugin manager…%f" command mkdir -p "$HOME/.local/share/zinit" && command chmod g-rwX "$HOME/.local/share/zinit" command git clone https://github.com/zdharma-continuum/zinit "$HOME/.local/share/zinit/zinit.git" && \ print -P "%F{33} %F{34}Installation successful.%f%b" || \ print -P "%F{160} The clone has failed.%f%b" fi source "$HOME/.local/share/zinit/zinit.git/zinit.zsh" autoload -Uz _zinit (( ${+_comps} )) && _comps[zinit]=_zinit # Basic plugins zinit load zsh-users/zsh-syntax-highlighting zinit load zsh-users/zsh-autosuggestions zinit load ael-code/zsh-colored-man-pages # Directory jumping eval "$(zoxide init zsh)" # Prompt theme zinit ice depth=1 zinit light romkatv/powerlevel10k # History HISTFILE=~/.zsh_history HISTSIZE=100000 HISTFILESIZE=300000 SAVEHIST=10000 setopt INC_APPEND_HISTORY_TIME setopt EXTENDED_HISTORY # To customize prompt, run `p10k configure` or edit ~/.p10k.zsh. [[ -f ~/.p10k.zsh ]] && source ~/.p10k.zsh EOF chown "$username:$username" "$zshrc" || warn "Failed to change owner of $zshrc." fi if command -v chsh >/dev/null 2>&1; then chsh -s /bin/zsh "$username" >/dev/null 2>&1 || true fi } set_sshd_option() { local key="$1" local value="$2" local file="/etc/ssh/sshd_config" if [ ! -f "$file" ]; then warn "$file not found; sshd may not be installed correctly." return 1 fi if grep -Eq "^[#[:space:]]*$key\\b" "$file"; then sed -i "s|^[#[:space:]]*$key\\b.*|$key $value|" "$file" else printf '\n%s %s\n' "$key" "$value" >> "$file" fi } setup_authorized_keys() { local username="$1" local url="$2" local home="/home/$username" local ssh_dir="$home/.ssh" local auth_file="$ssh_dir/authorized_keys" local tmp_file="$auth_file.tmp" local i if [ -z "$url" ]; then warn "No SSH key URL provided; skipping SSH key setup." return 1 fi if [ ! -d "$home" ]; then warn "Home directory $home does not exist; cannot configure authorized_keys." return 1 fi mkdir -p "$ssh_dir" chmod 700 "$ssh_dir" info "Fetching SSH public keys for user '$username' from:" info " $url" i=1 while [ "$i" -le 10 ]; do info "Download attempt $i/10..." if curl -fsS "$url" -o "$tmp_file"; then if [ -s "$tmp_file" ]; then mv "$tmp_file" "$auth_file" chown "$username:$username" "$auth_file" chmod 600 "$auth_file" info "SSH public keys installed at $auth_file." return 0 else warn "Downloaded file is empty; retrying..." fi else warn "Failed to download SSH keys (attempt $i)." fi i=$((i + 1)) sleep 3 done rm -f "$tmp_file" warn "Unable to fetch SSH keys from $url after 10 attempts." warn "SSH password authentication will remain enabled to avoid locking you out." return 1 } configure_sshd() { local disable_password="$1" info "Configuring sshd..." if [ ! -f /etc/ssh/sshd_config ]; then warn "/etc/ssh/sshd_config not found, starting sshd once to generate default config..." rc-service sshd start >/dev/null 2>&1 || service sshd start >/dev/null 2>&1 || true rc-service sshd stop >/dev/null 2>&1 || service sshd stop >/dev/null 2>&1 || true fi set_sshd_option "PubkeyAuthentication" "yes" if [ "$disable_password" -eq 1 ]; then set_sshd_option "PasswordAuthentication" "no" else set_sshd_option "PasswordAuthentication" "yes" fi # Keepalive settings to avoid frequent disconnects set_sshd_option "ClientAliveInterval" "60" set_sshd_option "ClientAliveCountMax" "3" set_sshd_option "TCPKeepAlive" "yes" # Do not change PermitRootLogin here to avoid surprises; keep distro defaults. } enable_sshd_service() { info "Enabling and starting sshd service..." rc-update add sshd default >/dev/null 2>&1 || rc-update add sshd default || true if ! rc-service sshd restart >/dev/null 2>&1 && \ ! service sshd restart >/dev/null 2>&1; then warn "Failed to restart sshd; please check manually with 'rc-service sshd status'." else info "sshd is running." fi } main() { require_root check_alpine install_base_packages configure_timezone set_root_password local username username="$(prompt_for_username)" create_user_if_needed "$username" setup_zsh_for_user "$username" local setup_keys_choice local ssh_disable_password=0 printf 'Do you want to configure SSH key-based login for this user now? [y/N]: ' read -r setup_keys_choice || setup_keys_choice="" case "$setup_keys_choice" in y|Y) if setup_authorized_keys "$username" "$SSH_AUTH_KEYS_URL"; then ssh_disable_password=1 else ssh_disable_password=0 fi ;; *) info "Skipping SSH key-based login configuration." ;; esac configure_sshd "$ssh_disable_password" enable_sshd_service info "Initialization complete." if [ "$ssh_disable_password" -eq 1 ]; then info "SSH password authentication is disabled; key-based login is enabled." else info "SSH password authentication is still enabled." fi info "New user: $username" info "Timezone: $(cat /etc/timezone 2>/dev/null || echo 'unknown')" } main "$@"