# Брауз-тормоз на флуд заказами: с одного IP не чаще 10 заказов в минуту # (burst 10 сглаживает «двойной клик»). Работает ДО запроса в CRM. limit_req_zone $binary_remote_addr zone=orders_zone:1m rate=10r/m; # Общий тормоз на прочее /api/ (NF-04): флуд catalog/stoplist выедал пул БД # CRM. Легитимный сайт опрашивает эти эндпоинты раз в минуту-десять, # 120r/m с burst 60 — с большим запасом. Панель персонала ходит на CRM # напрямую (127.0.0.1:47613) и этот лимит не замечает. limit_req_zone $binary_remote_addr zone=api_zone:1m rate=120r/m; server { listen 80; server_name _; root /usr/share/nginx/html; index index.html; # Реальный IP гостя для лимитеров CRM. Доверие к X-Forwarded-For — # только 127.0.0.1 и docker-подсеть compose (NF-10): раньше верили всем # RFC1918-пирам, и LAN-клиент при экспоузе сайта мог вращать per-IP # лимитеры подделкой заголовка. Подсеть зафиксирована в # docker-compose.yml (networks.default.ipam) — при смене её там # обновите и здесь. set_real_ip_from 127.0.0.1; set_real_ip_from 172.18.0.0/16; real_ip_header X-Forwarded-For; real_ip_recursive on; # single-file сборка (~750 KB html) без gzip = лишний трафик гостям gzip on; gzip_comp_level 5; gzip_min_length 1024; gzip_vary on; gzip_types text/css application/javascript application/json image/svg+xml application/xml text/plain; server_tokens off; # Базовые заголовки безопасности. Внимание nginx: location со своим # add_header (например /media/) НЕ наследует их — там заголовки повторены. add_header X-Content-Type-Options nosniff always; add_header X-Frame-Options DENY always; add_header Referrer-Policy strict-origin-when-cross-origin always; # CSP: бандл инлайнен в single-file index.html (поэтому 'unsafe-inline' у # script-src), плюс Метрика/GA и Google Fonts. object/base/frame-ancestors # наглухо закрыты — внедрённый в страницу тег не подгрузит и не отправит. add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://mc.yandex.ru https://yastatic.net https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: https://mc.yandex.ru https://www.google-analytics.com https://www.googletagmanager.com; connect-src 'self' https://mc.yandex.ru https://www.google-analytics.com https://analytics.google.com; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'" always; # Заказы и стоп-лист живут в CRM-контейнере (см. docker-compose.yml). # Точный location = /api/orders ниже переопределяет этот для POST заказов # со своей зоной (приоритет nginx: точный бьёт префиксный). location /api/ { limit_req zone=api_zone burst=60 nodelay; limit_req_status 429; proxy_pass http://crm:47613; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_connect_timeout 5s; proxy_read_timeout 15s; } # Точка создания заказа — отдельный тормоз поверх лимитеров CRM # (nginx отвечает 429 раньше, чем запрос дойдёт до Python). location = /api/orders { limit_req zone=orders_zone burst=10 nodelay; limit_req_status 429; proxy_pass http://crm:47613; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_connect_timeout 5s; proxy_read_timeout 15s; } # Входная точка SPA: ТОЛЬКО no-cache — в single-file сборке внутри # index.html инлайнен весь бандл, закэшировать его на 7 дней = гости # не увидят обновлений сайта до конца срока кэша. (Точное совпадение # бьёт регэксп-локейшен расширений ниже; прочие .html честно 404.) location = /index.html { add_header Cache-Control "no-cache"; add_header X-Content-Type-Options nosniff always; add_header X-Frame-Options DENY always; add_header Referrer-Policy strict-origin-when-cross-origin always; add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://mc.yandex.ru https://yastatic.net https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: https://mc.yandex.ru https://www.google-analytics.com https://www.googletagmanager.com; connect-src 'self' https://mc.yandex.ru https://www.google-analytics.com https://analytics.google.com; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'" always; } # SEO-файлы: точный Content-Type и кэш не больше часа # (types {} сбрасывает mime.types — иначе .xml ушёл бы как text/xml) location = /robots.txt { types { } default_type text/plain; expires 1h; } location = /sitemap.xml { types { } default_type application/xml; expires 1h; } # Загруженные владельцем фото позиций — из CRM (/media/.). # ^~ обязателен: без него регэксп-локейшен расширений (*.jpg, *.avif…) # перехватывал бы запросы, и все фото отдавали 404 вместо прокси в CRM. location ^~ /media/ { proxy_pass http://crm:47613; proxy_http_version 1.1; proxy_set_header Host $host; expires 7d; add_header Cache-Control "public, max-age=604800"; add_header X-Content-Type-Options nosniff always; add_header X-Frame-Options DENY always; add_header Referrer-Policy strict-origin-when-cross-origin always; add_header Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'" always; } # Vite SPA fallback: client-side routes should resolve to index.html. location / { try_files $uri $uri/ /index.html; } location = /healthz { access_log off; add_header Content-Type text/plain; return 200 'ok\n'; } # Файлы с расширениями отдаются только с диска: несуществующие — честный # 404, а не index.html (soft-404 вреден для поисковиков). SPA-fallback # выше работает только для путей без расширения. location ~* \.(?:css|js|mjs|cjs|json|map|webmanifest|txt|xml|svg|ico|png|jpg|jpeg|gif|webp|avif|jfif|woff2?|ttf|otf|eot|mp4|webm|mp3|wav|ogg|pdf|html)$ { expires 7d; add_header Cache-Control "public, max-age=604800"; add_header X-Content-Type-Options nosniff always; add_header X-Frame-Options DENY always; add_header Referrer-Policy strict-origin-when-cross-origin always; add_header Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'" always; try_files $uri =404; } }