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.
+
+
+
+
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 "$@"