SEO-сети и мульти-IP

Сотни сайтов на сервере с несколькими IP: свой IP на сайт, шаблоны Nginx и массовый SSL

6 шагов 17 мин чтения 76 просмотров 0
Содержание

Сервер для сетки сайтов с одним или несколькими блоками /24 может обслуживать сотни сайтов, каждый на собственном IP-адресе. Прокликивать панель управления сотни раз долго, и в процессе легко ошибиться; небольшой набор шаблонов и скрипт делают то же самое за минуты и настраивают все сайты одинаково. В статье показано, как спланировать соответствие доменов и IP, не допустить «просвечивания» сайтов на чужих IP, генерировать виртуальные хосты Nginx из шаблона, массово выпускать SSL-сертификаты через acme.sh и проверять, что каждый домен отвечает на своём IP.

Предполагается, что все IP уже привязаны к серверу (см. привязка целой подсети /24 в Linux) и что A-запись каждого домена указывает на назначенный ему IP. Опробуйте скрипт на двух-трёх доменах, прежде чем запускать его для сотен.

Шаг 1: Спланируйте соответствие доменов и IP

Ведите один простой текстовый файл как единственный источник истины: по одной строке на сайт с доменом и его IP. Всё остальное генерируется из него.

mkdir -p /root/cluster
cat > /root/cluster/sites.txt <<'EOF2'
example-a.com 203.0.113.10
example-b.com 203.0.113.11
example-c.net 203.0.113.12
EOF2

# Check that every IP in the list is actually bound on the server
awk '{print $2}' /root/cluster/sites.txt | while read ip; do ip -br addr | grep -qw "$ip" || echo "NOT BOUND: $ip"; done

Шаг 2: Не дайте сайтам «просвечивать» на других IP

Nginx сначала выбирает адрес прослушивания (IP и порт), а затем сопоставляет имя хоста; на запрос с неизвестным именем хоста отвечает сервер по умолчанию (default server) этого адреса, а если он не задан — первый сайт, настроенный на этом адресе. Для сетки сайтов это значит, что любой, кто откроет голый IP или отправит на него имя хоста другого сайта, увидит один из ваших сайтов, и это связывает сайты между собой. Закрывайте такие запросы в двух местах: общий перехватчик (catch-all) для IP, на которых ещё нет сайтов (ниже), и отдельный default server для каждого IP в шаблоне (шаг 3).

cat > /etc/nginx/conf.d/00-default.conf <<'EOF2'
server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name _;
    return 444;
}
EOF2

# Hide the Nginx version in headers and error pages
sed -i 's/^\s*#\?\s*server_tokens.*/    server_tokens off;/' /etc/nginx/nginx.conf
grep -q 'server_tokens off' /etc/nginx/nginx.conf || sed -i '/http {/a\    server_tokens off;' /etc/nginx/nginx.conf

Шаг 3: Создайте шаблон виртуального хоста

В шаблоне используются плейсхолдеры, которые скрипт подставляет для каждого сайта. Каждый сайт слушает только свой IP, а запросы с любым другим именем хоста на этом IP закрываются.

mkdir -p /etc/nginx/sites-cluster /www/sites
cat > /root/cluster/vhost-http.tpl <<'EOF2'
# Unknown hostnames on this IP: close the connection
server {
    listen __IP__:80 default_server;
    server_name _;
    return 444;
}

server {
    listen __IP__:80;
    server_name __DOMAIN__ www.__DOMAIN__;
    root /www/sites/__DOMAIN__/public;
    index index.html index.php;
    access_log /www/sites/__DOMAIN__/logs/access.log;
    error_log  /www/sites/__DOMAIN__/logs/error.log;

    location /.well-known/acme-challenge/ { allow all; }
    location / { try_files $uri $uri/ =404; }
}
EOF2

# Load the generated files from the http block
grep -q 'sites-cluster' /etc/nginx/nginx.conf || sed -i '/http {/a\    include /etc/nginx/sites-cluster/*.conf;' /etc/nginx/nginx.conf

Шаг 4: Сгенерируйте все сайты одним скриптом

cat > /root/cluster/deploy.sh <<'EOF2'
#!/bin/bash
set -e
while read -r DOMAIN IP; do
  [ -z "$DOMAIN" ] && continue
  mkdir -p /www/sites/$DOMAIN/public /www/sites/$DOMAIN/logs
  [ -f /www/sites/$DOMAIN/public/index.html ] || echo "<h1>$DOMAIN</h1>" > /www/sites/$DOMAIN/public/index.html
  sed -e "s/__DOMAIN__/$DOMAIN/g" -e "s/__IP__/$IP/g" /root/cluster/vhost-http.tpl > /etc/nginx/sites-cluster/$DOMAIN.conf
