diff --git a/devops/terraform/modules/forgejo/cloud-init.yaml.tpl b/devops/terraform/modules/forgejo/cloud-init.yaml.tpl index 3cea45a..a480d8f 100644 --- a/devops/terraform/modules/forgejo/cloud-init.yaml.tpl +++ b/devops/terraform/modules/forgejo/cloud-init.yaml.tpl @@ -432,9 +432,9 @@ write_files: deploy: resources: limits: - memory: 512M + memory: 3G reservations: - memory: 256M + memory: 1G networks: forgejo-internal: @@ -471,11 +471,11 @@ write_files: } ${domain} { - # Rate limiting - 100 requests per minute per IP + # Rate limiting - 500 requests per minute per IP rate_limit { zone forgejo_zone { key {remote_host} - events 100 + events 500 window 1m } } diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index beb8759..5bbaf84 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -182,7 +182,7 @@ services: JWT_SECRET: ${JWT_SECRET} volumes: - uploads:/app/uploads:ro - - letsencrypt_data:/etc/letsencrypt:ro + - /etc/letsencrypt:/etc/letsencrypt:ro - certbot_webroot:/var/www/certbot:ro networks: - frontend @@ -203,7 +203,7 @@ services: image: certbot/certbot:latest restart: unless-stopped volumes: - - letsencrypt_data:/etc/letsencrypt + - /etc/letsencrypt:/etc/letsencrypt - certbot_webroot:/var/www/certbot entrypoint: ["/bin/sh", "-c"] command: @@ -227,5 +227,4 @@ volumes: redis_data: ome_logs: uploads: - letsencrypt_data: certbot_webroot: diff --git a/openresty/nginx.conf b/openresty/nginx.conf index 9213421..63efd3d 100644 --- a/openresty/nginx.conf +++ b/openresty/nginx.conf @@ -91,17 +91,43 @@ http { # Default for most uploads (images, stickers) client_max_body_size 5m; + # ========================================================================== + # HTTP Server - Redirect to HTTPS (except ACME challenges) + # ========================================================================== server { listen 80; - server_name localhost; + server_name _; # ACME challenge endpoint for Let's Encrypt certificate validation - # This must come before any access control blocks location /.well-known/acme-challenge/ { root /var/www/certbot; try_files $uri =404; } + # Redirect all other HTTP traffic to HTTPS + location / { + return 301 https://$host$request_uri; + } + } + + # ========================================================================== + # HTTPS Server - Main application server + # ========================================================================== + server { + listen 443 ssl http2; + server_name beeta.realms.pub; + + # SSL certificates (obtained by certbot on host, mounted via docker-compose) + ssl_certificate /etc/letsencrypt/live/beeta.realms.pub/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/beeta.realms.pub/privkey.pem; + + # Modern SSL configuration + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; + ssl_prefer_server_ciphers off; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 1d; + # Site-wide uberban check - blocks banned fingerprints from all endpoints access_by_lua_block { -- Skip OPTIONS requests (CORS preflight) @@ -114,7 +140,7 @@ http { # Security headers for the whole server add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; - + # Fixed: Serve uploaded files with correct configuration location /uploads/ { # Use root directive with absolute path to avoid alias+try_files bug diff --git a/terraform/main.tf b/terraform/main.tf index 2f2c612..d52906c 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -38,20 +38,21 @@ locals { module "app_server" { source = "./modules/app_server" - project_name = var.project_name - environment = var.environment - region = var.region - vpc_uuid = var.vpc_uuid - vpc_ip_range = var.vpc_ip_range - ssh_keys = local.all_ssh_key_ids - droplet_size = var.app_droplet_size - droplet_image = var.app_droplet_image - ssh_port = var.app_ssh_port - domain = var.app_domain - enable_backups = var.enable_droplet_backups - tags = local.common_tags - manage_dns = var.manage_dns - dns_zone = var.dns_zone - dns_record_name = var.dns_record_name - forgejo_registry = var.forgejo_registry + project_name = var.project_name + environment = var.environment + region = var.region + vpc_uuid = var.vpc_uuid + vpc_ip_range = var.vpc_ip_range + ssh_keys = local.all_ssh_key_ids + droplet_size = var.app_droplet_size + droplet_image = var.app_droplet_image + ssh_port = var.app_ssh_port + domain = var.app_domain + enable_backups = var.enable_droplet_backups + tags = local.common_tags + manage_dns = var.manage_dns + dns_zone = var.dns_zone + dns_record_name = var.dns_record_name + forgejo_registry = var.forgejo_registry + letsencrypt_email = var.letsencrypt_email } diff --git a/terraform/modules/app_server/cloud-init.yaml.tpl b/terraform/modules/app_server/cloud-init.yaml.tpl index 2087234..7f7c845 100644 --- a/terraform/modules/app_server/cloud-init.yaml.tpl +++ b/terraform/modules/app_server/cloud-init.yaml.tpl @@ -229,4 +229,20 @@ runcmd: - systemctl enable unattended-upgrades - systemctl start unattended-upgrades -final_message: "Realms app server ready after $UPTIME seconds. Deploy via Forgejo CI/CD." + # Install certbot for SSL certificates + - DEBIAN_FRONTEND=noninteractive apt-get -o DPkg::Lock::Timeout=60 install -y certbot + + # Create directories for certbot webroot + - mkdir -p /opt/realms/certbot_webroot + + # Obtain initial SSL certificate (standalone mode - no webserver running yet) + # This runs before Docker services start, so port 80 is free + - | + certbot certonly --standalone \ + --non-interactive \ + --agree-tos \ + --email ${letsencrypt_email} \ + -d ${domain} \ + || echo "Certbot failed - certificate may need to be obtained manually after DNS propagates" + +final_message: "Realms app server ready after $UPTIME seconds. SSL cert obtained for ${domain}. Deploy via Forgejo CI/CD." diff --git a/terraform/modules/app_server/main.tf b/terraform/modules/app_server/main.tf index fa0b3c4..7e27036 100644 --- a/terraform/modules/app_server/main.tf +++ b/terraform/modules/app_server/main.tf @@ -15,10 +15,11 @@ resource "digitalocean_droplet" "app" { ipv6 = true user_data = templatefile("${path.module}/cloud-init.yaml.tpl", { - ssh_port = var.ssh_port - vpc_ip_range = var.vpc_ip_range - domain = var.domain - forgejo_registry = var.forgejo_registry + ssh_port = var.ssh_port + vpc_ip_range = var.vpc_ip_range + domain = var.domain + forgejo_registry = var.forgejo_registry + letsencrypt_email = var.letsencrypt_email }) tags = var.tags diff --git a/terraform/modules/app_server/variables.tf b/terraform/modules/app_server/variables.tf index 04cad88..f14cf9b 100644 --- a/terraform/modules/app_server/variables.tf +++ b/terraform/modules/app_server/variables.tf @@ -99,3 +99,12 @@ variable "forgejo_registry" { type = string default = "qbit.realms.pub" } + +# ============================================================================= +# SSL Certificate Configuration +# ============================================================================= + +variable "letsencrypt_email" { + description = "Email for Let's Encrypt certificate notifications" + type = string +} diff --git a/terraform/variables.tf b/terraform/variables.tf index 7b38b09..da8871f 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -139,3 +139,12 @@ variable "forgejo_registry" { type = string default = "qbit.realms.pub" } + +# ============================================================================= +# SSL Certificate Configuration +# ============================================================================= + +variable "letsencrypt_email" { + description = "Email for Let's Encrypt certificate notifications" + type = string +}