시간은 그냥 '참고값'이 아니에요

개발하다 보면 NTP(Network Time Protocol)를 그냥 "시계 맞추는 프로토콜" 정도로 생각하기 쉬운데요. 실제로는 인증서 검증, 토큰 만료, 재전송 공격 방어, 로그 순서 보장 등 거의 모든 보안 결정의 기준값이 바로 이 시간이에요.

문제는 NTP가 1985년에 설계된 이후로 인증 기능이 전혀 없다는 점이에요. 클라이언트가 48바이트를 보내면, 누군가 48바이트를 돌려주고, 클라이언트는 그걸 그대로 믿어요. 서명도 없고 신원 확인도 없죠.

X.509 인증서의 notBefore/notAfter를 검증하려면 현재 시간이 필요하고, 현재 시간을 신뢰하려면 인증서 검증이 필요하다? 이게 바로 순환 구조예요.

과거에는 시계가 몇 초 틀려도 운영상 불편한 정도였는데, 지금은 다릅니다. 특히 CA/Browser Forum의 SC-081v3로 TLS 인증서 유효기간이 2026년 3월 200일 → 2027년 3월 100일 → 2029년 3월 47일로 단축되면, 시계가 틀어진 호스트의 자동 갱신 루프는 아무도 안 보는 새벽에 조용히 실패해요.

Meta는 이 문제를 풀기 위해 nts.meta.com을 공개하고, 서버·클라이언트·프로토콜 구현을 전부 GitHub에 오픈소스로 풀었어요. 근거자료는 Meta 엔지니어링 블로그에서 확인할 수 있어요.

Developer configuring chrony NTS pool on terminal for authenticated NTP time sync Development Concept Image

NTS는 2단계로 동작해요

Phase 1: NTS-KE (키 교환)

TCP/4460 위에서 TLS 1.3으로 핸드셰이크하고, ALPN은 ntske/1을 써요. 여기서 AEAD 알고리즘을 협상하고, RFC 5705 exporter로 세션 키 2개(C2S, S2C)를 뽑아내요. 서버는 쿠키 8개를 발급하고, 어떤 NTP 서버로 붙어야 하는지 알려주고 연결을 닫아요. 패킷마다가 아니라 딱 한 번이에요.

Phase 2: 인증된 NTP

UDP/123으로 NTPv4 패킷을 보내는데, 여기에 NTS 확장 필드가 붙어요. 요청에는 고유 식별자, 쿠키, 인증자가 들어가고요. 서버는 쿠키를 열어 세션 키를 복구하고 인증자를 검증한 뒤 응답해요. 위조 패킷은 검증 실패로 조용히 드롭되고, NTS NAK도 보내지 않아서 공격자가 이 실패를 악용해 클라이언트를 강제로 재키교환 시킬 수 없어요.

상태 없는 쿠키 설계가 핵심

보통 쿠키라고 하면 서버가 상태를 들고 있어야 한다고 생각하는데, Meta는 키 링을 복제하지 않아요. 대신 마스터 시크릿과 현재 epoch day로 봉인 키를 파생시켜요.

# 쿠키 봉인 키 파생 (의사 코드)
# master: 서버 공유 마스터 시크릿
# day: Unix epoch 기준 24시간 단위 정수 (타임존/DST 없음)

import hmac, hashlib, struct

def derive_cookie_seal_key(master: bytes, day: int) -> bytes:
    # HKDF-SHA256(master, salt=BE32(day), info="fbnts-cookie-seal-v1")
    salt = struct.pack(">I", day)
    prk = hmac.new(salt, master, hashlib.sha256).digest()
    return hmac.new(prk, b"fbnts-cookie-seal-v1", hashlib.sha256).digest()

# 서버는 이 키로 쿠키를 AES-SIV로 열고 닫음
# 키 링 복제 없음, 세션 테이블 없음, 공유 상태 없음

이 설계 덕분에 KE 서버와 NTP 응답 서버가 서로 다른 머신이어도 아무것도 주고받을 필요가 없어요. 서버 추가는 그냥 서버 추가예요.

chrony 설정은 딱 한 줄

# /etc/chrony/chrony.conf
pool nts.meta.com nts iburst maxsources 5

nts.meta.com은 키 교환만 담당하고, 핸드셰이크 중에 실제 NTP 응답자를 알려줘요. 그래서 설정한 호스트와 실제로 통신하는 호스트가 달라요. server 대신 pool을 쓰는 이유는 단일 장애점을 피하고 NTP가 여러 소스로 다수결을 할 수 있게 하기 위해서예요.

