diff --git a/docker/backend/Dockerfile b/docker/backend/Dockerfile index 0fad5e1d..8099d540 100644 --- a/docker/backend/Dockerfile +++ b/docker/backend/Dockerfile @@ -61,16 +61,13 @@ ENTRYPOINT ["./entrypoint.sh"] # ========================================================= FROM owasp/modsecurity-crs:nginx-alpine AS waf -USER root +USER nginx # Logs ModSecurity RUN mkdir -p /var/log/modsec && chown -R nginx:nginx /var/log/modsec -# Certificats SSL -RUN mkdir -p /etc/nginx/certs -COPY docker/backend/certs/cert.pem /etc/nginx/certs/cert.pem -COPY docker/backend/certs/key.pem /etc/nginx/certs/key.pem -RUN chown -R nginx:nginx /etc/nginx/certs && chmod 640 /etc/nginx/certs/key.pem +# Certificats SSL — montés via volume docker-compose, pas embarqués dans l'image +RUN mkdir -p /etc/nginx/certs && chown -R nginx:nginx /etc/nginx/certs COPY docker/backend/nginx.conf /etc/nginx/conf.d/app.conf COPY docker/backend/custom-rules.conf /etc/nginx/modsec/custom-rules.conf diff --git a/docker/backend/nginx.conf b/docker/backend/nginx.conf index 0ce4b572..2518edd7 100644 --- a/docker/backend/nginx.conf +++ b/docker/backend/nginx.conf @@ -1,88 +1,207 @@ # ========================================================= -# Request ID for correlation +# Request ID correlation # ========================================================= map $http_x_request_id $req_id { default $http_x_request_id; - "" $request_id; + "" $request_id; } # ========================================================= -# Backend interne (réseau Docker) +# Upstreams # ========================================================= upstream backend { server backend:8080; keepalive 32; } +upstream frontend { + server frontend:80; + keepalive 8; +} + # ========================================================= # HTTP → HTTPS redirect # ========================================================= server { listen 80; listen [::]:80; - server_name 5.181.0.112.nip.io; + server_name _; + return 301 https://$host$request_uri; } # ========================================================= -# HTTPS Server (WAF) +# HTTPS — Hardened # ========================================================= server { listen 443 ssl; listen [::]:443 ssl; - server_name 5.181.0.112.nip.io; + http2 on; + server_name _; - ssl_certificate /etc/nginx/certs/cert.pem; - ssl_certificate_key /etc/nginx/certs/key.pem; - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers HIGH:!aNULL:!MD5; + server_tokens off; - client_max_body_size 100M; + # --------------------------------------------------- + # TLS + # --------------------------------------------------- + ssl_certificate /etc/nginx/certs/fullchain.pem; + ssl_certificate_key /etc/nginx/certs/privkey.pem; + 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:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256; + ssl_prefer_server_ciphers off; + + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 1d; + ssl_session_tickets off; + + ssl_stapling on; + ssl_stapling_verify on; + ssl_trusted_certificate /etc/nginx/certs/fullchain.pem; + resolver 1.1.1.1 8.8.8.8 valid=300s; + resolver_timeout 5s; + + # --------------------------------------------------- + # En-têtes de sécurité + # --------------------------------------------------- + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "0" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=()" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; connect-src 'self' https:; frame-ancestors 'self';" always; + + # --------------------------------------------------- + # Limites et timeouts + # --------------------------------------------------- + client_max_body_size 100M; + client_body_buffer_size 128k; + client_header_buffer_size 1k; + large_client_header_buffers 4 8k; + + client_body_timeout 30s; + client_header_timeout 30s; + send_timeout 30s; + keepalive_timeout 65s; + + # --------------------------------------------------- + # Gzip (statique uniquement — évite BREACH sur l'API) + # --------------------------------------------------- + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 5; + gzip_min_length 1000; + gzip_types text/plain text/css text/javascript application/javascript application/json image/svg+xml; + + # --------------------------------------------------- + # ModSecurity WAF + # --------------------------------------------------- modsecurity on; modsecurity_rules_file /etc/nginx/modsec/custom-rules.conf; - location / { + # --------------------------------------------------- + # API backend Go + # --------------------------------------------------- + location /api/ { limit_except GET POST PUT PATCH DELETE OPTIONS { deny all; } if ($request_method = 'OPTIONS') { - add_header 'Access-Control-Allow-Origin' "$http_origin" always; - add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE, OPTIONS' always; - add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-Request-ID' always; - add_header 'Access-Control-Allow-Credentials' 'true' always; - add_header 'Access-Control-Max-Age' 86400 always; - add_header 'Content-Length' 0; - add_header 'Content-Type' 'text/plain charset=UTF-8'; + add_header 'Access-Control-Allow-Origin' "$http_origin" always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-Request-ID' always; + add_header 'Access-Control-Allow-Credentials' 'true' always; + add_header 'Access-Control-Max-Age' 86400 always; + add_header 'Content-Length' 0; + add_header 'Content-Type' 'text/plain charset=UTF-8'; return 204; } - proxy_pass http://backend; + proxy_pass http://backend; proxy_http_version 1.1; - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Request-ID $req_id; - proxy_set_header Connection ""; + proxy_set_header Connection ""; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Request-ID $req_id; + proxy_hide_header X-Powered-By; proxy_connect_timeout 60s; - proxy_send_timeout 300s; - proxy_read_timeout 300s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; + + proxy_buffer_size 128k; + proxy_buffers 4 256k; + proxy_busy_buffers_size 256k; } - add_header X-Frame-Options "SAMEORIGIN" always; - add_header X-Content-Type-Options "nosniff" always; - add_header X-XSS-Protection "1; mode=block" always; - add_header Referrer-Policy "strict-origin-when-cross-origin" always; + # --------------------------------------------------- + # Webhook Telegram + # --------------------------------------------------- + location = /webhook/telegram { + limit_except POST { deny all; } + proxy_pass http://backend; + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # --------------------------------------------------- + # Fichiers uploadés (images / vidéos) + # --------------------------------------------------- + location /uploads/ { + alias /usr/share/nginx/html/uploads/; + expires 7d; + add_header Cache-Control "public, no-transform" always; + add_header X-Content-Type-Options "nosniff" always; + + location ~* \.(php|pl|py|cgi|sh|rb|exe)$ { + deny all; + } + } + + # --------------------------------------------------- + # Frontend React SPA + # --------------------------------------------------- + location / { + proxy_pass http://frontend; + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # --------------------------------------------------- + # Blocage fichiers sensibles + # --------------------------------------------------- location ~ /\. { deny all; access_log off; log_not_found off; } - location ~* (\.env|\.git|package\.json|package-lock\.json|yarn\.lock|Dockerfile|docker-compose\.yml)$ { + location ~* \.(env|git|sql|bak|log|conf|ini|sh)$ { + deny all; + access_log off; + log_not_found off; + } + + location ~* (package\.json|package-lock\.json|yarn\.lock|Dockerfile|docker-compose)$ { + deny all; + access_log off; + log_not_found off; + } + + # Bloquer scans WordPress / PHP courants + location ~* (wp-admin|wp-login|wp-content|xmlrpc|\.php)$ { deny all; access_log off; log_not_found off; diff --git a/docker/docker-compose-prod.yml b/docker/docker-compose-prod.yml index 150e2bfd..f1dcf212 100644 --- a/docker/docker-compose-prod.yml +++ b/docker/docker-compose-prod.yml @@ -1,4 +1,5 @@ services: + # ========================================================= # Backend Go # ========================================================= @@ -26,6 +27,9 @@ services: - REDIS_PASSWORD=${REDIS_PASSWORD} - TOMTOM_API_KEY=${TOMTOM_API_KEY} - API_PORT=${API_PORT:-8080} + - TELEGRAM_WEBHOOK_URL=${TELEGRAM_WEBHOOK_URL} + - TELEGRAM_WEBHOOK_SECRET=${TELEGRAM_WEBHOOK_SECRET} + - NOWPAYMENTS_IPN_SECRET=${NOWPAYMENTS_IPN_SECRET} volumes: - backend_uploads:/app/uploads networks: @@ -37,7 +41,23 @@ services: condition: service_healthy # ========================================================= - # WAF (Nginx + ModSecurity) + # Frontend Web (React/Vite — servi en HTTP interne) + # ========================================================= + frontend: + build: + context: .. + dockerfile: docker/frontend/Dockerfile + container_name: gestion-frontend + restart: unless-stopped + networks: + - gestion-network + depends_on: + - backend + + # ========================================================= + # WAF (Nginx + ModSecurity) — point d'entrée public + # Les certificats fullchain.pem + privkey.pem doivent être + # placés dans ./certs/ à côté de ce fichier avant le deploy. # ========================================================= waf: build: @@ -56,10 +76,12 @@ services: - "443:443" volumes: - backend_uploads:/usr/share/nginx/html/uploads:ro + - ./certs:/etc/nginx/certs:ro networks: - gestion-network depends_on: - backend + - frontend # ========================================================= # PostgreSQL diff --git a/docker/frontend/Dockerfile b/docker/frontend/Dockerfile new file mode 100644 index 00000000..8594b8ff --- /dev/null +++ b/docker/frontend/Dockerfile @@ -0,0 +1,34 @@ +# ========================================================= +# Stage 1: Build React/Vite +# ========================================================= +FROM node:22-alpine AS builder + +WORKDIR /app + +COPY frontend-prep/package.json frontend-prep/package-lock.json* ./ +RUN npm ci --ignore-scripts + +COPY frontend-prep/ . +RUN npm run build + +# ========================================================= +# Stage 2: Nginx HTTP (TLS terminé par le WAF) +# ========================================================= +FROM nginx:alpine AS runtime + +COPY docker/frontend/nginx.conf /etc/nginx/conf.d/default.conf + +COPY --from=builder /app/dist /usr/share/nginx/html + +RUN chown -R nginx:nginx /usr/share/nginx/html && \ + mkdir -p /var/cache/nginx && \ + chown -R nginx:nginx /var/cache/nginx && \ + chown -R nginx:nginx /var/log/nginx && \ + touch /var/run/nginx.pid && \ + chown nginx:nginx /var/run/nginx.pid + +USER nginx + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/docker/frontend/nginx.conf b/docker/frontend/nginx.conf new file mode 100644 index 00000000..6d95b8c4 --- /dev/null +++ b/docker/frontend/nginx.conf @@ -0,0 +1,16 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } +}