lenec ru

← все посты

nginx reverse proxy для Node: рабочий конфиг с TLS

13K

На моём проекте Node-приложение слушает 127.0.0.1:3000, а наружу торчит nginx на 443. Так живёт уже несколько лет: обновления Node не трогают сертификаты, а правила лимитов и редиректов лежат рядом, в одном месте.

Покажу конфиг, который реально стоит у меня на бою, и проговорю, что в нём важно, а что я туда добавил после первой же ночной паги.

Базовый расклад

Структура простая:

  • Node слушает только локальный интерфейс: 127.0.0.1:3000;
  • nginx терминирует TLS, проксирует на Node, отдаёт статику;
  • сертификат — Let's Encrypt через certbot, лежит в /etc/letsencrypt/live/example.com/.

Файл — /etc/nginx/sites-available/example.com.conf, потом симлинк в sites-enabled/.

Сам конфиг

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://example.com$request_uri;
    }
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:10m;
    ssl_session_tickets off;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;

    add_header Strict-Transport-Security "max-age=63072000" always;
    add_header X-Content-Type-Options nosniff;
    add_header X-Frame-Options SAMEORIGIN;
    add_header Referrer-Policy strict-origin-when-cross-origin;

    client_max_body_size 10m;
    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;
    gzip_min_length 1024;

    location /_static/ {
        alias /var/www/example.com/dist/;
        expires 30d;
        access_log off;
    }

    location / {
        proxy_pass http://127.0.0.1:3000;
        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 Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_read_timeout 60s;
        proxy_send_timeout 60s;
    }
}

server {
    listen 443 ssl http2;
    server_name www.example.com;
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    return 301 https://example.com$request_uri;
}

Что здесь действительно важно

map для Upgrade

Блок map $http_upgrade $connection_upgrade в самом верху — без него WebSocket-соединения от Node-приложения работают как повезёт. С ним — стабильно: nginx отдаёт Connection: upgrade, когда клиент его запросил, и close, когда нет.

Перенаправление с www и с http

У меня одна каноническая форма — https://example.com. Всё остальное (http://, www.) уходит в 301. Это сразу решает половину будущих проблем с куками и SEO.

X-Forwarded-Proto

Express и большинство Node-фреймворков по умолчанию не знают, что они стоят за прокси. Из-за этого req.protocol отдаёт http, и тогда res.redirect('/dashboard') может уехать на http://. Лечится двумя вещами:

  • nginx ставит X-Forwarded-Proto $scheme (он у меня уже есть);
  • Express зовёт app.set('trust proxy', 1).

Без этой пары увидишь mixed content и редиректы в неожиданные места.

client_max_body_size

Дефолт у nginx — 1 МБ. Я однажды три часа искал, почему не загружается аватарка 1.5 МБ. Помни про этот параметр, если в Node-приложении есть аплоад файлов.

Статика прямо из nginx

Node-сервер отдаёт HTML/JSON, а ассеты — пусть забирает nginx сам. У меня /_static/ мапится на dist/, который собирает фронт. Это:

  • снимает с Node нагрузку по отдаче js/css;
  • включает кеш на 30 дней;
  • гасит логи в access — иначе там быстро будет каша.

В Node-приложении тогда ставлю префикс ассетов /_static/, чтобы они не путались с роутами.

HSTS, gzip, заголовки безопасности

HSTS включаю не сразу. Сначала проверяю, что HTTPS реально едет везде, и только потом ставлю max-age=63072000. Если включить с самого начала, а потом сертификат сломается — пользователи не смогут зайти даже по http.

Gzip даёт ощутимый профит на JSON-ответах от API и на html. Brotli — ещё чуть лучше, но требует отдельного модуля; если его нет в дистрибутиве, оставляю gzip и сплю спокойно.

Как я это раскатываю и проверяю

sudo nginx -t
sudo systemctl reload nginx

Перед reload я всегда смотрю на nginx -t. Один раз я случайно положил тестовый сервер, потому что ребутнул сразу. Теперь — только через тест.

Дальше — чек снаружи:

curl -I https://example.com
curl -I https://www.example.com
curl -I http://example.com

Хочется убедиться, что:

  • https-ответ — 200 (или то, что должен отдавать Node);
  • www уходит в 301 на голый домен;
  • http тоже уходит в 301 на https.

Что я бы добавил, если проект чуть больше

Когда сервис вырастает за один сервер, я обычно тащу:

  • limit_req_zone и limit_req — на эндпоинты логина и регистрации, чтобы не позволить перебор;
  • отдельный upstream с несколькими backend-инстансами Node;
  • отдельный лог-формат с временем ответа upstream — это спасает при разборе медленных запросов.

Если хочется готовых cipher-настроек, я смотрю ssl-config от Mozilla в режиме intermediate и оттуда копирую блок шифров и параметры протокола под мою версию nginx.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.