#!/usr/bin/env bash set -euo pipefail # Generated at: {{ meta.generated_at }} if [[ "${EUID}" -ne 0 ]]; then echo "Run as root: sudo bash $0" exit 1 fi log() { echo "[setup] $*"; } ensure_pkg() { local pkg="$1" if ! dpkg -s "$pkg" >/dev/null 2>&1; then apt-get update -y DEBIAN_FRONTEND=noninteractive apt-get install -y "$pkg" fi } backup_file() { local f="$1" if [[ -f "$f" ]]; then cp -a "$f" "${f}.bak.$(date +%Y%m%d%H%M%S)" fi } {% if flags.install_docker %} install_docker() { log "Installing Docker + compose plugin" ensure_pkg ca-certificates ensure_pkg curl ensure_pkg gnupg ensure_pkg lsb-release ensure_pkg acl install -m 0755 -d /etc/apt/keyrings if [[ ! -f /etc/apt/keyrings/docker.gpg ]]; then curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg chmod a+r /etc/apt/keyrings/docker.gpg fi local codename codename="$(. /etc/os-release && echo "$VERSION_CODENAME")" cat >/etc/apt/sources.list.d/docker.list </dev/null 2>&1; then jq ". + {\"data-root\": \"${docker_dir}\"}" /etc/docker/daemon.json > /tmp/daemon.json.tmp && mv /tmp/daemon.json.tmp /etc/docker/daemon.json else # Fallback: create new daemon.json with data-root cat >/etc/docker/daemon.json </etc/docker/daemon.json </dev/null 2>&1; then # system user, no home by default; create home only if you want it useradd --system --create-home --home-dir "/home/${user}" --shell /usr/sbin/nologin "${user}" fi # docker group exists after docker install; create if missing if ! getent group docker >/dev/null 2>&1; then groupadd docker fi usermod -aG docker "${user}" # Create docker data directory with restricted permissions local docker_dir="{{ params.docker_data_dir }}" log "Creating docker data directory: ${docker_dir}" # Create directory if it doesn't exist if [[ ! -d "${docker_dir}" ]]; then mkdir -p "${docker_dir}" fi # Set ownership to docker user and docker group chown -R "${user}:docker" "${docker_dir}" # Set permissions: owner (rwx), group (rwx), others (no access) chmod 770 "${docker_dir}" # Set ACL to ensure only docker user and docker group have access ensure_pkg acl setfacl -R -m u:"${user}":rwx "${docker_dir}" setfacl -R -m g:docker:rwx "${docker_dir}" setfacl -R -m o::--- "${docker_dir}" setfacl -R -d -m u:"${user}":rwx "${docker_dir}" setfacl -R -d -m g:docker:rwx "${docker_dir}" setfacl -R -d -m o::--- "${docker_dir}" log "Docker directory ${docker_dir} created with restricted permissions for ${user}" # Optional: allow admins to run docker as that user using sudo # This does NOT allow interactive login to the user. cat >/etc/sudoers.d/90-docker-operator </dev/null 2>&1; then echo "Interface not found: $i" exit 1 fi done backup_file /etc/netplan/01-lan.yaml cat >/etc/netplan/01-lan.yaml <<'EOF' network: version: 2 renderer: networkd ethernets: {% for i in params.lan_ifaces %} {{ i }}: dhcp4: false {% endfor %} bonds: bond0: interfaces: {% for i in params.lan_ifaces %} - {{ i }} {% endfor %} parameters: mode: 802.3ad mii-monitor-interval: 100 bridges: br0: interfaces: - bond0 addresses: - {{ params.static_ip_cidr }} routes: - to: default via: {{ params.gateway_ip }} nameservers: addresses: {% for d in params.dns %} - {{ d }} {% endfor %} EOF netplan generate netplan apply log "Netplan applied" } {% endif %} {% if flags.prelogin_banner %} setup_prelogin_banner() { log "Configuring SSH pre-login banner" ensure_pkg openssh-server # Use /etc/issue.net for SSH Banner cat >/etc/issue.net <<'EOF' {{ params.prelogin_text }} EOF # Replace owner placeholders with actual values sed -i "s|\[OWNER_NAME\]|{{ params.owner_name }}|g" /etc/issue.net sed -i "s|\[OWNER_WEBSITE\]|{{ params.owner_website }}|g" /etc/issue.net sed -i "s|\[OWNER_EMAIL\]|{{ params.owner_email }}|g" /etc/issue.net backup_file /etc/ssh/sshd_config # Ensure Banner points to /etc/issue.net if grep -qE '^\s*Banner\s+' /etc/ssh/sshd_config; then sed -i 's|^\s*Banner\s\+.*|Banner /etc/issue.net|' /etc/ssh/sshd_config else echo "Banner /etc/issue.net" >> /etc/ssh/sshd_config fi systemctl restart ssh log "Pre-login banner configured" } {% endif %} {% if flags.postlogin_banner %} setup_postlogin_banner() { log "Configuring post-login MOTD" # Simple approach: overwrite /etc/motd cat >/etc/motd <<'EOF' {{ params.postlogin_text }} EOF # Replace owner placeholders with actual values sed -i "s|\[OWNER_NAME\]|{{ params.owner_name }}|g" /etc/motd sed -i "s|\[OWNER_WEBSITE\]|{{ params.owner_website }}|g" /etc/motd sed -i "s|\[OWNER_EMAIL\]|{{ params.owner_email }}|g" /etc/motd log "Post-login banner configured" } {% endif %} {% if flags.ssh_2fa %} install_google_authenticator() { log "Installing Google Authenticator (PAM module)" # Only install the package, do not configure it ensure_pkg libpam-google-authenticator log "Google Authenticator installed. Manual configuration required." log "To configure 2FA, users must run: google-authenticator" log "Then configure PAM and SSH settings manually." } {% endif %} {% if flags.system_update %} system_update() { log "Updating system packages" apt-get update -y DEBIAN_FRONTEND=noninteractive apt-get upgrade -y DEBIAN_FRONTEND=noninteractive apt-get dist-upgrade -y apt-get autoremove -y apt-get autoclean -y log "System updated" } {% endif %} {% if flags.auto_security_updates %} setup_auto_security_updates() { log "Configuring automatic security updates" ensure_pkg unattended-upgrades # Enable automatic security updates cat >/etc/apt/apt.conf.d/50unattended-upgrades <<'EOF' Unattended-Upgrade::Allowed-Origins { "${distro_id}:${distro_codename}-security"; "${distro_id}ESMApps:${distro_codename}-apps-security"; "${distro_id}ESM:${distro_codename}-infra-security"; }; Unattended-Upgrade::AutoFixInterruptedDpkg "true"; Unattended-Upgrade::MinimalSteps "true"; Unattended-Upgrade::Remove-Unused-Kernel-Packages "true"; Unattended-Upgrade::Remove-Unused-Dependencies "true"; Unattended-Upgrade::Automatic-Reboot "false"; EOF # Enable automatic updates echo 'APT::Periodic::Update-Package-Lists "1";' > /etc/apt/apt.conf.d/20auto-upgrades echo 'APT::Periodic::Unattended-Upgrade "1";' >> /etc/apt/apt.conf.d/20auto-upgrades systemctl enable unattended-upgrades systemctl restart unattended-upgrades log "Automatic security updates configured" } {% endif %} {% if flags.setup_timezone %} setup_timezone() { log "Setting timezone to {{ params.timezone }}" ensure_pkg tzdata timedatectl set-timezone {{ params.timezone }} log "Timezone configured" } {% endif %} {% if flags.setup_hostname %} setup_hostname() { {% if params.hostname %} log "Setting hostname to {{ params.hostname }}" hostnamectl set-hostname {{ params.hostname }} echo "127.0.0.1 {{ params.hostname }}" >> /etc/hosts log "Hostname configured" {% else %} log "Skipping hostname setup (not specified)" {% endif %} } {% endif %} {% if flags.setup_ntp %} setup_ntp() { log "Configuring NTP time synchronization" ensure_pkg chrony # Configure chrony with reliable time servers backup_file /etc/chrony/chrony.conf cat >/etc/chrony/chrony.conf <<'EOF' pool 0.pool.ntp.org iburst pool 1.pool.ntp.org iburst pool 2.pool.ntp.org iburst pool 3.pool.ntp.org iburst # Record the rate at which the system clock gains/losses time driftfile /var/lib/chrony/drift # Allow the system clock to be stepped in the first three updates makestep 1.0 3 # Enable kernel synchronization of the real-time clock rtcsync # Enable hardware timestamping on all interfaces #hwtimestamp * # Increase the minimum number of selectable sources #minsources 2 # Allow NTP client access from local network #allow 192.168.0.0/16 # Serve time even if not synchronized to a time source #local stratum 10 # Specify file containing keys for NTP authentication keyfile /etc/chrony/chrony.keys # Save the drift between the system clock and the hardware clock #initstepslew 10 client1.example.com client2.example.com # Get TAI-UTC offset and leap seconds from the system tz database leapsectz right/UTC EOF systemctl enable chronyd systemctl restart chronyd chronyc sources log "NTP configured" } {% endif %} {% if flags.setup_swap %} setup_swap() { log "Configuring swap file ({{ params.swap_size_gb }}GB)" # Swap Configuration Explanation: # Swap is virtual memory stored on disk. When RAM is full, Linux moves # less-used data to swap to free up RAM. This prevents "out of memory" # crashes but is slower than RAM. Swappiness (0-100) controls how # aggressively Linux uses swap. Lower values (10) prefer RAM, higher (60+) # use swap more aggressively. For servers, 10 is recommended. # Check if swap already exists if swapon --show | grep -q "/swapfile"; then log "Swap file already exists, skipping" return fi # Create swap file fallocate -l {{ params.swap_size_gb }}G /swapfile || dd if=/dev/zero of=/swapfile bs=1G count={{ params.swap_size_gb }} chmod 600 /swapfile mkswap /swapfile swapon /swapfile # Make swap permanent if ! grep -q "/swapfile" /etc/fstab; then echo "/swapfile none swap sw 0 0" >> /etc/fstab fi # Configure swappiness (10 = less aggressive, 60 = default) # Lower value = prefer RAM, higher = use swap more if ! grep -q "vm.swappiness" /etc/sysctl.conf; then echo "vm.swappiness=10" >> /etc/sysctl.conf sysctl vm.swappiness=10 fi log "Swap file configured ({{ params.swap_size_gb }}GB, swappiness=10)" } {% endif %} {% if flags.ssh_harden %} harden_ssh() { log "Hardening SSH configuration" ensure_pkg openssh-server backup_file /etc/ssh/sshd_config # Disable root login sed -i 's|^#*PermitRootLogin.*|PermitRootLogin no|' /etc/ssh/sshd_config # Change SSH port if specified {% if params.ssh_port != 22 %} sed -i "s|^#*Port.*|Port {{ params.ssh_port }}|" /etc/ssh/sshd_config {% endif %} # Security settings sed -i 's|^#*PasswordAuthentication.*|PasswordAuthentication yes|' /etc/ssh/sshd_config sed -i 's|^#*PubkeyAuthentication.*|PubkeyAuthentication yes|' /etc/ssh/sshd_config sed -i 's|^#*PermitEmptyPasswords.*|PermitEmptyPasswords no|' /etc/ssh/sshd_config sed -i 's|^#*MaxAuthTries.*|MaxAuthTries 3|' /etc/ssh/sshd_config sed -i 's|^#*ClientAliveInterval.*|ClientAliveInterval 300|' /etc/ssh/sshd_config sed -i 's|^#*ClientAliveCountMax.*|ClientAliveCountMax 2|' /etc/ssh/sshd_config # Disable X11 forwarding for security sed -i 's|^#*X11Forwarding.*|X11Forwarding no|' /etc/ssh/sshd_config # Use only strong ciphers if ! grep -q "^Ciphers" /etc/ssh/sshd_config; then echo "Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr" >> /etc/ssh/sshd_config fi systemctl restart ssh log "SSH hardened" } {% endif %} {% if flags.install_fail2ban %} install_fail2ban() { log "Installing and configuring Fail2ban" ensure_pkg fail2ban # Create local jail configuration cat >/etc/fail2ban/jail.local </dev/null 2>&1; then log "User {{ params.new_admin_username }} already exists" else # Create user with sudo access useradd -m -s /bin/bash {{ params.new_admin_username }} usermod -aG sudo {{ params.new_admin_username }} # Create .ssh directory mkdir -p /home/{{ params.new_admin_username }}/.ssh chmod 700 /home/{{ params.new_admin_username }}/.ssh # Add SSH public keys if provided {% if params.ssh_public_keys %} cat >/home/{{ params.new_admin_username }}/.ssh/authorized_keys <<'EOF' {% for key in params.ssh_public_keys %} {{ key }} {% endfor %} EOF chmod 600 /home/{{ params.new_admin_username }}/.ssh/authorized_keys chown -R {{ params.new_admin_username }}:{{ params.new_admin_username }} /home/{{ params.new_admin_username }}/.ssh log "SSH keys added for {{ params.new_admin_username }}" {% else %} log "No SSH keys provided - user will need to set password or add keys manually" {% endif %} log "Admin user {{ params.new_admin_username }} created with sudo access" log "IMPORTANT: Set a password with: passwd {{ params.new_admin_username }}" fi } {% endif %} {% if flags.install_monitoring_tools %} install_monitoring_tools() { log "Installing monitoring and system utilities" # Essential monitoring tools ensure_pkg htop ensure_pkg iotop ensure_pkg net-tools ensure_pkg curl ensure_pkg wget ensure_pkg vim ensure_pkg nano ensure_pkg tree ensure_pkg unzip ensure_pkg zip ensure_pkg rsync ensure_pkg tmux ensure_pkg screen log "Monitoring tools installed" } {% endif %} {% if flags.install_build_tools %} install_build_tools() { log "Installing build tools and development utilities" ensure_pkg build-essential ensure_pkg git ensure_pkg make ensure_pkg cmake ensure_pkg pkg-config log "Build tools installed" } {% endif %} main() { log "Starting setup" # System Setup (run first) {% if flags.system_update %} system_update {% endif %} {% if flags.setup_timezone %} setup_timezone {% endif %} {% if flags.setup_hostname %} setup_hostname {% endif %} {% if flags.setup_ntp %} setup_ntp {% endif %} {% if flags.setup_swap %} setup_swap {% endif %} {% if flags.auto_security_updates %} setup_auto_security_updates {% endif %} # Security & Hardening {% if flags.ssh_harden %} harden_ssh {% endif %} {% if flags.install_fail2ban %} install_fail2ban {% endif %} {% if flags.prelogin_banner %} setup_prelogin_banner {% endif %} {% if flags.postlogin_banner %} setup_postlogin_banner {% endif %} {% if flags.ssh_2fa %} install_google_authenticator {% endif %} # User Management {% if flags.create_admin_user %} create_admin_user {% endif %} # Docker & Services {% if flags.install_docker %} install_docker {% endif %} {% if flags.docker_admin_user %} create_docker_admin_user {% endif %} {% if flags.open_ports %} setup_firewall {% endif %} {% if flags.combine_lan %} setup_netplan_bond_bridge {% endif %} # Monitoring & Utilities {% if flags.install_monitoring_tools %} install_monitoring_tools {% endif %} {% if flags.install_build_tools %} install_build_tools {% endif %} log "Done" } main "$@"