mirror of
https://github.com/deadcxap/init_scripts.git
synced 2026-07-02 05:43:40 +03:00
460 lines
15 KiB
Bash
460 lines
15 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
|
|
LOG_FILE=/var/log/core_setup.log
|
|
exec > >(tee -a "$LOG_FILE") 2>&1
|
|
|
|
log() {
|
|
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*"
|
|
}
|
|
|
|
SUMMARY=()
|
|
|
|
run() {
|
|
local desc="$1"
|
|
shift
|
|
log "$desc"
|
|
local output
|
|
if output=$("$@" 2>&1); then
|
|
log "OK: $desc"
|
|
SUMMARY+=("$desc: OK")
|
|
else
|
|
local rc=$?
|
|
log "ERROR: $desc (code $rc)"
|
|
SUMMARY+=("$desc: ERROR")
|
|
echo "$output" >&2
|
|
exit $rc
|
|
fi
|
|
}
|
|
|
|
wait_for_apt() {
|
|
local lock_files=(
|
|
/var/lib/dpkg/lock-frontend
|
|
/var/lib/dpkg/lock
|
|
/var/lib/apt/lists/lock
|
|
/var/cache/apt/archives/lock
|
|
)
|
|
local timeout=900 # максимум 15 минут ждать
|
|
local waited=0
|
|
|
|
while true; do
|
|
local locked=false
|
|
for f in "${lock_files[@]}"; do
|
|
if fuser "$f" >/dev/null 2>&1; then
|
|
locked=true
|
|
break
|
|
fi
|
|
done
|
|
|
|
if ! $locked; then
|
|
return 0
|
|
fi
|
|
|
|
if (( waited >= timeout )); then
|
|
echo "Timeout waiting for apt/dpkg lock (maybe unattended-upgrades?)" >&2
|
|
return 1
|
|
fi
|
|
|
|
sleep 1
|
|
((waited++))
|
|
done
|
|
}
|
|
|
|
|
|
print_summary() {
|
|
printf '\n\n=========================\n==== Итоговая сводка ====\n=========================\n\n'
|
|
for item in "${SUMMARY[@]}"; do
|
|
echo "$item"
|
|
done
|
|
}
|
|
|
|
cleanup() {
|
|
local rc=$?
|
|
|
|
if [[ -n "${CURL_HOME:-}" ]]; then
|
|
rm -rf "$CURL_HOME"
|
|
fi
|
|
unset CURL_HOME GITHUB_TOKEN GH_AUTH_HEADER GIT_TERMINAL_PROMPT
|
|
|
|
if [[ $rc -ne 0 ]]; then
|
|
log "Скрипт завершился с ошибкой (код $rc)"
|
|
else
|
|
log "Скрипт завершился успешно"
|
|
fi
|
|
print_summary
|
|
}
|
|
|
|
trap 'log "Ошибка (код $?) на строке $LINENO"' ERR
|
|
trap cleanup EXIT
|
|
|
|
usage() {
|
|
cat <<USAGE
|
|
Usage: $0 --user NAME --sshkey KEY [options]
|
|
-u, --user New user to create with passwordless sudo
|
|
-k, --sshkey Public SSH key string (e.g., "ssh-rsa AAAA...")
|
|
-s, --ssh-ip IP (optional) IP allowed to access SSH
|
|
-m, --monitor-ip IP (optional) IP allowed to access Beszel port 45876/tcp
|
|
-v, --vector URL (optional) URL of Vector sink
|
|
-r, --role NAME (optional) Role name to bootstrap
|
|
-n, --netbird-key KEY (optional) Netbird setup key
|
|
-o, --hostname NAME (optional) Hostname to set on server
|
|
USAGE
|
|
}
|
|
|
|
USERNAME=""
|
|
SSH_KEY=""
|
|
SSH_ALLOWED_IP=""
|
|
MONITOR_IP=""
|
|
VECTOR_ENDPOINT=""
|
|
ROLE=""
|
|
NETBIRD_KEY=""
|
|
HOSTNAME_VALUE=""
|
|
parse_args() {
|
|
local opts
|
|
if ! opts=$(getopt \
|
|
-o u:k:s:m:v:r:n:i:p:o:h \
|
|
--long user:,sshkey:,ssh-ip:,monitor-ip:,vector:,role:,netbird-key:,hostname:,help \
|
|
-n "$0" -- "$@"); then
|
|
usage
|
|
exit 1
|
|
fi
|
|
eval set -- "$opts"
|
|
while true; do
|
|
case "$1" in
|
|
-u|--user) USERNAME="$2"; shift 2 ;;
|
|
-k|--sshkey) SSH_KEY="$2"; shift 2 ;;
|
|
-s|--ssh-ip) SSH_ALLOWED_IP="$2"; shift 2 ;;
|
|
-m|--monitor-ip) MONITOR_IP="$2"; shift 2 ;;
|
|
-v|--vector) VECTOR_ENDPOINT="$2"; shift 2 ;;
|
|
-r|--role) ROLE="$2"; shift 2 ;;
|
|
-n|--netbird-key) NETBIRD_KEY="$2"; shift 2 ;;
|
|
-o|--hostname) HOSTNAME_VALUE="$2"; shift 2 ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
--) shift; break ;;
|
|
*) log "Unknown parameter: $1"; usage; exit 1 ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
parse_args "$@"
|
|
|
|
if [[ -z "$USERNAME" || -z "$SSH_KEY" ]]; then
|
|
usage
|
|
exit 1
|
|
fi
|
|
|
|
if [[ $(id -u) -ne 0 ]]; then
|
|
echo "This script must be run as root" >&2
|
|
exit 1
|
|
fi
|
|
|
|
install_packages() {
|
|
run "Waiting for apt lock" wait_for_apt
|
|
run "Updating package index" apt-get update -y
|
|
run "Installing base packages" apt-get install -y --no-install-recommends sudo curl wget git ufw logrotate unattended-upgrades ca-certificates gnupg lsb-release apt-transport-https jq hstr bat eza duf
|
|
run "Installing all updates" apt-get upgrade -y
|
|
}
|
|
|
|
setup_hostname() {
|
|
[[ -z "$HOSTNAME_VALUE" ]] && return
|
|
|
|
run "Setting hostname to $HOSTNAME_VALUE" hostnamectl set-hostname "$HOSTNAME_VALUE"
|
|
run "Updating /etc/hosts with hostname $HOSTNAME_VALUE" bash -c "
|
|
if grep -qE '^127\.0\.1\.1[[:space:]]+' /etc/hosts; then
|
|
sed -i -E 's/^127\\.0\\.1\\.1[[:space:]].*/127.0.1.1 ${HOSTNAME_VALUE}/' /etc/hosts
|
|
else
|
|
printf '127.0.1.1 %s\\n' '$HOSTNAME_VALUE' >> /etc/hosts
|
|
fi
|
|
"
|
|
}
|
|
|
|
setup_timezone() {
|
|
run "Setting timezone to Europe/Moscow" timedatectl set-timezone Europe/Moscow
|
|
}
|
|
|
|
setup_unattended_upgrades() {
|
|
run "Configuring unattended upgrades" bash -c "cat >/etc/apt/apt.conf.d/20auto-upgrades <<'EOF'
|
|
APT::Periodic::Update-Package-Lists \"1\";
|
|
APT::Periodic::Download-Upgradeable-Packages \"1\";
|
|
APT::Periodic::AutocleanInterval \"7\";
|
|
APT::Periodic::Unattended-Upgrade \"1\";
|
|
EOF"
|
|
run "Enabling unattended upgrades" systemctl enable --now unattended-upgrades.service
|
|
}
|
|
|
|
create_user() {
|
|
run "Creating user $USERNAME" bash -c "id '$USERNAME' >/dev/null 2>&1 || adduser --disabled-password --gecos '' '$USERNAME'"
|
|
run "Granting sudo privileges to $USERNAME" bash -c "usermod -aG sudo '$USERNAME' && printf '%s ALL=(ALL) NOPASSWD:ALL\\n' '$USERNAME' >/etc/sudoers.d/90-$USERNAME && chmod 0440 /etc/sudoers.d/90-$USERNAME"
|
|
}
|
|
|
|
configure_ssh() {
|
|
run "Configuring SSH access" bash -c "
|
|
install -d -m 700 -o \"$USERNAME\" -g \"$USERNAME\" \"/home/$USERNAME/.ssh\"
|
|
printf \"%s\n\" \"$SSH_KEY\" > \"/home/$USERNAME/.ssh/authorized_keys\"
|
|
chmod 600 \"/home/$USERNAME/.ssh/authorized_keys\"
|
|
chown -R \"$USERNAME\":\"$USERNAME\" \"/home/$USERNAME/.ssh\"
|
|
if ! grep -qE '^[[:space:]]*Include[[:space:]]+/etc/ssh/sshd_config.d/\*.conf' /etc/ssh/sshd_config; then
|
|
sed -i '1iInclude /etc/ssh/sshd_config.d/*.conf' /etc/ssh/sshd_config
|
|
fi
|
|
install -d -m 755 /etc/ssh/sshd_config.d
|
|
dir=/etc/ssh/sshd_config.d
|
|
shopt -s nullglob
|
|
for f in \"\$dir\"/*.conf; do
|
|
base=\$(basename \"\$f\")
|
|
if [[ \$base == 00-* ]]; then
|
|
mv \"\$f\" \"\$dir/01-\$base\"
|
|
elif [[ \$base != [0-9][0-9]-* ]]; then
|
|
mv \"\$f\" \"\${f%.conf}.disabled\"
|
|
fi
|
|
done
|
|
shopt -u nullglob
|
|
newfile=\"\$dir/00-hardening.conf\"
|
|
printf \"%s\n\" 'PasswordAuthentication no' 'PermitRootLogin no' 'KbdInteractiveAuthentication no' > \"\$newfile\"
|
|
chown root:root \"\$newfile\"
|
|
chmod 0644 \"\$newfile\"
|
|
sshd -t
|
|
systemctl reload sshd 2>/dev/null || systemctl reload ssh 2>/dev/null || systemctl restart sshd 2>/dev/null || systemctl restart ssh
|
|
"
|
|
|
|
run "Checking SSH configuration" bash -c "sshd -T | grep -q '^passwordauthentication no$' && sshd -T | grep -q '^permitrootlogin no$' && sshd -T | grep -q '^kbdinteractiveauthentication no$'"
|
|
}
|
|
|
|
configure_ufw() {
|
|
run "Resetting UFW" ufw --force reset
|
|
run "Setting UFW defaults" bash -c "ufw default deny incoming && ufw default allow outgoing"
|
|
run "Allow HTTPS" ufw allow 443/tcp comment 'HTTPS'
|
|
if [[ -n "$SSH_ALLOWED_IP" ]]; then
|
|
run "Allow SSH from $SSH_ALLOWED_IP" ufw allow from "$SSH_ALLOWED_IP" to any port 22 proto tcp comment 'SSH'
|
|
else
|
|
run "Allow SSH from anywhere" ufw allow 22/tcp comment 'SSH'
|
|
fi
|
|
if [[ -n "$MONITOR_IP" ]]; then
|
|
run "Allow Beszel from $MONITOR_IP" ufw allow from "$MONITOR_IP" to any port 45876 proto tcp comment 'Beszel monitoring'
|
|
fi
|
|
run "Enable UFW" ufw --force enable
|
|
run "Checking UFW active" bash -c "ufw status | grep -q 'Status: active'"
|
|
run "Checking UFW SSH rule" bash -c "ufw status | grep -q '22/tcp'"
|
|
run "Checking UFW HTTPS rule" bash -c "ufw status | grep -q '443/tcp'"
|
|
if [[ -n "$MONITOR_IP" ]]; then
|
|
run "Checking UFW Beszel rule" bash -c "ufw status | grep -q '45876/tcp'"
|
|
fi
|
|
}
|
|
|
|
install_docker() {
|
|
if ! command -v docker >/dev/null 2>&1; then
|
|
run "Waiting for apt lock" wait_for_apt
|
|
run "Installing Docker" bash -c "install -m 0755 -d /etc/apt/keyrings && curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg && echo 'deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable' | tee /etc/apt/sources.list.d/docker.list >/dev/null && apt-get update -y && apt-get install -y --no-install-recommends docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin"
|
|
fi
|
|
run "Adding $USERNAME to docker group" usermod -aG docker "$USERNAME"
|
|
run "Checking Docker service" systemctl is-active --quiet docker
|
|
run "Checking Docker CLI" docker --version
|
|
run "Checking docker compose" docker compose version
|
|
run "Checking docker ps" docker ps >/dev/null
|
|
}
|
|
|
|
configure_fail2ban() {
|
|
run "Waiting for apt lock" wait_for_apt
|
|
run "Installing fail2ban" apt-get install -y --no-install-recommends fail2ban
|
|
run "Configuring fail2ban" bash -c "cat >/etc/fail2ban/jail.local <<'EOF'
|
|
[sshd]
|
|
enabled = true
|
|
bantime = 10m
|
|
findtime = 10m
|
|
maxretry = 3
|
|
EOF"
|
|
run "Enabling fail2ban" systemctl enable --now fail2ban
|
|
}
|
|
|
|
configure_logrotate() {
|
|
run "Configuring logrotate for Docker logs" bash -c "cat >/etc/logrotate.d/docker <<'EOF'
|
|
/var/lib/docker/containers/*/*.log {
|
|
rotate 7
|
|
daily
|
|
compress
|
|
missingok
|
|
delaycompress
|
|
copytruncate
|
|
}
|
|
EOF"
|
|
}
|
|
|
|
configure_hstr() {
|
|
run "Enabling HSTR bash configuration for $USERNAME" sudo -u "$USERNAME" -H bash -lc "hstr --show-bash-configuration >> ~/.bashrc && source ~/.bashrc"
|
|
}
|
|
|
|
install_custom_motd() {
|
|
run "Installing custom MOTD" bash -c "curl -sSL https://raw.githubusercontent.com/Skrepysh/remnawave-motd/refs/heads/main/install-motd.sh | sudo bash"
|
|
run "Ensuring SSH MOTD is enabled" bash -c "
|
|
cfg='/etc/ssh/sshd_config.d/99-motd.conf'
|
|
install -d -m 755 /etc/ssh/sshd_config.d
|
|
printf '%s\n' 'PrintMotd yes' 'UsePAM yes' > "\$cfg"
|
|
chmod 0644 "\$cfg"
|
|
chown root:root "\$cfg"
|
|
sshd -t
|
|
systemctl reload sshd 2>/dev/null || systemctl reload ssh 2>/dev/null || systemctl restart sshd 2>/dev/null || systemctl restart ssh
|
|
"
|
|
}
|
|
|
|
create_aliases_file() {
|
|
run "Creating /etc/profile.d/my-aliases.sh" bash -c "cat >/etc/profile.d/my-aliases.sh <<'EOF'
|
|
export PATH=\"/home/deadcxap/.local/bin:\$PATH\"
|
|
|
|
alias dc='docker compose'
|
|
alias dcu='docker compose up -d'
|
|
alias dcd='docker compose down'
|
|
alias dcl='docker compose logs -f -t'
|
|
alias dcp='docker compose pull'
|
|
dcr() {
|
|
docker compose down && docker compose up -d && docker compose logs -f -t
|
|
}
|
|
dcur() {
|
|
docker compose pull && docker compose down && docker compose up -d && docker compose logs -f -t
|
|
}
|
|
alias ..='cd ..'
|
|
alias lll='eza -lahgF --icons --group-directories-first'
|
|
alias bat='batcat'
|
|
EOF
|
|
# /etc/profile подхватывает readable *.sh из /etc/profile.d для всех пользователей
|
|
chmod 0644 /etc/profile.d/my-aliases.sh"
|
|
}
|
|
|
|
configure_sysctl() {
|
|
run "Configuring sysctl parameters" bash -c "tee /etc/sysctl.d/90-vrbee.conf >/dev/null <<'EOF'
|
|
net.core.default_qdisc = fq
|
|
net.ipv4.tcp_congestion_control = bbr
|
|
net.core.rmem_max = 16777216
|
|
net.core.wmem_max = 16777216
|
|
net.ipv4.tcp_rmem = 4096 87380 16777216
|
|
net.ipv4.tcp_wmem = 4096 65536 16777216
|
|
net.core.netdev_max_backlog = 16384
|
|
net.ipv4.tcp_max_syn_backlog = 16384
|
|
net.ipv4.ip_local_port_range = 10000 65535
|
|
EOF"
|
|
run "Applying sysctl parameters" bash -c "sysctl --system >/dev/null"
|
|
}
|
|
|
|
install_netbird() {
|
|
[[ -z "$NETBIRD_KEY" ]] && return
|
|
run "Waiting for apt lock" wait_for_apt
|
|
run "Installing Netbird" bash -c "curl -fsSL https://pkgs.netbird.io/install.sh | sh"
|
|
run "Starting Netbird" netbird up --setup-key "$NETBIRD_KEY"
|
|
run "Checking Netbird service" systemctl is-active --quiet netbird
|
|
run "Checking Netbird connection" bash -c "netbird status | grep -qi 'connected'"
|
|
}
|
|
|
|
setup_vector() {
|
|
[[ -z "$VECTOR_ENDPOINT" ]] && return
|
|
if ! command -v vector >/dev/null 2>&1; then
|
|
run "Waiting for apt lock" wait_for_apt
|
|
run "Installing Vector" bash -c "curl -1sLf 'https://repositories.timber.io/public/vector/cfg/setup/bash.deb.sh' | bash && apt-get install -y --no-install-recommends vector"
|
|
fi
|
|
run "Configuring Vector" bash -c "cat >/etc/vector/vector.toml <<'EOF'
|
|
[sources.syslog]
|
|
type = 'file'
|
|
include = ['/var/log/*.log']
|
|
|
|
[sources.docker]
|
|
type = 'docker_logs'
|
|
|
|
[sinks.out]
|
|
type = 'http'
|
|
inputs = ['syslog', 'docker']
|
|
uri = '$VECTOR_ENDPOINT'
|
|
encoding.codec = 'json'
|
|
EOF"
|
|
run "Enabling Vector" systemctl enable --now vector
|
|
run "Checking Vector service" systemctl is-active --quiet vector
|
|
}
|
|
|
|
setup_role() {
|
|
[[ -z "$ROLE" ]] && return
|
|
local TEMP_DIR ROLE_SRC REPO_URL ROLE_URL service_dir service_name target_dir init_script
|
|
REPO_URL="https://github.com/deadcxap/init_scripts.git"
|
|
ROLE_URL="https://api.github.com/repos/deadcxap/init_scripts/contents/$ROLE"
|
|
log "Checking role $ROLE exists in repository"
|
|
if curl -fsSL -o /dev/null "$ROLE_URL" 2>/dev/null; then
|
|
log "OK: role $ROLE exists in repository"
|
|
SUMMARY+=("Role exists check: OK")
|
|
else
|
|
log "WARN: role $ROLE not found in repository, skipping"
|
|
SUMMARY+=("Role check: WARN")
|
|
return
|
|
fi
|
|
TEMP_DIR=$(mktemp -d)
|
|
run "Cloning role repository (sparse)" bash -c "
|
|
git --config-env=http.https://github.com/.extraheader=GH_AUTH_HEADER \
|
|
clone --depth=1 --filter=blob:none --sparse \"$REPO_URL\" \"$TEMP_DIR\"
|
|
git --config-env=http.https://github.com/.extraheader=GH_AUTH_HEADER \
|
|
-C \"$TEMP_DIR\" sparse-checkout set \"$ROLE\"
|
|
"
|
|
|
|
ROLE_SRC="$TEMP_DIR/$ROLE"
|
|
if [[ ! -d "$ROLE_SRC" ]]; then
|
|
log "WARN: role directory $ROLE_SRC not found after clone, skipping"
|
|
SUMMARY+=("Role copy: WARN")
|
|
run "Cleaning up role repository" rm -rf "$TEMP_DIR"
|
|
return
|
|
fi
|
|
run "Copying role files" cp -r "$ROLE_SRC"/. /opt/
|
|
for service_dir in "$ROLE_SRC"/*; do
|
|
[ -d "$service_dir" ] || continue
|
|
service_name="$(basename "$service_dir")"
|
|
target_dir="/opt/$service_name"
|
|
run "Setting ownership for $target_dir" chown -R "$USERNAME:$USERNAME" "$target_dir"
|
|
init_script="$target_dir/init.sh"
|
|
if [[ -f "$init_script" ]]; then
|
|
# run "Running init.sh for $service_name" sudo -u "$USERNAME" -H bash "$init_script"
|
|
|
|
run "делаю $init_script исполняемым" sudo chmod +x $init_script
|
|
echo "запускаю $init_script отдельной командой"
|
|
echo "============================="
|
|
cd $target_dir
|
|
sudo -u "$USERNAME" -H bash "$init_script"
|
|
echo "============================="
|
|
run "Waiting for $service_name stack to be Up" bash -c "
|
|
set -e
|
|
dir=\"$target_dir\"
|
|
timeout=\"${COMPOSE_WAIT_TIMEOUT:-180}\"
|
|
interval=\"${COMPOSE_WAIT_INTERVAL:-5}\"
|
|
end=\$((SECONDS + timeout))
|
|
while (( SECONDS < end )); do
|
|
if cd \"\$dir\" && docker compose ps | grep -q 'Up'; then
|
|
exit 0
|
|
fi
|
|
sleep \"\$interval\"
|
|
done
|
|
echo \"WARN: Timed out: containers not Up after \${timeout}s\" >&2
|
|
exit 0
|
|
"
|
|
else
|
|
log "WARN: init.sh not found in $target_dir, skipping"
|
|
SUMMARY+=("init.sh for $service_name: MISSING")
|
|
fi
|
|
done
|
|
run "Cleaning up role repository" rm -rf "$TEMP_DIR"
|
|
}
|
|
|
|
main() {
|
|
install_packages
|
|
configure_sysctl
|
|
setup_timezone
|
|
setup_hostname
|
|
create_user
|
|
configure_ssh
|
|
configure_ufw
|
|
install_docker
|
|
configure_fail2ban
|
|
configure_logrotate
|
|
configure_hstr
|
|
create_aliases_file
|
|
install_custom_motd
|
|
install_netbird
|
|
setup_vector
|
|
setup_role
|
|
setup_unattended_upgrades
|
|
}
|
|
|
|
main
|