diff --git a/README.md b/README.md index 51b6763..4cf3edf 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # Automated Linux Server Setup -This project provides a fully automated Bash script to set up a production-ready Linux server or Docker stack on **Debian**, **Ubuntu**, **CentOS**, or **RHEL**. The script installs and configures all essential components for web hosting and applications. +This project provides a fully automated Bash script to set up an enterprise-ready Linux server or Docker stack on **Debian**, **Ubuntu**, **CentOS**, or **RHEL**. The script installs, configures, and hardens all essential components for web hosting and applications while maintaining detailed audit logs. ## Features - **Operating Systems**: Debian 12+, Ubuntu 20.04+, CentOS 7+, RHEL 7+ -- **Installed Components**: +- **Installed & Configured Components**: - Nginx (web server) - PHP-FPM (configurable version, e.g., 8.2) @@ -15,16 +15,21 @@ This project provides a fully automated Bash script to set up a production-ready - Fail2Ban (brute-force protection) - Firewall (UFW on Debian/Ubuntu, firewalld on CentOS/RHEL) - SSL certificates via Certbot (Let's Encrypt) + - Automated MariaDB backups with daily cron job + - Optional SSH hardening (disable password auth, restrict root login) + - Automatic OS security updates (configurable) - **Modes**: - `native`: installation directly on the host server - `docker`: Docker Compose stack with DB, Nginx, PHP-FPM, phpMyAdmin -- **Multi-Domain Support**: single or multiple domains can be configured simultaneously -- **Automatic Nginx vhosts** for each domain -- **Non-interactive MariaDB setup** with root password -- **Docker support** for instant containerized deployment +- **Enterprise Enhancements** + - Central execution log at `/var/log/enterprise-server-setup.log` + - Multi-domain support with per-virtual-host isolation (`example.com,www.example.com;api.example.com`) + - Automatic Nginx vhosts and HTTPS provisioning for every domain group + - Non-interactive MariaDB setup with secure root credential storage in `/root/.my.cnf` + - Docker support for instant containerized deployment ## Requirements @@ -44,8 +49,10 @@ chmod +x automated-linux-server-setup.sh ### 2. Native Installation (without Docker) +Multiple domain groups are separated by semicolons (`;`). Domains within a group share the same virtual host and should be comma-separated (`example.com,www.example.com`). + ```bash -sudo DOMAINS="example.com,www.example.com" \ +sudo DOMAINS="example.com,www.example.com;api.example.com" \ EMAIL="admin@example.com" \ MODE=native \ DB_ROOT_PASS="securepassword" \ @@ -63,37 +70,45 @@ sudo DOMAINS="example.com" \ After execution, the Docker Compose stack is located under `/opt/`. +Logs for every run are appended to `/var/log/enterprise-server-setup.log`. A lightweight state file is written to `/var/local/enterprise-server-setup/last-run` to help with auditing. + ## Configuration - **Web root**: `/var/www//html` (native) or `/opt//www` (docker) -- **MariaDB**: Root password is set automatically, default user `root` -- **phpMyAdmin**: Access via `/phpmyadmin` or port 8080 for Docker -- **Nginx vhosts**: automatically generated for each domain group +- **MariaDB**: Root password is stored in `/root/.my.cnf` for safe automation access +- **phpMyAdmin**: Access via `/phpmyadmin` (native) or port 8080 for Docker +- **Nginx vhosts**: automatically generated for each domain group with HTTPS enforcement - **PHP-FPM Socket**: `/var/run/php/php-fpm.sock` (native) +- **Backups**: Daily cron at 02:00 writes dumps to `/var/backups/mariadb` ## Options -- `--domains` - comma-separated domains +- `--domains` - semicolon-delimited domain groups; commas separate aliases within a group - `--mode` - `native` or `docker` (default: native) - `--email` - administrator email for SSL - `--db-root-pass` - MariaDB root password - `--php` - PHP version (default: 8.2) - `--force` - overwrite existing configurations +- Environment toggles: + - `ENABLE_AUTO_UPDATES=false` to skip unattended upgrades + - `ENABLE_SSH_HARDENING=true` to enforce key-based SSH authentication ## Security Measures -- Fail2Ban to protect against brute-force attacks -- Firewall configuration (UFW/firewalld) on standard ports -- SSL certificates via Let's Encrypt -- Non-interactive MariaDB setup removes insecure defaults +- Fail2Ban to protect against brute-force attacks (with hardened jail profiles) +- Firewall configuration (UFW/firewalld) on standard ports plus phpMyAdmin (8080) +- SSL certificates via Let's Encrypt with automatic HTTP→HTTPS redirect +- Non-interactive MariaDB setup removes insecure defaults and stores credentials securely +- Automated daily database dumps with retention policy (14 days) +- Optional SSH hardening and unattended OS updates ## Next Steps -1. Place your website files in the web root: `/var/www//html` or `/opt//www` -2. Check Nginx configuration: `nginx -t` -3. Check PHP-FPM: `systemctl status php-fpm` -4. Access MariaDB: `mysql -u root -p` -5. Access phpMyAdmin: `http:///phpmyadmin` (native) or `http://:8080` (Docker) +1. Review `/var/log/enterprise-server-setup.log` for the full execution transcript. +2. Place your website files in the web root: `/var/www//html` or `/opt//www`. +3. Validate services: `nginx -t`, `systemctl status php-fpm`, and `systemctl status mariadb` (native). +4. Access MariaDB using the stored credentials: `mysql --defaults-file=/root/.my.cnf`. +5. Access phpMyAdmin: `https:///phpmyadmin` (native) or `http://:8080` (Docker). ## Support & Issues diff --git a/install-configure-script.bash b/install-configure-script.bash index f20d10e..200d8b9 100644 --- a/install-configure-script.bash +++ b/install-configure-script.bash @@ -1,105 +1,218 @@ #!/usr/bin/env bash -# Automated Linux Server Setup +# Automated Linux Server Setup - Enterprise Edition # Supports: Debian (12+), Ubuntu (20.04+), CentOS (7+), RHEL (7+) # Installs and configures: Nginx, PHP-FPM, MariaDB (MySQL-compatible), phpMyAdmin, # Node.js, Fail2Ban, firewall (UFW or firewalld), Certbot (Let's Encrypt) -# Supports: single or multiple domains; two modes: "native" (install on host) or "docker" (deploy via Docker Compose) -# Usage: curl -fsSL https://example.com/automated-linux-server-setup.sh | sudo DOMAIN=example.com MODE=native bash -# or: sudo bash automated-linux-server-setup.sh --domains "example.com,www.example.com" --mode docker +# Optional Docker stack deployment. set -euo pipefail IFS=$'\n\t' -# --------------------------- CONFIG (edit / pass env vars) --------------------------- -# Example environment variables (can be passed before running the script): -# DOMAIN (single) or DOMAINS (comma-separated) -# MODE: native | docker (default: native) -# EMAIL: admin@example.com (for Let's Encrypt) -# DB_ROOT_PASS: secure_root_password -# WEB_USER: www-data (default for Debian/Ubuntu), nginx for CentOS/RHEL will be adjusted -# PHP_VERSION: 8.2 (adjustable) - -# Parse args (simple) -DOMAINS="" -MODE="native" -EMAIL="" -DB_ROOT_PASS="" -PHP_VERSION="8.2" -FORCE=false +SCRIPT_NAME=$(basename "$0") +LOG_FILE="/var/log/enterprise-server-setup.log" +STATE_DIR="/var/local/enterprise-server-setup" +mkdir -p "$(dirname "$LOG_FILE")" "$STATE_DIR" +touch "$LOG_FILE" +chmod 600 "$LOG_FILE" + +exec > >(tee -a "$LOG_FILE") 2>&1 + +log(){ + local level=$1; shift + printf '[%s] [%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$level" "$*" +} + +die(){ + log "ERROR" "$*" + exit 1 +} + +trap 'handle_error $? ${LINENO}' ERR +handle_error(){ + local exit_code=$1 + local line=$2 + log "ERROR" "Script failed at line ${line} with exit code ${exit_code}. Review ${LOG_FILE}." + exit "$exit_code" +} + +cleanup(){ + log "INFO" "${SCRIPT_NAME} finished. Review ${LOG_FILE} for a persistent record." +} +trap cleanup EXIT + +# --------------------------- CONFIG (env vars or args) --------------------------- +DOMAINS_RAW=${DOMAINS_RAW:-""} +MODE=${MODE:-native} +EMAIL=${EMAIL:-""} +DB_ROOT_PASS=${DB_ROOT_PASS:-""} +PHP_VERSION=${PHP_VERSION:-"8.2"} +FORCE=${FORCE:-false} +ENABLE_SSH_HARDENING=${ENABLE_SSH_HARDENING:-false} +ENABLE_AUTO_UPDATES=${ENABLE_AUTO_UPDATES:-true} +WEB_USER="" +WEB_GROUP="" +PRIMARY_DOMAIN="" +declare -a DOMAIN_GROUPS=() + +usage(){ + cat < spaces - IFS=',' read -r -a DOMAIN_ARRAY <<< "$DOMAINS" -else - echo "ERROR: No domains provided. Provide via DOMAINS env or --domains. Exiting." >&2 - exit 1 +if [ -n "${DOMAINS-}" ] && [ -z "$DOMAINS_RAW" ]; then + DOMAINS_RAW="$DOMAINS" fi - -if [ -n "${EMAIL-}" ]; then - : # keep +if [ -z "$DOMAINS_RAW" ]; then + die "No domains provided. Supply via --domains or DOMAINS environment variable." fi if [ -z "$DB_ROOT_PASS" ]; then - # generate a random one but show the user - DB_ROOT_PASS=$(openssl rand -base64 18) - echo "[INFO] No DB root password provided; generated: $DB_ROOT_PASS" + command -v openssl >/dev/null 2>&1 || die "openssl is required to generate a database password. Install it first." + DB_ROOT_PASS=$(openssl rand -base64 24) + log "WARN" "No DB root password provided; generated a secure password automatically." fi # --------------------------- UTILS --------------------------- -log(){ echo -e "[INFO] $*"; } -err(){ echo -e "[ERROR] $*" >&2; } - detect_os(){ . /etc/os-release || true OS_ID=${ID,,} OS_LIKE=${ID_LIKE,,} OS_VERSION=${VERSION_ID,,} - log "Detected OS: $OS_ID (like: $OS_LIKE) version: $OS_VERSION" + log "INFO" "Detected OS: $OS_ID (like: $OS_LIKE) version: $OS_VERSION" } require_root(){ if [ "$EUID" -ne 0 ]; then - err "This script must be run as root. Retry with sudo."; exit 1 + die "This script must be run as root. Retry with sudo." fi } +verify_prerequisites(){ + local required_cmds=(curl wget tar systemctl) + for bin in "${required_cmds[@]}"; do + command -v "$bin" >/dev/null 2>&1 || die "Required command '$bin' not found. Install it and retry." + done +} + +parse_domain_groups(){ + local raw="$1" + local sanitized + IFS=';' read -ra sanitized <<< "$raw" + DOMAIN_GROUPS=() + for entry in "${sanitized[@]}"; do + local trimmed=${entry//[[:space:]]/} + [ -n "$trimmed" ] || continue + DOMAIN_GROUPS+=("$trimmed") + done + if [ "${#DOMAIN_GROUPS[@]}" -eq 0 ]; then + die "Unable to parse provided domains." + fi + PRIMARY_DOMAIN=$(printf '%s' "${DOMAIN_GROUPS[0]}" | awk -F',' '{print $1}') + log "INFO" "Primary domain resolved as: ${PRIMARY_DOMAIN}" +} + +determine_web_user(){ + case "$OS_ID" in + ubuntu|debian) + WEB_USER=${WEB_USER:-www-data} + WEB_GROUP=${WEB_GROUP:-www-data} + ;; + centos|rhel) + WEB_USER=${WEB_USER:-nginx} + WEB_GROUP=${WEB_GROUP:-nginx} + ;; + *) + die "Unsupported OS: $OS_ID" + ;; + esac +} + +check_system_resources(){ + local mem_required=1000 + local mem_available + mem_available=$(awk '/MemTotal/ {print int($2/1024)}' /proc/meminfo) + if (( mem_available < mem_required )); then + log "WARN" "Less than ${mem_required}MB RAM detected (${mem_available}MB). Installation may fail." + if ! $FORCE; then + die "Insufficient memory. Rerun with --force if you want to continue regardless." + fi + fi +} + +update_system_packages(){ + log "INFO" "Updating base operating system packages" + case "$OS_ID" in + ubuntu|debian) + apt-get update + apt-get -y upgrade + apt-get -y dist-upgrade + apt-get -y autoremove + ;; + centos|rhel) + yum -y update + ;; + esac +} + +ensure_state_marker(){ + echo "mode=${MODE}" > "${STATE_DIR}/last-run" +} + # --------------------------- INSTALL COMMON TOOLS --------------------------- install_common(){ + log "INFO" "Installing base packages" case "$OS_ID" in ubuntu|debian) + export DEBIAN_FRONTEND=noninteractive apt-get update - apt-get install -y ca-certificates curl wget gnupg lsb-release software-properties-common unzip git openssh-server + apt-get install -y ca-certificates curl wget gnupg lsb-release software-properties-common unzip git openssh-server rsync cron ;; centos|rhel) - yum install -y epel-release yum-utils curl wget unzip git openssh-server + yum install -y epel-release yum-utils curl wget unzip git openssh-server rsync cronie systemctl enable --now sshd || true ;; *) - err "Unsupported OS: $OS_ID"; exit 1 + die "Unsupported OS: $OS_ID" ;; esac } # --------------------------- PACKAGE INSTALLERS --------------------------- install_node(){ - NODE_VERSION=20 + local NODE_VERSION=20 + log "INFO" "Installing Node.js ${NODE_VERSION}.x" case "$OS_ID" in ubuntu|debian) curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - @@ -113,29 +226,35 @@ install_node(){ } install_php(){ + log "INFO" "Installing PHP ${PHP_VERSION}" case "$OS_ID" in ubuntu|debian) - # use sury PPA for latest PHP - apt-get install -y lsb-release ca-certificates apt-transport-https - curl -fsSL https://packages.sury.org/php/apt.gpg | apt-key add - - echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list + export DEBIAN_FRONTEND=noninteractive + apt-get install -y lsb-release ca-certificates apt-transport-https gnupg + if [ ! -f /etc/apt/sources.list.d/php.list ]; then + curl -fsSL https://packages.sury.org/php/apt.gpg | gpg --dearmor -o /etc/apt/trusted.gpg.d/sury-php.gpg + echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list + fi apt-get update - apt-get install -y php${PHP_VERSION} php${PHP_VERSION}-fpm php${PHP_VERSION}-mysql php${PHP_VERSION}-cli php${PHP_VERSION}-curl php${PHP_VERSION}-gd php${PHP_VERSION}-mbstring php${PHP_VERSION}-xml php${PHP_VERSION}-zip + apt-get install -y php${PHP_VERSION} php${PHP_VERSION}-fpm php${PHP_VERSION}-mysql php${PHP_VERSION}-cli \ + php${PHP_VERSION}-curl php${PHP_VERSION}-gd php${PHP_VERSION}-mbstring php${PHP_VERSION}-xml php${PHP_VERSION}-zip php${PHP_VERSION}-bcmath ;; centos|rhel) - # Remi repo for PHP - yum install -y https://rpms.remirepo.net/enterprise/remi-release-${OS_VERSION}.rpm || true + yum install -y https://rpms.remirepo.net/enterprise/remi-release-${OS_VERSION%%.*}.rpm || true yum install -y yum-utils - yum-config-manager --enable remi-php82 || true - yum install -y php php-fpm php-mysqlnd php-cli php-curl php-gd php-mbstring php-xml php-zip + local remi_stream=${PHP_VERSION//./} + yum-config-manager --enable remi-php${remi_stream} || true + yum install -y php php-fpm php-mysqlnd php-cli php-curl php-gd php-mbstring php-xml php-zip php-bcmath ;; esac } install_database(){ + log "INFO" "Installing MariaDB server" case "$OS_ID" in ubuntu|debian) - DEBIAN_FRONTEND=noninteractive apt-get install -y mariadb-server mariadb-client + export DEBIAN_FRONTEND=noninteractive + apt-get install -y mariadb-server mariadb-client systemctl enable --now mariadb ;; centos|rhel) @@ -143,7 +262,6 @@ install_database(){ systemctl enable --now mariadb ;; esac - # Secure installation (non-interactive) mysql --user=root </etc/fail2ban/jail.d/hardening.conf <<'JAIL' +[sshd] +enabled = true +port = ssh +maxretry = 5 +findtime = 600 +bantime = 3600 + +[nginx-http-auth] +enabled = true +JAIL + systemctl enable --now fail2ban } install_certbot(){ + log "INFO" "Installing Certbot" case "$OS_ID" in ubuntu|debian) apt-get install -y certbot python3-certbot-nginx @@ -209,35 +343,186 @@ install_certbot(){ } configure_firewall(){ + log "INFO" "Configuring firewall" case "$OS_ID" in ubuntu|debian) - # UFW apt-get install -y ufw ufw default deny incoming ufw default allow outgoing ufw allow OpenSSH ufw allow 'Nginx Full' + ufw allow 8080/tcp ufw --force enable ;; centos|rhel) - # firewalld yum install -y firewalld || true systemctl enable --now firewalld firewall-cmd --permanent --add-service=http firewall-cmd --permanent --add-service=https firewall-cmd --permanent --add-service=ssh + firewall-cmd --permanent --add-port=8080/tcp firewall-cmd --reload ;; esac } + +# --------------------------- SECURITY & MAINTENANCE --------------------------- +configure_mysql_root_client(){ + cat >/root/.my.cnf </dev/null 2>&1; then + yum install -y dnf-automatic || true + systemctl enable --now dnf-automatic.timer || true + else + yum install -y yum-cron || true + systemctl enable --now yum-cron || true + fi + ;; + esac + log "INFO" "Automatic security updates configured" +} + +apply_ssh_hardening(){ + if ! $ENABLE_SSH_HARDENING; then + log "INFO" "SSH hardening disabled (ENABLE_SSH_HARDENING=false)" + return + fi + local config_dir=/etc/ssh/sshd_config.d + mkdir -p "$config_dir" + cat >${config_dir}/99-enterprise-hardening.conf <<'SSH' +PasswordAuthentication no +PermitRootLogin prohibit-password +ClientAliveInterval 300 +ClientAliveCountMax 2 +SSH + systemctl reload sshd 2>/dev/null || systemctl reload ssh 2>/dev/null || true + log "INFO" "Applied conservative SSH hardening profile" +} + +create_backup_tooling(){ + local backup_dir=/var/backups/mariadb + local backup_script=/usr/local/sbin/backup-mariadb.sh + mkdir -p "$backup_dir" + cat >$backup_script <<'BACKUP' +#!/usr/bin/env bash +set -euo pipefail +STAMP=$(date +%F) +DEST=/var/backups/mariadb +mkdir -p "$DEST" +mysqldump --defaults-file=/root/.my.cnf --single-transaction --quick --lock-tables=false --all-databases > "$DEST/all-${STAMP}.sql" +find "$DEST" -type f -mtime +14 -delete +BACKUP + chmod 700 $backup_script + cat >/etc/cron.d/mariadb-backup <:8080 +EOF + else + cat </html + - Check nginx config: nginx -t + - Check php-fpm: systemctl status php${PHP_VERSION}-fpm + - Manage MariaDB: mysql --defaults-file=/root/.my.cnf + - Access phpMyAdmin at https:///phpmyadmin +EOF + fi +} # --------------------------- NGINX VHOST GENERATOR --------------------------- generate_nginx_vhost(){ - server_names="$1" - site_name=$(echo $server_names | tr ' ' '_' | tr ',' '_') + local server_names="$1" + local site_name + local root_dir + local fastcgi_block + local has_sites_enabled=0 + + site_name=$(echo "$server_names" | tr ' ' '_' | tr ',' '_') root_dir="/var/www/${site_name}/html" mkdir -p "$root_dir" - chown -R www-data:www-data "$root_dir" || chown -R nginx:nginx "$root_dir" || true + chown -R "$WEB_USER":"$WEB_GROUP" "$root_dir" || true + + if [[ "$OS_ID" =~ (ubuntu|debian) ]]; then + fastcgi_block=$' include snippets/fastcgi-php.conf;\n fastcgi_pass unix:/var/run/php/php'"${PHP_VERSION}"$'-fpm.sock;' + else + fastcgi_block=$' include fastcgi_params;\n fastcgi_pass unix:/run/php-fpm/www.sock;\n fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;' + fi + + if [ -d /etc/nginx/sites-enabled ]; then + has_sites_enabled=1 + fi + mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled cat > /etc/nginx/sites-available/${site_name}.conf </dev/null 2>&1; then + log "WARN" "Certbot not installed; skipping SSL issuance for ${domains_str}" + return 0 + fi + local cert_args + cert_args=$(echo "$domains_str" | tr ',' ' ') + if certbot --nginx --keep-until-expiring --redirect --non-interactive --agree-tos --email "${EMAIL}" $(printf ' -d %s' $cert_args); then + log "INFO" "SSL certificate issued for: ${domains_str}" + else + log "WARN" "Certbot failed for ${domains_str}. Review ${LOG_FILE}" fi - certbot --nginx -d $(echo $domains_str | tr ',' ' -d ') --non-interactive --agree-tos --email ${EMAIL} || { - err "Certbot failed for $domains_str"; return 1 - } - log "SSL certificate issued for: $domains_str" } - # --------------------------- DOCKER MODE: write docker-compose.yml --------------------------- write_docker_compose(){ - site_tag=$(echo ${DOMAIN_ARRAY[0]} | tr '.' '_') - mkdir -p /opt/${site_tag} - cat > /opt/${site_tag}/docker-compose.yml <"${stack_dir}/docker-compose.yml" < /opt/${site_tag}/nginx/conf.d/${site_tag}.conf <"${stack_dir}/nginx/conf.d/${site_tag}.conf" <"${stack_dir}/www/index.php" <<'PHP' +/html - - Check nginx config: nginx -t - - Check php-fpm: systemctl status php${PHP_VERSION}-fpm - - Manage MariaDB: mysql -u root -p - -EOF +verify_services +summary +ensure_state_marker