От новой Ubuntu VM до сайта: HTTPS, уведомления и ротация логов
Автор: Казачкин Даниил Михайлович · Обновлено
Рабочий сервер — это воспроизводимая последовательность с контрольными точками. На учебной Ubuntu VM настроим доступ, HTTP-сайт, HTTPS и эксплуатационные проверки. Для публичного сертификата нужны собственный домен, правильные DNS-записи и доступность проверки центра сертификации; локальный пример не выпускает сертификат автоматически.
Подготовить доступ и обновления
Создайте отдельного администратора средствами Ubuntu, добавьте его публичный ключ, проверьте владельца домашнего каталога и права SSH-файлов. В новом терминале убедитесь, что работает вход и нужные команды sudo. Сохраните консоль VM, затем примените настройки SSH из [урока 47](/lessons/without-university/linux-developer-foundations/linux-developer-47). Не отключайте текущий доступ до проверки следующего.
Перед включением UFW разрешите фактический SSH-порт и проверьте сетевой экран провайдера. Для сайта нужны TCP 80 и 443; открытое правило не запускает слушающий процесс. Выполните обновление индексов APT, изучите план обновления пакетов, установите согласованные обновления и проверьте необходимость reboot. Автоматические security updates настраиваются отдельно с контролем результатов.
Сначала HTTP и точный virtual host
Установите nginx штатным пакетом. Создайте каталог сайта /srv/lab-site с читаемым index.html, разрешите пользователю Nginx проход по всем каталогам пути и создайте конфигурацию сервера:
server {
listen 80;
server_name lab.example.org;
root /srv/lab-site;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}Замените домен своим, включите файл в конфигурацию Nginx по правилам пакета. sudo nginx -t проверяет синтаксис и доступность необходимых файлов; только после успеха применяют sudo systemctl reload nginx. Проверьте curl --resolve lab.example.org:80:127.0.0.1 http://lab.example.org/ непосредственно на VM и внешний запрос с другой машины. Первый запрос проверяет выбор virtual host, но специально обходит публичный DNS. Конфигурация Nginx.
Если работает страница по умолчанию, проверьте server_name, Host запроса и полный вывод nginx -T. При 403 исследуйте права всех каталогов пути, индексный файл и политики доступа; при 404 — соответствие URI и root. Эти ошибки не исправляются одинаковым chmod 777.
HTTPS и продление
Установите Certbot способом, который поддерживает выбранная Ubuntu, вместе с интеграцией Nginx. Проверьте A/AAAA-записи: недоступный IPv6 тоже может помешать проверке домена. Для обычного HTTP-01 порт 80 должен быть доступен извне.
sudo certbot --nginx -d lab.example.org
sudo certbot certificates
sudo certbot renew --dry-run
systemctl list-timers --all | grep certbotПервая команда изменяет конфигурацию и получает настоящий сертификат для вашего домена. Не выполняйте её для example.org. После выпуска проверьте HTTPS без -k, срок действия и перенаправление HTTP. renew --dry-run использует тестовый процесс продления и позволяет проверить challenge, но не обещает, что будущая сеть и DNS останутся исправными. Способ расписания зависит от установки Certbot: это может быть timer, cron или механизм snap. Зафиксируйте именно свой. Руководство Certbot.
Уведомление: от df до подтверждённой отправки
Сначала воспроизведите весь маршрут на локальном HTTP mock, который вернёт {"ok":true,"result":{"message_id":17}}. Реальный адаптер подключают только к своему разрешённому чату. В защищённом каталоге создайте curl.conf с режимом 600:
url = "http://127.0.0.1:18081/sendMessage"Для настоящего Telegram URL заменяется на https://api.telegram.org/bot<TOKEN>/sendMessage внутри этого файла. Не передавайте токен в argv, shell history или журнал. Ограничьте доступ к каталогу, не добавляйте verbose и не публикуйте файл. chat_id сам по себе токеном аутентификации не является; всё равно избегайте ненужной публикации идентификатора.
Чтобы локальный endpoint действительно существовал, сохраните mock-telegram.py и запустите python3 mock-telegram.py 18081 в отдельном терминале:
import http.server
import json
import sys
import urllib.parse
class Handler(http.server.BaseHTTPRequestHandler):
def do_POST(self):
size = int(self.headers.get("Content-Length", "0"))
fields = urllib.parse.parse_qs(self.rfile.read(size).decode())
valid = bool(fields.get("chat_id") and fields.get("text"))
rate = self.path == "/rate"
response = {"ok": valid and self.path == "/sendMessage"}
if rate:
response["parameters"] = {"retry_after": 3}
self.send_response(429 if rate else 200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(json.dumps(response).encode())
def log_message(self, *args):
pass
server = http.server.HTTPServer(("127.0.0.1", int(sys.argv[1])), Handler)
print(server.server_address[1], flush=True)
server.serve_forever()Сначала дождитесь напечатанного порта. Mock слушает только loopback и никуда не пересылает сообщения. URL /sendMessage возвращает ok=true при обоих полях, /false — ok=false, /rate — HTTP429 с retry_after. Для каждой проверки меняйте только URL в собственном curl.conf; завершите mock через Ctrl+C после упражнения. Если порт занят, выберите свободный и одинаково измените команду запуска и URL.
Сохраните notify-disk.sh. Аргументы — порог, проверяемый путь, curl.conf и chat_id. Коды выхода: 0 — порог не превышен; 1 — превышен, сообщение принято; 2 — ошибочный ввод или замер; 3 — ошибка уведомления.
#!/usr/bin/env bash
set -uo pipefail
limit=${1:-80}
path=${2:-.}
config=${3:-}
chat_id=${4:-}
[[ $limit =~ ^(0|[1-9][0-9]?|100)$ && -r $config && -n $chat_id ]] || exit 2
if ! report=$(LC_ALL=C df -P -- "$path"); then exit 2; fi
used=$(awk 'NR==2 {gsub(/%/, "", $5); print $5}' <<< "$report")
[[ $used =~ ^[0-9]+$ ]] || exit 2
(( used >= limit )) || exit 0
notify_tmp=$(mktemp -d) || exit 2
trap 'rm -f -- "$notify_tmp/message.txt" "$notify_tmp/response.json"; rmdir -- "$notify_tmp"' EXIT
printf 'Disk usage: %s%%; threshold: %s%%; path: %s\n' \
"$used" "$limit" "$path" > "$notify_tmp/message.txt"
curl_status=0
http_status=$(curl --config "$config" --silent --show-error \
--fail-with-body --connect-timeout 3 --max-time 10 \
--output "$notify_tmp/response.json" --write-out '%{http_code}' \
--data-urlencode "chat_id=$chat_id" \
--data-urlencode "text@$notify_tmp/message.txt") || curl_status=$?
if (( curl_status != 0 )); then
printf 'Notification failed: curl=%s http=%s\n' "$curl_status" "$http_status" >&2
if [[ $http_status == 429 ]]; then
jq -r '.parameters.retry_after // "not supplied"' "$notify_tmp/response.json" >&2
fi
exit 3
fi
[[ $http_status =~ ^2[0-9][0-9]$ ]] || exit 3
jq -e '.ok == true' "$notify_tmp/response.json" >/dev/null || exit 3
printf 'Notification accepted; disk=%s%%\n' "$used"
exit 1Порог 0 на существующем пути гарантированно включает отправку в тесте. Локальный ответ HTTP 200/ok=true должен дать код 1; HTTP 200/ok=false, HTTP 429 и тайм-аут — 3; отсутствующий путь — 2 и ни одного запроса. Текст передаётся URL-кодированным вместе с chat_id. Успешный HTTP без ok=true не считается успешной отправкой. При 429 скрипт выводит retry_after, но не устраивает бесконечные повторы: расписание должно отложить следующую попытку. Между запусками полезно хранить состояние тревоги, объединять повторы и отправлять восстановление после нормализации. Inode проверяют отдельным замером df -i. Контракт sendMessage.
Ротация с настоящим переоткрытием файла
У Nginx используйте правило штатного пакета, проверив его postrotate. Для своего приложения договоритесь о поддерживаемом механизме переоткрытия. В следующей лаборатории Python-процесс реализует SIGUSR1 специально для этой задачи. Создайте writer.py:
import signal
import sys
log = open(sys.argv[1], "a", buffering=1)
def reopen(signum, frame):
global log
log.close()
log = open(sys.argv[1], "a", buffering=1)
log.write("after-reopen\n")
signal.signal(signal.SIGUSR1, reopen)
log.write("before-rotation\n")
while True:
signal.pause()Запустите его только на своём временном файле:
rotation_lab=$(mktemp -d)
python3 writer.py "$rotation_lab/app.log" &
writer_pid=$!Дождитесь строки before-rotation в app.log, чтобы обработчик сигнала успел установиться. Затем создайте правило; here-doc раскрывает только ваши временный путь и PID:
cat > "$rotation_lab/rotate.conf" <<EOF
$rotation_lab/app.log {
daily
rotate 7
compress
delaycompress
missingok
notifempty
create 0600
postrotate
kill -USR1 $writer_pid
endscript
}
EOF
logrotate --debug --state "$rotation_lab/state" "$rotation_lab/rotate.conf"
logrotate --force --state "$rotation_lab/state" "$rotation_lab/rotate.conf"Create без owner/group сохраняет их от исходного файла, а режим 0600 ограничивает доступ. После обработки сигнала старый app.log.1 должен содержать before-rotation, новый app.log — after-reopen. Проверьте stat -c '%i %a %n' "$rotation_lab/app.log" "$rotation_lab/app.log.1": разные inode и новый режим подтверждают смену файла. Завершите только свой процесс через kill -TERM "$writer_pid" и wait "$writer_pid", обработав ожидаемое завершение от сигнала.
Daily задаёт расписание ротации, rotate — число архивов, delaycompress оставляет последний архив несжатым до следующей ротации. Отдельный state-файл не затрагивает системный logrotate. Copytruncate имеет окно потери записей между копированием и обрезанием; его выбирают только при осознанных ограничениях приложения. Для реальной службы заменяют сигнал сохранённому PID на штатный механизм конкретного service manager и задают её пользователя/группу. Поведение logrotate.
Задание: сохраните доказательства HTTP/HTTPS, dry-run продления, четырёх веток уведомления и смены inode после ротации. Отметьте отдельно локальный тест протокола и настоящую доставку: успешный mock не подтверждает права бота или существование внешнего чата.
Проверить срок локального сертификата
Для локального PEM-сертификата openssl x509 -in certificate.pem -noout -enddate покажет дату окончания. openssl x509 -in certificate.pem -noout -checkend 604800 проверяет, не истечёт ли он в ближайшие семь суток, и возвращает ненулевой код при близком истечении. Этот тест не проверяет доверенную цепочку, соответствие домену и отзыв сертификата; успешный HTTPS-клиент проверяет другой набор условий. Ошибку чтения PEM также нельзя выдавать за нормальный срок действия.
Проверки OpenSSL x509 различают дату истечения и полноценную проверку доверия.