Перейти к основному содержимому

Настройка Nginx Reverse Proxy

·1656 слов·8 минут
DevOps • Networks • Security • Infrastructure
Автор
DevOps • Networks • Security • Infrastructure
DevOps/Network/Infra Engineer, CyberSecurity Expert

Настройка Nginx Reverse Proxy
#

Nginx часто используется как reverse proxy — промежуточный веб-сервер, который принимает HTTP/HTTPS-запросы от клиентов и перенаправляет их на внутренние приложения. При этом все приложения могут находиться на разных серверах или слушать разные локальные порты, а снаружи будут доступны через обычные DNS-имена:

https://api.example.com
https://app.example.com
https://monitoring.example.com

Reverse Proxy и Redirect
#

Перед настройкой nginx важно понимать разницу между proxy_pass и обычным HTTP-редиректом. При использовании proxy_pass клиент обращается к nginx, nginx сам отправляет запрос на backend-сервер и возвращает клиенту его ответ, при этом адрес в браузере не меняется: пользователь открывает https://api.example.com, nginx может получить данные с http://10.40.1.20:5000, но клиент об этом backend-адресе не знает.

location / {
    proxy_pass http://10.40.1.20:5000;
}

Схематично это выглядит так:

Client -> https://api.example.com -> Nginx -> http://10.40.1.20:5000
Client <-        response         <- Nginx <-        response

Redirect работает иначе: nginx не обращается к другому серверу вместо клиента, а возвращает клиенту ответ 301 или 302 с новым адресом, после чего уже браузер самостоятельно выполняет новый запрос и адрес в адресной строке меняется.

return 301 https://api.example.com$request_uri;

Например:

Client -> http://api.example.com
              |
              v
         301 Redirect
              |
              v
Client -> https://api.example.com

Поэтому типичная конфигурация reverse proxy использует оба механизма одновременно: HTTP на 80 порту перенаправляется через 301 на HTTPS, а HTTPS-запрос на 443 порту уже проксируется через proxy_pass во внутреннее приложение.

Установка Nginx
#

На Ubuntu установка выполняется через apt:

sudo apt update
sudo apt install -y nginx

Запускаем nginx и добавляем его в автозагрузку:

sudo systemctl enable --now nginx

Проверяем состояние:

systemctl status nginx

Версию nginx можно посмотреть командой:

nginx -v

Структура конфигурации
#

Основной конфигурационный файл nginx:

/etc/nginx/nginx.conf

На Ubuntu внутри блока http {} обычно подключаются дополнительные конфигурационные файлы:

