Certifikat nie je expirnuty, vas node ano: Time Drift rozbitie TLS a JWT v Kubernetes
Time drift spravi z TLS a JWT divny, prerusovany horor. “Certifikat expiroval? Ale obnovili sme ho minuly tyzden!” Alertovy dashboard zacal svietit TLS handshake zlyhaniami napriec viacerymi sluzbami. Niektore pody zlyhavali pri pripojeni na externe API. Ine zamietali JWT ako expirnuty. Ale ked som skontroloval certifikaty, boli platne este mesiace. Ked som skontroloval JWT, nebol expirnuty. Problem zmizol v case ked som zacal vysetrovat.
Potom sa to stalo znova o dva dni. Ten isty vzorec - burst zlyhaní, potom vsetko v poriadku. Tentokrat som bol rychlejsi. SSH na jeden z postihnutych nodov a spustil chronyc tracking. Tam to bolo: NTP prave opravilo 15-sekundovy drift krokovym upravenim. Pocas tych 15 sekund boli hodiny nodu dost nespravne aby posunuli notBefore kontroly certifikatov do “este neplatny” teritoria a JWT exp claims do “expirnuty” teritoria.
Frustrujuce bolo ze kym zacnete vysetrovat, dokazy su prec. NTP opravi hodiny, sluzby sa zotavia, a vsetko co mate su zahadne logy o expirnutych certifikatoch ktore v skutocnosti nie su expirnuty. Musite vediet ze treba pozriet NTP/chrony logy a time sync metriky aby ste videli ze problem boli samotne hodiny.
Prostredie: Kubernetes 1.28+, nody s NTP/chrony, sluzby pouzivajuce TLS a JWT autentikaciu
Pochopenie problemu
Kedy Time Validacia zlyha
TLS Certifikat validacia:
┌─────────────────────────────────────────────────────────────┐
│ Platnost certifikatu │
│ notBefore now() notAfter │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ 2024-01-01 2024-06-15 2025-01-01 │
│ │ │ │ │
│ └─────── platny ──┴────── platny ────┘ │
└─────────────────────────────────────────────────────────────┘
Hodiny sa posunu 15 sekund do MINULOSTI:
┌─────────────────────────────────────────────────────────────┐
│ notBefore now() │
│ │ │ (hodiny si myslia ze je 31.12.2023)│
│ ▼ ▼ │
│ 2024-01-01 2023-12-31 │
│ │ │ │
│ └── ESTE NEPLATNY! │
│ "certificate is not valid yet" │
└─────────────────────────────────────────────────────────────┘
Hodiny sa posunu 15 sekund do BUDUCNOSTI:
┌─────────────────────────────────────────────────────────────┐
│ now() notAfter │
│ │ │ │
│ ▼ ▼ │
│ 2025-01-02 2025-01-01 │
│ │ │ │
│ └── EXPIRNUTY! │
│ "certificate has expired"│
└─────────────────────────────────────────────────────────────┘
JWT Token validacia zlyha podobne
JWT claims:
{
"iat": 1718450000, // vydany: 15. jun 2024 12:00:00
"exp": 1718453600, // expiruje: 15. jun 2024 13:00:00
"nbf": 1718450000 // nie pred: 15. jun 2024 12:00:00
}
Normalna validacia (now = 12:30:00):
iat (12:00:00) < now (12:30:00) < exp (13:00:00) ✓
Drift hodin +35 minut (now si mysli ze je 13:05:00):
now (13:05:00) > exp (13:00:00)
→ "Token expirnuty" (ale nie je!)
Drift hodin -35 minut (now si mysli ze je 11:55:00):
now (11:55:00) < nbf (12:00:00)
→ "Token este neplatny" (ale mal by byt!)
Preco sa to stava v Kubernetes
Bezne scenare driftu hodin:
1. VM live migracia / pause-resume
┌────────────────────────────────────────────┐
│ Node VM pozastaveny pre migraciu │
│ Realny cas plynie: 30 sekund │
│ VM pokracuje │
│ Hodiny nodu su teraz 30 sekund v minulosti │
│ NTP eventualne opravi krokom │
└────────────────────────────────────────────┘
2. Udrzba cloud providera
┌────────────────────────────────────────────┐
│ Hypervisor robi udrzbu │
│ Guest VM hodiny stoja na N sekund │
│ Hodiny nahle pozadu ked VM pokracuje │
└────────────────────────────────────────────┘
3. NTP krokova korekcia
┌────────────────────────────────────────────┐
│ Hodiny nodu pomaly driftuju cez dni │
│ NTP zbada 15-sekundovy drift │
│ NTP skoci hodiny dopredu/dozadu │
│ Nahle: cas skoci! │
└────────────────────────────────────────────┘
4. Zla NTP konfiguracia
┌────────────────────────────────────────────┐
│ Nespravne NTP servery nastavene │
│ NTP server sam ma nespravny cas │
│ Vsetky nody "synchronizuju" nespravny cas │
└────────────────────────────────────────────┘
Diagnostika casovych problemov
Skontroluj aktualny stav time sync
# Na postihnutom node
# Pouzitie chrony (najbeznejsie na modernych systemoch)
chronyc tracking
# Reference ID : 169.254.169.123 (time.google.com)
# Stratum : 3
# System time : 0.000015 seconds fast of NTP time
# Last offset : +0.000012 seconds ← male je dobre
# Skontroluj zdroje
chronyc sources -v
# Pouzitie timedatectl
timedatectl status
# Hladaj: System clock synchronized: yes
Skontroluj nedavne casove skoky
# Chrony logy - hladaj skoky
journalctl -u chronyd --since "2 hours ago" | grep -i "step\|jump\|offset"
# Jun 15 12:00:00 node1 chronyd[1234]: System clock was stepped by 15.234567 seconds
# Kernel spravy o case
dmesg | grep -iE "clocksource|timekeep|tsc|time.*jump"
Prometheus metriky (ak dostupne)
# Aktualny casovy offset od NTP
node_timex_offset_seconds
# Status syncu (1 = synchronizovany)
node_timex_sync_status
# Alert na velky offset
abs(node_timex_offset_seconds) > 0.5
Reprodukcny Lab
Pouzitie libfaketime (bezpecne, process-lokalne)
# docker-compose.yml - Bezpecna simulacia time driftu
version: '3.8'
services:
tls-server:
image: nginx:alpine
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/certs:ro
ports:
- "8443:8443"
client-normal:
image: alpine:3.18
command: sh -c "apk add curl ca-certificates && sleep infinity"
client-future:
image: debian:bookworm
environment:
LD_PRELOAD: /usr/lib/x86_64-linux-gnu/faketime/libfaketime.so.1
FAKETIME: "+2d" # Tento kontajner si mysli ze je o 2 dni v buducnosti
command: sh -c "apt-get update && apt-get install -y curl libfaketime ca-certificates && sleep infinity"
client-past:
image: debian:bookworm
environment:
LD_PRELOAD: /usr/lib/x86_64-linux-gnu/faketime/libfaketime.so.1
FAKETIME: "-2d" # Tento kontajner si mysli ze je o 2 dni v minulosti
command: sh -c "apt-get update && apt-get install -y curl libfaketime ca-certificates && sleep infinity"
Vygeneruj kratkodoby testovaci certifikat
mkdir -p certs
openssl req -x509 -newkey rsa:2048 -nodes -days 1 \
-keyout certs/key.pem -out certs/cert.pem \
-subj "/CN=localhost" \
-addext "subjectAltName=DNS:localhost,DNS:tls-server"
Spusti demo
docker compose up -d
sleep 10
# Normalny klient - funguje
docker compose exec client-normal curl -vk https://tls-server:8443/
# Malo by ukazat: HTTP/1.1 200 OK
# Klient v buducnosti (+2 dni) - certifikat "expirnuty"
docker compose exec client-future curl -v https://tls-server:8443/
# Chyba: certificate has expired
# Klient v minulosti (-2 dni) - certifikat "este neplatny"
docker compose exec client-past curl -v https://tls-server:8443/
# Chyba: certificate is not yet valid
Oprava
1. Nakonfiguruj NTP na slew namiesto step
# /etc/chrony/chrony.conf
# Pouzi spolahlive NTP servery
server time.google.com iburst
server time.cloudflare.com iburst
# Povol step len pri boote (prvy 3 aktualizacie), potom slew
makestep 1.0 3
# Po boote pouzi slew (postupna uprava)
maxslewrate 500
# Zapni RTC sync
rtcsync
# Loguj vyznamne udalosti
log tracking measurements statistics
logdir /var/log/chrony
2. Monitoruj Time Sync
# Prometheus alertovacie pravidla
groups:
- name: time-sync
rules:
- alert: NodeTimeOffsetHigh
expr: |
abs(node_timex_offset_seconds) > 0.1
for: 2m
labels:
severity: warning
annotations:
summary: "Node {{ $labels.instance }} casovy offset je {{ $value }}s"
description: "Time drift detekovany - moze sposobovat TLS/JWT zlyhania"
- alert: NodeTimeNotSynced
expr: |
node_timex_sync_status != 1
for: 5m
labels:
severity: critical
annotations:
summary: "Node {{ $labels.instance }} cas nie je synchronizovany"
description: "NTP sync strateny - validacia certifikatov a tokenov zlyha"
3. Pridaj JWT Clock Skew Toleranciu
// Go - Pridaj toleranciu skew hodin k JWT validacii
import (
"github.com/golang-jwt/jwt/v5"
"time"
)
func validateToken(tokenString string) (*jwt.Token, error) {
return jwt.Parse(tokenString, keyFunc,
// Povol 30 sekund skew hodin
jwt.WithLeeway(30*time.Second),
)
}
// Java - JJWT clock skew
Jwts.parser()
.setAllowedClockSkewSeconds(30) // 30 sekundova tolerancia
.setSigningKey(key)
.parseClaimsJws(token);
# Python - PyJWT clock skew
import jwt
from datetime import timedelta
jwt.decode(
token,
key,
algorithms=["RS256"],
leeway=timedelta(seconds=30) # 30 sekundova tolerancia
)
4. Pouzi Monotonny cas pre trvania
// Zle: Pouzivanie wall clock pre ubehnuty cas
startTime := time.Now()
// ... praca ...
elapsed := time.Now().Sub(startTime) // Moze byt negativne ak hodiny skocili!
// Spravne: time.Since pouziva monotonny clock interno
startTime := time.Now()
// ... praca ...
elapsed := time.Since(startTime) // Pouziva monotonnu cast
// Java - Pouzi nanoTime pre trvania, nie currentTimeMillis
// Zle:
long start = System.currentTimeMillis();
// ... praca ...
long elapsed = System.currentTimeMillis() - start; // Moze ist do zaporu!
// Spravne:
long start = System.nanoTime();
// ... praca ...
long elapsed = System.nanoTime() - start; // Vzdy rastie
Checklist
## Time Drift Debugging Checklist
### Okamzita detekcia
- [ ] Skontroluj aktualny offset: `chronyc tracking`
- [ ] Skontroluj nedavne skoky: `journalctl -u chronyd | grep step`
- [ ] Skontroluj kernel casove udalosti: `dmesg | grep -i clock`
- [ ] Skontroluj NTP sync status: `timedatectl status`
### Root Cause
- [ ] VM pause/resume udalosti (live migracia)
- [ ] NTP nespravna konfiguracia
- [ ] Nespravne NTP servery
- [ ] Nestabilita clocksource
### Oprava
- [ ] Nakonfiguruj chrony na slew po boote: `makestep 1.0 3`
- [ ] Pridaj JWT clock skew toleranciu (30s odporucane)
- [ ] Monitoruj time offset s alertingom
- [ ] Pouzi monotonny cas pre vypocty trvani
### Prevencia
- [ ] Alert na |offset| > 100ms
- [ ] Alert na sync status != 1
- [ ] Loguj time step udalosti
- [ ] Prehodnost VM live migration politiky
Zaver
Time drift je jeden z tych problemov co sa zda ze by sa nemal stat v 2025. Mame NTP. Mame atomove hodiny podporujuce time servery cloud providerov. A predsa hodiny vaseho Kubernetes nodu mozu byt dost nespravne aby rozbili TLS a JWT validaciu.
Jadrom problemu je ze NTP je navrhnuty na opravu driftu, nie jeho prevenciu. Ked je vas VM pozastaveny pre migraciu, alebo ked ma hypervisor moment nestability, hodiny vaseho nodu zaostanu. NTP si vsimne a opravi to - casto “step” upravou ktora skoci hodiny dopredu. Pre to kratke okno pred korekciou a pocas samotneho skoku, casova validacia zlyha.
Co robi toto zvlast frustrujucim je ze dokazy miznii. Kym SSH na node a skontrolujete, NTP uz opravilo hodiny. Certifikaty su platne. JWT nie su expirnuty. Mate len zahadne logy o expirnutych veciach ktore v skutocnosti nie su expirnuty.
Klucove principy:
- Wall-clock cas nie je spolahlivy - moze skocit dopredu aj dozadu
- NTP opravuje drift ale moze sposobit skoky - nahly 15-sekundovy skok rozbije vsetko kratko
- TLS a JWT validacia verí hodinam - nespravne hodiny = false “expired” chyby
- Dokazy miznii po oprave NTP - kontroluj chrony logy, nie len aktualny cas
- Pridaj clock skew toleranciu - 30 sekund tolerancie zvladne vacsinu drift scenarov
Nabuduce ked uvidite “certificate expired” pre certifikat ktory nie je expirnuty, najprv skontrolujte historiu time sync vaseho nodu.
Suvisiace clanky
- Split-Brain z Clock Step Backwards - Wall time v lease-based systemoch
- gRPC Keepalive Transport Closing - Dalsi “funguje potom zlyha” debugging pribeh
Súvisiace články
Split-Brain z Posunu Hodín Dozadu: Wall Time v Lease-Based Systémoch
Dva nody súčasne veria že držia leader lease. Príčina: malá NTP korekcia hodín dozadu kombinovaná s kódom ktorý mieša wall-clock čas s duration-based timeoutmi.
ingress-nginx reload búrky: prečo 502 špičky sedia s Ingress churnom
Reloady NGINX Ingressu vedia dropovať keep-alive a robiť 502 špičky pri častých zmenách. Runbook na dôkaz reloadu, zníženie churnu a hardening.
tcpdump vidí SYN, ale služba timeoutuje: pasca listen backlogu
Klienti timeoutujú, tcpdump ukazuje SYN (niekedy aj SYN-ACK), ale aplikácia nič neloguje. Častý vinník: Linux listen/accept fronty, ktoré sa pri load-e alebo CPU starvation preplnia.
Pakety prichadzaju ale aplikacia timeoutuje: rp_filter pasca v Kubernetes
tcpdump ukazuje pakety ktore prichadzaju, ale aplikacia nic nevidi. Vinik: Linux reverse path filtering ticho zahadzuje pakety predtym nez dosiahnu iptables, sposobene asymetrickym routovanim.
Citujte tento článok
Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.