<message>Update the _set_golden_from_path function to improve the handling of existing golden image files. Replace the existing unlink logic with a more robust method that safely removes files or broken symlinks using the missing_ok parameter. This change enhances the reliability of the backup upload process by ensuring that stale references are properly cleared before setting a new golden image path.
1071 lines
36 KiB
Bash
Executable File
1071 lines
36 KiB
Bash
Executable File
#!/bin/bash
|
|
# Deployment script for GNSS Guard Server to AWS EC2 (Debian)
|
|
# Uses Docker with Nginx + Certbot for SSL
|
|
|
|
set -e
|
|
|
|
# =============================================================================
|
|
# CONFIGURATION - Edit these values before deploying
|
|
# =============================================================================
|
|
|
|
# AWS EC2 Instance
|
|
SERVER_USER="admin"
|
|
SERVER_HOST="gnss.tototheo.com"
|
|
SERVER_PORT="22"
|
|
|
|
# SSH Key file (relative to project root or absolute path)
|
|
SSH_KEY="server/.cert/Cortex-01.pem"
|
|
|
|
# Environment file to deploy (relative to project root)
|
|
# Copy server/env.example to server/.env.prod and configure it
|
|
ENV_FILE="server/.env.prod"
|
|
|
|
# Domain for SSL (required for Let's Encrypt)
|
|
# Must match GNSS_SERVER_DOMAIN in your env file
|
|
SERVER_DOMAIN="gnss.tototheo.com"
|
|
|
|
# Email for Let's Encrypt notifications
|
|
LETSENCRYPT_EMAIL="alexander.s@tototheo.com"
|
|
|
|
# =============================================================================
|
|
# END OF CONFIGURATION
|
|
# =============================================================================
|
|
|
|
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REMOTE_PATH="/opt/gnss-guard-server"
|
|
|
|
# Helper for display: include port in ssh commands if non-standard
|
|
if [ "${SERVER_PORT}" = "22" ]; then
|
|
SSH_DISPLAY="ssh -i ${SSH_KEY} ${SERVER_USER}@${SERVER_HOST}"
|
|
else
|
|
SSH_DISPLAY="ssh -i ${SSH_KEY} -p ${SERVER_PORT} ${SERVER_USER}@${SERVER_HOST}"
|
|
fi
|
|
|
|
# Colors for output
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
NC='\033[0m' # No Color
|
|
|
|
log_info() {
|
|
echo -e "${GREEN}[INFO]${NC} $1"
|
|
}
|
|
|
|
log_warn() {
|
|
echo -e "${YELLOW}[WARN]${NC} $1"
|
|
}
|
|
|
|
log_error() {
|
|
echo -e "${RED}[ERROR]${NC} $1"
|
|
}
|
|
|
|
# Resolve SSH key path
|
|
get_ssh_key_path() {
|
|
if [[ "${SSH_KEY}" = /* ]]; then
|
|
echo "${SSH_KEY}"
|
|
else
|
|
echo "${PROJECT_DIR}/${SSH_KEY}"
|
|
fi
|
|
}
|
|
|
|
# SSH/SCP wrapper with key-based auth
|
|
ssh_cmd() {
|
|
local key_path=$(get_ssh_key_path)
|
|
ssh -i "${key_path}" -o ConnectTimeout=10 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p ${SERVER_PORT} "$@"
|
|
}
|
|
|
|
scp_cmd() {
|
|
local key_path=$(get_ssh_key_path)
|
|
scp -i "${key_path}" -P ${SERVER_PORT} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$@"
|
|
}
|
|
|
|
# =============================================================================
|
|
# HELPER FUNCTIONS
|
|
# =============================================================================
|
|
|
|
test_connection() {
|
|
log_info "Testing SSH connection to ${SERVER_USER}@${SERVER_HOST}:${SERVER_PORT}..."
|
|
if ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "echo 'Connection successful'" 2>/dev/null; then
|
|
log_info "SSH connection successful"
|
|
return 0
|
|
else
|
|
log_error "SSH connection failed. Please ensure:"
|
|
log_error " 1. EC2 instance is running and accessible at ${SERVER_HOST}"
|
|
log_error " 2. Security group allows SSH from your IP"
|
|
log_error " 3. SSH key is configured at ${SSH_KEY}"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
install_docker() {
|
|
log_info "Installing Docker..."
|
|
|
|
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "sudo bash -s" << 'DOCKER_EOF'
|
|
set -e
|
|
|
|
# Check if Docker is already installed
|
|
if command -v docker &> /dev/null; then
|
|
echo "Docker is already installed"
|
|
docker --version
|
|
exit 0
|
|
fi
|
|
|
|
# Install Docker using official script
|
|
curl -fsSL https://get.docker.com | sh
|
|
|
|
# Add current user to docker group
|
|
usermod -aG docker $SUDO_USER || true
|
|
|
|
# Install Docker Compose plugin, rsync, and PostgreSQL client
|
|
apt-get update
|
|
apt-get install -y docker-compose-plugin rsync postgresql-client
|
|
|
|
# Start and enable Docker
|
|
systemctl enable docker
|
|
systemctl start docker
|
|
|
|
echo "Docker installed successfully"
|
|
docker --version
|
|
docker compose version
|
|
DOCKER_EOF
|
|
|
|
log_info "Docker installed"
|
|
}
|
|
|
|
ensure_rsync() {
|
|
# Ensure rsync is installed on the remote server
|
|
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "which rsync > /dev/null 2>&1 || sudo apt-get update && sudo apt-get install -y rsync" > /dev/null 2>&1
|
|
}
|
|
|
|
ensure_postgresql_client() {
|
|
# Ensure postgresql-client is installed on the remote server
|
|
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "which psql > /dev/null 2>&1 || sudo apt-get update && sudo apt-get install -y postgresql-client" > /dev/null 2>&1
|
|
}
|
|
|
|
install_fail2ban() {
|
|
log_info "Installing and configuring fail2ban..."
|
|
|
|
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "sudo bash -s" << 'FAIL2BAN_EOF'
|
|
set -e
|
|
|
|
# Install fail2ban
|
|
if ! command -v fail2ban-server &> /dev/null; then
|
|
apt-get update
|
|
apt-get install -y fail2ban
|
|
echo "fail2ban installed"
|
|
else
|
|
echo "fail2ban already installed"
|
|
fi
|
|
|
|
# Create local jail configuration
|
|
cat > /etc/fail2ban/jail.local << 'JAIL_EOF'
|
|
# GNSS Guard Server - fail2ban configuration
|
|
# Auto-generated by deploy_server.sh
|
|
|
|
[DEFAULT]
|
|
# Ban for 1 hour after 5 failures
|
|
bantime = 3600
|
|
findtime = 600
|
|
maxretry = 5
|
|
|
|
# Use iptables for banning
|
|
banaction = iptables-multiport
|
|
|
|
# Email notifications (uncomment and configure if needed)
|
|
# destemail = admin@example.com
|
|
# sendername = Fail2Ban
|
|
# action = %(action_mwl)s
|
|
|
|
# Whitelist internal networks (add your office IPs if needed)
|
|
ignoreip = 127.0.0.1/8 ::1
|
|
|
|
# =============================================================================
|
|
# SSH Protection
|
|
# =============================================================================
|
|
[sshd]
|
|
enabled = true
|
|
port = ssh
|
|
filter = sshd
|
|
logpath = /var/log/auth.log
|
|
maxretry = 5
|
|
bantime = 3600
|
|
|
|
# =============================================================================
|
|
# Nginx Protection (HTTP/HTTPS)
|
|
# =============================================================================
|
|
|
|
# Block IPs with too many login failures (401)
|
|
[nginx-login]
|
|
enabled = true
|
|
port = http,https
|
|
filter = nginx-login
|
|
logpath = /var/log/nginx/access.log
|
|
backend = auto
|
|
maxretry = 10
|
|
findtime = 300
|
|
bantime = 3600
|
|
|
|
# Block IPs with excessive 4xx errors (scanners)
|
|
[nginx-badbots]
|
|
enabled = true
|
|
port = http,https
|
|
filter = nginx-badbots
|
|
logpath = /var/log/nginx/access.log
|
|
backend = auto
|
|
maxretry = 30
|
|
findtime = 60
|
|
bantime = 7200
|
|
|
|
# Block IPs hammering the validation endpoint
|
|
[nginx-ratelimit]
|
|
enabled = true
|
|
port = http,https
|
|
filter = nginx-ratelimit
|
|
logpath = /var/log/nginx/access.log
|
|
backend = auto
|
|
maxretry = 100
|
|
findtime = 60
|
|
bantime = 1800
|
|
JAIL_EOF
|
|
|
|
# Create nginx login filter (detects failed login attempts)
|
|
cat > /etc/fail2ban/filter.d/nginx-login.conf << 'FILTER_EOF'
|
|
# Fail2Ban filter for nginx login failures
|
|
# Matches 401 responses on login-related endpoints
|
|
|
|
[Definition]
|
|
failregex = ^<HOST> .* "(GET|POST) /login.*" 401
|
|
^<HOST> .* "(GET|POST) /api/v1/.*" 401
|
|
ignoreregex =
|
|
FILTER_EOF
|
|
|
|
# Create nginx badbots filter (detects scanners and bad bots)
|
|
cat > /etc/fail2ban/filter.d/nginx-badbots.conf << 'FILTER_EOF'
|
|
# Fail2Ban filter for nginx bad bots and scanners
|
|
# Matches excessive 403/404 errors
|
|
|
|
[Definition]
|
|
failregex = ^<HOST> .* "(GET|POST|HEAD) .*" (403|404)
|
|
ignoreregex = ^<HOST> .* "(GET|POST) /api/v1/validation"
|
|
^<HOST> .* "GET /health"
|
|
^<HOST> .* "GET /static/"
|
|
^<HOST> .* "GET /favicon.ico"
|
|
FILTER_EOF
|
|
|
|
# Create nginx rate limit filter (detects endpoint hammering)
|
|
cat > /etc/fail2ban/filter.d/nginx-ratelimit.conf << 'FILTER_EOF'
|
|
# Fail2Ban filter for nginx rate limiting
|
|
# Matches high-frequency requests to any endpoint
|
|
|
|
[Definition]
|
|
failregex = ^<HOST> .* "(GET|POST) /api/v1/validation" [0-9]{3}
|
|
ignoreregex =
|
|
FILTER_EOF
|
|
|
|
# Ensure nginx log directory and files exist before fail2ban starts
|
|
mkdir -p /var/log/nginx
|
|
touch /var/log/nginx/access.log
|
|
touch /var/log/nginx/error.log
|
|
chmod 644 /var/log/nginx/access.log /var/log/nginx/error.log
|
|
|
|
# Enable fail2ban
|
|
systemctl enable fail2ban
|
|
|
|
# Restart fail2ban and wait for it to start
|
|
systemctl restart fail2ban
|
|
sleep 2
|
|
|
|
# Check if fail2ban started successfully
|
|
if systemctl is-active --quiet fail2ban; then
|
|
echo "fail2ban configured and started successfully"
|
|
fail2ban-client status
|
|
else
|
|
echo "fail2ban service failed to start, checking logs..."
|
|
journalctl -u fail2ban --no-pager -n 20
|
|
# Try to start it again
|
|
systemctl start fail2ban
|
|
fi
|
|
FAIL2BAN_EOF
|
|
|
|
log_info "fail2ban installed and configured"
|
|
}
|
|
|
|
ensure_database() {
|
|
log_info "Ensuring database exists..."
|
|
|
|
# Ensure postgresql-client is available
|
|
ensure_postgresql_client
|
|
|
|
# Extract database connection info from env file on server
|
|
local db_url=$(ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "grep '^GNSS_SERVER_DATABASE_URL=' ${REMOTE_PATH}/.env.prod 2>/dev/null | cut -d'=' -f2- | tr -d '\"' | tr -d \"'\"")
|
|
|
|
if [ -z "${db_url}" ]; then
|
|
log_warn "GNSS_SERVER_DATABASE_URL not found in .env.prod, skipping database check"
|
|
return 0
|
|
fi
|
|
|
|
# Parse the database URL: postgresql://user:password@host:port/database
|
|
local db_user=$(echo "${db_url}" | sed -n 's|postgresql://\([^:]*\):.*|\1|p')
|
|
local db_pass=$(echo "${db_url}" | sed -n 's|postgresql://[^:]*:\([^@]*\)@.*|\1|p')
|
|
local db_host=$(echo "${db_url}" | sed -n 's|postgresql://[^@]*@\([^:/]*\).*|\1|p')
|
|
local db_port=$(echo "${db_url}" | sed -n 's|postgresql://[^@]*@[^:]*:\([0-9]*\)/.*|\1|p')
|
|
local db_name=$(echo "${db_url}" | sed -n 's|postgresql://[^@]*@[^/]*/\([^?]*\).*|\1|p')
|
|
|
|
[ -z "${db_port}" ] && db_port="5432"
|
|
|
|
if [ -z "${db_host}" ] || [ -z "${db_name}" ] || [ -z "${db_user}" ]; then
|
|
log_warn "Could not parse database URL, skipping database check"
|
|
return 0
|
|
fi
|
|
|
|
log_info "Checking if database '${db_name}' exists on ${db_host}..."
|
|
|
|
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "PGPASSWORD='${db_pass}' psql -h ${db_host} -p ${db_port} -U ${db_user} -d postgres -tc \"SELECT 1 FROM pg_database WHERE datname = '${db_name}'\" | grep -q 1 || PGPASSWORD='${db_pass}' psql -h ${db_host} -p ${db_port} -U ${db_user} -d postgres -c \"CREATE DATABASE ${db_name}\""
|
|
|
|
if [ $? -eq 0 ]; then
|
|
log_info "Database '${db_name}' ready"
|
|
else
|
|
log_warn "Could not verify/create database (may require manual creation)"
|
|
fi
|
|
}
|
|
|
|
deploy_server_files() {
|
|
log_info "Deploying server files to ${SERVER_USER}@${SERVER_HOST}:${REMOTE_PATH}..."
|
|
|
|
ensure_rsync
|
|
|
|
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "sudo mkdir -p ${REMOTE_PATH} && sudo chown ${SERVER_USER}:${SERVER_USER} ${REMOTE_PATH}"
|
|
|
|
local ssh_key_path=$(get_ssh_key_path)
|
|
local rsync_ssh="ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p ${SERVER_PORT}"
|
|
if [ -n "${SSH_KEY}" ] && [ -f "${ssh_key_path}" ]; then
|
|
rsync_ssh="${rsync_ssh} -i ${ssh_key_path}"
|
|
fi
|
|
|
|
# Backup SSL nginx config if it exists
|
|
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "test -f ${REMOTE_PATH}/nginx/conf.d/default.conf && \
|
|
grep -q 'ssl_certificate' ${REMOTE_PATH}/nginx/conf.d/default.conf && \
|
|
cp ${REMOTE_PATH}/nginx/conf.d/default.conf /tmp/nginx-ssl-backup.conf || true" 2>/dev/null
|
|
|
|
rsync -avz --delete \
|
|
-e "${rsync_ssh}" \
|
|
--exclude='.env*' \
|
|
--exclude='env.example' \
|
|
--exclude='.cert/' \
|
|
--exclude='*.pem' \
|
|
--exclude='.DS_Store' \
|
|
--exclude='__pycache__' \
|
|
--exclude='*.pyc' \
|
|
"${PROJECT_DIR}/server/" "${SERVER_USER}@${SERVER_HOST}:${REMOTE_PATH}/"
|
|
|
|
# Restore SSL nginx config if it was backed up
|
|
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "test -f /tmp/nginx-ssl-backup.conf && \
|
|
cp /tmp/nginx-ssl-backup.conf ${REMOTE_PATH}/nginx/conf.d/default.conf && \
|
|
rm /tmp/nginx-ssl-backup.conf && echo 'SSL nginx config restored' || true" 2>/dev/null
|
|
|
|
log_info "Server files deployed successfully"
|
|
}
|
|
|
|
deploy_env_file() {
|
|
log_info "Deploying server environment file..."
|
|
|
|
local env_file_path="${PROJECT_DIR}/${ENV_FILE}"
|
|
|
|
if [ ! -f "${env_file_path}" ]; then
|
|
log_error "Environment file not found: ${env_file_path}"
|
|
log_error ""
|
|
log_error "Please create it first:"
|
|
log_error " cp server/env.example server/.env.prod"
|
|
log_error " # Edit server/.env.prod with your configuration"
|
|
return 1
|
|
fi
|
|
|
|
if ! grep -q "^GNSS_SERVER_DATABASE_URL=" "${env_file_path}" || \
|
|
grep -q "your-password\|your-rds-endpoint" "${env_file_path}"; then
|
|
log_warn "Environment file may have placeholder values!"
|
|
log_warn "Please ensure GNSS_SERVER_DATABASE_URL is properly configured."
|
|
fi
|
|
|
|
scp_cmd "${env_file_path}" "${SERVER_USER}@${SERVER_HOST}:${REMOTE_PATH}/.env.prod"
|
|
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "chmod 600 ${REMOTE_PATH}/.env.prod"
|
|
|
|
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "cat >> ${REMOTE_PATH}/.env.prod << EOF
|
|
|
|
# SSL settings (added by deploy script)
|
|
LETSENCRYPT_EMAIL=${LETSENCRYPT_EMAIL}
|
|
EOF"
|
|
|
|
log_info "Environment file deployed"
|
|
}
|
|
|
|
clean_docker() {
|
|
log_info "Cleaning Docker build cache and unused images..."
|
|
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "docker system prune -f && docker builder prune -f 2>/dev/null || true"
|
|
log_info "Docker cache cleaned"
|
|
}
|
|
|
|
stop_server() {
|
|
log_info "Stopping Docker containers..."
|
|
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "cd ${REMOTE_PATH} && docker compose down 2>/dev/null || true"
|
|
log_info "Containers stopped"
|
|
}
|
|
|
|
build_and_start() {
|
|
local CLEAN_BUILD=$1
|
|
|
|
ensure_database
|
|
|
|
if [ "$CLEAN_BUILD" = "true" ]; then
|
|
clean_docker
|
|
fi
|
|
|
|
log_info "Building and starting Docker containers..."
|
|
|
|
if ! ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "cd ${REMOTE_PATH} && docker compose up -d --build" 2>&1; then
|
|
log_warn "Build failed, attempting clean rebuild..."
|
|
clean_docker
|
|
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "cd ${REMOTE_PATH} && docker compose build --no-cache && docker compose up -d"
|
|
fi
|
|
|
|
sleep 5
|
|
|
|
log_info "Container status:"
|
|
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "cd ${REMOTE_PATH} && docker compose ps"
|
|
}
|
|
|
|
setup_ssl() {
|
|
log_info "Setting up SSL certificate with Let's Encrypt..."
|
|
|
|
if [ -z "${SERVER_DOMAIN}" ] || [ "${SERVER_DOMAIN}" = "gnss.yourdomain.com" ]; then
|
|
log_error "SERVER_DOMAIN not configured properly"
|
|
log_error "Edit deploy_server.sh and set SERVER_DOMAIN"
|
|
return 1
|
|
fi
|
|
|
|
log_info "Requesting SSL certificate for ${SERVER_DOMAIN}..."
|
|
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "cd ${REMOTE_PATH} && docker compose run --rm --entrypoint certbot certbot certonly --webroot --webroot-path=/var/www/certbot --email ${LETSENCRYPT_EMAIL} --agree-tos --no-eff-email --non-interactive -d ${SERVER_DOMAIN}"
|
|
|
|
log_info "Configuring nginx for SSL..."
|
|
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "cd ${REMOTE_PATH}/nginx/conf.d && \
|
|
cp gnss-guard-ssl.conf.template gnss-guard-ssl.conf && \
|
|
sed -i 's/YOUR_DOMAIN_HERE/${SERVER_DOMAIN}/g' gnss-guard-ssl.conf && \
|
|
rm -f default.conf && \
|
|
mv gnss-guard-ssl.conf default.conf"
|
|
|
|
log_info "Reloading nginx..."
|
|
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "cd ${REMOTE_PATH} && docker compose restart nginx"
|
|
|
|
log_info "SSL setup complete!"
|
|
log_info "Your server is now accessible at: https://${SERVER_DOMAIN}"
|
|
}
|
|
|
|
show_status() {
|
|
log_info "Server status:"
|
|
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "cd ${REMOTE_PATH} && docker compose ps"
|
|
}
|
|
|
|
show_logs() {
|
|
local FOLLOW=$1
|
|
if [ "$FOLLOW" = "true" ]; then
|
|
log_info "Following server logs (Ctrl+C to stop)..."
|
|
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "cd ${REMOTE_PATH} && docker compose logs -f gnss-server"
|
|
else
|
|
log_info "Recent server logs:"
|
|
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "cd ${REMOTE_PATH} && docker compose logs --tail=100 gnss-server"
|
|
fi
|
|
}
|
|
|
|
show_rds_instructions() {
|
|
echo ""
|
|
echo "============================================================================="
|
|
echo "AWS RDS PostgreSQL Setup Instructions"
|
|
echo "============================================================================="
|
|
echo ""
|
|
echo "1. Create an RDS PostgreSQL instance:"
|
|
echo " - Engine: PostgreSQL 15+"
|
|
echo " - Instance class: db.t3.micro (free tier) or larger"
|
|
echo " - Storage: 20GB General Purpose SSD"
|
|
echo " - VPC: Same as your EC2 instance"
|
|
echo " - Public access: No (recommended)"
|
|
echo ""
|
|
echo "2. Configure Security Group:"
|
|
echo " - Allow inbound PostgreSQL (port 5432) from EC2 security group"
|
|
echo ""
|
|
echo "3. Create database and user (connect from EC2 instance):"
|
|
echo " psql -h YOUR_RDS_ENDPOINT -U postgres"
|
|
echo " CREATE DATABASE gnss_guard;"
|
|
echo " CREATE USER gnss_admin WITH PASSWORD 'YOUR_SECURE_PASSWORD';"
|
|
echo " GRANT ALL PRIVILEGES ON DATABASE gnss_guard TO gnss_admin;"
|
|
echo ""
|
|
echo "4. Create and configure your environment file:"
|
|
echo " cp server/env.example server/.env.prod"
|
|
echo " # Edit server/.env.prod and set GNSS_SERVER_DATABASE_URL"
|
|
echo ""
|
|
echo "============================================================================="
|
|
}
|
|
|
|
show_ec2_instructions() {
|
|
echo ""
|
|
echo "============================================================================="
|
|
echo "AWS EC2 Setup Instructions"
|
|
echo "============================================================================="
|
|
echo ""
|
|
echo "1. Launch EC2 instance:"
|
|
echo " - AMI: Debian 12 (or Ubuntu 22.04)"
|
|
echo " - Instance type: t3.micro (free tier) or t3.small"
|
|
echo " - Storage: 20GB"
|
|
echo ""
|
|
echo "2. Configure Security Group - Allow inbound:"
|
|
echo " - SSH (22) from your IP"
|
|
echo " - HTTP (80) from anywhere"
|
|
echo " - HTTPS (443) from anywhere"
|
|
echo " - PostgreSQL (5432) to/from your RDS security group"
|
|
echo ""
|
|
echo "3. Connect and test:"
|
|
echo " ssh -i your-key.pem admin@YOUR_EC2_IP"
|
|
echo ""
|
|
echo "4. Configure this script:"
|
|
echo " SERVER_HOST=\"YOUR_EC2_IP\""
|
|
echo " SERVER_USER=\"admin\" # or 'ubuntu' for Ubuntu AMI"
|
|
echo ""
|
|
echo "============================================================================="
|
|
}
|
|
|
|
# =============================================================================
|
|
# MAIN
|
|
# =============================================================================
|
|
|
|
show_fail2ban_status() {
|
|
log_info "fail2ban status:"
|
|
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "sudo fail2ban-client status 2>/dev/null || echo 'fail2ban not installed'"
|
|
echo ""
|
|
log_info "Jail details:"
|
|
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "sudo fail2ban-client status sshd 2>/dev/null || true"
|
|
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "sudo fail2ban-client status nginx-login 2>/dev/null || true"
|
|
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "sudo fail2ban-client status nginx-badbots 2>/dev/null || true"
|
|
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "sudo fail2ban-client status nginx-ratelimit 2>/dev/null || true"
|
|
}
|
|
|
|
# =============================================================================
|
|
# LOCAL MODE - Run server locally with converted client database
|
|
# =============================================================================
|
|
|
|
run_local() {
|
|
local asset_name="${1:-Local Asset}"
|
|
local client_db="${PROJECT_DIR}/data/gnss_guard.db"
|
|
local server_db="${PROJECT_DIR}/server/data/server_local.db"
|
|
local env_file="${PROJECT_DIR}/server/.env.local"
|
|
|
|
log_info "Running server in local mode..."
|
|
|
|
# Check if client database exists
|
|
if [ ! -f "${client_db}" ]; then
|
|
log_error "Client database not found: ${client_db}"
|
|
log_error "Please ensure data/gnss_guard.db exists (from client installation)"
|
|
return 1
|
|
fi
|
|
|
|
# Check for Python
|
|
if ! command -v python3 &> /dev/null; then
|
|
log_error "Python 3 is required but not found"
|
|
return 1
|
|
fi
|
|
|
|
# Create server data directory
|
|
mkdir -p "${PROJECT_DIR}/server/data"
|
|
|
|
# Convert client database to server format
|
|
log_info "Converting client database to server format..."
|
|
log_info " Asset name: ${asset_name}"
|
|
|
|
if ! python3 "${PROJECT_DIR}/server/tools/import_client_db_to_sqlite.py" \
|
|
--asset-name "${asset_name}" \
|
|
--input "${client_db}" \
|
|
--output "${server_db}"; then
|
|
log_error "Database conversion failed"
|
|
return 1
|
|
fi
|
|
|
|
echo ""
|
|
|
|
# Create local environment file
|
|
log_info "Creating local environment file..."
|
|
cat > "${env_file}" << EOF
|
|
# Local server configuration (auto-generated)
|
|
# SQLite database for local testing
|
|
|
|
GNSS_SERVER_DATABASE_URL=sqlite:///${server_db}
|
|
GNSS_SERVER_WEB_USERNAME=test
|
|
GNSS_SERVER_WEB_PASSWORD=Tototheo.25!
|
|
GNSS_SERVER_SECRET_KEY=local-dev-secret-key-change-in-production
|
|
GNSS_SERVER_DEBUG=true
|
|
GNSS_SERVER_HOST=127.0.0.1
|
|
GNSS_SERVER_PORT=8000
|
|
EOF
|
|
|
|
log_info "Environment file created: ${env_file}"
|
|
echo ""
|
|
|
|
# Install server dependencies if needed
|
|
if [ -f "${PROJECT_DIR}/server/requirements.txt" ]; then
|
|
log_info "Checking server dependencies..."
|
|
pip3 install -q -r "${PROJECT_DIR}/server/requirements.txt" 2>/dev/null || {
|
|
log_warn "Some dependencies may not be installed. Running anyway..."
|
|
}
|
|
fi
|
|
|
|
echo ""
|
|
log_info "=============================================="
|
|
log_info "Starting local GNSS Guard Server"
|
|
log_info "=============================================="
|
|
echo ""
|
|
log_info "Dashboard: http://localhost:8000"
|
|
log_info "Login: admin / localadmin123"
|
|
echo ""
|
|
log_info "Press Ctrl+C to stop the server"
|
|
echo ""
|
|
|
|
# Change to server directory and run
|
|
cd "${PROJECT_DIR}/server"
|
|
|
|
# Set environment file path
|
|
export ENV_FILE="${env_file}"
|
|
|
|
# Run the server using uvicorn
|
|
python3 -m uvicorn main:app --host 127.0.0.1 --port 8000 --reload --env-file "${env_file}"
|
|
}
|
|
|
|
unban_ip() {
|
|
local ip=$1
|
|
log_info "Unbanning IP ${ip} from all jails..."
|
|
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "sudo fail2ban-client unban ${ip} 2>/dev/null || echo 'IP not banned or fail2ban not running'"
|
|
log_info "Done"
|
|
}
|
|
|
|
# =============================================================================
|
|
# IMPORT CLIENT DATABASES - Import .db files from server/import/ to PostgreSQL
|
|
# =============================================================================
|
|
# Filename format: {asset_id}_{asset_name}.db
|
|
# Example: 2_msc_charlotte.db -> Asset ID: 2, Asset Name: "MSC Charlotte"
|
|
# =============================================================================
|
|
|
|
import_client_databases() {
|
|
local import_dir="${PROJECT_DIR}/server/import"
|
|
local import_script="${PROJECT_DIR}/server/tools/import_client_db_to_postgres.py"
|
|
|
|
# Check if import directory exists and has .db files
|
|
if [ ! -d "${import_dir}" ]; then
|
|
log_info "No import directory found at server/import/, skipping import"
|
|
return 0
|
|
fi
|
|
|
|
local db_files=$(find "${import_dir}" -maxdepth 1 -name "*.db" -type f 2>/dev/null)
|
|
if [ -z "${db_files}" ]; then
|
|
log_info "No .db files found in server/import/, skipping import"
|
|
return 0
|
|
fi
|
|
|
|
log_info "Found client databases to import:"
|
|
echo "${db_files}" | while read -r f; do
|
|
echo " - $(basename "$f")"
|
|
done
|
|
echo ""
|
|
|
|
# Get database URL from env file on server
|
|
local db_url=$(ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "grep '^GNSS_SERVER_DATABASE_URL=' ${REMOTE_PATH}/.env.prod 2>/dev/null | cut -d'=' -f2- | tr -d '\"' | tr -d \"'\"")
|
|
|
|
if [ -z "${db_url}" ]; then
|
|
log_error "GNSS_SERVER_DATABASE_URL not found in .env.prod, cannot import databases"
|
|
return 1
|
|
fi
|
|
|
|
# Create import directory on server
|
|
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "mkdir -p ${REMOTE_PATH}/import"
|
|
|
|
# Copy import script to server
|
|
log_info "Deploying import script to server..."
|
|
scp_cmd "${import_script}" "${SERVER_USER}@${SERVER_HOST}:${REMOTE_PATH}/import/"
|
|
|
|
# Install psycopg2 on server if not present
|
|
log_info "Ensuring psycopg2-binary is installed on server..."
|
|
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "sudo apt-get update -qq && sudo apt-get install -y -qq python3-pip libpq-dev > /dev/null 2>&1; sudo pip3 install --break-system-packages psycopg2-binary 2>/dev/null || sudo pip3 install psycopg2-binary 2>/dev/null || pip3 install psycopg2-binary"
|
|
|
|
# Process each .db file
|
|
echo "${db_files}" | while read -r db_file; do
|
|
local filename=$(basename "${db_file}")
|
|
|
|
# Parse filename: {asset_id}_{asset_name}.db
|
|
# Example: 2_msc_charlotte.db -> asset_id=2, asset_name="MSC Charlotte"
|
|
local asset_id=$(echo "${filename}" | sed -n 's/^\([0-9]*\)_.*/\1/p')
|
|
local asset_name_raw=$(echo "${filename}" | sed -n 's/^[0-9]*_\(.*\)\.db$/\1/p')
|
|
|
|
if [ -z "${asset_id}" ] || [ -z "${asset_name_raw}" ]; then
|
|
log_warn "Could not parse filename '${filename}' (expected format: {id}_{name}.db)"
|
|
log_warn "Skipping this file..."
|
|
continue
|
|
fi
|
|
|
|
# Convert asset name: msc_charlotte -> MSC Charlotte
|
|
# Replace underscores with spaces, capitalize each word
|
|
local asset_name=$(echo "${asset_name_raw}" | tr '_' ' ' | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) tolower(substr($i,2))}1')
|
|
# Handle uppercase acronyms like MSC (use space/start boundaries for compatibility)
|
|
asset_name=$(echo "${asset_name}" | sed 's/^Msc /MSC /g; s/ Msc / MSC /g; s/ Msc$/ MSC/g; s/^Msc$/MSC/g')
|
|
|
|
log_info "Importing: ${filename}"
|
|
log_info " Asset ID: ${asset_id}"
|
|
log_info " Asset Name: ${asset_name}"
|
|
|
|
# Copy database file to server
|
|
scp_cmd "${db_file}" "${SERVER_USER}@${SERVER_HOST}:${REMOTE_PATH}/import/"
|
|
|
|
# Run import script on server
|
|
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "cd ${REMOTE_PATH}/import && python3 import_client_db_to_postgres.py \
|
|
--input '${filename}' \
|
|
--asset-name '${asset_name}' \
|
|
--asset-id ${asset_id} \
|
|
--database-url '${db_url}'"
|
|
|
|
if [ $? -eq 0 ]; then
|
|
log_info "Successfully imported ${filename}"
|
|
# Optionally move processed file to .imported
|
|
# ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "mv ${REMOTE_PATH}/import/${filename} ${REMOTE_PATH}/import/${filename}.imported"
|
|
else
|
|
log_error "Failed to import ${filename}"
|
|
fi
|
|
|
|
echo ""
|
|
done
|
|
|
|
log_info "Database import complete"
|
|
}
|
|
|
|
show_help() {
|
|
echo "Usage: $0 [OPTIONS]"
|
|
echo ""
|
|
echo "GNSS Guard Server Deployment Script (Docker + Nginx)"
|
|
echo ""
|
|
echo "Options:"
|
|
echo " --start, -s Deploy and start server (default if no options)"
|
|
echo " --debug, -d Deploy and follow logs in terminal (foreground mode)"
|
|
echo " --ssl Setup SSL certificate with Let's Encrypt"
|
|
echo " --clean, -c Clean Docker cache before building"
|
|
echo " --stop Stop server containers"
|
|
echo " --restart Restart server containers"
|
|
echo " --status Show container status"
|
|
echo " --logs Show recent server logs"
|
|
echo " --logs-all Show all container logs"
|
|
echo " --fail2ban Show fail2ban status and banned IPs"
|
|
echo " --unban IP Unban a specific IP address"
|
|
echo " --import Import client databases from server/import/ to PostgreSQL"
|
|
echo " --local [NAME] Run server locally with converted client database"
|
|
echo " --rds Show RDS setup instructions"
|
|
echo " --ec2 Show EC2 setup instructions"
|
|
echo " --help, -h Show this help message"
|
|
echo ""
|
|
echo "Environment variables:"
|
|
echo " SERVER_USER SSH username (default: admin)"
|
|
echo " SERVER_HOST Server hostname/IP (default: from config)"
|
|
echo " SERVER_PORT SSH port (default: 22)"
|
|
echo " ENV_FILE Path to env file (default: server/.env.prod)"
|
|
echo ""
|
|
echo "Examples:"
|
|
echo " $0 Deploy and start server (production mode)"
|
|
echo " $0 --debug Deploy and follow logs in terminal"
|
|
echo " $0 --ssl Setup SSL certificate after deployment"
|
|
echo " $0 --clean Deploy with clean Docker cache"
|
|
echo " $0 --status Check server status"
|
|
echo " $0 --local Run locally with data/gnss_guard.db"
|
|
echo " $0 --local 'MSC Charlotte' Run locally with custom asset name"
|
|
echo " $0 --import Import databases from server/import/"
|
|
echo ""
|
|
echo "Import database format:"
|
|
echo " Place .db files in server/import/ with format: {id}_{name}.db"
|
|
echo " Example: 2_msc_charlotte.db -> Asset ID: 2, Name: 'MSC Charlotte'"
|
|
echo ""
|
|
echo "First-time deployment:"
|
|
echo " 1. Setup AWS: $0 --ec2 (follow instructions)"
|
|
echo " 2. Setup RDS: $0 --rds (follow instructions)"
|
|
echo " 3. Create env file: cp server/env.example server/.env.prod"
|
|
echo " 4. Edit server/.env.prod with your configuration"
|
|
echo " 5. Edit CONFIGURATION section in this script"
|
|
echo " 6. Run: $0"
|
|
echo " 7. Configure DNS to point ${SERVER_DOMAIN} to your server"
|
|
echo " 8. Run: $0 --ssl"
|
|
echo ""
|
|
echo "Access:"
|
|
echo " Before SSL: http://${SERVER_HOST}"
|
|
echo " After SSL: https://${SERVER_DOMAIN}"
|
|
echo ""
|
|
}
|
|
|
|
# Parse options
|
|
DEBUG_MODE=false
|
|
CLEAN_BUILD=false
|
|
SSL_SETUP=false
|
|
STOP_ONLY=false
|
|
RESTART_ONLY=false
|
|
STATUS_ONLY=false
|
|
LOGS_ONLY=false
|
|
LOGS_ALL=false
|
|
SHOW_RDS=false
|
|
SHOW_EC2=false
|
|
SHOW_FAIL2BAN=false
|
|
UNBAN_IP=""
|
|
START_MODE=true
|
|
LOCAL_MODE=false
|
|
LOCAL_ASSET_NAME=""
|
|
IMPORT_ONLY=false
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--start|-s)
|
|
START_MODE=true
|
|
shift
|
|
;;
|
|
--debug|-d)
|
|
DEBUG_MODE=true
|
|
START_MODE=true
|
|
shift
|
|
;;
|
|
--ssl)
|
|
SSL_SETUP=true
|
|
START_MODE=false
|
|
shift
|
|
;;
|
|
--clean|-c)
|
|
CLEAN_BUILD=true
|
|
shift
|
|
;;
|
|
--stop)
|
|
STOP_ONLY=true
|
|
START_MODE=false
|
|
shift
|
|
;;
|
|
--restart)
|
|
RESTART_ONLY=true
|
|
START_MODE=false
|
|
shift
|
|
;;
|
|
--status)
|
|
STATUS_ONLY=true
|
|
START_MODE=false
|
|
shift
|
|
;;
|
|
--logs)
|
|
LOGS_ONLY=true
|
|
START_MODE=false
|
|
shift
|
|
;;
|
|
--logs-all)
|
|
LOGS_ALL=true
|
|
START_MODE=false
|
|
shift
|
|
;;
|
|
--fail2ban)
|
|
SHOW_FAIL2BAN=true
|
|
START_MODE=false
|
|
shift
|
|
;;
|
|
--import)
|
|
IMPORT_ONLY=true
|
|
START_MODE=false
|
|
shift
|
|
;;
|
|
--local)
|
|
LOCAL_MODE=true
|
|
START_MODE=false
|
|
# Check for optional asset name
|
|
if [[ -n "$2" && ! "$2" =~ ^-- ]]; then
|
|
LOCAL_ASSET_NAME="$2"
|
|
shift
|
|
fi
|
|
shift
|
|
;;
|
|
--unban)
|
|
if [[ -n "$2" && ! "$2" =~ ^-- ]]; then
|
|
UNBAN_IP="$2"
|
|
START_MODE=false
|
|
shift 2
|
|
else
|
|
log_error "--unban requires an IP address"
|
|
exit 1
|
|
fi
|
|
;;
|
|
--rds)
|
|
SHOW_RDS=true
|
|
START_MODE=false
|
|
shift
|
|
;;
|
|
--ec2)
|
|
SHOW_EC2=true
|
|
START_MODE=false
|
|
shift
|
|
;;
|
|
--help|-h)
|
|
show_help
|
|
exit 0
|
|
;;
|
|
*)
|
|
log_error "Unknown option: $1"
|
|
echo "Use --help for usage information"
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Handle info-only commands (no connection needed)
|
|
if [ "$SHOW_RDS" = "true" ]; then
|
|
show_rds_instructions
|
|
exit 0
|
|
fi
|
|
|
|
if [ "$SHOW_EC2" = "true" ]; then
|
|
show_ec2_instructions
|
|
exit 0
|
|
fi
|
|
|
|
# Test connection for all other operations
|
|
test_connection || exit 1
|
|
|
|
# Handle single operations
|
|
if [ "$STOP_ONLY" = "true" ]; then
|
|
stop_server
|
|
exit 0
|
|
fi
|
|
|
|
if [ "$RESTART_ONLY" = "true" ]; then
|
|
stop_server
|
|
build_and_start "$CLEAN_BUILD"
|
|
exit 0
|
|
fi
|
|
|
|
if [ "$STATUS_ONLY" = "true" ]; then
|
|
show_status
|
|
exit 0
|
|
fi
|
|
|
|
if [ "$LOGS_ONLY" = "true" ]; then
|
|
show_logs "true"
|
|
exit 0
|
|
fi
|
|
|
|
if [ "$LOGS_ALL" = "true" ]; then
|
|
log_info "Following all container logs (Ctrl+C to stop)..."
|
|
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "cd ${REMOTE_PATH} && docker compose logs -f"
|
|
exit 0
|
|
fi
|
|
|
|
if [ "$SHOW_FAIL2BAN" = "true" ]; then
|
|
show_fail2ban_status
|
|
exit 0
|
|
fi
|
|
|
|
if [ -n "$UNBAN_IP" ]; then
|
|
unban_ip "$UNBAN_IP"
|
|
exit 0
|
|
fi
|
|
|
|
if [ "$LOCAL_MODE" = "true" ]; then
|
|
run_local "$LOCAL_ASSET_NAME"
|
|
exit 0
|
|
fi
|
|
|
|
if [ "$IMPORT_ONLY" = "true" ]; then
|
|
import_client_databases
|
|
exit 0
|
|
fi
|
|
|
|
if [ "$SSL_SETUP" = "true" ]; then
|
|
setup_ssl
|
|
exit 0
|
|
fi
|
|
|
|
# Full deployment
|
|
log_info "Starting deployment to ${SERVER_USER}@${SERVER_HOST}"
|
|
log_info "Target: ${REMOTE_PATH}"
|
|
|
|
install_docker
|
|
stop_server
|
|
deploy_server_files
|
|
deploy_env_file || exit 1
|
|
build_and_start "$CLEAN_BUILD"
|
|
|
|
# Install fail2ban after containers are running (so nginx logs exist)
|
|
install_fail2ban
|
|
|
|
# Import client databases from server/import/ if any exist
|
|
import_client_databases
|
|
|
|
echo ""
|
|
log_info "Deployment completed successfully!"
|
|
echo ""
|
|
log_info "Server management commands:"
|
|
log_info " Status: $0 --status"
|
|
log_info " Stop: $0 --stop"
|
|
log_info " Restart: $0 --restart"
|
|
log_info " Logs: $0 --logs"
|
|
echo ""
|
|
|
|
if [ "$DEBUG_MODE" = "true" ]; then
|
|
log_info "Starting debug mode (following logs)..."
|
|
log_info "Press Ctrl+C to stop"
|
|
echo ""
|
|
show_logs "true"
|
|
else
|
|
log_info "GNSS Guard Server is now running!"
|
|
echo ""
|
|
log_info "Next steps:"
|
|
log_info " 1. Ensure DNS is configured: ${SERVER_DOMAIN} -> ${SERVER_HOST}"
|
|
log_info " 2. Run: $0 --ssl"
|
|
log_info " 3. Access dashboard: https://${SERVER_DOMAIN}"
|
|
echo ""
|
|
log_info "Import assets:"
|
|
log_info " curl -X POST https://${SERVER_DOMAIN}/api/v1/admin/assets/import/batch \\"
|
|
log_info " -H 'Content-Type: application/json' \\"
|
|
log_info " -d @.configs/assets.csv"
|
|
fi
|
|
|
|
|
|
# ============================================================================
|
|
# QUICK REFERENCE COMMANDS
|
|
# ============================================================================
|
|
#
|
|
# Deploy and manage server:
|
|
# ./deploy_server.sh # Deploy and start (production)
|
|
# ./deploy_server.sh --debug # Deploy and follow logs
|
|
# ./deploy_server.sh --clean # Deploy with clean Docker cache
|
|
# ./deploy_server.sh --ssl # Setup SSL certificate
|
|
#
|
|
# Server management:
|
|
# ./deploy_server.sh --status # Show container status
|
|
# ./deploy_server.sh --stop # Stop containers
|
|
# ./deploy_server.sh --restart # Restart containers
|
|
# ./deploy_server.sh --logs # Follow server logs
|
|
# ./deploy_server.sh --logs-all # Follow all container logs
|
|
#
|
|
# Security (fail2ban):
|
|
# ./deploy_server.sh --fail2ban # Show fail2ban status and banned IPs
|
|
# ./deploy_server.sh --unban IP # Unban a specific IP address
|
|
#
|
|
# Local development:
|
|
# ./deploy_server.sh --local # Run locally with data/gnss_guard.db
|
|
# ./deploy_server.sh --local 'My Asset' # Run locally with custom asset name
|
|
#
|
|
# Import client databases:
|
|
# ./deploy_server.sh --import # Import .db files from server/import/
|
|
# # Filename format: {id}_{name}.db (e.g., 2_msc_charlotte.db)
|
|
#
|
|
# Setup instructions:
|
|
# ./deploy_server.sh --ec2 # EC2 setup instructions
|
|
# ./deploy_server.sh --rds # RDS setup instructions
|
|
#
|
|
# Direct SSH access:
|
|
# ssh -i server/.cert/Cortex-01.pem admin@gnss.tototheo.com
|
|
#
|
|
# Docker commands on server:
|
|
# docker compose ps # Container status
|
|
# docker compose logs -f # Follow all logs
|
|
# docker compose exec gnss-server bash # Shell into server
|
|
#
|
|
# ============================================================================
|