# chrony sources 확인
chronyc -N authdata
# Name/IP address  Mode  KeyID  Type  KLen  Last  Atmp  NAK  Cook  CLen
# time1.meta.com   NTS   5      30    128   27    0     0    8     68
# time2.meta.com   NTS   7      30    128   142   0     0    8     68
# time3.meta.com   NTS   10     30    128   548   0     0    8     68
# time4.meta.com   NTS   1      30    128   1177  0     0    8     68
# time5.meta.com   NTS   2      30    128   1177  0     0    8     68

Type 30은 협상된 AEAD(AES-128-GCM-SIV), KLen 128은 세션 키 길이, CLen 68은 쿠키 크기(36바이트 봉투 + 16바이트 키 2개), Cook 8은 보유 쿠키 수, NAK 0은 거부된 적 없음을 의미해요.

Network diagram showing NTS key establishment phase over TLS 1.3 and authenticated NTP packets System Abstract Visual

NTS가 막아주는 것과 못 막는 것

항목NTS 지원 여부설명
MITM 시계 조작✅ 방어서명 검증 실패 패킷은 드롭. 시계 되돌리기/앞당기기 공격 차단
재전송 공격✅ 방어고유 식별자 불일치로 매칭 실패
클라이언트 추적✅ 방어쿠키가 응답 인증자 안에 암호화되어 매 교환마다 다른 blob
지연 공격(Delay)❌ 불가RFC 8915 명시. 패킷 보류 시 최대 지연의 절반까지 시계 이동 가능
부트스트래핑❌ 불가NTS-KE가 TLS 위에서 돌아서 결국 시계가 필요 (순환 문제)
잘못된 서버❌ 불가경로는 인증하지만 답의 정확성은 보증 안 함. 쿼럼/모니터링 필수
단조성(Monotonic)❌ 불가wall clock은 윤초나 보정으로 뒤로 갈 수 있음. 경과 시간 측정엔 monotonic clock 사용

진짜 큰 문제는 모바일이에요

서버 진영은 chrony, ntpsec이 이미 NTS를 구현했고, 공개 NTS 서버 목록도 PTB, Netnod, time.nl 등 꽤 있지만 Android와 iOS의 기본 시간 클라이언트는 여전히 무인증 SNTP예요.

  • Android: 플랫폼 시간 동기화는 UDP/123 위의 plain SNTP (고정 48바이트, 확장 필드 처리 없음) + NITZ + GNSS. NTS 경로 없음.
  • Apple: timed가 time.apple.com과 동기화하는데 NTS 지원은 확인되지 않음.

이 두 스택이 수십억 대의 폰·워치·헤드셋 시계를 세팅하는데, 전부 무인증 UDP 패킷을 신뢰하고 있는 거예요. 벤더가 chrony를 따로 올려서 우회할 수는 있지만, 플랫폼이 직접 해야 할 일을 우회하는 것뿐이죠.

2036년 NTP 32비트 래핑 이후로 시계가 밀린 기기는 TLS 실패 → 업데이트 서버 접근 불가 → 패치 배포 불가의 데드락에 빠져요. 공장 초기화나 서비스 센터행밖에 답이 없죠. 함대 단위로 생각하면 그냥 물류 문제가 돼요.

Smartphone platform time client still using unauthenticated SNTP packet without NTS support Software Concept Art

실무 적용을 위한 정리

  1. 서버 운영자라면: 공개 시간 서비스를 돌리고 있다면 NTS를 켜세요. Meta 구현체가 GitHub에 있어요.
  2. NTP 클라이언트 관리자라면: RFC 8915를 구현하거나 chrony/ntpsec으로 갈아타세요.
  3. 모바일 OS 시간 스택 담당자라면: 여기가 진짜 갭이에요. Android/iOS 둘 다 아직 무인증 패킷이에요.
  4. 일반 백엔드 개발자라면: 경과 시간 측정에는 반드시 monotonic clock을 쓰고, 시계 보정이 필요한 로직은 쿼럼과 sanity limit을 두세요.

NTS는 만능이 아니에요. 경로는 인증하지만 답의 정확성은 보증하지 않고, 지연 공격은 못 막고, 부트스트래핑 순환 문제도 남아 있어요. 그래도 "아무도 아닌 누군가가 준 시계"에서 "검증된 발신자가 준 시계"로 넘어가는 것만으로도 인증서 갱신 자동화 시대에는 충분히 의미 있는 진전이에요.

함께 보면 좋은 글

본 콘텐츠는 신뢰할 수 있는 출처를 바탕으로 AI 도구를 활용하여 초안이 작성되었으며, 편집자의 검토를 거쳐 발행되었습니다. 전문가의 조언을 대체하지 않습니다.