Wprowadzenie: „wczoraj działało, dziś certyfikat jest nieważny”

Istnieje klasa incydentów, która potrafi się maskować lepiej niż jakakolwiek inna. Skrypt, który od miesięcy chodził przez proxy do dziesiątek API, nagle zaczyna padać przy każdym żądaniu. W logu widzisz certificate has expired albo certificate is not yet valid. Inżynier otwiera certyfikat docelowej strony w przeglądarce na swoim laptopie: termin ważności w porządku, łańcuch zaufania zielony. Uruchamia skrypt bez proxy z laptopa — działa. Wraca do proxy — znowu odmowa. Wniosek nasuwa się sam: winne jest proxy.

I ten wniosek jest prawie zawsze błędny. Nie chodzi o to, że proxy „podmienia certyfikat” albo „psuje TLS”. Chodzi o to, że skrypt uruchamia się na innej maszynie — na serwerze, w maszynie wirtualnej, w kontenerze — i na tej maszynie zegar systemowy uciekł. O kilka minut, godzin albo dni. Proxy po prostu okazało się najbardziej widoczną zmienną w równaniu i przyjęło na siebie cios.

Rozbierzemy ten temat na części. Dowiesz się, w których dokładnie miejscach współczesne protokoły opierają się na bieżącym czasie i dlaczego przesunięcie zegara w różne strony daje różne objawy. Zobaczysz konkretne komunikaty błędów w curl, Pythonie i Node.js, żeby rozpoznawać problem po pierwszej linii stack trace’u. Zrozumiesz, dlaczego zegary uciekają właśnie na maszynach wirtualnych i w kontenerach oraz dlaczego zainstalowany demon synchronizacji nie gwarantuje, że działa. Otrzymasz procedurę diagnozy na jedną minutę, działające konfiguracje chrony i systemd-timesyncd oraz checklistę, którą warto przejść, zanim napiszesz do wsparcia dostawcy proxy.

Od razu ustalmy granice. Ten artykuł nie jest o wystawianiu certyfikatów, o łańcuchach zaufania ani o magazynach certyfikatów głównych. Rozpatrujemy jedną jedyną przyczynę awarii — błędny czas systemowy — i wszystko, co z niej wynika przy pracy przez proxy. To wąski temat, ale właśnie on wypala godziny debugowania zespołom, które zajmują się integracjami, monitoringiem, zbieraniem danych publicznych i automatyzacją przez infrastrukturę proxy taką jak Proxeon.

Podstawy: gdzie czas jest krytyczny i dlaczego protokoły w ogóle patrzą na zegar

Zacznijmy od prostego pytania. Po co protokołom sieciowym wiedzieć, która jest godzina? Odpowiedź jest jedna: żeby ograniczać czas życia zaufania. Każdy sekret — certyfikat, token, podpis, kod jednorazowy — prędzej czy później może wyciec. Jeśli ma termin ważności, szkoda z wycieku jest ograniczona. Ale ograniczenie terminu działa tylko wtedy, gdy strona sprawdzająca zna bieżący czas. A bierze go z jednego miejsca — z zegara systemowego maszyny, na której wykonuje się weryfikacja.

Stąd fundamentalny wniosek: każda weryfikacja czasu jest lokalna. Serwer nie pyta klienta, która jest godzina. Klient nie pyta serwera. Każdy patrzy na swój zegar i wyciąga wniosek. Jeśli zegary obu stron się rozjeżdżają, widzą różną rzeczywistość, a jedna z nich uznaje drugą za intruza.

Sprawdzanie terminu ważności certyfikatu w TLS

Każdy certyfikat X.509 zawiera dwa pola: notBefore (ważny od) i notAfter (ważny do). Przy uzgadnianiu TLS klient porównuje bieżący czas ze swojego zegara z tym przedziałem. Moment wystawienia certyfikatu przez serwer nie ma tu nic do rzeczy — liczy się tylko to, co klient uważa za bieżący czas.

Kierunek przesunięcia determinuje objaw. Jeśli zegar klienta spóźnia się, świeżo wystawione certyfikaty wyglądają, jakby jeszcze ich nie było: błąd „certificate is not yet valid”. Jeśli zegar klienta spieszy się, certyfikaty wydają się wygasłe: „certificate has expired”. To pierwsza diagnostyczna wskazówka, którą warto zapamiętać.

Dlaczego temat się zaostrzył właśnie teraz? Bo certyfikaty stają się krótkotrwałe. Wiele publicznych centrów certyfikacji wystawia certyfikaty ważne 90 dni, pojawiły się warianty na 6 dni, a wymagania branżowe systematycznie skracają maksymalny okres: od marca 2026 roku — 200 dni, dalej 100 dni, a do 2029 roku — 47 dni. Kiedyś zegar spóźniający się o dwa tygodnie prawie nigdy nie trafiał na świeży certyfikat. Teraz docelowa strona może wymieniać certyfikat co kilka dni, a maszyna spóźniona o trzy doby będzie go odrzucać jako „jeszcze nieważny”.

Do tej samej grupy należą mechanizmy towarzyszące: odpowiedzi OCSP i listy odwołań mają własne okna ważności, znaczniki Certificate Transparency zawierają pieczątki czasu, a bilety sesji TLS 1.3 żyją ograniczony czas. Wszystkie są sprawdzane według lokalnego zegara.

JWT: pola exp, nbf i iat

JSON Web Token to najpopularniejszy format tokenów dostępu w API. W jego treści są trzy pola czasowe w formacie Unix epoch: iat (kiedy wystawiony), nbf (not before — od kiedy ważny) i exp (kiedy wygasa). Strona przyjmująca token porównuje je ze swoim zegarem, zazwyczaj z niewielkim zapasem (leeway) 30–60 sekund.

Tu pojawia się ciekawa asymetria. Zwykle token wystawia serwer według swojego zegara, a klient buforuje go do momentu exp — ale już według swojego zegara. Jeśli klient spóźnia się o pięć minut, będzie uważał token za żywy jeszcze pięć minut po tym, jak serwer zaczął go odrzucać. Dostajesz niewyjaśnialne 401 pod koniec życia każdego tokenu. Jeśli zaś klient sam podpisuje JWT — na przykład do uwierzytelniania konta usługowego — i jego zegar spieszy się, serwer widzi iat w przyszłości i odrzuca token jako jeszcze nieważny.

Podpisy żądań ze znacznikiem czasu

Wiele API wymaga podpisywania żądania za pomocą HMAC, włączając do podpisywanego łańcucha bieżący czas. Najbardziej znany przykład to schemat Signature Version 4, który wykorzystują magazyny obiektowe zgodne z S3. Serwer przyjmuje żądanie, jeśli jego znacznik czasu różni się od zegara serwera nie więcej niż o 15 minut; inaczej zwraca błąd RequestTimeTooSkewed. Bramy płatnicze, API giełdowe i brokerskie, korporacyjne szyny danych stosują podobne schematy z oknami od 30 sekund do kilku minut. Uwierzytelnianie Kerberos w sieciach korporacyjnych domyślnie toleruje rozbieżność 5 minut.

