bashgen/templates/script.sh.j2

614 lines
17 KiB
Django/Jinja

#!/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 <<EOF
deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu ${codename} stable
EOF
apt-get update -y
DEBIAN_FRONTEND=noninteractive apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# Configure Docker to use custom data directory if specified
{% if flags.docker_admin_user %}
local docker_dir="{{ params.docker_data_dir }}"
if [[ -d "${docker_dir}" ]]; then
log "Configuring Docker to use custom data directory: ${docker_dir}"
mkdir -p /etc/docker
backup_file /etc/docker/daemon.json
# Create or update daemon.json
if [[ -f /etc/docker/daemon.json ]]; then
# Use jq if available, otherwise use sed/python fallback
if command -v jq >/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 <<EOF
{
"data-root": "${docker_dir}"
}
EOF
fi
else
cat >/etc/docker/daemon.json <<EOF
{
"data-root": "${docker_dir}"
}
EOF
fi
chmod 644 /etc/docker/daemon.json
log "Docker daemon.json configured. Restart Docker to apply: systemctl restart docker"
fi
{% endif %}
systemctl enable --now docker
log "Docker installed"
}
{% endif %}
{% if flags.docker_admin_user %}
create_docker_admin_user() {
local user="{{ params.admin_username }}"
log "Creating docker operator user: ${user} (non-login)"
if ! id "${user}" >/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 <<EOF
# Allow members of sudo group to run docker commands as {{ params.admin_username }}
%sudo ALL=( {{ params.admin_username }} ) NOPASSWD: /usr/bin/docker, /usr/bin/docker-compose, /usr/bin/docker-*
EOF
chmod 0440 /etc/sudoers.d/90-docker-operator
log "Docker operator user configured"
log "NOTE: To use custom docker directory, configure Docker daemon.json:"
log " { \"data-root\": \"${docker_dir}\" }"
}
{% endif %}
{% if flags.open_ports %}
setup_firewall() {
log "Configuring UFW firewall"
ensure_pkg ufw
ufw --force reset
ufw default deny incoming
ufw default allow outgoing
{% for p in params.ports %}
ufw allow {{ p }}/tcp
{% endfor %}
ufw --force enable
ufw status verbose || true
log "UFW configured"
}
{% endif %}
{% if flags.combine_lan %}
setup_netplan_bond_bridge() {
log "Writing netplan for bond+bridge (static IP)"
# WARNING: This can lock you out if done over SSH on a remote host.
# Make sure you have console/iLO/IPMI access or test carefully.
local ifaces=( {% for i in params.lan_ifaces %}"{{ i }}"{% if not loop.last %} {% endif %}{% endfor %} )
# Basic validation
for i in "${ifaces[@]}"; do
if ! ip link show "$i" >/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 <<EOF
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 5
destemail = root@localhost
sendername = Fail2Ban
action = %(action_)s
[sshd]
enabled = true
port = {{ params.ssh_port }}
logpath = %(sshd_log)s
backend = %(sshd_backend)s
maxretry = 3
bantime = 7200
EOF
systemctl enable fail2ban
systemctl restart fail2ban
fail2ban-client status
log "Fail2ban configured"
}
{% endif %}
{% if flags.create_admin_user %}
create_admin_user() {
log "Creating admin user: {{ params.new_admin_username }}"
if id "{{ params.new_admin_username }}" >/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 "$@"