Часы на сервере ушли на минуту и сломали авторизацию
Вечером всё работало, утром — нет: клиенты падают с TLS-хендшейком, API отдаёт 401 на токены, которые ещё час назад были валидны, у части пользователей не проходит вход по коду из приложения-аутентификатора. В коде и конфиге — ни одной правки за последние недели. Причина почти всегда одна и та же: системные часы сервера тихо разъехались с реальным временем, и всё, что построено на сверке временных меток, начало сыпаться разом.
Содержание
Симптомы: что именно ломается, когда время расходится
Рассинхронизация часов редко выглядит как «время неправильное» — она выглядит как набор разрозненных ошибок в разных подсистемах, и это первая причина, почему её не сразу опознают.
TLS-хендшейки. Клиент или сервер проверяет срок действия сертификата по своим локальным часам. Если часы ушли вперёд — сертификат, который на самом деле ещё действителен, воспринимается как просроченный. Если часы отстали — новый сертификат воспринимается как «ещё не действителен» (certificate is not yet valid, NotBefore). Типичные сообщения в логах:
curl: (60) SSL certificate problem: certificate is not yet valid
x509: certificate has expired or is not yet valid
SSL_ERROR_UNSAFE_NEGOTIATION / ERR_CERT_DATE_INVALID (в браузере)
Важно: сам сертификат может быть выпущен правильно и не просрочен — проблема исключительно в том, что сравнивают его срок действия с неверным локальным временем.
JWT и HMAC-подписанные токены. Практически все схемы токенов несут поля iat (issued at), exp (expiration), иногда nbf (not before). Проверяющая сторона сравнивает эти метки со своими часами. Часы сервера, который выпускает токен, ушли вперёд на минуту — и токен для всех остальных сервисов выглядит выпущенным «из будущего»; ушли назад — токен, выданный секунду назад, уже «истёк» по меркам сервиса, который его проверяет. В логах это обычно:
jwt expired at ...
Token used before issued
invalid_grant: Token is not yet valid
Особенно неприятно это в микросервисной архитектуре: у каждого сервиса свои часы, и если они разъехались даже на пару секунд между собой (не говоря о минуте), то токен, выпущенный одним сервисом, может не проходить проверку у другого — притом что сама подпись HMAC/RSA полностью корректна.
TOTP-коды двухфакторной аутентификации. Алгоритм TOTP (Google Authenticator, Authy и подобные) генерирует код из общего секрета и текущего времени, округлённого до окна (обычно 30 секунд). Сервер и приложение на телефоне независимо вычисляют один и тот же код только если их часы синхронны в пределах допустимого окна (обычно ±1 шаг, то есть около 30-60 секунд суммарно). Часы сервера, ушедшие на минуту, выталкивают его за пределы этого окна — и пользователь получает invalid TOTP code с кодом, который у него на экране абсолютно верный.
Если у вас в проекте используется двухфакторка на панели управления или на VPN, стоит заранее прочитать про настройку двухфакторной аутентификации для панелей управления — там же разобрано, как TOTP-окно связано именно с системным временем сервера.
Побочные симптомы, которые часто списывают на что угодно, кроме времени: письма уходят с неправильным Date: в заголовке и попадают в спам, логи путаются местами (события «из будущего» в журналах), cron задачи срабатывают не в то время или не срабатывают вовсе, репликация БД жалуется на конфликты по временным меткам, S3-совместимые API (подпись запроса включает временную метку) отклоняют запросы с RequestTimeTooSkewed.
Как это выясняется: смотрим на реальное время
Первый шаг диагностики — не лезть в код авторизации, а сравнить, что сервер думает о времени, с тем, какое время на самом деле.
date -u
Сравните вывод с любым независимым источником: телефон, time.is, вывод date на соседнем сервере, который точно синхронизирован. Разница даже в 5-10 секунд уже способна ронять TOTP на границе окна; разница в минуту рвёт практически всё, что завязано на временные метки.
Более информативная команда — timedatectl, которая сразу показывает и локальное время, и статус синхронизации:
timedatectl status
Пример вывода, где сразу видна проблема:
Local time: Fri 2026-08-28 14:32:07 UTC
Universal time: Fri 2026-08-28 14:32:07 UTC
RTC time: Fri 2026-08-28 14:31:04 UTC
Time zone: UTC (UTC, +0000)
System clock synchronized: no
NTP service: active
RTC in local TZ: no
Ключевая строка — System clock synchronized: no. Служба NTP формально запущена (active), но фактической синхронизации нет — это ловушка, о которой ниже. Расхождение между Local time и RTC time в этом примере — больше минуты, и это именно RTC (аппаратные часы), с которых система стартовала при последней перезагрузке или сбое службы времени.
Если используется chrony, полезно посмотреть на его собственную оценку смещения:
chronyc tracking
Reference ID : 00000000 ()
Stratum : 0
Ref time (UTC) : Thu Jan 01 00:00:00 1970
System time : 68.234511 seconds fast of NTP time
Last offset : +0.000000000 seconds
RMS offset : 0.000000000 seconds
Stratum : 0 и Reference ID : 00000000 — признак того, что chrony вообще не видит ни одного рабочего источника времени. Строка System time : 68.234511 seconds fast of NTP time — это и есть та самая минута с лишним, которая всё сломала.
Для systemd-timesyncd аналогичная проверка:
timedatectl timesync-status
journalctl -u systemd-timesyncd -n 50
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверПочему часы вообще разъезжаются
Аппаратные часы сервера (RTC) сами по себе неточны и накапливают дрейф — доли секунды в час не редкость даже на исправном железе. В нормальной ситуации это компенсирует служба синхронизации времени (chrony или systemd-timesyncd, реже классический ntpd), которая регулярно сверяется с внешними NTP-серверами и подстраивает системные часы. Проблема начинается, когда эта служба перестаёт фактически работать — и происходит это по нескольким типичным причинам.
Служба не запущена или упала. Простейший случай: chronyd не стартовал после перезагрузки, упал и не перезапустился, либо был случайно остановлен вручную и об этом забыли.
systemctl status chronyd
systemctl status systemd-timesyncd
NTP-порт заблокирован файрволом. NTP работает по UDP на порту 123. Если исходящий UDP/123 заблокирован на уровне файрвола сервера, облачного security group или сети провайдера, служба синхронизации формально запущена и даже отображается как active, но не может достучаться ни до одного сервера времени — то самое состояние System clock synchronized: no при NTP service: active из примера выше. Проверить доступность вручную:
# UDP-запрос к пулу NTP-серверов
ntpdate -q pool.ntp.org
# или для systemd-timesyncd
sudo timedatectl set-ntp true && sleep 5 && timedatectl timesync-status
Если ntpdate -q висит и уходит в таймаут — порт действительно недоступен наружу, и дело в файрволе или сетевой политике, а не в конфиге самой службы.
Дрейф часов виртуальной машины. На виртуализации (KVM/QEMU, Xen, VMware, Hyper-V) гостевая ОС получает эмулированные часы, и при высокой нагрузке на хост, миграции VM между физическими узлами или паузе гипервизора (например, при снапшоте) гостевые часы могут скачком разойтись с реальным временем — независимо от того, работает ли внутри VM служба синхронизации. Обычная NTP-синхронизация плавно подстраивает небольшие расхождения, но резкий скачок при паузе гипервизора она отрабатывает не мгновенно, особенно если после скачка сама служба синхронизации не переподнялась штатно. Признак именно этого сценария — расхождение появляется резко (после миграции/снапшота/ребута хоста), а не растёт постепенно.
RTC в неправильном часовом поясе. Отдельная и частая путаница: аппаратные часы (RTC) хранят время либо в UTC, либо в локальной зоне, и если ОС считает, что RTC хранит UTC, а на самом деле там локальное время (или наоборот) — при каждой загрузке система стартует со смещением, равным разнице часовых поясов. Это не «дрейф», а системная ошибка конфигурации, и она обычно проявляется сразу после установки ОС или миграции с одной платформы на другую. Смежная тема — настройка часовых поясов на сервере, стоит свериться, если сервер вообще не в UTC.
Фикс: восстановить синхронизацию и убедиться, что она реальная
Мало убедиться, что служба «включена» — нужно убедиться, что она реально синхронизирует время, потому что именно разрыв между «активна» и «синхронизирует» и стал причиной инцидента.
Шаг 1. Убедиться, что chrony (или timesyncd) установлен и запущен.
# Debian/Ubuntu
sudo apt install -y chrony
sudo systemctl enable --now chronyd
# AlmaLinux/RHEL
sudo dnf install -y chrony
sudo systemctl enable --now chronyd
Шаг 2. Проверить конфиг источников времени (/etc/chrony/chrony.conf на Debian/Ubuntu, /etc/chrony.conf на RHEL-семействе). Убедитесь, что там прописаны реально доступные пулы:
pool pool.ntp.org iburst
pool time.cloudflare.com iburst
Директива iburst заставляет chrony при старте отправить сразу несколько запросов подряд вместо одного — это резко ускоряет первичную синхронизацию после простоя.
Шаг 3. Проверить и, если нужно, открыть UDP/123 наружу на файрволе сервера и в security group облачного провайдера:
# ufw
sudo ufw allow out 123/udp
# iptables
sudo iptables -A OUTPUT -p udp --dport 123 -j ACCEPT
Шаг 4. Принудительно форсировать шаг времени, если расхождение большое (по умолчанию chrony может отказаться скачком менять время на величину больше нескольких секунд и вместо этого будет медленно «подтягивать» его, что для минуты расхождения растянется надолго):
sudo chronyc makestep
Эта команда даёт chrony разрешение немедленно скорректировать часы одним шагом, а не постепенной подстройкой. После неё стоит перепроверить статус:
chronyc tracking
timedatectl status
Дождитесь, пока System clock synchronized не станет yes, а Stratum в выводе chronyc tracking не станет небольшим числом (1-4 — нормальный диапазон для сервера, синхронизирующегося через интернет-пул).
Шаг 5. Если используется systemd-timesyncd вместо chrony:
sudo timedatectl set-ntp true
sudo systemctl restart systemd-timesyncd
timedatectl timesync-status
Шаг 6. После восстановления времени — перепроверить сами сервисы, которые упали: перезапустить процессы, которые могли закэшировать неверное время в состоянии (некоторые демоны читают время один раз при старте), проверить, что TLS-хендшейки и выдача JWT снова работают штатно. Если проблема была именно в NTP-порте и вы недавно настраивали файрвол или переносили сервер к другому провайдеру, стоит заодно свериться с материалом про настройку NTP и синхронизацию времени сервера — там разобрана базовая настройка с нуля, а не только восстановление после сбоя.
Профилактика: мониторить рассинхронизацию как отдельную метрику
Инцидент с часами почти никогда не происходит внезапно — служба синхронизации обычно уже некоторое время как перестала фактически работать, просто расхождение до поры до времени было небольшим и ничего не ломало. Единственный надёжный способ поймать это до того, как отвалится авторизация — мониторить именно рассинхронизацию времени как метрику, а не полагаться на то, что «служба помечена активной».
Практические варианты:
- Zabbix/Prometheus. node_exporter отдаёт метрику
node_timex_offset_seconds— смещение системных часов относительно NTP-источника по оценке ядра. Разумный порог для алерта — единицы секунд (например, больше 2-3 секунд уже повод разобраться, не дожидаясь минуты). Отдельно стоит алертить наnode_timex_sync_status, если экспортер её отдаёт — это прямой аналогSystem clock synchronizedизtimedatectl. - Простой cron-скрипт с внешней сверкой, если полноценного мониторинга нет:
#!/bin/bash
OFFSET=$(chronyc tracking | grep "System time" | awk '{print $4}')
THRESHOLD=5
if (( $(echo "$OFFSET > $THRESHOLD" | bc -l) )); then
echo "Часы разошлись на ${OFFSET}s" | mail -s "ALERT: clock drift" ops@example.com
fi
- Healthchecks.io или Uptime Kuma можно использовать для мониторинга самого факта, что скрипт-проверка времени вообще выполняется по расписанию — если синхронизация упала настолько, что cron перестал триггериться корректно, отсутствие сигнала само по себе будет алертом. Подробнее в материале про мониторинг cron-задач через healthchecks.io.
- Алерт на смену Stratum. Резкий переход
Stratumиз состояния «1-4» в «0» илиunreachableв выводеchronyc sources— надёжный ранний признак того, что сервер потерял связь со всеми источниками времени, ещё до того, как накопится заметное расхождение.
Таблица порогов для ориентира (это именно ориентир — конкретные допустимые значения зависят от того, что именно у вас завязано на время):
| Расхождение часов | Что уже может сломаться |
|---|---|
| до 1-2 секунд | обычно ничего заметного |
| 5-15 секунд | возможны редкие сбои TOTP на границе окна, начинают отклоняться самые строгие HMAC-подписи с коротким exp |
| 30-60 секунд | массовые отказы TOTP, часть JWT с коротким временем жизни, S3-подобные подписанные запросы |
| больше 60 секунд | TLS-хендшейки, большинство токен-схем, репликация БД, почтовые заголовки — весь стек симптомов из начала статьи |
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Почему NTP service: active в timedatectl status, если синхронизации на самом деле нет?
Строка NTP service показывает только то, что демон синхронизации (chronyd/systemd-timesyncd) запущен как процесс — это не значит, что он успешно связался с внешним сервером времени. За фактический статус синхронизации отвечает отдельная строка System clock synchronized; именно на неё нужно смотреть при диагностике.
Можно ли просто выставить время вручную командой date -s и не разбираться дальше?
Можно как экстренный шаг, чтобы прямо сейчас погасить инцидент, но это не решение — через какое-то время часы снова разъедутся, если не восстановлена реальная работа службы синхронизации. После ручной правки обязательно проверьте chronyc tracking/timedatectl status, чтобы убедиться, что синхронизация действительно работает, а не просто время один раз подправлено руками.
Нужно ли синхронизировать время внутри Docker-контейнеров отдельно?
Обычно нет — контейнеры по умолчанию используют системные часы хост-ОС (ядро одно на всех), поэтому синхронизировать нужно именно хост. Исключение — некоторые настройки виртуализации с отдельным пространством имён времени или явно изолированные окружения, где стоит проверить это отдельно.
Почему на виртуальной машине время скачет сильнее, чем на железном сервере?
Потому что гостевые часы VM эмулируются гипервизором и зависят от того, как часто и насколько точно гипервизор их обновляет; пауза VM (снапшот, миграция, высокая нагрузка хоста) может дать заметный скачок, которого не бывает у физических часов при нормальной работе. NTP-синхронизация внутри гостя всё равно нужна и работает, но она компенсирует дрейф постепенно, а не мгновенно гасит резкий скачок.
TLS-хендшейк падает именно из-за часов или может быть другая причина?
Сообщения вида certificate has expired or is not yet valid или certificate is not yet valid — почти всегда именно про рассинхронизацию времени на одной из сторон (клиент или сервер), если при этом сертификат по факту не просрочен. Если хотите разобраться в механике самого хендшейка глубже, есть отдельный разбор — что происходит в TLS-рукопожатии до первого байта данных.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →