Сервер для сетки сайтов с одним или несколькими блоками /24 может обслуживать сотни сайтов, каждый на собственном IP-адресе. Прокликивать панель управления сотни раз долго, и в процессе легко ошибиться; небольшой набор шаблонов и скрипт делают то же самое за минуты и настраивают все сайты одинаково. В статье показано, как спланировать соответствие доменов и IP, не допустить «просвечивания» сайтов на чужих IP, генерировать виртуальные хосты Nginx из шаблона, массово выпускать SSL-сертификаты через acme.sh и проверять, что каждый домен отвечает на своём 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
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
В шаблоне используются плейсхолдеры, которые скрипт подставляет для каждого сайта. Каждый сайт слушает только свой 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
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 и снова запустите скрипт; существующее содержимое не перезаписывается.
Установите 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.
# 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 умеет привязывать каждый сайт к IP в настройках сайта, и для нескольких десятков сайтов этого достаточно (см. назначение отдельного IP каждому сайту). Для сотен сайтов шаблоны и скрипты быстрее и надёжнее, а aaPanel можно по-прежнему использовать для баз данных и мониторинга.
Если сайты подгружают контент или обращаются к API, исходящие запросы по умолчанию уходят с основного IP сервера. Чтобы они шли с IP каждого сайта, см. раздел об исходящем IP в статье назначение отдельного IP каждому сайту.
Для SEO-сеток распределение сайтов по нескольким блокам /24 (разным C-классам) и различия в шаблонах, аккаунтах аналитики и данных хостинга важнее, чем количество IP. См. серверы для SEO-сеток с IP из нескольких C-классов.
Если проблема не решена, создайте тикет — техническая поддержка IMIDC работает 24/7. Укажите IP сервера, версию ОС, проблемный домен и IP, а также вывод nginx -t, чтобы мы быстрее нашли причину.