done < /root/cluster/sites.txt
chown -R www-data:www-data /www/sites 2>/dev/null || chown -R nginx:nginx /www/sites
nginx -t && systemctl reload nginx
EOF2
chmod +x /root/cluster/deploy.sh && /root/cluster/deploy.sh

Загрузите содержимое каждого сайта в /www/sites/DOMAIN/public. Чтобы позже добавить сайты, допишите строки в sites.txt и снова запустите скрипт; существующее содержимое не перезаписывается.

Шаг 5: Массовый выпуск SSL-сертификатов

Установите acme.sh и выберите Let's Encrypt в качестве CA (см. бесплатный SSL через Certbot или acme.sh). Цикл ниже выпускает сертификат для каждого домена через его веб-корень, устанавливает его и добавляет HTTPS-блок server на собственном IP сайта.

cat > /root/cluster/ssl.sh <<'EOF2'
#!/bin/bash
mkdir -p /etc/nginx/ssl
while read -r DOMAIN IP; do
  [ -z "$DOMAIN" ] && continue
  [ -f /etc/nginx/ssl/$DOMAIN.crt ] && continue          # already done
  ~/.acme.sh/acme.sh --issue -d $DOMAIN -d www.$DOMAIN -w /www/sites/$DOMAIN/public || { echo "FAILED: $DOMAIN"; continue; }
  ~/.acme.sh/acme.sh --install-cert -d $DOMAIN \
     --key-file /etc/nginx/ssl/$DOMAIN.key --fullchain-file /etc/nginx/ssl/$DOMAIN.crt \
     --reloadcmd "systemctl reload nginx"
  cat >> /etc/nginx/sites-cluster/$DOMAIN.conf <<EOF3
server {
    listen $IP:443 ssl default_server;
    ssl_reject_handshake on;      # unknown hostnames on this IP (Nginx 1.19.4+)
}
server {
    listen $IP:443 ssl;
    http2 on;
    server_name $DOMAIN www.$DOMAIN;
    ssl_certificate     /etc/nginx/ssl/$DOMAIN.crt;
    ssl_certificate_key /etc/nginx/ssl/$DOMAIN.key;
    root /www/sites/$DOMAIN/public;
    index index.html index.php;
    access_log /www/sites/$DOMAIN/logs/access.log;
}
EOF3
  sleep 5
done < /root/cluster/sites.txt
nginx -t && systemctl reload nginx
EOF2
chmod +x /root/cluster/ssl.sh && /root/cluster/ssl.sh

Запускайте скрипт партиями по несколько десятков доменов: Let's Encrypt ограничивает число новых сертификатов, которые один аккаунт может заказать за короткое время. Для доменов, DNS которых ещё не указывает на нужный IP, выпуск завершится ошибкой; исправьте DNS и запустите скрипт снова — уже готовые домены он пропустит. Продление выполняется автоматически через cron-задание acme.sh.

Шаг 6: Проверьте каждый сайт

# Each domain must answer on its own IP with HTTP 200 (or 301 if you redirect to HTTPS)
while read -r DOMAIN IP; do
  code=$(curl -s -o /dev/null -w '%{http_code}' --resolve $DOMAIN:80:$IP http://$DOMAIN/)
  dns=$(dig +short $DOMAIN A | tail -1)
  echo "$DOMAIN $IP http=$code dns=$dns $( [ "$dns" = "$IP" ] && echo OK || echo DNS-MISMATCH)"
done < /root/cluster/sites.txt

# The bare IP must not show any site (connection closed = correct)
curl -sI http://203.0.113.10/ || echo "closed as expected"
curl -skI https://203.0.113.10/ || echo "handshake rejected as expected"

Частые вопросы

Можно ли сделать это в aaPanel?

Да: aaPanel умеет привязывать каждый сайт к IP в настройках сайта, и для нескольких десятков сайтов этого достаточно (см. назначение отдельного IP каждому сайту). Для сотен сайтов шаблоны и скрипты быстрее и надёжнее, а aaPanel можно по-прежнему использовать для баз данных и мониторинга.

Должен ли исходящий трафик тоже идти с IP каждого сайта?

Если сайты подгружают контент или обращаются к API, исходящие запросы по умолчанию уходят с основного IP сервера. Чтобы они шли с IP каждого сайта, см. раздел об исходящем IP в статье назначение отдельного IP каждому сайту.

Как распределять сайты по блокам IP?

Для SEO-сеток распределение сайтов по нескольким блокам /24 (разным C-классам) и различия в шаблонах, аккаунтах аналитики и данных хостинга важнее, чем количество IP. См. серверы для SEO-сеток с IP из нескольких C-классов.

Если проблема не решена, создайте тикет — техническая поддержка IMIDC работает 24/7. Укажите IP сервера, версию ОС, проблемный домен и IP, а также вывод nginx -t, чтобы мы быстрее нашли причину.

Помог ли вам данный ответ?

Похожие руководства