Zwróć uwagę: podpis tworzy twój klient według swojego zegara. Proxy fizycznie nie uczestniczy w tym procesie — jedynie przekazuje bajty. Dlatego błąd „nieprawidłowy znacznik czasu w podpisie” przez proxy to zawsze pytanie do zegara twojej maszyny.

Kody jednorazowe TOTP

Kody dwuskładnikowego uwierzytelniania według standardu TOTP są obliczane ze wspólnego sekretu i numeru 30-sekundowego interwału. Strona sprawdzająca zazwyczaj dopuszcza plus-minus jeden interwał. Jeśli twój zegar uciekł o minutę, kody całkowicie przestają być przyjmowane. Dotyczy to też własnych integracji, gdzie konto usługowe generuje TOTP programowo do logowania w portalu korporacyjnym.

Mniej oczywiści konsumenci czasu

  • Buforowanie HTTP. Nagłówki Date, Expires i max-age są porównywane z lokalnym zegarem; przy przesunięciu cache albo nie działa, albo oddaje nieaktualne dane.
  • Cookies z Expires. Klient ze spóźniającym się zegarem trzyma je dłużej, ze spieszącym — wyrzuca od razu.
  • HSTS i pinning. Dyrektywy max-age też są liczone lokalnie.
  • Rotacja certyfikatów wewnątrz klastrów. Komponenty orkiestratorów wymieniają certyfikaty według harmonogramu i przy przesunięciu zegara na węźle same przestają sobie ufać.

Głębsze spojrzenie: czyj zegar się liczy w łańcuchu klient — proxy — serwer

Nałóżmy teraz ten obraz na realną topologię. Przy pracy przez proxy w łańcuchu są co najmniej trzy maszyny z trzema zestawami zegarów: twój klient, serwer proxy i serwer docelowy. Kluczowe pytanie diagnostyczne: na której z nich odbywa się weryfikacja, która pada?

HTTP CONNECT i SOCKS5: proxy nie widzi TLS

Przy żądaniu do zasobu HTTPS przez proxy HTTP klient wysyła polecenie CONNECT z nazwą hosta i portem. Proxy nawiązuje połączenie TCP z celem i dalej ślepo przekazuje bajty w obie strony. Uzgadnianie TLS odbywa się przez proxy, między twoim klientem a serwerem docelowym. Proxy nie widzi certyfikatu, nie może go zmienić i nie uczestniczy w weryfikacji. To samo dotyczy SOCKS5.

Wniosek praktyczny: błąd „certificate is not yet valid” albo „certificate has expired” przy pracy przez przezroczysty tunel fizycznie nie może być wywołany przez proxy. Jego źródłem jest albo zegar twojej maszyny, albo magazyn zaufanych korzeni na niej, albo faktycznie wygasły certyfikat na celu. Pierwsze dwa warianty są lokalne. A pierwszy z nich sprawdza się w dziesięć sekund.

Kiedy sprawdza się więcej zegarów: TLS do samego proxy

Jeśli łączysz się z proxy po szyfrowanym kanale (schemat https:// dla samego proxy), klient sprawdza dwa certyfikaty: certyfikat serwera proxy i certyfikat strony docelowej. Oba sprawdzenia wykonuje się według zegara klienta. Objawy są takie same, ale w szczegółowym wyniku curl będzie widać, na którym etapie — „Proxy certificate” czy „Server certificate” — nastąpiła odmowa.

Co zależy od zegara samego proxy

Zegar serwera proxy wpływa na ograniczony zestaw rzeczy: poprawność własnych logów, działanie jego listenera TLS (na przykład pobieranie odpowiedzi OCSP) oraz weryfikację danych autoryzacyjnych, jeśli dostawca używa tokenów z ograniczonym terminem. Infrastruktura Proxeon synchronizuje czas na swoich węzłach standardowo, a w przeważającej większości incydentów zegar proxy nie ma nic do rzeczy: nie uczestniczy ani w weryfikacji certyfikatu celu, ani w tworzeniu twoich podpisów, ani w walidacji twoich JWT. To wszystko robi klient i serwer docelowy.

Zegar serwera docelowego

Czasami zegar ucieka po drugiej stronie. Objaw: błędy pojawiają się tylko z jednym hostem, a do pozostałych dostęp jest normalny; nagłówek Date w odpowiedzi tego hosta wyraźnie różni się od Date innych serwerów. Niewiele możesz z tym zrobić poza powiadomieniem właściciela i, jeśli API na to pozwala, skompensowaniem przesunięcia we własnych żądaniach — niektóre biblioteki potrafią obliczyć korektę na podstawie nagłówka Date z odpowiedzi z błędem i podpisać kolejne żądanie z uwzględnieniem przesunięcia.

Opóźnienie i „przeterminowanie w kolejce”

Proxy dodaje opóźnienie — od dziesiątek milisekund do sekund przy ponownym łączeniu kanałów mobilnych i rotacji IP. Dla okna 15 minut to nic. Ale jest subtelna pułapka w logice ponowień. Jeśli podpisałeś żądanie, włożyłeś je do kolejki, a wysłałeś kilka minut później po serii retryów z oczekiwaniem, znacznik czasu w podpisie się przeterminowuje. Przez wolny lub przeciążony kanał zdarza się to częściej. Zasada: podpisuj bezpośrednio przed wysłaniem i podpisuj ponownie przy każdej próbie, a nie używaj wielokrotnie gotowego podpisanego obiektu.

Przesunięcie: wielkość i kierunek

Zbierzmy zależność objawu od przesunięcia w jedną listę — przyda się przy czytaniu logów:

  • Zegar klienta spóźnia się: „certificate is not yet valid”, JWT z błędem nbf lub iat przy samopodpisywaniu, własne zbuforowane tokeny wydają się żywe, ale serwer odpowiada 401 „expired”, podpisy są odrzucane jako zbyt stare, TOTP nie jest przyjmowany.
  • Zegar klienta spieszy się: „certificate has expired”, własne JWT są odrzucane jako mające iat z przyszłości, podpisy są odrzucane jako „z przyszłości”, tokeny są wyrzucane z cache przed terminem, TOTP nie jest przyjmowany.
  • Przesunięcie poniżej sekundy: żaden z wymienionych mechanizmów tego nie zauważy.
  • Przesunięcie 30 sekund — 5 minut: padają TOTP, JWT z małym leeway, rygorystyczne schematy HMAC.
  • Przesunięcie powyżej 15 minut: padają praktycznie wszystkie podpisy żądań i Kerberos.
  • Przesunięcie w dniach: zaczynają padać połączenia TLS do stron z krótkotrwałymi certyfikatami.

Jak błąd wygląda w różnych klientach: komunikaty, które trzeba rozpoznawać od pierwszej linii

Umiejętność rozpoznania problemu po tekście błędu oszczędza więcej czasu niż jakakolwiek metodologia. Poniżej charakterystyczne sformułowania dla popularnych klientów. Podano je tak, jak drukują je biblioteki z OpenSSL pod spodem; numery linii w nawiasach będą się różnić między wersjami.

curl

Podstawowe komunikaty przy błędnym czasie:

curl: (60) SSL certificate problem: certificate is not yet valid
curl: (60) SSL certificate problem: certificate has expired

Kod 60 to CURLE_PEER_FAILED_VERIFICATION, wspólny dla wszystkich problemów z weryfikacją certyfikatu. Odróżnić przyczynę czasową od problemu z korzeniami można tylko po tekście po dwukropku. Z flagą -v pojawia się przydatna linia z numerycznym kodem weryfikacji OpenSSL: 9 odpowiada „not yet valid”, 10 — „has expired”

* SSL certificate verify result: certificate is not yet valid (9), continuing anyway.

Linia „continuing anyway” pojawia się tylko przy fladze -k; bez niej połączenie zostanie zerwane. W Windows z backendem Schannel tekst jest inny:

curl: (60) schannel: SEC_E_CERT_EXPIRED (0x80090328) - The received certificate has expired.

Przykład pełnego sprawdzenia przez proxy ze szczegółowym wyjściem, żeby zobaczyć i etap (proxy czy cel), i wynik weryfikacji:

curl -v --proxy http://LOGIN:PASSWORD@PROXY_HOST:PORT https://example.com/ 2>&1 | grep -iE 'verify|expire|valid|date'

Python: ssl, requests, httpx, aiohttp

Standardowy moduł ssl rzuca wyjątek z wymownym przyrostkiem:

ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate is not yet valid (_ssl.c:1010)

Biblioteka requests opakowuje go w SSLError i dodaje otoczkę puli połączeń, przez co słowa kluczowe znajdują się na samym końcu długiego łańcucha:

requests.exceptions.SSLError: HTTPSConnectionPool(host='example.com', port=443): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired (_ssl.c:1010)')))

Ważny szczegół przy pracy przez proxy: przy tunelu CONNECT błąd weryfikacji certyfikatu celu przychodzi właśnie jako SSLError, a nie jako ProxyError. ProxyError pojawia się, gdy nie udało się nawiązać połączenia z samym proxy. Więc typ wyjątku już podpowiada, gdzie szukać.

httpx formułuje krócej:

httpx.ConnectError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired (_ssl.c:1010)

aiohttp — własną klasą:

aiohttp.client_exceptions.ClientConnectorCertificateError: Cannot connect to host example.com:443 ssl:True [SSLCertVerificationError: (1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate is not yet valid (_ssl.c:1010)')]

Błędy JWT w PyJWT przy weryfikacji tokenu po twojej stronie:

jwt.exceptions.ExpiredSignatureError: Signature has expired
jwt.exceptions.ImmatureSignatureError: The token is not yet valid (nbf)

A oto minimalny powtarzalny przykład przez proxy, który warto trzymać pod ręką jako test case:

import requests, ssl, sys
PROXY = "http://LOGIN:PASSWORD@PROXY_HOST:PORT"
try:
r = requests.get("https://example.com/", proxies={"https": PROXY}, timeout=15)
print(r.status_code, r.headers.get("Date"))
except requests.exceptions.SSLError as e:
msg = str(e)
if "not yet valid" in msg or "has expired" in msg:
print("Похоже на сдвиг системных часов, проверьте date -u")
print(msg)

Małe wyjaśnienie: symbol przejścia do nowej linii w tym i kolejnych przykładach pokazano encją HTML, żeby znaczniki pozostały w jednej linii; po skopiowaniu do edytora zamieni się w zwykłe złamanie linii.

Node.js: https, fetch, undici

Node drukuje krótkie komunikaty i, co cenne, zaopatruje błąd w pole code:

Error: certificate is not yet valid
code: 'CERT_NOT_YET_VALID'
Error: certificate has expired
code: 'CERT_HAS_EXPIRED'

Pułapka wbudowanego fetch: z zewnątrz widzisz tylko TypeError: fetch failed, a prawdziwa przyczyna jest ukryta we właściwości cause. Loguj ją jawnie:

try {
 const res = await fetch("https://example.com/");
 console.log(res.status, res.headers.get("date"));
} catch (e) {
 console.error(e.message, e.cause && e.cause.code, e.cause && e.cause.message);
}

Do routowania fetch przez proxy w Node używa się ProxyAgent z pakietu undici. Błąd certyfikatu celu przyjdzie wtedy z tym samym kodem CERT_HAS_EXPIRED lub CERT_NOT_YET_VALID w cause. Dla klasycznego modułu https i bibliotek takich jak axios stosuje się agenta z pakietu https-proxy-agent — kody błędów są te same, bo generuje je ten sam OpenSSL wewnątrz Node.

Biblioteka jsonwebtoken przy weryfikacji tokenów rozróżnia dwa przypadki czasowe:

TokenExpiredError: jwt expired
NotBeforeError: jwt not active

Błędy po stronie serwera przy podpisach

Tutaj tekst ustala API, ale typowe sformułowania są rozpoznawalne. U magazynów zgodnych z S3:

<Code>RequestTimeTooSkewed</Code>
<Message>The difference between the request time and the current time is too large.</Message>

U serwerów OAuth — nagłówek WWW-Authenticate z error="invalid_token" i opisem w rodzaju „The token expired” albo „Token used before issued”. U API finansowych — komunikaty o timestamp outside of recvWindow albo „Timestamp for this request is outside of the allowed window”. Jeśli widzisz którąkolwiek z tych fraz, zegar to pierwsza rzecz do sprawdzenia.

Dlaczego zegary uciekają: wirtualizacja, kontenery, brak demona, ręczna korekta

Kwarcowy generator na płycie głównej to nie wzorzec. Jego typowy błąd to 10–100 ppm, czyli do 1–9 sekund dryfu na dobę bez korekcji. Przez miesiąc bez synchronizacji nazbiera się kilka minut. To tło, które ma każda maszyna. Ale główne incydenty wywołuje nie powolny dryf, a skoki. Przeanalizujmy źródła.

Maszyny wirtualne: pauza, snapshot, migracja

Maszyna wirtualna nie posiada fizycznego zegara. Otrzymuje czas przez źródło parawirtualne (kvm-clock, Hyper-V reference time, odpowiedniki u innych hiperwizorów). Gdy gość jest wstrzymany, zapisany w snapshot albo migruje między hostami, jego wewnętrzny zegar „zamarza”. Po wznowieniu jądro albo otrzymuje korektę od hiperwizora, albo nie — zależy to od ustawień agentów gościa. Klasyczny scenariusz: zespół przywrócił serwer testowy ze snapshotu sprzed dwóch tygodni i obudził się on z zegarem spóźnionym o 14 dni. Wszystkie certyfikaty wystawione w tych dwóch tygodniach są dla niego „jeszcze nieważne”.

Osobny kłopot to konflikt dwóch źródeł prawdy. Agent gościa hiperwizora okresowo „podciąga” czas do hosta, a wewnątrz gościa działa własny demon NTP. Jeśli host sam nie jest zsynchronizowany, gość otrzymuje dwa sprzeczne sygnały i zegar może skakać w tę i z powrotem. Zalecenie większości poradników wirtualizacyjnych: albo synchronizacja od hiperwizora, albo NTP wewnątrz, ale nie oba naraz.

Laptopy developerów: sen i desktopowe wirtualki

Docker Desktop i WSL2 działają wewnątrz lekkiej maszyny wirtualnej. Po uśpieniu lub hibernacji laptopa zegar tej VM nierzadko spóźnia się dokładnie o czas snu — godzinę, noc, weekend. System hosta czas podciągnie, a wirtualka — nie zawsze. Efekt: w poniedziałek rano wszystkie skrypty w kontenerach padają na TLS, a w terminalu hosta wszystko w porządku. Właśnie ta sytuacja najczęściej rodzi skargę „działa bez proxy z laptopa, nie działa przez proxy w kontenerze” — przy czym proxy w obu przypadkach mogłoby w ogóle nie uczestniczyć w weryfikacji.

Kontenery

Kontener nie ma własnego zegara czasu rzeczywistego — używa zegara hosta. Omówimy to szczegółowo w osobnym rozdziale, tu ustalmy najważniejsze: jeśli host uciekł, uciekły wszystkie kontenery, a naprawić tego z wnętrza kontenera nie można.

Brak demona synchronizacji albo jego niesprawność

Minimalne obrazy, „gołe” instalacje i samodzielnie składane systemy nierzadko idą w ogóle bez klienta NTP. A tam, gdzie jest, może nie działać z dziesiątka powodów: zablokowany wychodzący UDP na port 123 przez firewall; nierozwiązywane nazwy puli; usługa zainstalowana, ale zamaskowana; jednocześnie zainstalowane chrony i timesyncd, które sobie przeszkadzają; przesunięcie przy starcie przekroczyło próg, przy którym klasyczny ntpd odmawia korekcji i kończy działanie. Szczególnie wrażliwe są serwery, których jedyne wyjście do internetu prowadzi przez HTTP-proxy sieci korporacyjnej: NTP po UDP przez takie proxy nie przejdzie, a zegar zostaje sam sobie.

Brak RTC i rozładowana bateria

Komputery jednopłytkowe typu Raspberry Pi i wiele mini-PC nie mają zegara sprzętowego z baterią. Przy starcie startują albo z epoki, albo z daty ostatniego zapisu, i tak żyją do pierwszej udanej synchronizacji. U starych serwerów siada bateria CMOS, a po każdym wyłączeniu BIOS resetuje datę do fabrycznej. Jeśli synchronizacja jest niedostępna, maszyna pracuje z datą sprzed dekady i każde połączenie TLS jest skazane na porażkę.

Ręczna korekta i pomyłki ze strefami czasowymi

Polecenie date -s wykonane „żeby sprawdzić hipotezę” i zapomniane — częsty wątek. Kolejny to dwusystemowy rozruch Windows i Linux na jednej maszynie: Windows trzyma w zegarze sprzętowym czas lokalny, Linux domyślnie oczekuje UTC, i po każdym restarcie w drugi system czas przesuwa się o wartość strefy czasowej, dla czasu moskiewskiego — o trzy godziny. I wreszcie pomyłka między czasem a strefą czasową: zmiana zmiennej TZ albo pliku /etc/localtime zmienia wyświetlanie, ale nie czas systemowy. Porównywać trzeba tylko UTC.

Diagnoza w minutę: porównanie z zewnętrznym źródłem i dopuszczalny rozrzut

Celem tej procedury jest w 60 sekund odpowiedzieć na jedno pytanie: czy zegar na maszynie, z której wykonuje się żądania przez proxy, chodzi prawidłowo. Wszystkie polecenia wykonuje się na tej samej maszynie lub w tym samym kontenerze, w którym pada klient, a nie na twoim laptopie.

Krok 1. Spójrz na UTC i porównaj z dowolnym zaufanym źródłem

date -u

Porównaj z zegarem telefonu zsynchronizowanego z operatorem sieci. Rozbieżność w minutach widać od razu. Do sekund potrzebne są kroki poniżej.

Krok 2. Pobierz czas z nagłówka HTTP Date przez to samo proxy

Każdy poprawny serwer HTTP zwraca nagłówek Date według swojego (zwykle zsynchronizowanego) zegara. Sztuczka: pytaj po zwykłym HTTP, nie HTTPS, bo przy dużym przesunięciu TLS się nie nawiąże i nagłówka w ogóle nie dostaniesz:

date -u; curl -sI --proxy http://LOGIN:PASSWORD@PROXY_HOST:PORT http://example.com/ | grep -i '^date'

Dwie linie powinny się zgodzić z dokładnością do sekundy-dwóch (nagłówek ma ziarnistość sekundową plus opóźnienie kanału). Zauważyłeś minutę i więcej — diagnoza gotowa. Ten sposób jest szczególnie cenny, bo nie wymaga UDP, działa przez dowolne proxy i nie zależy od TLS.

Ten sam sposób w Pythonie — wygodnie wpiąć w healthcheck serwisu, który chodzi przez Proxeon lub dowolne inne proxy:

import email.utils, time, requests
PROXY = "http://LOGIN:PASSWORD@PROXY_HOST:PORT"
r = requests.head("http://example.com/", proxies={"http": PROXY}, timeout=10)
server_ts = email.utils.parsedate_to_datetime(r.headers["Date"]).timestamp()
skew = time.time() - server_ts
print(f"Смещение локальных часов: {skew:+.1f} с")
if abs(skew) > 30:
raise SystemExit("Часы уехали, синхронизация обязательна")

Krok 3. Zapytaj system, czy uważa się za zsynchronizowany

timedatectl

Szukaj dwóch linii: „System clock synchronized: yes” i „NTP service: active”. Każde „no” lub „inactive” — powód, by przejść do rozdziału o konfiguracji. Jeśli stoi chrony:

chronyc tracking

Patrz na „System time” (wielkość i znak przesunięcia), „Stratum” (nie powinno być 0) i „Leap status” (powinno być Normal, a nie „Not synchronised”).

Krok 4. Jednorazowe zapytanie do serwera NTP bez zmiany zegara

Jeśli UDP na zewnątrz jest otwarty, można zmierzyć przesunięcie dokładnie, nic nie zmieniając:

chronyd -Q 'pool pool.ntp.org iburst'

Albo, jeśli jest zainstalowany ntpdate:

ntpdate -q pool.ntp.org

Wyjście pokaże offset w sekundach ze znakiem.

Krok 5. Potwierdź, że to właśnie czas psuje TLS

Narzędzie openssl potrafi chodzić przez HTTP-proxy i drukuje wynik weryfikacji z numerycznym kodem:

openssl s_client -connect example.com:443 -proxy PROXY_HOST:PORT -servername example.com </dev/null 2>/dev/null | grep 'Verify return code'

„Verify return code: 9 (certificate is not yet valid)” albo „10 (certificate has expired)” przy żywym certyfikacie — wyczerpujący dowód. Uwzględnij, że s_client w większości buildów nie obsługuje autoryzacji na proxy przez login i hasło; do tego sprawdzenia wygodnie użyć autoryzacji po IP, którą obsługuje Proxeon. Daty samego certyfikatu wyciągniesz tak:

openssl s_client -connect example.com:443 -proxy PROXY_HOST:PORT -servername example.com </dev/null 2>/dev/null | openssl x509 -noout -dates

Jaki rozrzut uznawać za normę

  • Do 100 milisekund — zdrowa maszyna z działającym NTP. Nic nie trzeba robić.
  • 100 ms — 1 sekunda — akceptowalne dla wszystkich omawianych protokołów, ale mówi o słabym źródle lub rzadkim odpytywaniu. Warto sprawdzić konfigurację.
  • 1 — 30 sekund — strefa żółta. TLS i podpisy z szerokim oknem jeszcze działają, TOTP i JWT z małym leeway mogą się już potykać. Napraw dziś.
  • Powyżej 30 sekund — strefa czerwona. Przyjmij, że każdy błąd uwierzytelniania lub TLS jest wywołany zegarem, dopóki nie udowodnisz inaczej.

Jak celowo odtworzyć problem

Żeby upewnić się, że twoja obsługa błędów poprawnie rozpoznaje awarię czasową, nie trzeba ruszać zegara systemowego. Biblioteka libfaketime podmienia czas dla jednego procesu:

faketime '2024-01-01 12:00:00' curl -sS https://example.com/ -o /dev/null

Dostaniesz „certificate is not yet valid” dla dowolnej strony z certyfikatem wystawionym po podanej dacie. Do sprawdzenia certyfikatu bez sieci OpenSSL obsługuje flagę -attime z czasem uniksowym, co jest wygodne do testów w CI.

Konfiguracja synchronizacji: systemd-timesyncd i chrony, i jak się upewnić, że to naprawdę działa

Instalacja pakietu to nie synchronizacja. Poniżej dwa działające schematy i, co ważniejsze, metodyka weryfikacji.

Wybór: timesyncd czy chrony

systemd-timesyncd — minimalistyczny klient SNTP wchodzący w skład systemd. Odpytuje jeden serwer z listy, umie korygować zegar skokowo i płynnie, nie działa jako serwer, nie obsługuje źródeł sprzętowych. Wystarcza dla stacji roboczych i niekrytycznych serwerów, gdzie potrzebna jest dokładność rzędu dziesiątek milisekund.

chrony — pełna implementacja NTP. Kombinuje kilka źródeł, szybko się zbiega po długim przestoju, zachowuje się starannie przy przerywanej sieci, obsługuje NTS (NTP z uwierzytelnianiem przez TLS), może rozdzielać czas w sieci. To domyślny wybór dla serwerów, maszyn wirtualnych i wszelkich węzłów, przez które idą podpisane żądania i praca z proxy.

Ważne: dwa demony jednocześnie — źródło chaosu. Przy instalacji chrony dystrybucje z reguły wyłączają timesyncd, ale sprawdź to jawnie.

Wariant A: systemd-timesyncd

Konfiguracja w pliku /etc/systemd/timesyncd.conf:

[Time]
NTP=ntp.your-provider.internal 0.pool.ntp.org 1.pool.ntp.org
FallbackNTP=2.pool.ntp.org 3.pool.ntp.org
PollIntervalMinSec=32
PollIntervalMaxSec=2048

Włączenie i sprawdzenie:

sudo timedatectl set-ntp true
sudo systemctl restart systemd-timesyncd
timedatectl timesync-status

W wyjściu timesync-status powinno być wypełnione Server, Stratum różne od 0, a pole Offset — w milisekundach. Jeśli serwer nie jest wskazany albo stoi „Packet count: 0”, odpowiedzi od NTP nie przychodzą — prawie na pewno zablokowany UDP 123.

Wariant B: chrony

Plik /etc/chrony/chrony.conf (w niektórych systemach /etc/chrony.conf):

server ntp.your-provider.internal iburst prefer
pool pool.ntp.org iburst maxsources 4
driftfile /var/lib/chrony/drift
makestep 1.0 3
rtcsync
logdir /var/log/chrony

Omówmy kluczowe dyrektywy, bo to one decydują o zachowaniu przy awariach:

  • iburst — przy starcie wysyła serię zapytań pod rząd i synchronizuje się w sekundach, a nie w minutach.
  • makestep 1.0 3 — jeśli przesunięcie przekracza 1 sekundę, w pierwszych trzech aktualizacjach dozwolony jest skok, dalej tylko płynne podciąganie. Bez tej dyrektywy po snapshocie lub śnie chrony będzie „doganiać” zegar tygodniami. Dla serwerów, które często zasypiają lub migrują, niektórzy ustawiają makestep 1.0 -1, pozwalając na skok zawsze.
  • rtcsync — okresowo zapisuje czas do zegara sprzętowego, żeby po restarcie startować z bliskiej wartości.
  • Wewnętrzny serwer dostawcy pierwszy — platformy chmurowe zwykle udostępniają adres NTP w sieci; jest dostępny nawet przy zablokowanym zewnętrznym UDP i ma minimalne opóźnienie. Weź adres z dokumentacji swojego dostawcy.

Uruchomienie:

sudo systemctl enable --now chrony
sudo chronyc makestep
chronyc tracking
chronyc sources -v

Jak poznać, że synchronizacja naprawdę działa

To najważniejsza część rozdziału. Oznaki działającej synchronizacji w chrony:

  1. W chronyc tracking pole Reference ID nie jest zerowe, a Stratum mieści się w zakresie 2–4 dla typowego serwera. Stratum 0 lub 10 z pustym Reference ID oznacza „pracuję sam ze sobą”.
  2. Leap status: Normal. Wartość „Not synchronised” — zegar nie jest przez nikogo korygowany.
  3. System time pokazuje przesunięcie w milisekundach lub mikrosekundach, a nie w sekundach.
  4. W chronyc sources przynajmniej jedno źródło jest oznaczone gwiazdką (aktualnie wybrane) lub plusem (uczestniczy w kombinacji). Znak zapytania przy wszystkich źródłach — odpowiedzi nie przychodzą, szukaj firewalla lub DNS.
  5. Last offset i RMS offset maleją z czasem, a nie rosną.

Dla timesyncd równoważnym sprawdzeniem jest „System clock synchronized: yes” w timedatectl i niezerowy Packet count w timesync-status.

Kiedy NTP jest zablokowany przez firewall

Jeśli na serwerze dozwolone jest tylko wychodzące HTTP(S) przez proxy, masz trzy drogi. Pierwsza — poprosić o otwarcie UDP 123 przynajmniej do wewnętrznego serwera NTP; to standardowa i bezpieczna prośba do administratorów sieci. Druga — synchronizować się od hiperwizora lub hosta, jeśli to wirtualka lub kontener. Trzecia — kompromis przez narzędzie htpdate, które ustawia zegar według nagłówka Date z odpowiedzi HTTP; dokładność rzędu sekundy, dla TLS, JWT i podpisów żądań to wystarcza, a umie działać przez HTTP-proxy. Nie najbardziej elegancki, ale działający wariant dla izolowanych segmentów.

Windows

Na hostach Windows usługę czasu sprawdza się tak:

w32tm /query /status
w32tm /resync

W środowisku domenowym źródłem jest kontroler domeny, poza domeną — skonfigurowany serwer NTP. Pole Last Successful Sync Time powinno być świeże.

Kontenery i zegar: co jest dziedziczone od hosta, a co nie

Kontener to zestaw izolowanych przestrzeni nazw nad wspólnym jądrem. Izolacja dotyczy procesów, sieci, systemu plików, punktów montowania. Ale zegar czasu rzeczywistego (CLOCK_REALTIME) nie jest izolowany. Przestrzenie nazw czasu, które pojawiły się w Linuxie 5.6, pozwalają przesuwać tylko zegary monotoniczne i czas od rozruchu — to potrzebne do migracji kontenerów i nie wpływa na czas kalendarzowy. Stąd kilka twardych konsekwencji.

Co jest dziedziczone

  • Czas kalendarzowy. Wszystkie kontenery widzą dokładnie ten sam czas co host. Jeśli host uciekł — uciekły wszystkie.
  • Synchronizacja. Działający na hoście chrony automatycznie „naprawia” czas we wszystkich kontenerach.
  • Skoki. Jeśli host skorygował zegar skokowo, aplikacje w kontenerach zobaczą ten skok natychmiast.

Co nie jest dziedziczone

  • Strefa czasowa. Plik /etc/localtime i zmienna TZ żyją wewnątrz obrazu. W minimalnych obrazach Alpine pakiet tzdata może nie istnieć, a kontener zawsze pokazuje UTC. To nie błąd czasu, a błąd wyświetlania, ale regularnie wprowadza w błąd przy czytaniu logów.
  • Magazyn certyfikatów głównych. To nie temat tego artykułu, ale przypomnijmy: on też jest wewnątrz obrazu.
  • Prawo zmiany czasu. Wywołanie systemowe ustawienia czasu wymaga CAP_SYS_TIME, której kontener domyślnie nie ma. Polecenie date -s wewnątrz kontenera zakończy się błędem „Operation not permitted”. Uruchamianie demona NTP w zwykłym kontenerze nie ma sensu, a dawanie mu CAP_SYS_TIME to zła praktyka: zacznie zarządzać zegarem całego hosta.

Konsekwencje praktyczne

Diagnozę z poprzedniego rozdziału można i trzeba wykonywać wewnątrz kontenera — date -u i zapytanie o nagłówek Date przez proxy pokażą realny obraz. Ale naprawa zawsze odbywa się na hoście. Jeśli kontener działa w Kubernetesie, potrzebny host to konkretny węzeł, na którym zaplanowano pod; upewnij się, że chrony wchodzi do bazowego obrazu węzłów i że monitoring zbiera metrykę przesunięcia z każdego węzła.

Docker Desktop i WSL2 na laptopach to osobna historia, bo „hostem” dla kontenerów jest tam ukryta maszyna wirtualna, skłonna do spóźniania się po śnie. Dla WSL2 szybka naprawa — podciągnąć czas z zegara sprzętowego, który hiperwizor trzyma w porządku:

sudo hwclock -s

Dla Docker Desktop radykalny, ale niezawodny sposób — restart aplikacji; po starcie VM synchronizuje się z hostem. W świeżych wersjach problem występuje rzadziej, ale całkiem nie zniknął, dlatego healthcheck na przesunięcie zegara w środowisku dev to nie paranoja, a oszczędność porannej godziny.

Przykład: healthcheck w pliku compose

Idea — nie podnosić roboczego kontenera, jeśli zegar hosta uciekł dalej niż dopuszczalne. Sprawdzenie używa tej samej sztuczki z nagłówkiem Date po zwykłym HTTP przez proxy, żeby nie zależeć od TLS:

healthcheck:
 test: ["CMD", "python", "/app/clock_check.py"]
 interval: 5m
 timeout: 20s
 retries: 1

Skrypt clock_check.py — ten, który napisaliśmy w rozdziale o diagnozie, z progiem 30 sekund. Orkiestrator oznaczy kontener jako niezdrowy i zobaczysz przyczynę w statusie, a nie w stercie błędów TLS w logach aplikacji.

Typowe błędy i przypadki z praktyki

Rozdział o tym, czego nie robić, jest pożyteczniejszy niż rozdział o tym, co robić. Zebrano tu rozwiązania, które wydają się logiczne w momencie incydentu i drogo kosztują później, plus kilka charakterystycznych historii z liczbami.

Błąd 1: wyłączyć weryfikację certyfikatu

Flaga -k, verify=False, NODE_TLS_REJECT_UNAUTHORIZED=0. Skrypt znowu „działa”, zadanie zamknięte. Ale podpisy żądań i JWT nadal się nie zgadzają, TOTP nie jest przyjmowany, a ty wyłączyłeś jedyny mechanizm, który chronił kanał. Zegar przy tym pozostaje krzywy i za tydzień zepsuje coś jeszcze. Wyłączenie weryfikacji jest dopuszczalne dokładnie na jedno polecenie w ramach diagnozy i nigdy — w kodzie.

Błąd 2: zwiększyć leeway albo okno podpisu „z zapasem”

Podnieść tolerancję weryfikacji JWT do 10 minut — to rozszerzyć okno na ponowne użycie przechwyconego tokenu 20 razy. Rzeczywisty problem (zegar) przy tym nigdzie nie ucieka. Prawidłowy zapas dla JWT to 30–60 sekund, dla podpisów HMAC — to, co ustala dostawca API.

Błąd 3: poprawiać czas ręcznie zamiast synchronizacji

date -s naprawia objaw na parę godzin. Dryf i następny snapshot zwrócą wszystko z powrotem, tylko teraz nikt nie będzie pamiętał, że czas poprawiano ręcznie.

Błąd 4: dwa demony czasu jednocześnie

chrony i timesyncd, albo chrony i agent gościa hiperwizora z włączoną synchronizacją, ciągną zegar w różne strony. W logach chrony widać to jako ciągłe duże korekty. Zostaw jedno źródło prawdy.

Błąd 5: porównywać czas lokalny z UTC

Różnica dokładnie 3, 5 lub 7 godzin — prawie zawsze nie awaria zegara, a strefy czasowe. Zawsze porównuj date -u ze źródłem UTC, a w logach pisz czas z jawnym wskazaniem strefy.

Błąd 6: ufać statusowi „installed”

Rola Ansible zainstalowała chrony, zadanie zielone. A UDP 123 jest zablokowany i chrony jest zsynchronizowany „sam ze sobą”. Sprawdzenie w konfiguracji infrastruktury powinno patrzeć na Leap status i przesunięcie, a nie na fakt istnienia pakietu.

Błąd 7: używać wielokrotnie podpisanego żądania przy retryach

Zwłaszcza przez proxy z ponownymi połączeniami. Każda próba powinna dostać nowy znacznik czasu i nowy podpis.

Przypadek A: snapshot stanowiska testowego

Zespół zbierający publiczne dane o cenach trzymał pulę workerów w maszynach wirtualnych i co tydzień przywracał stanowisko z „czystego” snapshotu. Po jednym z przywróceń wszystkie workery chodzące przez proxy zaczęły padać z „certificate is not yet valid” na jednej trzeciej docelowych stron — dokładnie tych, które używały 90-dniowych certyfikatów i zdążyły je wymienić przez 12 dni życia snapshotu. Z laptopa inżyniera bez proxy strony się otwierały. Półtorej godziny zeszło na przełączanie pul i taryf u dostawcy proxy, zanim ktoś wykonał date -u i zobaczył 12-dniowe spóźnienie. Poprawka: chrony z makestep w bazowym obrazie stanowiska i sprawdzanie przesunięcia w skrypcie po odtworzeniu. Powtórek nie było.

Przypadek B: izolowany segment i mini-PC

Mini-PC bez zegara sprzętowego stał w segmencie sieci, gdzie na zewnątrz dozwolone było tylko HTTP przez korporacyjne proxy. Po planowym wyłączeniu prądu wystartował z datą zbudowania obrazu — 14 miesięcy w przeszłości. Żadne połączenie TLS się nie nawiązywało, co wyglądało jak całkowita awaria proxy. Rozwiązanie: otwarto UDP 123 do wewnętrznego serwera NTP firmy, chrony dostał dyrektywę makestep 1.0 -1, a na wypadek braku NTP dodano uruchamianie htpdate przez proxy przy starcie.

Przypadek C: eksport do magazynu obiektowego z Docker Desktop

Programista debugował eksport plików do magazynu zgodnego z S3 przez proxy z kontenera na laptopie. Po przerwie obiadowej wszystkie żądania zaczęły zwracać RequestTimeTooSkewed. Przesunięcie zegara maszyny wirtualnej Docker Desktop — 23 minuty, tuż powyżej 15-minutowego okna. Restart Docker Desktop rozwiązał sprawę w minutę; do projektu dodano healthcheck przesunięcia, który od razu pokazuje przyczynę.

Przypadek D: 50 sekund i TOTP

Serwis zautomatyzował logowanie do własnego portalu korporacyjnego kontem usługowym z dwuskładnikowym uwierzytelnianiem TOTP. Kody przestały być przyjmowane: serwer odpowiadał, że kod jest nieprawidłowy. Przesunięcie zegara węzła — 50 sekund, bo timesyncd był zainstalowany, ale zamaskowany przy przejściu na chrony, którego ostatecznie nie włączono. Przy tolerancji jednego 30-sekundowego interwału to wystarczyło. Zero zmian w kodzie, tylko uruchomienie chrony.

Narzędzia i checklista „zanim obwinisz proxy”

Zbierzmy wszystko w formę, którą można załączyć do zgłoszenia lub wpiąć do runbooka. Najpierw narzędzia, potem kolejność.

Narzędzia

  • date -u, timedatectl, chronyc, hwclock — stan zegara i synchronizacji.
  • curl -v --proxy, openssl s_client -proxy — sprawdzenie TLS przez proxy z kodem weryfikacji.
  • chronyd -Q, ntpdate -q — jednorazowy pomiar przesunięcia bez zmiany zegara.
  • Nagłówek Date po HTTP — uniwersalne źródło czasu, działające przez dowolne proxy.
  • libfaketime, openssl -attime — odtworzenie awarii i testowanie obsługi błędów.
  • htpdate — awaryjna synchronizacja w segmentach, gdzie zablokowany jest UDP.
  • Metryki — eksportery dla chrony lub timesyncd oddają przesunięcie do systemu monitoringu; próg alertu 1 sekunda, krytyczny — 30 sekund.

Checklista

  1. Gdzie dokładnie wykonuje się padający kod? Laptop, serwer, VM, kontener, pod. Wszystkie sprawdzenia robimy tam.
  2. Czy date -u zgadza się z telefonem? Rozbieżność w minutach — stop, napraw zegar.
  3. Czy nagłówek Date po http:// przez proxy zgadza się z lokalnym UTC w granicach paru sekund? Jeśli nie — zegar.
  4. Czy timedatectl mówi synchronized: yes, a chronyc tracking pokazuje Leap status Normal? Jeśli nie — synchronizacja nie działa, niezależnie od tego, co jest zainstalowane.
  5. Czy tekst błędu zawiera not yet valid, has expired, RequestTimeTooSkewed, jwt expired, jwt not active, timestamp? Każde z tych słów — o czasie.
  6. Bez proxy z tej samej maszyny błąd jest taki sam? Jeśli tak — proxy nie ma nic do rzeczy.
  7. Przez proxy z innej maszyny błędu nie ma? Jeśli tak — problem jest w pierwszej maszynie.
  8. Czy openssl s_client przez proxy pokazuje Verify return code 9 lub 10 przy żywych datach certyfikatu? Diagnoza potwierdzona.
  9. Błędy tylko z jednym hostem docelowym? Porównaj jego Date z innymi — możliwe, że zegar uciekł po jego stronie.
  10. Czy podpisy są formowane na nowo przy każdej próbie? Jeśli nie — popraw logikę retryów.
  11. Czy był niedawno snapshot, sen, migracja, restart po długim wyłączeniu? Każde „tak” — prawie na pewno przyczyna.
  12. Dopiero po wszystkich „nie” — zwrócenie się do dostawcy proxy z załączonymi wynikami date -u, chronyc tracking i curl -v. Wtedy wsparcie Proxeon lub dowolnego innego serwisu rozwiąże sprawę w jednej rundzie korespondencji, a nie w pięciu.

FAQ: częste pytania o czas, proxy i awarie

Czy proxy samo może zniekształcić czas w moich żądaniach lub w TLS?

Przy tunelowaniu przez CONNECT lub SOCKS5 — nie. Uzgadnianie TLS przebiega między twoim klientem a serwerem docelowym, proxy widzi tylko zaszyfrowane bajty. Znaczniki czasu w podpisach i JWT tworzy twój kod na twojej maszynie. Jedyne, co proxy może dodać, to opóźnienie, a ono mierzy się w milisekundach, nie w minutach.

Dlaczego z laptopa bez proxy działa, a z serwera przez proxy nie?

Bo to dwie różne maszyny z dwoma różnymi zestawami zegarów. Porównanie jest niepoprawne z założenia: zmieniły się od razu dwie zmienne. Żeby wyizolować wpływ proxy, uruchom żądanie z serwera bez proxy. Żeby wyizolować wpływ maszyny — uruchom przez proxy z laptopa. Prawie zawsze okaże się, że błąd jest przywiązany do maszyny.

Jak dokładny musi być czas do pracy przez proxy?

Dla TLS wystarcza dokładność w godzinach, jeśli certyfikaty są długotrwałe, i w dniach dla krótkotrwałych. Dla podpisów żądań typowe okno to 5–15 minut. Dla JWT — 30–60 sekund zapasu. Dla TOTP — plus-minus 30 sekund. Działający NTP daje milisekundy, dlatego praktyczny standard to przesunięcie poniżej 1 sekundy, a alert przy przekroczeniu 30 sekund.

Czy mogę uruchomić klienta NTP wewnątrz kontenera?

Technicznie — tylko z CAP_SYS_TIME, a wtedy będzie zarządzać zegarem całego hosta, co narusza izolację i wchodzi w konflikt z demonem na hoście. Właściwe podejście — synchronizacja na hoście lub węźle klastra. W kontenerze zostaw tylko sprawdzanie przesunięcia.

Co robić, jeśli UDP 123 jest szczelnie zamknięty, a wyjście jest tylko przez HTTP-proxy?

Poproś o otwarcie UDP przynajmniej do wewnętrznego NTP firmy lub dostawcy chmurowego — to standardowa potrzeba infrastrukturalna. Zanim się to rozwiąże, używaj synchronizacji od hiperwizora lub htpdate po nagłówkach Date przez proxy. Uwzględnij, że NTS chroni NTP przez TLS na porcie TCP 4460, ale same pakiety czasu nadal idą po UDP, więc NTS nie omija blokady UDP.

Zegar spieszy się o kilka minut, ale TLS działa. Dlaczego więc padają żądania do API?

Bo TLS ma okno w dniach, a podpisy i tokeny — w sekundach i minutach. To normalna sytuacja: certyfikaty przechodzą weryfikację, a podpis HMAC ze znacznikiem czasu przesuniętym o trzy minuty w przyszłość jest odrzucany przez serwer z twardym oknem. Patrz na tekst błędu z API, a nie tylko na to, czy połączenie się udało.

Jak poznać, że uciekł zegar serwera docelowego, a nie mój?

Porównaj nagłówek Date z odpowiedzi problematycznego hosta z Date dwóch-trzech innych hostów i ze swoim date -u. Jeśli twój zegar zgadza się z większością, a jeden host odbiega — problem jest po jego stronie. Niektóre klienty SDK potrafią automatycznie kompensować przesunięcie serwera, obliczając je z odpowiedzi z błędem czasu.

Czy trzeba podpisywać żądanie ponownie przy kolejnej próbie przez proxy?

Tak. Podpis zawiera znacznik czasu i jest ważny w granicach okna. Jeśli między pierwszą próbą a powtórzeniem minęły minuty oczekiwania i ponownych połączeń, stary podpis może wyjść za okno. Formuj podpis w momencie wysłania każdej próby.

Jak wpiąć sprawdzanie zegara w monitoring?

Zbieraj przesunięcie z chrony lub timesyncd do systemu metryk i ustaw dwa progi: ostrzeżenie przy 1 sekundzie, krytyczny przy 30 sekundach. Dodatkowo — healthcheck serwisu po nagłówku Date przez działające proxy; łapie sytuacje, gdy demon czasu formalnie żyje, a zegar mimo to uciekł, na przykład po odtworzeniu snapshotu.

Czy w 2026 roku coś się zmieniło?

Główna zmiana — skrócenie terminów życia publicznych certyfikatów i masowe rozpowszechnienie automatycznego odnawiania. To sprawia, że nawet niewielkie spóźnienia zegara, w dniach, są realną przyczyną awarii TLS, podczas gdy kiedyś takie awarie były rzadkością. Równocześnie rośnie udział krótkich JWT i rygorystycznych okien podpisów w API. Innymi słowy, wymagania co do dokładności zegara na maszynach klienckich rosną, a synchronizacja czasu przestaje być „ustawieniem domyślnym, o którym się nie myśli”.

Zakończenie: czas jako pierwszy punkt diagnozy

Podsumujmy. Sprawdzanie terminu ważności certyfikatu, pól exp i nbf w JWT, znacznika czasu w podpisie żądania i kodu jednorazowego — to wszystko lokalne porównania z zegarem systemowym. Przy pracy przez proxy w łańcuchu pojawia się jeszcze jedna maszyna i uwaga naturalnie się na nią przenosi, choć ani uzgadnianie TLS, ani podpis, ani walidacja tokenu przez tunel proxy nie przechodzą. Proxy w tych scenariuszach to tylko transport. A zegary uciekają z całkiem przyziemnych powodów: snapshoty i pauza maszyn wirtualnych, sen laptopów z Docker Desktop i WSL2, brak lub niesprawność demona synchronizacji, zamknięty UDP, rozładowana bateria, ręczna korekta.

Praktyczny program jest prosty i mieści się w trzech krokach. Pierwszy — przyzwyczaić się rozpoznawać błędy czasowe po tekście: not yet valid, has expired, RequestTimeTooSkewed, jwt expired, jwt not active. Drugi — zrobić sprawdzanie przesunięcia zegara pierwszym punktem każdego rozbioru incydentu z proxy: date -u, nagłówek Date po HTTP przez proxy, timedatectl lub chronyc tracking. To minuta, która oszczędza godziny. Trzeci — zrobić tak, żeby problem nie wracał: chrony z iburst i makestep w bazowych obrazach, jedno źródło czasu na maszynę, synchronizacja na hoście dla kontenerów, metryka przesunięcia w monitoringu i healthcheck w serwisach, które chodzą przez proxy.

Kiedy to wszystko jest zrobione, zwrócenie się do dostawcy proxy staje się sensowne: przychodzisz z wykluczoną hipotezą i gotowymi wynikami poleceń, a rozbiór idzie po istocie. A w większości przypadków zwrócenie się po prostu nie będzie potrzebne — bo zegar okaże się pierwszym, co sprawdzisz, i ostatnim, co cię zaskoczy.