Skip to content

Instantly share code, notes, and snippets.

@benlacey57
Last active December 19, 2025 22:33
Show Gist options
  • Select an option

  • Save benlacey57/d7b3a68228b77191e57296aa1a0f5d0a to your computer and use it in GitHub Desktop.

Select an option

Save benlacey57/d7b3a68228b77191e57296aa1a0f5d0a to your computer and use it in GitHub Desktop.
#!/bin/bash
#==============================================================================
# Complete Ubuntu Server Setup Script
# - System hardening
# - SSH/SFTP for ubuntu user
# - Docker installation
# - Hostname: media.home
# - SSL certificate with mkcert
# - Cockpit web admin
# - Tailscale VPN
#==============================================================================
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
MAGENTA='\033[0;35m'
NC='\033[0m'
# Configuration
HOSTNAME="media"
DOMAIN="home"
FQDN="${HOSTNAME}.${DOMAIN}"
UBUNTU_USER="ubuntu"
SSH_PORT="22"
NEW_SSH_PORT="2222"
LOG_FILE="/var/log/media-server-setup.log"
ERROR_LOG="/var/log/media-server-setup-errors.log"
# Error tracking
ERRORS_OCCURRED=0
WARNINGS_OCCURRED=0
#==============================================================================
# Utility Functions
#==============================================================================
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}
log_error() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $*" | tee -a "$LOG_FILE" "$ERROR_LOG" >&2
((ERRORS_OCCURRED++))
}
log_warning() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] WARNING: $*" | tee -a "$LOG_FILE"
((WARNINGS_OCCURRED++))
}
print_header() {
clear
echo -e "${GREEN}╔════════════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ Media Server Complete Setup ║${NC}"
echo -e "${GREEN}║ Ubuntu 22.04/24.04 LTS ║${NC}"
echo -e "${GREEN}╚════════════════════════════════════════════════════════════════╝${NC}"
echo ""
}
print_section() {
echo ""
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${CYAN} $1${NC}"
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
}
print_step() {
echo -e "${BLUE}→${NC} $1"
}
print_success() {
echo -e "${GREEN}✓${NC} $1"
}
print_warning() {
echo -e "${YELLOW}⚠${NC} $1"
}
print_error() {
echo -e "${RED}✗${NC} $1"
}
check_root() {
if [[ $EUID -ne 0 ]]; then
print_error "This script must be run as root"
echo "Run: sudo bash $0"
exit 1
fi
}
check_os() {
if [[ ! -f /etc/os-release ]]; then
print_error "Cannot detect OS"
exit 1
fi
. /etc/os-release
if [[ "$ID" != "ubuntu" ]]; then
print_error "This script is designed for Ubuntu"
echo "Detected: $PRETTY_NAME"
exit 1
fi
log "OS detected: $PRETTY_NAME"
}
backup_file() {
local file=$1
if [[ -f "$file" ]]; then
cp "$file" "${file}.backup.$(date +%Y%m%d-%H%M%S)"
log "Backed up: $file"
fi
}
handle_error() {
local exit_code=$?
local line_number=$1
if [[ $exit_code -ne 0 ]]; then
print_error "Error occurred at line $line_number (exit code: $exit_code)"
log_error "Error at line $line_number with exit code $exit_code"
echo ""
read -p "Continue anyway? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Installation aborted."
exit $exit_code
fi
fi
}
trap 'handle_error $LINENO' ERR
#==============================================================================
# Pre-Installation Checks
#==============================================================================
pre_install_checks() {
print_section "Pre-Installation Checks"
check_root
check_os
# Check internet
print_step "Checking internet connectivity..."
if ping -c 1 8.8.8.8 &> /dev/null; then
print_success "Internet connection available"
else
print_warning "No internet connection detected"
fi
# Check disk space
local available=$(df -BG / | tail -1 | awk '{print $4}' | sed 's/G//')
if [[ $available -lt 20 ]]; then
print_warning "Low disk space: ${available}GB available (recommend 50GB+)"
else
print_success "Disk space: ${available}GB available"
fi
# Check RAM
local ram=$(free -g | awk '/^Mem:/{print $2}')
if [[ $ram -lt 2 ]]; then
print_warning "Low RAM: ${ram}GB (recommend 4GB+)"
else
print_success "RAM: ${ram}GB"
fi
# Confirm configuration
echo ""
echo -e "${YELLOW}Configuration Summary:${NC}"
echo " Hostname: ${FQDN}"
echo " SSH Port: ${NEW_SSH_PORT}"
echo " Ubuntu User: ${UBUNTU_USER}"
echo " Services: Docker, Cockpit, Tailscale"
echo " SSL: Self-signed via mkcert"
echo ""
read -p "Proceed with installation? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Installation cancelled"
exit 0
fi
log "Pre-installation checks completed"
}
#==============================================================================
# System Updates
#==============================================================================
update_system() {
print_section "System Updates"
print_step "Updating package lists..."
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq 2>&1 | tee -a "$LOG_FILE" || log_warning "Package update had issues"
print_success "Package lists updated"
print_step "Upgrading installed packages..."
apt-get upgrade -y -qq 2>&1 | tee -a "$LOG_FILE" || log_warning "Package upgrade had issues"
print_success "Packages upgraded"
log "System updates completed"
}
#==============================================================================
# Set Hostname
#==============================================================================
set_hostname() {
print_section "Hostname Configuration"
print_step "Setting hostname to: ${FQDN}"
# Set hostname
hostnamectl set-hostname "${HOSTNAME}" 2>&1 | tee -a "$LOG_FILE"
# Update /etc/hosts
backup_file /etc/hosts
sed -i "/127.0.1.1/d" /etc/hosts
cat >> /etc/hosts << EOF
127.0.1.1 ${FQDN} ${HOSTNAME}
EOF
# Update /etc/hostname
echo "${HOSTNAME}" > /etc/hostname
print_success "Hostname configured: ${CYAN}${FQDN}${NC}"
log "Hostname configuration completed"
}
#==============================================================================
# Install Essential Packages
#==============================================================================
install_essential_packages() {
print_section "Installing Essential Packages"
print_step "Installing system packages..."
export DEBIAN_FRONTEND=noninteractive
local packages=(
# Core utilities
"apt-transport-https"
"ca-certificates"
"curl"
"wget"
"gnupg"
"lsb-release"
"software-properties-common"
# Development tools
"build-essential"
"git"
"vim"
"nano"
# System monitoring
"htop"
"iotop"
"iftop"
"ncdu"
"net-tools"
"dnsutils"
"smartmontools"
"lm-sensors"
# Utilities
"tmux"
"screen"
"tree"
"zip"
"unzip"
"p7zip-full"
"jq"
"rsync"
# Python
"python3"
"python3-pip"
"python3-venv"
# Network
"openssh-server"
"openssh-sftp-server"
"vsftpd"
# Security
"ufw"
"fail2ban"
"unattended-upgrades"
"apt-listchanges"
# Libraries
"libssl-dev"
"libffi-dev"
"libxml2-dev"
"libxslt1-dev"
"libjpeg-dev"
"libpng-dev"
"libfreetype6-dev"
)
local failed_packages=()
local installed_count=0
for package in "${packages[@]}"; do
if dpkg -l | grep -q "^ii ${package} "; then
print_success "${package} (already installed)"
((installed_count++))
else
if apt-get install -y -qq "$package" 2>&1 | tee -a "$LOG_FILE"; then
print_success "${package}"
((installed_count++))
else
print_warning "${package} (failed - continuing)"
log_warning "Package installation failed: ${package}"
failed_packages+=("$package")
fi
fi
done
echo ""
print_success "Packages installed: ${installed_count}/${#packages[@]}"
if [[ ${#failed_packages[@]} -gt 0 ]]; then
print_warning "Failed packages: ${failed_packages[*]}"
fi
log "Essential packages installation completed"
}
#==============================================================================
# Create and Configure Ubuntu User
#==============================================================================
setup_ubuntu_user() {
print_section "Ubuntu User Configuration"
# Create ubuntu user if doesn't exist
if ! id "$UBUNTU_USER" &>/dev/null; then
print_step "Creating ubuntu user..."
useradd -m -s /bin/bash -c "Ubuntu System User" "$UBUNTU_USER" 2>&1 | tee -a "$LOG_FILE"
print_success "User created: ${UBUNTU_USER}"
else
print_success "User ${UBUNTU_USER} already exists"
fi
# Set password
echo ""
print_step "Set password for ${UBUNTU_USER}:"
passwd "$UBUNTU_USER"
# Add to groups
print_step "Adding ${UBUNTU_USER} to system groups..."
usermod -aG sudo "$UBUNTU_USER" 2>&1 | tee -a "$LOG_FILE"
usermod -aG adm "$UBUNTU_USER" 2>&1 | tee -a "$LOG_FILE"
usermod -aG systemd-journal "$UBUNTU_USER" 2>&1 | tee -a "$LOG_FILE"
print_success "Added to groups: sudo, adm, systemd-journal"
# Setup home directory structure
local user_home="/home/${UBUNTU_USER}"
# SSH directory
print_step "Configuring SSH directory..."
mkdir -p "${user_home}/.ssh"
chmod 700 "${user_home}/.ssh"
touch "${user_home}/.ssh/authorized_keys"
chmod 600 "${user_home}/.ssh/authorized_keys"
chown -R "${UBUNTU_USER}:${UBUNTU_USER}" "${user_home}/.ssh"
print_success "SSH directory configured"
# FTP/SFTP directories
print_step "Creating FTP/SFTP directories..."
mkdir -p "${user_home}/ftp"/{upload,download,shared}
mkdir -p "${user_home}/backups"
mkdir -p "${user_home}/scripts"
mkdir -p "${user_home}/media"
chown -R "${UBUNTU_USER}:${UBUNTU_USER}" "${user_home}/ftp"
chown -R "${UBUNTU_USER}:${UBUNTU_USER}" "${user_home}/backups"
chown -R "${UBUNTU_USER}:${UBUNTU_USER}" "${user_home}/scripts"
chown -R "${UBUNTU_USER}:${UBUNTU_USER}" "${user_home}/media"
print_success "FTP/SFTP directories created"
# Add SSH key (optional)
echo ""
read -p "Add SSH public key for ${UBUNTU_USER}? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo ""
echo "Paste your SSH public key (or press Enter to skip):"
read -r ssh_key
if [[ -n "$ssh_key" ]]; then
echo "$ssh_key" >> "${user_home}/.ssh/authorized_keys"
chown "${UBUNTU_USER}:${UBUNTU_USER}" "${user_home}/.ssh/authorized_keys"
print_success "SSH key added"
log "SSH key configured for ubuntu user"
fi
fi
log "Ubuntu user configuration completed"
}
#==============================================================================
# Configure SSH
#==============================================================================
configure_ssh() {
print_section "SSH Configuration"
print_step "Backing up SSH configuration..."
backup_file /etc/ssh/sshd_config
print_step "Creating hardened SSH configuration..."
cat > /etc/ssh/sshd_config << EOF
# Media Server SSH Configuration
# Generated: $(date)
# Network
Port ${NEW_SSH_PORT}
AddressFamily inet
ListenAddress 0.0.0.0
# Protocol
Protocol 2
# Host Keys
HostKey /etc/ssh/ssh_host_ed25519_key
HostKey /etc/ssh/ssh_host_rsa_key
# Ciphers and algorithms
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr
MACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,umac-128-etm@openssh.com
# Authentication
LoginGraceTime 30
PermitRootLogin no
StrictModes yes
MaxAuthTries 3
MaxSessions 10
PubkeyAuthentication yes
PasswordAuthentication yes
PermitEmptyPasswords no
ChallengeResponseAuthentication no
UsePAM yes
# Disable insecure features
HostbasedAuthentication no
IgnoreRhosts yes
X11Forwarding no
PrintMotd no
PrintLastLog yes
TCPKeepAlive yes
PermitUserEnvironment no
Compression no
ClientAliveInterval 300
ClientAliveCountMax 2
UseDNS no
Banner /etc/ssh/banner
# Logging
SyslogFacility AUTH
LogLevel VERBOSE
# SFTP Configuration
Subsystem sftp internal-sftp
# Allow specific users
AllowUsers ${UBUNTU_USER}
# SFTP Chroot (optional - commented out)
#Match User ${UBUNTU_USER}
# ChrootDirectory /home/${UBUNTU_USER}/ftp
# ForceCommand internal-sftp
# AllowTcpForwarding no
# X11Forwarding no
EOF
# Create SSH banner
cat > /etc/ssh/banner << 'EOF'
╔════════════════════════════════════════════════════════════════╗
║ AUTHORIZED ACCESS ONLY ║
║ Media Server System ║
║ ║
║ Unauthorized access to this system is forbidden and will be ║
║ prosecuted by law. By accessing this system, you agree that ║
║ your actions may be monitored if unauthorized usage is ║
║ suspected. ║
╚════════════════════════════════════════════════════════════════╝
EOF
# Test SSH config
print_step "Testing SSH configuration..."
if sshd -t 2>&1 | tee -a "$LOG_FILE"; then
print_success "SSH configuration valid"
else
print_error "SSH configuration has errors!"
log_error "SSH configuration test failed"
return 1
fi
# Restart SSH
print_step "Restarting SSH service..."
systemctl reload sshd 2>&1 | tee -a "$LOG_FILE"
print_success "SSH service restarted"
print_success "SSH configured on port ${CYAN}${NEW_SSH_PORT}${NC}"
print_warning "Test connection: ${CYAN}ssh -p ${NEW_SSH_PORT} ${UBUNTU_USER}@$(hostname -I | awk '{print $1}')${NC}"
log "SSH configuration completed"
}
#==============================================================================
# Configure VSFTPD
#==============================================================================
configure_ftp() {
print_section "FTP Server Configuration"
print_step "Configuring vsftpd..."
backup_file /etc/vsftpd.conf
cat > /etc/vsftpd.conf << EOF
# Media Server FTP Configuration
# Generated: $(date)
# Basic settings
listen=YES
listen_ipv6=NO
anonymous_enable=NO
local_enable=YES
write_enable=YES
local_umask=022
dirmessage_enable=YES
use_localtime=YES
xferlog_enable=YES
connect_from_port_20=YES
# Security
chroot_local_user=NO
secure_chroot_dir=/var/run/vsftpd/empty
pam_service_name=vsftpd
ssl_enable=NO
# Performance
pasv_enable=YES
pasv_min_port=40000
pasv_max_port=40100
# User restrictions
userlist_enable=YES
userlist_file=/etc/vsftpd.userlist
userlist_deny=NO
# Logging
xferlog_std_format=YES
log_ftp_protocol=YES
EOF
# Create user list
echo "${UBUNTU_USER}" > /etc/vsftpd.userlist
# Enable and start vsftpd
systemctl enable vsftpd 2>&1 | tee -a "$LOG_FILE"
systemctl restart vsftpd 2>&1 | tee -a "$LOG_FILE"
print_success "FTP server configured"
print_success "FTP user: ${CYAN}${UBUNTU_USER}${NC}"
log "FTP configuration completed"
}
#==============================================================================
# Install Docker
#==============================================================================
install_docker() {
print_section "Docker Installation"
if command -v docker &> /dev/null; then
print_success "Docker already installed"
docker --version
return 0
fi
print_step "Removing old Docker versions..."
apt-get remove -y docker docker-engine docker.io containerd runc 2>/dev/null || true
print_step "Adding Docker GPG key..."
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg 2>&1 | tee -a "$LOG_FILE"
chmod a+r /etc/apt/keyrings/docker.gpg
print_success "Docker GPG key added"
print_step "Adding Docker repository..."
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
print_step "Updating package lists..."
apt-get update -qq 2>&1 | tee -a "$LOG_FILE"
print_step "Installing Docker packages..."
DEBIAN_FRONTEND=noninteractive apt-get install -y \
docker-ce \
docker-ce-cli \
containerd.io \
docker-buildx-plugin \
docker-compose-plugin 2>&1 | tee -a "$LOG_FILE"
print_step "Starting Docker service..."
systemctl start docker 2>&1 | tee -a "$LOG_FILE"
systemctl enable docker 2>&1 | tee -a "$LOG_FILE"
# Create docker group and add ubuntu user
groupadd -f docker
usermod -aG docker "$UBUNTU_USER" 2>&1 | tee -a "$LOG_FILE"
# Configure Docker daemon
print_step "Configuring Docker daemon..."
mkdir -p /etc/docker
cat > /etc/docker/daemon.json << 'EOF'
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"storage-driver": "overlay2"
}
EOF
systemctl restart docker 2>&1 | tee -a "$LOG_FILE"
print_success "Docker installed successfully"
docker --version
docker compose version
# Test Docker
print_step "Testing Docker..."
if docker run --rm hello-world 2>&1 | tee -a "$LOG_FILE" | grep -q "Hello from Docker"; then
print_success "Docker is working correctly"
else
print_warning "Docker test failed"
fi
log "Docker installation completed"
}
#==============================================================================
# Install and Configure mkcert
#==============================================================================
install_mkcert() {
print_section "SSL Certificate Generation (mkcert)"
print_step "Installing mkcert..."
# Install mkcert
if ! command -v mkcert &> /dev/null; then
# Download and install mkcert
curl -JLO "https://dl.filippo.io/mkcert/latest?for=linux/amd64" 2>&1 | tee -a "$LOG_FILE"
chmod +x mkcert-v*-linux-amd64
mv mkcert-v*-linux-amd64 /usr/local/bin/mkcert
print_success "mkcert installed"
else
print_success "mkcert already installed"
fi
# Install local CA
print_step "Installing local Certificate Authority..."
mkcert -install 2>&1 | tee -a "$LOG_FILE"
print_success "Local CA installed"
# Create certificates directory
mkdir -p /etc/ssl/media-server
cd /etc/ssl/media-server
# Generate certificates
print_step "Generating SSL certificates..."
mkcert "${FQDN}" "*.${DOMAIN}" localhost 127.0.0.1 ::1 2>&1 | tee -a "$LOG_FILE"
# Rename files for convenience
mv "${FQDN}+4.pem" cert.pem 2>/dev/null || true
mv "${FQDN}+4-key.pem" key.pem 2>/dev/null || true
# Set permissions
chmod 644 cert.pem
chmod 600 key.pem
print_success "SSL certificates generated"
print_success "Certificate: ${CYAN}/etc/ssl/media-server/cert.pem${NC}"
print_success "Private key: ${CYAN}/etc/ssl/media-server/key.pem${NC}"
# Also copy for ubuntu user
local user_home="/home/${UBUNTU_USER}"
mkdir -p "${user_home}/.local/share/mkcert"
cp -r "$(mkcert -CAROOT)"/* "${user_home}/.local/share/mkcert/" 2>/dev/null || true
chown -R "${UBUNTU_USER}:${UBUNTU_USER}" "${user_home}/.local"
log "mkcert installation and certificate generation completed"
}
#==============================================================================
# Install Cockpit
#==============================================================================
install_cockpit() {
print_section "Cockpit Installation"
print_step "Installing Cockpit packages..."
local cockpit_packages=(
"cockpit"
"cockpit-system"
"cockpit-networkmanager"
"cockpit-storaged"
"cockpit-packagekit"
)
# Optional packages
local optional_packages=(
"cockpit-docker"
"cockpit-podman"
"cockpit-machines"
)
local installed=0
for package in "${cockpit_packages[@]}"; do
if DEBIAN_FRONTEND=noninteractive apt-get install -y "$package" 2>&1 | tee -a "$LOG_FILE"; then
((installed++))
print_success "${package}"
else
print_warning "${package} (failed)"
log_warning "Cockpit package failed: $package"
fi
done
# Try optional packages
for package in "${optional_packages[@]}"; do
if DEBIAN_FRONTEND=noninteractive apt-get install -y "$package" 2>&1 | tee -a "$LOG_FILE"; then
print_success "${package} (optional)"
else
print_warning "${package} (not available - skipping)"
fi
done
if [[ $installed -eq 0 ]]; then
log_error "No Cockpit packages installed"
return 1
fi
# Configure Cockpit
print_step "Configuring Cockpit..."
# Configure port
mkdir -p /etc/systemd/system/cockpit.socket.d/
cat > /etc/systemd/system/cockpit.socket.d/listen.conf << EOF
[Socket]
ListenStream=
ListenStream=9090
EOF
# Configure SSL
mkdir -p /etc/cockpit
cat > /etc/cockpit/cockpit.conf << EOF
[WebService]
Origins = https://${FQDN}:9090 https://localhost:9090
ProtocolHeader = X-Forwarded-Proto
AllowUnencrypted = false
EOF
# Link SSL certificates
ln -sf /etc/ssl/media-server/cert.pem /etc/cockpit/ws-certs.d/1-cert.pem 2>/dev/null || true
ln -sf /etc/ssl/media-server/key.pem /etc/cockpit/ws-certs.d/1-key.pem 2>/dev/null || true
systemctl daemon-reload
# Enable and start
print_step "Starting Cockpit service..."
systemctl enable --now cockpit.socket 2>&1 | tee -a "$LOG_FILE"
sleep 2
if systemctl is-active --quiet cockpit.socket; then
print_success "Cockpit is running"
print_success "Access at: ${CYAN}https://$(hostname -I | awk '{print $1}'):9090${NC}"
else
print_warning "Cockpit failed to start"
systemctl status cockpit.socket --no-pager | tee -a "$LOG_FILE"
fi
log "Cockpit installation completed"
}
#==============================================================================
# Install Tailscale
#==============================================================================
install_tailscale() {
print_section "Tailscale Installation"
if command -v tailscale &> /dev/null; then
print_success "Tailscale already installed"
tailscale version
return 0
fi
print_step "Adding Tailscale repository..."
# Add Tailscale's GPG key
curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/$(lsb_release -cs).noarmor.gpg | tee /usr/share/keyrings/tailscale-archive-keyring.gpg >/dev/null 2>&1 | tee -a "$LOG_FILE"
# Add Tailscale's repository
curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/$(lsb_release -cs).tailscale-keyring.list | tee /etc/apt/sources.list.d/tailscale.list 2>&1 | tee -a "$LOG_FILE"
# Update and install
print_step "Installing Tailscale..."
apt-get update -qq 2>&1 | tee -a "$LOG_FILE"
DEBIAN_FRONTEND=noninteractive apt-get install -y tailscale 2>&1 | tee -a "$LOG_FILE"
print_success "Tailscale installed"
# Start Tailscale
print_step "Configuring Tailscale..."
echo ""
echo -e "${YELLOW}Tailscale setup requires authentication${NC}"
echo "You will need to:"
echo " 1. Follow the URL that appears"
echo " 2. Authenticate in your browser"
echo " 3. Approve this device"
echo ""
read -p "Start Tailscale setup now? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
print_step "Starting Tailscale..."
# Start Tailscale with hostname
tailscale up --hostname="${HOSTNAME}" --accept-routes
if [[ $? -eq 0 ]]; then
print_success "Tailscale is connected"
# Show status
echo ""
print_step "Tailscale status:"
tailscale status
echo ""
print_step "Tailscale IP:"
tailscale ip -4
else
print_warning "Tailscale setup incomplete"
echo "Complete setup later with: sudo tailscale up --hostname=${HOSTNAME}"
fi
else
print_warning "Tailscale not started"
echo "Start later with: sudo tailscale up --hostname=${HOSTNAME}"
fi
log "Tailscale installation completed"
}
#==============================================================================
# System Hardening
#==============================================================================
apply_system_hardening() {
print_section "System Hardening"
print_step "Applying security hardening..."
# 1. Configure sysctl for security
print_step "Configuring kernel parameters..."
backup_file /etc/sysctl.conf
cat >> /etc/sysctl.conf << 'EOF'
#==============================================================================
# Security Hardening - Added by media server setup
#==============================================================================
# IP Forwarding (disabled for security, enable if needed for Docker/Tailscale)
net.ipv4.ip_forward = 0
net.ipv6.conf.all.forwarding = 0
# Disable IPv6 (optional - enable if you use IPv6)
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
net.ipv6.conf.lo.disable_ipv6 = 1
# Network Security
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.secure_redirects = 0
net.ipv4.conf.default.secure_redirects = 0
net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.default.log_martians = 1
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.tcp_syncookies = 1
# Kernel Security
kernel.dmesg_restrict = 1
kernel.kptr_restrict = 2
kernel.yama.ptrace_scope = 1
kernel.unprivileged_bpf_disabled = 1
net.core.bpf_jit_harden = 2
# File System Security
fs.protected_hardlinks = 1
fs.protected_symlinks = 1
fs.suid_dumpable = 0
EOF
sysctl -p > /dev/null 2>&1
print_success "Kernel parameters configured"
# 2. Set file permissions on sensitive files
print_step "Setting file permissions..."
chmod 644 /etc/passwd
chmod 644 /etc/group
chmod 600 /etc/shadow
chmod 600 /etc/gshadow
chmod 644 /etc/hosts.allow
chmod 644 /etc/hosts.deny
print_success "File permissions set"
# 3. Disable core dumps
print_step "Disabling core dumps..."
echo "* hard core 0" >> /etc/security/limits.conf
cat > /etc/profile.d/disable-core-dumps.sh << 'EOF'
# Disable core dumps
ulimit -c 0
EOF
chmod +x /etc/profile.d/disable-core-dumps.sh
print_success "Core dumps disabled"
# 4. Configure password policy
print_step "Configuring password policy..."
apt-get install -y libpam-pwquality 2>&1 | tee -a "$LOG_FILE"
backup_file /etc/security/pwquality.conf
cat > /etc/security/pwquality.conf << 'EOF'
# Password quality requirements
minlen = 12
dcredit = -1
ucredit = -1
lcredit = -1
ocredit = -1
minclass = 3
maxrepeat = 3
maxclassrepeat = 2
EOF
print_success "Password policy configured"
# 5. Configure login security
print_step "Configuring login security..."
backup_file /etc/login.defs
# Set password aging
sed -i 's/^PASS_MAX_DAYS.*/PASS_MAX_DAYS 90/' /etc/login.defs
sed -i 's/^PASS_MIN_DAYS.*/PASS_MIN_DAYS 1/' /etc/login.defs
sed -i 's/^PASS_WARN_AGE.*/PASS_WARN_AGE 7/' /etc/login.defs
print_success "Login security configured"
# 6. Disable unused filesystems
print_step "Disabling unused filesystems..."
cat > /etc/modprobe.d/disable-filesystems.conf << 'EOF'
install cramfs /bin/true
install freevxfs /bin/true
install jffs2 /bin/true
install hfs /bin/true
install hfsplus /bin/true
install udf /bin/true
EOF
print_success "Unused filesystems disabled"
# 7. Configure audit logging
print_step "Configuring audit logging..."
apt-get install -y auditd audispd-plugins 2>&1 | tee -a "$LOG_FILE" || true
if command -v auditctl &> /dev/null; then
systemctl enable auditd 2>&1 | tee -a "$LOG_FILE"
systemctl start auditd 2>&1 | tee -a "$LOG_FILE"
print_success "Audit logging enabled"
else
print_warning "Audit logging not available"
fi
# 8. Disable unused services
print_step "Disabling unused services..."
local services_to_disable=(
"avahi-daemon"
"cups"
"isc-dhcp-server"
"isc-dhcp-server6"
"rpcbind"
"rsync"
)
for service in "${services_to_disable[@]}"; do
if systemctl is-enabled "$service" &>/dev/null; then
systemctl disable "$service" 2>/dev/null || true
systemctl stop "$service" 2>/dev/null || true
fi
done
print_success "Unused services disabled"
log "System hardening completed"
}
#==============================================================================
# Configure Firewall (UFW)
#==============================================================================
configure_firewall() {
print_section "Firewall Configuration"
print_step "Configuring UFW firewall..."
# Install UFW if not present
if ! command -v ufw &> /dev/null; then
apt-get install -y ufw 2>&1 | tee -a "$LOG_FILE"
fi
# Reset UFW to default
ufw --force reset 2>&1 | tee -a "$LOG_FILE"
# Default policies
ufw default deny incoming
ufw default allow outgoing
# Allow SSH (custom port)
ufw allow ${NEW_SSH_PORT}/tcp comment 'SSH'
print_success "SSH port ${NEW_SSH_PORT} allowed"
# Allow SFTP (same as SSH)
print_success "SFTP (port ${NEW_SSH_PORT}) allowed"
# Allow FTP
ufw allow 21/tcp comment 'FTP'
ufw allow 40000:40100/tcp comment 'FTP Passive'
print_success "FTP ports allowed"
# Allow Cockpit
ufw allow 9090/tcp comment 'Cockpit'
print_success "Cockpit port 9090 allowed"
# Allow HTTP/HTTPS
ufw allow 80/tcp comment 'HTTP'
ufw allow 443/tcp comment 'HTTPS'
print_success "HTTP/HTTPS ports allowed"
# Allow common media server ports
ufw allow 8096/tcp comment 'Jellyfin'
ufw allow 32400/tcp comment 'Plex'
ufw allow 8989/tcp comment 'Sonarr'
ufw allow 7878/tcp comment 'Radarr'
ufw allow 8686/tcp comment 'Lidarr'
ufw allow 9000/tcp comment 'Portainer'
print_success "Media server ports allowed"
# Allow Tailscale
ufw allow 41641/udp comment 'Tailscale'
print_success "Tailscale port allowed"
# Rate limiting for SSH
ufw limit ${NEW_SSH_PORT}/tcp comment 'SSH rate limit'
# Enable UFW
ufw --force enable 2>&1 | tee -a "$LOG_FILE"
print_success "Firewall enabled and configured"
echo ""
print_step "Firewall rules:"
ufw status numbered | tee -a "$LOG_FILE"
log "Firewall configuration completed"
}
#==============================================================================
# Configure Fail2ban
#==============================================================================
configure_fail2ban() {
print_section "Fail2ban Configuration"
print_step "Configuring Fail2ban..."
# Create local configuration
cat > /etc/fail2ban/jail.local << EOF
[DEFAULT]
# Ban settings
bantime = 1h
findtime = 10m
maxretry = 5
banaction = ufw
# Email notifications (configure if needed)
destemail = root@localhost
sendername = Fail2ban-${HOSTNAME}
action = %(action_mwl)s
[sshd]
enabled = true
port = ${NEW_SSH_PORT}
logpath = /var/log/auth.log
maxretry = 3
bantime = 24h
findtime = 10m
[sshd-ddos]
enabled = true
port = ${NEW_SSH_PORT}
logpath = /var/log/auth.log
maxretry = 10
findtime = 60
[cockpit]
enabled = true
port = 9090
logpath = /var/log/auth.log
maxretry = 5
bantime = 1h
[vsftpd]
enabled = true
port = 21
logpath = /var/log/vsftpd.log
maxretry = 3
bantime = 1h
EOF
# Start and enable Fail2ban
systemctl enable fail2ban 2>&1 | tee -a "$LOG_FILE"
systemctl restart fail2ban 2>&1 | tee -a "$LOG_FILE"
sleep 2
if systemctl is-active --quiet fail2ban; then
print_success "Fail2ban is running"
# Show status
echo ""
fail2ban-client status | tee -a "$LOG_FILE"
else
print_warning "Fail2ban failed to start"
fi
log "Fail2ban configuration completed"
}
#==============================================================================
# Configure Automatic Updates
#==============================================================================
configure_automatic_updates() {
print_section "Automatic Security Updates"
print_step "Configuring unattended-upgrades..."
# Configure automatic updates
cat > /etc/apt/apt.conf.d/50unattended-upgrades << 'EOF'
Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}";
"${distro_id}:${distro_codename}-security";
"${distro_id}ESMApps:${distro_codename}-apps-security";
"${distro_id}ESM:${distro_codename}-infra-security";
};
Unattended-Upgrade::DevRelease "false";
Unattended-Upgrade::AutoFixInterruptedDpkg "true";
Unattended-Upgrade::MinimalSteps "true";
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
Unattended-Upgrade::Remove-New-Unused-Dependencies "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "false";
Unattended-Upgrade::Automatic-Reboot-Time "03:00";
Unattended-Upgrade::Automatic-Reboot-WithUsers "false";
EOF
# Enable automatic updates
cat > /etc/apt/apt.conf.d/20auto-upgrades << 'EOF'
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Download-Upgradeable-Packages "1";
APT::Periodic::AutocleanInterval "7";
APT::Periodic::Unattended-Upgrade "1";
EOF
print_success "Automatic security updates configured"
log "Automatic updates configuration completed"
}
#==============================================================================
# Create Management Scripts
#==============================================================================
create_management_scripts() {
print_section "Creating Management Scripts"
print_step "Creating system management scripts..."
# Create scripts directory
mkdir -p /usr/local/bin/media-server
# 1. System Info Script
cat > /usr/local/bin/media-server/system-info.sh << 'SYSINFO_EOF'
#!/bin/bash
GREEN='\033[0;32m'
CYAN='\033[0;36m'
YELLOW='\033[1;33m'
NC='\033[0m'
echo -e "${GREEN}╔════════════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ Media Server System Information ║${NC}"
echo -e "${GREEN}╚════════════════════════════════════════════════════════════════╝${NC}"
echo ""
# System
echo -e "${CYAN}System:${NC}"
echo " Hostname: $(hostname -f)"
echo " OS: $(lsb_release -d | cut -f2)"
echo " Kernel: $(uname -r)"
echo " Uptime: $(uptime -p)"
echo ""
# Network
echo -e "${CYAN}Network:${NC}"
ip -4 addr show | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v 127.0.0.1 | while read ip; do
echo " IP: $ip"
done
if command -v tailscale &> /dev/null; then
TS_IP=$(tailscale ip -4 2>/dev/null || echo "Not connected")
echo " Tailscale IP: $TS_IP"
fi
echo ""
# Services
echo -e "${CYAN}Services:${NC}"
systemctl is-active --quiet sshd && echo " ✓ SSH" || echo " ✗ SSH"
systemctl is-active --quiet docker && echo " ✓ Docker" || echo " ✗ Docker"
systemctl is-active --quiet cockpit.socket && echo " ✓ Cockpit" || echo " ✗ Cockpit"
systemctl is-active --quiet vsftpd && echo " ✓ FTP" || echo " ✗ FTP"
systemctl is-active --quiet fail2ban && echo " ✓ Fail2ban" || echo " ✗ Fail2ban"
systemctl is-active --quiet tailscaled && echo " ✓ Tailscale" || echo " ✗ Tailscale"
echo ""
# Docker
if command -v docker &> /dev/null; then
echo -e "${CYAN}Docker:${NC}"
echo " Version: $(docker --version | awk '{print $3}' | sed 's/,//')"
echo " Containers: $(docker ps -q | wc -l) running"
echo ""
fi
# Resources
echo -e "${CYAN}Resources:${NC}"
df -h / | tail -1 | awk '{printf " Disk: %s / %s (%s used)\n", $3, $2, $5}'
free -h | grep "Mem:" | awk '{printf " RAM: %s / %s\n", $3, $2}'
echo " Load: $(uptime | awk -F'load average:' '{print $2}')"
echo ""
# SSL
if [[ -f /etc/ssl/media-server/cert.pem ]]; then
echo -e "${CYAN}SSL Certificate:${NC}"
echo " Location: /etc/ssl/media-server/"
EXPIRY=$(openssl x509 -enddate -noout -in /etc/ssl/media-server/cert.pem | cut -d= -f2)
echo " Expires: $EXPIRY"
echo ""
fi
# Access URLs
echo -e "${CYAN}Access URLs:${NC}"
LOCAL_IP=$(ip route get 1 | awk '{print $7}' | head -1)
echo " Cockpit: https://${LOCAL_IP}:9090"
echo " SSH: ssh -p 2222 ubuntu@${LOCAL_IP}"
echo " SFTP: sftp -P 2222 ubuntu@${LOCAL_IP}"
echo " FTP: ftp://${LOCAL_IP}"
echo ""
SYSINFO_EOF
chmod +x /usr/local/bin/media-server/system-info.sh
ln -sf /usr/local/bin/media-server/system-info.sh /usr/local/bin/system-info
# 2. Firewall Management Script
cat > /usr/local/bin/media-server/firewall-manage.sh << 'FW_EOF'
#!/bin/bash
if [[ $EUID -ne 0 ]]; then
echo "This script must be run as root"
exit 1
fi
case "${1:-status}" in
status)
ufw status numbered
;;
allow)
if [[ -z "$2" ]]; then
echo "Usage: $0 allow <port/service>"
exit 1
fi
ufw allow "$2"
;;
deny)
if [[ -z "$2" ]]; then
echo "Usage: $0 deny <port/service>"
exit 1
fi
ufw deny "$2"
;;
delete)
if [[ -z "$2" ]]; then
echo "Usage: $0 delete <rule-number>"
exit 1
fi
ufw delete "$2"
;;
*)
echo "Usage: $0 {status|allow|deny|delete} [argument]"
exit 1
;;
esac
FW_EOF
chmod +x /usr/local/bin/media-server/firewall-manage.sh
ln -sf /usr/local/bin/media-server/firewall-manage.sh /usr/local/bin/firewall
# 3. Docker Quick Commands
cat > /usr/local/bin/media-server/docker-quick.sh << 'DOCKER_EOF'
#!/bin/bash
case "${1:-help}" in
ps)
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
;;
logs)
if [[ -z "$2" ]]; then
echo "Usage: $0 logs <container-name>"
exit 1
fi
docker logs -f "$2"
;;
restart)
if [[ -z "$2" ]]; then
echo "Usage: $0 restart <container-name>"
exit 1
fi
docker restart "$2"
;;
stop)
if [[ -z "$2" ]]; then
echo "Usage: $0 stop <container-name>"
exit 1
fi
docker stop "$2"
;;
stats)
docker stats
;;
clean)
docker system prune -af
;;
*)
cat << 'EOF'
Docker Quick Commands:
ps List running containers
logs <container> View container logs
restart <container> Restart a container
stop <container> Stop a container
stats Show resource usage
clean Clean unused images/containers
EOF
;;
esac
DOCKER_EOF
chmod +x /usr/local/bin/media-server/docker-quick.sh
ln -sf /usr/local/bin/media-server/docker-quick.sh /usr/local/bin/dkr
print_success "Management scripts created"
print_success "Commands: ${CYAN}system-info${NC}, ${CYAN}firewall${NC}, ${CYAN}dkr${NC}"
log "Management scripts creation completed"
}
#==============================================================================
# Configure MOTD
#==============================================================================
configure_motd() {
print_section "Message of the Day Configuration"
print_step "Configuring MOTD..."
# Disable default MOTD scripts
chmod -x /etc/update-motd.d/* 2>/dev/null || true
# Create custom MOTD
cat > /etc/update-motd.d/00-header << 'MOTD_EOF'
#!/bin/bash
GREEN='\033[0;32m'
CYAN='\033[0;36m'
NC='\033[0m'
echo -e "${GREEN}"
cat << 'EOF'
╔════════════════════════════════════════════════════════════════╗
║ Welcome to Media Server ║
║ media.home ║
╚════════════════════════════════════════════════════════════════╝
EOF
echo -e "${NC}"
MOTD_EOF
cat > /etc/update-motd.d/10-sysinfo << 'MOTD_SYSINFO_EOF'
#!/bin/bash
CYAN='\033[0;36m'
GREEN='\033[0;32m'
NC='\033[0m'
# Get info
LOCAL_IP=$(ip route get 1 | awk '{print $7}' | head -1)
CONTAINERS=$(docker ps -q 2>/dev/null | wc -l)
echo -e "${CYAN}System Information:${NC}"
echo " IP Address: $LOCAL_IP"
echo " Uptime: $(uptime -p)"
echo " Docker Containers: $CONTAINERS running"
echo ""
echo -e "${CYAN}Quick Commands:${NC}"
echo " ${GREEN}system-info${NC} - Full system information"
echo " ${GREEN}firewall${NC} - Manage firewall rules"
echo " ${GREEN}dkr ps${NC} - List Docker containers"
echo ""
echo -e "${CYAN}Access:${NC}"
echo " Cockpit: https://${LOCAL_IP}:9090"
echo ""
MOTD_SYSINFO_EOF
chmod +x /etc/update-motd.d/00-header
chmod +x /etc/update-motd.d/10-sysinfo
print_success "MOTD configured"
log "MOTD configuration completed"
}
#==============================================================================
# Final Summary
#==============================================================================
print_completion_summary() {
print_section "Installation Complete!"
local LOCAL_IP=$(ip route get 1 | awk '{print $7}' | head -1)
local TS_IP=$(tailscale ip -4 2>/dev/null || echo "Not configured")
echo -e "${GREEN}╔════════════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ Media Server Setup Complete! ║${NC}"
echo -e "${GREEN}╚════════════════════════════════════════════════════════════════╝${NC}"
echo ""
echo -e "${CYAN}System Configuration:${NC}"
echo " Hostname: ${FQDN}"
echo " Local IP: ${LOCAL_IP}"
echo " Tailscale IP: ${TS_IP}"
echo ""
echo -e "${CYAN}Access Information:${NC}"
echo " SSH: ssh -p ${NEW_SSH_PORT} ${UBUNTU_USER}@${LOCAL_IP}"
echo " SFTP: sftp -P ${NEW_SSH_PORT} ${UBUNTU_USER}@${LOCAL_IP}"
echo " FTP: ftp://${LOCAL_IP}"
echo " Cockpit: https://${LOCAL_IP}:9090"
echo ""
echo -e "${CYAN}SSL Certificates:${NC}"
echo " Location: /etc/ssl/media-server/"
echo " Certificate: cert.pem"
echo " Private Key: key.pem"
echo ""
echo -e "${CYAN}Installed Services:${NC}"
echo " ✓ SSH/SFTP (Port ${NEW_SSH_PORT})"
echo " ✓ FTP (Port 21)"
echo " ✓ Docker & Docker Compose"
echo " ✓ Cockpit Web Admin"
echo " ✓ Tailscale VPN"
echo " ✓ UFW Firewall"
echo " ✓ Fail2ban"
echo " ✓ Automatic Updates"
echo ""
echo -e "${CYAN}Security Features:${NC}"
echo " ✓ System hardening applied"
echo " ✓ Firewall configured"
echo " ✓ Fail2ban active"
echo " ✓ Root login disabled"
echo " ✓ Password policy enforced"
echo " ✓ Automatic security updates"
echo ""
echo -e "${CYAN}Quick Commands:${NC}"
echo " system-info - System status"
echo " firewall - Manage firewall"
echo " dkr ps - Docker containers"
echo ""
echo -e "${CYAN}Log Files:${NC}"
echo " Main: ${LOG_FILE}"
echo " Errors: ${ERROR_LOG}"
echo ""
if [[ $ERRORS_OCCURRED -gt 0 ]]; then
echo -e "${YELLOW}⚠ ${ERRORS_OCCURRED} error(s) occurred during installation${NC}"
echo -e "${YELLOW} Check logs for details${NC}"
echo ""
fi
if [[ $WARNINGS_OCCURRED -gt 0 ]]; then
echo -e "${YELLOW}⚠ ${WARNINGS_OCCURRED} warning(s) occurred during installation${NC}"
echo ""
fi
echo -e "${YELLOW}Important Notes:${NC}"
echo " 1. Test SSH connection: ssh -p ${NEW_SSH_PORT} ${UBUNTU_USER}@${LOCAL_IP}"
echo " 2. Logout and login as ${UBUNTU_USER} to use Docker without sudo"
echo " 3. Configure Tailscale if not done: sudo tailscale up"
echo " 4. Access Cockpit at: https://${LOCAL_IP}:9090"
echo " 5. SSL certificate trusted only on this machine (mkcert CA)"
echo ""
echo -e "${YELLOW}Next Steps:${NC}"
echo " 1. Test all access methods"
echo " 2. Deploy Docker containers"
echo " 3. Configure media services"
echo " 4. Set up automated backups"
echo ""
log "Installation completed successfully"
log "Errors: ${ERRORS_OCCURRED}, Warnings: ${WARNINGS_OCCURRED}"
}
#==============================================================================
# Main Installation Flow
#==============================================================================
main() {
print_header
# Pre-checks
pre_install_checks
# System setup
update_system
set_hostname
install_essential_packages
# User setup
setup_ubuntu_user
# Network services
configure_ssh
configure_ftp
# Core services
install_docker
install_mkcert
install_cockpit
install_tailscale
# Security
apply_system_hardening
configure_firewall
configure_fail2ban
configure_automatic_updates
# Management
create_management_scripts
configure_motd
# Final summary
print_completion_summary
echo ""
echo -e "${GREEN}Setup complete!${NC}"
echo ""
echo -e "${YELLOW}Recommended: Reboot to apply all changes${NC}"
read -p "Reboot now? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "Rebooting in 10 seconds... (Ctrl+C to cancel)"
sleep 10
reboot
else
echo "Please reboot manually when ready: sudo reboot"
fi
}
# Run main installation
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment