Published on

EKS Pod 시간 드리프트로 STS·TLS 실패 해결하기

Authors

서버 시간이 몇 분만 틀어져도 인증·암호화 계층은 가차 없이 실패합니다. EKS에서는 이 문제가 더 교묘하게 나타나는데, 애플리케이션 Pod에서 STS(IRSA) 403/InvalidIdentityToken이 터지거나, 외부 HTTPS 호출이 x509: certificate has expired / not yet valid로 깨지면서 “IAM이 문제인가?”, “VPC 엔드포인트가 문제인가?”로 진단이 샛길로 빠지기 쉽습니다.

이 글은 EKS에서 Pod/노드 NTP 시간 드리프트로 인해 STS·TLS가 실패하는 전형적인 패턴을 정리하고, 빠르게 원인을 분리한 뒤 chrony/NTP 구성과 노드 레벨에서 근본 해결하는 방법을 다룹니다.

> IRSA 자체 구성(thumbprint, OIDC provider, trust policy) 문제로 403이 나는 케이스는 별도입니다. 그 흐름은 EKS OIDC Thumbprint 변경 후 IRSA 403 복구도 함께 참고하세요.

1) 증상: STS 403, TLS x509 오류가 같이 터진다

시간 드리프트가 의심되는 대표 로그는 아래와 같습니다.

STS/IRSA 계열

  • InvalidIdentityToken: Token is not valid yet
  • InvalidIdentityToken: Token has expired
  • RequestExpired: Request has expired
  • SignatureDoesNotMatch (일부 SDK/서명 요청)

IRSA를 쓰는 Pod에서 흔히 보이는 형태(예: AWS SDK)가 이런 식입니다.

botocore.exceptions.ClientError: An error occurred (InvalidIdentityToken) when calling the AssumeRoleWithWebIdentity operation: Token is not valid yet

TLS 계열

  • x509: certificate has expired or is not yet valid
  • certificate verify failed: certificate has expired
x509: certificate has expired or is not yet valid: current time 2026-02-23T01:12:10Z is before 2026-02-23T01:15:00Z

포인트는 “현재 시간(current time)”이 인증서 유효기간과 어긋난다는 문구입니다. 이 한 줄이 나오면 거의 확정입니다.

2) 왜 Pod 시간 문제가 STS와 TLS를 동시에 망가뜨리나

Kubernetes Pod는 별도의 “시간”을 갖지 않습니다. Pod는 노드 커널 시간을 그대로 공유합니다. 즉, Pod에서 시간이 틀려 보이면 99%는 노드(EC2) 시간이 틀린 것입니다.

이때 두 축이 동시에 깨집니다.

(1) STS/IRSA: WebIdentity 토큰의 시간 검증

IRSA는 대략 다음 흐름입니다.

  1. Pod에 projected service account token(OIDC JWT)이 마운트됨
  2. SDK가 AssumeRoleWithWebIdentity 호출
  3. STS가 JWT의 iat, nbf, exp와 “현재 시간”을 비교

노드 시간이 실제보다 미래Token is not valid yet, 과거Token has expired가 쉽게 발생합니다.

(2) TLS: 인증서 유효기간(notBefore/notAfter) 검증

TLS 핸드셰이크에서 클라이언트는 서버 인증서의 유효기간을 현재 시간과 비교합니다.

  • 시간이 과거not yet valid
  • 시간이 미래expired

결과적으로 “STS도 안 되고 HTTPS도 안 되는” 이상한 장애가 동시에 발생합니다.

3) 10분 안에 원인 분리: 애플리케이션 vs 노드 vs 네트워크

여기서 중요한 건 “IRSA 구성 문제”와 “시간 문제”를 분리하는 겁니다. 아래 순서로 보면 빠릅니다.

3.1 Pod에서 현재 시간 확인

가장 먼저 Pod에서 시간을 확인합니다.

kubectl exec -it deploy/myapp -- date -u
kubectl exec -it deploy/myapp -- sh -c 'date -u; cat /proc/uptime'

대부분은 노드 시간을 그대로 보여주므로, 여기서 UTC가 몇 분 이상 어긋나면 거의 결론입니다.

3.2 같은 노드의 다른 Pod도 확인 (노드 단위인지 확인)

Pod 하나만 문제가 아니라면 노드 문제일 확률이 높습니다.

# 같은 노드에 임시 디버그 Pod 띄우기
kubectl debug node/<node-name> -it --image=public.ecr.aws/amazonlinux/amazonlinux:2023 -- chroot /host bash

# (노드 chroot 안에서)
date -u

kubectl debug node가 막혀 있으면, 해당 노드에 스케줄된 아무 Pod에서든 date -u를 비교해도 됩니다(어차피 동일).

3.3 TLS 재현으로 “시간 문제”를 확정

외부 HTTPS를 한 번만 때려보면 x509 메시지에 시간이 찍히는 경우가 많습니다.

kubectl exec -it deploy/myapp -- sh -c 'apk add --no-cache curl >/dev/null 2>&1 || true; curl -vI https://sts.amazonaws.com 2>&1 | tail -n +1 | sed -n "1,80p"'

여기서 certificate has expired or is not yet valid가 나오면 네트워크/인증서 체인보다 시간을 먼저 의심해야 합니다.

3.4 STS 호출을 최소 재현

IRSA가 붙은 Pod에서 STS를 직접 호출해 봅니다.

kubectl exec -it deploy/myapp -- sh -c '
  aws sts get-caller-identity --region ap-northeast-2
'
  • 여기서 InvalidIdentityToken + “not valid yet/expired”가 나오면 시간 문제 가능성이 매우 큽니다.
  • 단, IRSA 설정 자체가 깨진 403도 있으니, 그 경우는 EKS IRSA는 되는데 S3만 403? 30분 진단처럼 엔드포인트/권한/리전까지 분기해서 보세요.

4) 근본 원인: EKS 노드에서 시간이 틀어지는 대표 케이스

EKS(EC2 기반)에서 시간 드리프트가 커지는 원인은 보통 아래 중 하나입니다.

4.1 chrony/ntpd 비활성화 또는 오동작

  • 커스텀 AMI에서 chrony가 제거/비활성화
  • 부팅 시 NTP 동기화 실패 후 그대로 방치
  • makestep 정책이 없어 큰 드리프트를 천천히만 보정(혹은 아예 못 함)

4.2 NTP 서버 접근 불가 (네트워크/보안그룹/ACL)

  • NTP는 기본적으로 UDP 123
  • 노드가 퍼블릭 인터넷이 없고(NAT 없음), 내부 NTP도 없음
  • NACL에서 UDP 123 차단
  • 회사망 NTP로만 동기화해야 하는데 라우팅이 없음

4.3 가상화/호스트 타임 이슈(드물지만 치명적)

  • 특정 인스턴스/하이퍼바이저에서 clock drift가 비정상적으로 커짐
  • CPU steal/과도한 부하로 timekeeping이 불안정

이 경우는 노드 교체가 가장 빠른 해결이 되기도 합니다.

5) 해결: chrony 기준으로 “정상 동기화” 상태 만들기

Amazon Linux 2/2023 계열은 보통 chrony를 사용합니다. 핵심은 (1) 서비스 활성화 (2) 올바른 NTP 소스 (3) 큰 오차를 즉시 보정입니다.

5.1 노드에서 chrony 상태 확인

SSM으로 접속하거나, 디버그로 노드에 들어가서 확인합니다.

# 서비스 상태
sudo systemctl status chronyd

# 동기화 소스
chronyc sources -v

# 추적 정보(오차, 보정 상태)
chronyc tracking

정상이라면 chronyc tracking에서 Leap status : Normal에 가깝고, System time 오차가 ms~수십 ms 수준으로 유지됩니다.

5.2 큰 드리프트를 즉시 보정(운영 주의)

이미 수분 이상 틀어졌다면 “서서히 보정”으로는 STS/TLS 장애가 오래 갑니다. 운영 영향(타임 점프) 고려 후, 빠르게 맞추는 편이 낫습니다.

# 즉시 step(시간 점프) 수행
sudo chronyc -a makestep

# 다시 확인
chronyc tracking

> 주의: 시간 점프는 일부 애플리케이션(특히 타임스탬프 기반 캐시/세션/DB 트랜잭션)에 영향을 줄 수 있습니다. 하지만 STS/TLS가 이미 깨진 상태라면, 대개는 빠른 정상화가 우선입니다.

5.3 chrony 설정 예시 (내부 NTP 사용)

인터넷이 막혀 있다면 VPC 내부에서 접근 가능한 NTP가 필요합니다(사내 NTP, AWS Managed Microsoft AD NTP, 혹은 자체 NTP 서버 등). 예시는 다음과 같습니다.

# /etc/chrony.conf 예시
server ntp1.corp.local iburst
server ntp2.corp.local iburst

# 큰 오차는 즉시 step 허용(부팅 후 3회까지)
makestep 1.0 3

# 드리프트 파일
driftfile /var/lib/chrony/drift

# 로깅(선택)
logdir /var/log/chrony

설정 후 재시작합니다.

sudo systemctl restart chronyd
chronyc sources -v
chronyc tracking

5.4 EKS Managed Node Group라면 “AMI/부트스트랩”로 영구화

수동으로 한 번 고쳐도, 노드가 교체되면 다시 발생할 수 있습니다. 따라서 노드 부팅 시점에 chrony가 정상 동작하도록 보장해야 합니다.

  • 커스텀 AMI를 쓴다면: AMI에 chrony 포함 + enable
  • Launch Template UserData에: 설정 배포 + systemctl enable --now chronyd

UserData 예시(개념용):

#!/bin/bash
set -euo pipefail

# chrony 설치(이미 있으면 스킵)
if ! command -v chronyc >/dev/null 2>&1; then
  yum install -y chrony
fi

cat >/etc/chrony.conf <<'EOF'
server ntp1.corp.local iburst
server ntp2.corp.local iburst
makestep 1.0 3
driftfile /var/lib/chrony/drift
EOF

systemctl enable --now chronyd
chronyc -a makestep || true

6) 네트워크 점검: UDP 123이 나가는가

NTP 소스를 퍼블릭으로 두는 경우(권장하진 않지만) NAT/라우팅/보안그룹/NACL이 모두 맞아야 합니다.

  • 노드 SG egress: UDP 123 허용
  • NACL: outbound/inbound ephemeral + UDP 123 응답 허용
  • NAT Gateway/Instance 경유 시: 경로 존재

테스트는 노드에서 chronyc sources -v가 가장 현실적입니다. 단순 포트 체크 도구가 없어도, 소스가 ^*(선택됨)로 뜨는지 보면 됩니다.

7) 장애 복구 절차(운영에서 가장 안전한 순서)

시간이 크게 틀어진 노드는 “고쳐서 살리기”보다 “교체”가 더 안전한 경우가 많습니다. 특히 Karpenter/ASG가 있으면 더 그렇습니다.

7.1 즉시 완화: 문제 노드 격리

kubectl cordon <node-name>
kubectl drain <node-name> --ignore-daemonsets --delete-emptydir-data

노드를 비우면, 새 노드로 스케줄되며 시간이 정상인 노드에서 STS/TLS가 즉시 회복될 수 있습니다.

7.2 근본 해결: 노드 이미지/부트스트랩 수정 후 롤링 교체

  • Managed Node Group: 업데이트 전략으로 롤링
  • Karpenter: NodeClass/AMI/Bootstrap 수정 후 자연 교체

노드가 늘지 않는 상황이면 Karpenter/스케일링 이슈도 함께 봐야 합니다. 그 경우 Karpenter 도입 후 EKS 노드가 안 늘 때 해결법 흐름이 도움이 됩니다.

8) 재발 방지: “시간”을 모니터링 항목으로 승격

시간 드리프트는 터지면 큰 장애인데, 모니터링은 종종 빠져 있습니다. 아래 중 하나는 권장합니다.

  • 노드에서 chronyc tracking의 offset을 CloudWatch Agent/SSM으로 수집
  • DaemonSet으로 주기적으로 date -u와 기준 시간(예: 내부 타임 API) 비교 후 임계치 알림
  • 장애 시그널 기반: x509 not yet valid, InvalidIdentityToken not valid yet 로그 패턴 알림

간단한 DaemonSet(개념)으로 로그만 남기는 예:

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: time-drift-checker
  namespace: kube-system
spec:
  selector:
    matchLabels:
      app: time-drift-checker
  template:
    metadata:
      labels:
        app: time-drift-checker
    spec:
      containers:
        - name: checker
          image: public.ecr.aws/amazonlinux/amazonlinux:2023
          command: ["bash","-lc"]
          args:
            - |
              while true; do
                echo "node_time_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)";
                sleep 60;
              done
          resources:
            requests:
              cpu: 10m
              memory: 16Mi
      terminationGracePeriodSeconds: 0

이 자체로 드리프트를 계산하진 않지만, 장애 시점의 시간을 빠르게 확인하는 데 도움이 됩니다. 실제로는 내부 기준 시간과 비교하거나 chrony 메트릭을 수집하는 쪽이 더 좋습니다.

9) 체크리스트 요약

  • Pod에서 date -u 확인 → 틀리면 노드 문제
  • TLS 에러에 current time ...가 찍히는지 확인
  • STS 에러가 not valid yet/expired인지 확인
  • 노드에서 chronyc sources -v, chronyc tracking 확인
  • NTP(UDP 123) 네트워크 경로/차단 점검
  • AMI/부트스트랩에 chrony 활성화 및 makestep 정책 반영
  • 문제 노드는 cordon/drain 후 교체가 가장 빠른 복구

EKS에서 STS와 TLS가 동시에 흔들릴 때, IAM/VPC/DNS를 파기 전에 “시간부터” 확인하면 진단 시간이 극적으로 줄어듭니다. 특히 InvalidIdentityToken: not valid yetx509: not yet valid가 한 번이라도 같이 보였다면, 그 순간부터는 거의 NTP/chrony/노드 시간 문제로 수렴합니다.