diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..71802c3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,39 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +*.egg-info/ +dist/ +build/ +.venv/ +venv/ +env/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Git +.git/ +.gitignore + +# Docker +Dockerfile +docker-compose.yml +.dockerignore + +# Documentation +*.md +README* + +# Logs +*.log diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7e88669 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +# Dockerfile for bash script generator FastAPI app +FROM python:3.11-slim + +# Set working directory +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements first for better layer caching +COPY bashgen/requirements.txt . + +# Install Python dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY bashgen/app.py . +COPY bashgen/templates/ ./templates/ + +# Copy banner markdown files +COPY workingscope/loginbanner.md workingscope/postloginbanner.md workingscope/ + +# Expose port +EXPOSE 8080 + +# Run the application +CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8080"] diff --git a/app.py b/app.py new file mode 100644 index 0000000..dc76a8c --- /dev/null +++ b/app.py @@ -0,0 +1,174 @@ +# FastAPI backend for bash script generator +from fastapi import FastAPI, Request, Form +from fastapi.responses import HTMLResponse, Response +from fastapi.templating import Jinja2Templates +from jinja2 import Environment, FileSystemLoader, StrictUndefined +from datetime import datetime +from pathlib import Path + +# Initialize FastAPI app +app = FastAPI(title="Bash Script Generator") +templates = Jinja2Templates(directory="templates") + +# Jinja2 environment for script template rendering +jinja_env = Environment( + loader=FileSystemLoader("templates"), + undefined=StrictUndefined, + autoescape=False, + trim_blocks=True, + lstrip_blocks=True, +) + +def _bool(v: str | None) -> bool: + # HTML checkbox sends value only when checked + return v is not None + +@app.get("/", response_class=HTMLResponse) +def index(request: Request): + """Serve the main HTML form page""" + return templates.TemplateResponse("index.html", {"request": request}) + +@app.post("/generate") +def generate( + # feature flags - System Setup + system_update: str | None = Form(default=None), + auto_security_updates: str | None = Form(default=None), + setup_timezone: str | None = Form(default=None), + setup_hostname: str | None = Form(default=None), + setup_ntp: str | None = Form(default=None), + setup_swap: str | None = Form(default=None), + + # feature flags - Security + ssh_harden: str | None = Form(default=None), + install_fail2ban: str | None = Form(default=None), + prelogin_banner: str | None = Form(default=None), + postlogin_banner: str | None = Form(default=None), + ssh_2fa: str | None = Form(default=None), + + # feature flags - Docker & Services + install_docker: str | None = Form(default=None), + docker_admin_user: str | None = Form(default=None), + open_ports: str | None = Form(default=None), + combine_lan: str | None = Form(default=None), + + # feature flags - User Management + create_admin_user: str | None = Form(default=None), + + # feature flags - Monitoring + install_monitoring_tools: str | None = Form(default=None), + install_build_tools: str | None = Form(default=None), + + # params + admin_username: str = Form(default="datamng"), + ssh_port: int = Form(default=22), + hostname: str = Form(default=""), + timezone: str = Form(default="UTC"), + new_admin_username: str = Form(default="admin"), + swap_size_gb: int = Form(default=2), + docker_data_dir: str = Form(default="/opt/docker"), + + # firewall ports (comma-separated) + ports_csv: str = Form(default="22,80,81,443"), + + # netplan params + lan_interfaces_csv: str = Form(default="eth0,eth1"), + static_ip_cidr: str = Form(default="192.168.1.9/24"), + gateway_ip: str = Form(default="192.168.1.1"), + dns_csv: str = Form(default="1.1.1.1,8.8.8.8"), + + # owner information + owner_name: str = Form(default="Scardus"), + owner_website: str = Form(default="https://scardustech.com"), + owner_email: str = Form(default="info@scardustech.com"), + + # SSH keys (multiline) + ssh_public_keys: str = Form(default=""), +): + """Generate bash script based on form inputs""" + # Load banner templates from markdown files + # Try multiple paths to support both local development and Docker container + base_path = Path(__file__).parent.parent + prelogin_banner_path = base_path / "workingscope" / "loginbanner.md" + postlogin_banner_path = base_path / "workingscope" / "postloginbanner.md" + + # If not found, try relative to current working directory (for Docker) + if not prelogin_banner_path.exists(): + prelogin_banner_path = Path("workingscope") / "loginbanner.md" + if not postlogin_banner_path.exists(): + postlogin_banner_path = Path("workingscope") / "postloginbanner.md" + + # Read banner content from markdown files + prelogin_text = "" + postlogin_text = "" + + if prelogin_banner_path.exists(): + with open(prelogin_banner_path, "r", encoding="utf-8") as f: + prelogin_text = f.read() + + if postlogin_banner_path.exists(): + with open(postlogin_banner_path, "r", encoding="utf-8") as f: + postlogin_text = f.read() + + # Build context for Jinja2 template + ctx = { + "meta": { + "generated_at": datetime.utcnow().isoformat() + "Z", + }, + "flags": { + # System Setup + "system_update": _bool(system_update), + "auto_security_updates": _bool(auto_security_updates), + "setup_timezone": _bool(setup_timezone), + "setup_hostname": _bool(setup_hostname), + "setup_ntp": _bool(setup_ntp), + "setup_swap": _bool(setup_swap), + # Security + "ssh_harden": _bool(ssh_harden), + "install_fail2ban": _bool(install_fail2ban), + "prelogin_banner": _bool(prelogin_banner), + "postlogin_banner": _bool(postlogin_banner), + "ssh_2fa": _bool(ssh_2fa), + # Docker & Services + "install_docker": _bool(install_docker), + "docker_admin_user": _bool(docker_admin_user), + "open_ports": _bool(open_ports), + "combine_lan": _bool(combine_lan), + # User Management + "create_admin_user": _bool(create_admin_user), + # Monitoring + "install_monitoring_tools": _bool(install_monitoring_tools), + "install_build_tools": _bool(install_build_tools), + }, + "params": { + "admin_username": admin_username.strip(), + "ssh_port": int(ssh_port), + "hostname": hostname.strip(), + "timezone": timezone.strip(), + "new_admin_username": new_admin_username.strip(), + "swap_size_gb": int(swap_size_gb), + "docker_data_dir": docker_data_dir.strip(), + "ports": [p.strip() for p in ports_csv.split(",") if p.strip()], + "lan_ifaces": [i.strip() for i in lan_interfaces_csv.split(",") if i.strip()], + "static_ip_cidr": static_ip_cidr.strip(), + "gateway_ip": gateway_ip.strip(), + "dns": [d.strip() for d in dns_csv.split(",") if d.strip()], + "owner_name": owner_name.strip(), + "owner_website": owner_website.strip(), + "owner_email": owner_email.strip(), + "ssh_public_keys": [k.strip() for k in ssh_public_keys.split("\n") if k.strip()], + "prelogin_text": prelogin_text, + "postlogin_text": postlogin_text, + }, + } + + # Render bash script from template + tpl = jinja_env.get_template("script.sh.j2") + script = tpl.render(**ctx) + + # Return as downloadable file + filename = "setup-server.sh" + return Response( + content=script, + media_type="application/x-sh", + headers={"Content-Disposition": f'attachment; filename="{filename}"'}, + ) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7b0849b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,18 @@ +# Docker Compose configuration for bash script generator +services: + bashgen: + build: + context: .. + dockerfile: bashgen/Dockerfile + container_name: bashgen-app + ports: + - "8083:8080" # Map host port 8083 to container port 8080 + restart: unless-stopped + environment: + - PYTHONUNBUFFERED=1 + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8080/')"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7c1af31 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +fastapi==0.115.0 +uvicorn==0.30.6 +jinja2==3.1.4 +python-multipart==0.0.9 diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..191a5be --- /dev/null +++ b/templates/index.html @@ -0,0 +1,160 @@ + + + + + Bash Script Generator + + + +

Server Setup Script Generator

+

Select options, then download a single .sh script.

+ +
+
+ System Setup + + + + + + +
+ +
+ Security & Hardening + + + + + +
+ +
+ Docker & Services + + + + +
+ +
+ User Management + +
+ +
+ Monitoring & Utilities + + +
+ +
+ General Configuration +
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ LAN bonding/bridging (only if enabled) + +
+ + +
+ +

This will write a netplan file and apply it. Test carefully (risk of remote lockout).

+
+ +
+ System Owner Information +
+ + +
+ +

Banners will use templates from loginbanner.md and postloginbanner.md

+
+ + +
+ + diff --git a/templates/script.sh.j2 b/templates/script.sh.j2 new file mode 100644 index 0000000..088b4fb --- /dev/null +++ b/templates/script.sh.j2 @@ -0,0 +1,613 @@ +#!/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 "$@"