#!/usr/bin/env bash set -euo pipefail 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" if "$@"; then log "OK: $desc" SUMMARY+=("$desc: OK") else local rc=$? log "ERROR: $desc (code $rc)" SUMMARY+=("$desc: ERROR") 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 [[ $rc -ne 0 ]]; then log "Скрипт завершился с ошибкой (код $rc)" else log "Скрипт завершился успешно" fi print_summary } trap 'log "Ошибка (код $?) на строке $LINENO"' ERR trap cleanup EXIT usage() { cat <&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 sudo curl wget git ufw logrotate unattended-upgrades ca-certificates gnupg lsb-release apt-transport-https jq } 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 $" 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" } 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 if [[ -n "$NETBIRD_KEY" && -n "$NETBIRD_IP" && -n "$NETBIRD_PORT" ]]; then run "Allow Netbird central from $NETBIRD_IP:$NETBIRD_PORT" ufw allow from "$NETBIRD_IP" to any port "$NETBIRD_PORT" proto tcp comment 'Netbird central' 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 if [[ -n "$NETBIRD_KEY" && -n "$NETBIRD_IP" && -n "$NETBIRD_PORT" ]]; then run "Checking UFW Netbird rule" bash -c "ufw status | grep -q '$NETBIRD_PORT/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 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 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_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 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"; 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 "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 " # run "Checking $service_name stack" bash -c "cd \"$target_dir\" && docker compose ps | grep -q 'Up'" run "Removing init.sh for $service_name" rm -f "$init_script" 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 create_user configure_ssh configure_ufw install_docker configure_fail2ban configure_logrotate install_netbird setup_vector setup_role setup_unattended_upgrades } main