include /etc/nginx/conf.d/*.conf;

Благодаря этому нет необходимости хранить конфигурацию всех сайтов внутри одного большого nginx.conf.

Для каждого сайта удобно использовать отдельный файл:

/etc/nginx/conf.d/
├── 00-default.conf
├── api.example.com.conf
├── app.example.com.conf
├── monitoring.example.com.conf
└── stage.example.com.conf

Так конфигурацию намного проще читать, изменять и переносить между серверами.

Отключение стандартного сайта Ubuntu
#

После установки nginx на Ubuntu обычно существует стандартная конфигурация:

/etc/nginx/sites-enabled/default

Если все виртуальные сайты будут храниться в /etc/nginx/conf.d/, стандартный сайт можно удалить:

sudo rm /etc/nginx/sites-enabled/default

После изменения конфигурации необходимо выполнить проверку:

sudo nginx -t

Если ошибок нет:

sudo systemctl reload nginx

Общие параметры SSL
#

Чтобы не повторять одинаковые TLS-настройки в каждом виртуальном сайте, их удобно вынести в отдельный файл:

/etc/nginx/snippets/ssl-common.conf

Создадим каталог:

sudo mkdir -p /etc/nginx/snippets

Создадим файл:

sudo nano /etc/nginx/snippets/ssl-common.conf

Пример:

ssl_session_timeout 5m;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;

Теперь в каждом сайте достаточно написать:

ssl_certificate /etc/nginx/ssl/example.com.crt;
ssl_certificate_key /etc/nginx/ssl/example.com.key;

include /etc/nginx/snippets/ssl-common.conf;

Создание default server
#

Если пользователь обращается к nginx непосредственно по IP-адресу:

https://52.50.21.40/

nginx всё равно должен выбрать какой-либо server {}.

Если явно не указать default_server, nginx использует первый подходящий виртуальный хост для данного адреса и порта.

Например:

server {
    listen 443 ssl http2;
    server_name app.example.com;
}

может случайно стать виртуальным сервером по умолчанию.

В результате запрос по IP может попасть в настоящий сайт.

Чтобы этого не происходило, создадим отдельную конфигурацию:

/etc/nginx/conf.d/00-default.conf

Создать её можно одной командой:

sudo tee /etc/nginx/conf.d/00-default.conf > /dev/null <<'EOF'
###### Default WebSites ######
server {
    listen 80 default_server;
    server_name _;

    return 444;
}

server {
    listen 443 ssl default_server;
    server_name _;

    ssl_certificate /etc/nginx/ssl/example.com.crt;
    ssl_certificate_key /etc/nginx/ssl/example.com.key;

    include /etc/nginx/snippets/ssl-common.conf;

    return 444;
}
EOF

После этого:

sudo nginx -t
sudo systemctl reload nginx

Теперь запросы к неизвестным доменам или непосредственно по IP не будут попадать в рабочие виртуальные сайты.

Что означает return 444
#

Код:

return 444;

является специальным поведением nginx.

Nginx закрывает TCP-соединение без отправки нормального HTTP-ответа.

Например:

curl http://52.50.21.40/

может вернуть:

Empty reply from server

В браузере обычно отображается ошибка вроде:

ERR_EMPTY_RESPONSE

или сообщение о том, что соединение было сброшено.

Это удобно для default virtual host, поскольку nginx вообще не раскрывает содержимое какого-либо сайта.

Как nginx выбирает виртуальный сайт
#

Рассмотрим:

listen 443 ssl http2;
server_name app.example.com;

Первая строка:

listen 443 ssl http2;

означает:

  • слушать TCP-порт 443;
  • использовать TLS;
  • разрешить HTTP/2.

Вторая строка:

server_name app.example.com;

определяет имя виртуального сайта.

Например клиент открывает:

https://app.example.com/

DNS преобразует имя в IP-адрес сервера:

app.example.com
        |
        v
52.50.21.40

После чего клиент подключается:

52.50.21.40:443

На одном IP и одном порту могут находиться десятки сайтов:

server {
    listen 443 ssl http2;
    server_name app.example.com;
}

server {
    listen 443 ssl http2;
    server_name api.example.com;
}

server {
    listen 443 ssl http2;
    server_name monitoring.example.com;
}

Nginx выбирает необходимый виртуальный сервер по имени сайта.

Если подходящего имени нет, используется default_server.

Хранение сертификатов
#

Для сертификатов можно создать отдельный каталог:

sudo mkdir -p /etc/nginx/ssl
sudo chmod 700 /etc/nginx/ssl

Например:

/etc/nginx/ssl/example.com.crt
/etc/nginx/ssl/example.com.key

Закрытый ключ желательно ограничить правами:

sudo chown root:root /etc/nginx/ssl/*
sudo chmod 600 /etc/nginx/ssl/*.key

Создание Reverse Proxy
#

Допустим, приложение работает на внутреннем сервере:

10.40.1.20:5000

и должно быть доступно как:

https://api.example.com

Создаём:

sudo nano /etc/nginx/conf.d/api.example.com.conf

Конфигурация:

###### WebSite api.example.com ######
server {
    listen 80;
    server_name api.example.com;

    access_log /var/log/nginx/api.example.com-access.log;
    error_log /var/log/nginx/api.example.com-error.log;

    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name api.example.com;

    access_log /var/log/nginx/api.example.com-ssl-access.log;
    error_log /var/log/nginx/api.example.com-ssl-error.log;

    ssl_certificate /etc/nginx/ssl/example.com.crt;
    ssl_certificate_key /etc/nginx/ssl/example.com.key;

    include /etc/nginx/snippets/ssl-common.conf;

    location / {
        proxy_pass http://10.40.1.20:5000;

        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

HTTP-запрос:

http://api.example.com

будет перенаправлен:

https://api.example.com

А HTTPS-запрос nginx отправит backend-серверу:

10.40.1.20:5000

proxy_pass
#

Главная директива reverse proxy:

proxy_pass http://10.40.1.20:5000;

Она определяет, куда nginx должен отправить запрос.

Backend может находиться локально:

proxy_pass http://localhost:5000;

на другом сервере:

proxy_pass http://10.40.1.20:8080;

или использовать HTTPS:

proxy_pass https://10.40.1.30:443;

Передача HTTP-заголовков
#

Обычно вместе с proxy_pass необходимо передавать несколько заголовков.

proxy_set_header Host $host;

Передаёт backend-серверу исходное имя сайта.

Например:

Host: api.example.com

Директива:

proxy_set_header X-Real-IP $remote_addr;

передаёт реальный IP клиента.

Например:

X-Real-IP: 203.0.113.25

Директива:

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

передаёт цепочку IP-адресов proxy-серверов и клиента.

Это необходимо, чтобы backend-приложение могло определить реальный адрес пользователя.

Проверка конфигурации
#

Перед каждым применением изменений необходимо выполнять:

nginx -t

При корректной конфигурации:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

После этого применяем изменения:

systemctl reload nginx

reload предпочтительнее restart, поскольку nginx перечитывает конфигурацию без жёсткого разрыва текущих соединений.

Автоматическое создание виртуальных сайтов
#

Чтобы каждый раз вручную не создавать однотипные конфигурационные файлы, можно написать небольшую утилиту:

/usr/local/sbin/nginx-add-site

Она будет принимать два параметра:

DOMAIN
BACKEND

Например:

nginx-add-site api.example.com http://10.40.1.20:5000

Скрипт:

#!/bin/bash

set -e

if [ "$#" -ne 2 ]; then
    echo "Usage: $0 <domain> <backend>"
    echo
    echo "Examples:"
    echo "  $0 app.example.com http://localhost:8080"
    echo "  $0 api.example.com https://10.40.1.20:8443"
    exit 1
fi

DOMAIN="$1"
BACKEND="$2"

CONF_DIR="/etc/nginx/conf.d"
CONF_FILE="${CONF_DIR}/${DOMAIN}.conf"

# ----------------------------
# Validate domain
# ----------------------------

if ! [[ "$DOMAIN" =~ ^([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$ ]]; then
    echo "ERROR: invalid domain:"
    echo "  $DOMAIN"
    exit 1
fi

# ----------------------------
# Validate backend
# ----------------------------

if ! [[ "$BACKEND" =~ ^https?:// ]]; then
    echo "ERROR: backend must start with http:// or https://"
    echo
    echo "Examples:"
    echo "  http://localhost:8080"
    echo "  https://10.40.1.20:8443"
    exit 1
fi

# ----------------------------
# Select SSL certificate
# ----------------------------

case "$DOMAIN" in
    *.example.com)
        SSL_CERT="/etc/nginx/ssl/example.com.crt"
        SSL_KEY="/etc/nginx/ssl/example.com.key"
        ;;

    *.example.net)
        SSL_CERT="/etc/nginx/ssl/example.net.crt"
        SSL_KEY="/etc/nginx/ssl/example.net.key"
        ;;

    *)
        echo "ERROR: no certificate mapping for:"
        echo "  $DOMAIN"
        exit 1
        ;;
esac

# ----------------------------
# Check files
# ----------------------------

if [ -f "$CONF_FILE" ]; then
    echo "ERROR: nginx config already exists:"
    echo "  $CONF_FILE"
    exit 1
fi

if [ ! -f "$SSL_CERT" ]; then
    echo "ERROR: certificate not found:"
    echo "  $SSL_CERT"
    exit 1
fi

if [ ! -f "$SSL_KEY" ]; then
    echo "ERROR: certificate key not found:"
    echo "  $SSL_KEY"
    exit 1
fi

# ----------------------------
# Create nginx config
# ----------------------------

cat > "$CONF_FILE" <<EOF
###### WebSite ${DOMAIN} ######
server {
    listen 80;
    server_name ${DOMAIN};

    access_log /var/log/nginx/${DOMAIN}-access.log;
    error_log /var/log/nginx/${DOMAIN}-error.log;

    return 301 https://\$server_name\$request_uri;
}

server {
    listen 443 ssl http2;
    server_name ${DOMAIN};

    access_log /var/log/nginx/${DOMAIN}-ssl-access.log;
    error_log /var/log/nginx/${DOMAIN}-ssl-error.log;

    ssl_certificate ${SSL_CERT};
    ssl_certificate_key ${SSL_KEY};

    include /etc/nginx/snippets/ssl-common.conf;

    location / {
        proxy_pass ${BACKEND};

        proxy_set_header Host \$host;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header X-Real-IP \$remote_addr;
    }
}
EOF

echo
echo "Created:"
echo "  $CONF_FILE"

echo
echo "Domain:"
echo "  $DOMAIN"

echo
echo "Backend:"
echo "  $BACKEND"

echo
echo "Certificate:"
echo "  $SSL_CERT"

# ----------------------------
# Test nginx
# ----------------------------

echo
echo "Checking nginx configuration..."

if nginx -t; then
    echo
    echo "Configuration OK."
    echo "Reloading nginx..."

    systemctl reload nginx

    echo
    echo "Site added successfully."
else
    echo
    echo "ERROR: nginx configuration test failed."
    echo "Rolling back..."

    rm -f "$CONF_FILE"

    echo
    echo "Removed:"
    echo "  $CONF_FILE"

    nginx -t

    exit 1
fi

Устанавливаем скрипт:

sudo mv nginx-add-site /usr/local/sbin/nginx-add-site

Выставляем права:

sudo chown root:root /usr/local/sbin/nginx-add-site
sudo chmod 755 /usr/local/sbin/nginx-add-site

Теперь новый reverse proxy можно создать одной командой:

sudo nginx-add-site api.example.com http://10.40.1.20:5000

Скрипт:

  1. проверит имя домена;
  2. проверит формат backend;
  3. определит сертификат;
  4. проверит наличие сертификата и ключа;
  5. создаст /etc/nginx/conf.d/api.example.com.conf;
  6. выполнит nginx -t;
  7. при успешной проверке выполнит systemctl reload nginx;
  8. при ошибке удалит созданный конфигурационный файл.

Просмотр всей конфигурации
#

Команда:

nginx -T

проверяет конфигурацию аналогично nginx -t, но дополнительно выводит всю итоговую конфигурацию nginx вместе со всеми include.

Это особенно удобно при использовании большого количества отдельных файлов.

Просмотр всех виртуальных сайтов
#

Чтобы быстро увидеть все server_name:

nginx -T 2>/dev/null | grep -n "server_name"

Чтобы посмотреть используемые порты:

nginx -T 2>/dev/null | grep -n "listen"

Для reverse proxy удобно сразу вывести основные параметры:

nginx -T 2>/dev/null | grep -n -E 'server_name|listen|proxy_pass'

Получается своего рода карта nginx:

listen 443 ssl http2;
server_name api.example.com;
proxy_pass http://10.40.1.20:5000;

listen 443 ssl http2;
server_name app.example.com;
proxy_pass http://10.40.1.30:8080;

Проверка Reverse Proxy через curl
#

Проверить HTTP:

curl -I http://api.example.com/

При включённом редиректе ожидаем:

HTTP/1.1 301 Moved Permanently
Location: https://api.example.com/

HTTPS:

curl -I https://api.example.com/

Для подробной диагностики:

curl -Iv https://api.example.com/

Если сертификат временно не доверенный:

curl -kIv https://api.example.com/

Логи
#

Основные логи nginx:

/var/log/nginx/access.log
/var/log/nginx/error.log

Но для каждого сайта лучше использовать отдельные файлы:

access_log /var/log/nginx/api.example.com-access.log;
error_log /var/log/nginx/api.example.com-error.log;

Просмотр в реальном времени:

tail -f /var/log/nginx/api.example.com-access.log

Ошибки:

tail -f /var/log/nginx/api.example.com-error.log

Можно одновременно смотреть несколько файлов:

tail -f /var/log/nginx/*error.log

Итоговая структура
#

В результате конфигурация сервера может выглядеть следующим образом:

/etc/nginx/
├── nginx.conf
├── conf.d/
│   ├── 00-default.conf
│   ├── api.example.com.conf
│   ├── app.example.com.conf
│   └── monitoring.example.com.conf
├── snippets/
│   └── ssl-common.conf
└── ssl/
    ├── example.com.crt
    └── example.com.key
    ├── example.net.crt
    └── example.net.key

А добавление нового сайта сводится к одной команде:

sudo nginx-add-site api.example.com http://10.40.1.20:5000

После чего скрипт автоматически создаёт virtual host, проверяет конфигурацию и выполняет безопасный reload nginx.

Таким образом nginx становится единой точкой входа для HTTP/HTTPS-трафика, а внутренние приложения можно размещать на отдельных серверах и портах, не публикуя их напрямую в интернет.

Related

Crontab в Linux
·861 слово·5 минут
Основные команды Docker
·1030 слов·5 минут
Journalctl в Linux
·535 слов·3 минут