- Published on
EKS Pod→S3 업로드 403 RequestTimeTooSkewed 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버리스나 매니지드 환경이라고 해서 시간 문제가 사라지지 않습니다. EKS에서 애플리케이션 Pod가 S3로 파일을 업로드하는데 갑자기 403 RequestTimeTooSkewed가 터지면, 대부분은 서명(Signature) 생성 시각과 AWS가 해석하는 요청 시각이 허용 오차(일반적으로 ±5분)를 벗어난 상황입니다.
이 글은 “왜 Pod에서만” 발생하는지, 그리고 노드 시간/컨테이너 시간/네트워크 경로/SDK 설정을 어떤 순서로 확인하면 빠르게 복구되는지에 초점을 맞춥니다. IRSA/STS 관련 403과 증상이 섞여 보일 때의 구분법도 함께 정리합니다.
관련해서 STS 토큰 만료로 403이 나는 케이스는 원인이 다르니 아래 글도 참고하세요: EKS Pod에서 STS 403 ExpiredToken 해결법
1) 증상: S3 403인데 AccessDenied가 아니다
대표 에러 메시지는 대개 아래 중 하나로 나타납니다.
RequestTimeTooSkewed: The difference between the request time and the server's time is too large.SignatureDoesNotMatch와 함께x-amz-date관련 힌트가 나오기도 함
중요한 포인트는, 이 에러는 권한(IAM) 문제가 아니라 서명에 포함되는 시간 문제라는 점입니다.
빠른 구분 체크
- AccessDenied: IAM/S3 정책/버킷 정책/KMS 권한 가능성
- ExpiredToken: IRSA/STS 토큰 갱신/clock skew/SDK 캐시 문제 가능성
- RequestTimeTooSkewed: 거의 항상 클럭 스큐(시간 불일치)
2) 원리: SigV4 서명은 “시간”에 민감하다
S3 업로드는 대부분 SigV4로 서명됩니다. SigV4는 다음을 전제로 합니다.
- 클라이언트가 만든
x-amz-date(또는 Date 헤더)가 AWS 서버 시간과 크게 다르지 않아야 함 - 시간 오차가 커지면 재전송/리플레이 공격 방지 목적으로 AWS가 요청을 거부
즉, Pod가 생성한 시간이 이상하거나, 요청이 나가는 경로에서 시간이 왜곡되거나(드물지만 프록시/미들박스), 혹은 SDK가 잘못된 시스템 시간을 읽는 환경이면 S3는 403을 반환합니다.
3) 가장 흔한 원인 5가지 (EKS 관점)
(1) 워커 노드(EC2) 시간 동기화 문제
EKS의 Pod 시간은 기본적으로 노드 커널 시간을 따라갑니다. 노드에서 NTP/chrony가 멈추거나, 부팅 직후 동기화가 지연되면 Pod 전체가 영향을 받습니다.
특히 아래 상황에서 발생 빈도가 올라갑니다.
- 커스텀 AMI 사용
- 보안 하드닝으로 NTP 포트(UDP 123) 차단
- 프라이빗 서브넷에서 NTP 접근 경로가 꼬임
(2) 컨테이너 이미지에서 tzdata/CA 등과 혼동
타임존(KST/UTC) 자체는 보통 문제를 만들지 않지만, 일부 앱이 로컬 타임존 문자열을 직접 파싱해 서명 시각을 만드는 경우(비표준 구현) 문제가 됩니다. 표준 SDK는 시스템 UTC 시각을 사용하므로 보통은 노드 시간이 핵심입니다.
(3) 프라이빗 서브넷 + NAT/프록시 환경에서 NTP가 막힘
노드가 인터넷으로 나가려면 NAT Gateway/Instance를 타는데, 보안 정책으로 UDP 123이 막혀 있으면 동기화가 실패합니다.
NAT 비용/구성 점검이 필요하면 이 글도 도움이 됩니다: VPC NAT Gateway 비용 폭증 10분 진단·절감
(4) 노드 교체/오토스케일 직후 특정 노드에서만 재현
클러스터 전체가 아니라 특정 노드에 스케줄된 Pod에서만 재현된다면, 거의 100% 그 노드의 시간 동기화 이슈입니다.
(5) SDK/애플리케이션이 서명 시각을 강제로 오버라이드
드물지만 다음과 같은 경우가 있습니다.
- 프록시 레이어에서 Date 헤더를 강제로 추가/변경
- 커스텀 SigV4 구현체가 로컬 시간을 잘못 사용
- JVM/컨테이너에서 시간 소스가 비정상(가상화 이슈/커널 이슈)
4) 10분 내 진단 플로우 (현장용)
아래 순서대로 보면 원인을 빠르게 좁힐 수 있습니다.
4.1 에러가 “어느 노드”에서 나는지 먼저 확인
Pod가 스케줄된 노드를 확인합니다.
kubectl get pod -n <ns> <pod-name> -o wide
노드가 특정 몇 대로 좁혀지면, 그 노드에서만 시간 확인을 하면 됩니다.
4.2 Pod 내부 시간 확인 (참고용)
kubectl exec -n <ns> <pod-name> -- date -u
대부분은 노드 시간을 그대로 보여줍니다.
4.3 노드에서 시간/동기화 상태 확인
노드에 SSM 또는 SSH로 접속해 확인합니다.
# 현재 시간
date -u
# systemd-timesyncd 사용 시
timedatectl
# chrony 사용 시
chronyc tracking
chronyc sources -v
여기서 핵심은:
System clock synchronized: yes/no- chrony tracking의
Last offset,RMS offset가 비정상적으로 큰지
4.4 AWS 서버 시간과 차이 비교(간이)
클라이언트(노드) 시간이 의심될 때는, AWS 응답 헤더의 Date를 보고 대략 비교할 수 있습니다.
curl -sI https://s3.amazonaws.com | grep -i '^date:'
# date -u 와 비교
노드의 date -u와 응답 Date:가 수분 이상 벌어지면 원인이 확정적입니다.
5) 해결 1순위: 노드 시간 동기화 복구(chrony 권장)
EKS 워커 노드(특히 Amazon Linux 2/2023, Ubuntu)에서 NTP 클라이언트를 정상화하는 것이 정답인 경우가 가장 많습니다.
5.1 chrony 설치/활성화 (예: Amazon Linux)
sudo yum install -y chrony
sudo systemctl enable --now chronyd
# 상태 확인
chronyc tracking
5.2 NTP 트래픽(UDP 123) 허용
- 노드 SG/NACL에서 아웃바운드 UDP 123이 막히지 않았는지 확인
- 프라이빗 서브넷이면 NAT/방화벽 장비에서 UDP 123 허용 필요
5.3 권장: AWS Time Sync Service 사용
AWS는 VPC 내부에서 접근 가능한 Time Sync 서비스(일반적으로 169.254.169.123)를 제공합니다. 외부 NTP가 차단된 환경에서 특히 유용합니다.
chrony 설정 예시(개념):
# /etc/chrony.conf
server 169.254.169.123 prefer iburst minpoll 4 maxpoll 4
적용 후:
sudo systemctl restart chronyd
chronyc sources -v
> 실제 배포에서는 Launch Template user-data 또는 AMI 레벨에서 표준화하는 것을 권장합니다.
6) 해결 2순위: “특정 노드만” 문제면 노드 교체가 가장 빠르다
운영 중 급하면 원인 분석과 별개로 문제 노드를 cordon/drain 후 교체하는 것이 서비스 복구에 효과적입니다.
# 스케줄 금지
kubectl cordon <node>
# 안전 퇴거(Deployment/ReplicaSet 기반일 때)
kubectl drain <node> --ignore-daemonsets --delete-emptydir-data
이후 ASG/Managed Node Group가 새 노드를 올리도록 유도합니다.
7) 해결 3순위: SDK/애플리케이션 레벨 보완(임시 처방)
근본은 노드 시간 동기화지만, 장애 완화 차원에서 SDK 설정을 점검할 수 있습니다.
7.1 AWS SDK 재시도/타임 스큐 보정
일부 SDK는 clock skew를 감지해 보정하거나 재시도를 수행합니다. 다만 시간이 크게 틀어지면 결국 실패합니다.
Node.js (AWS SDK v3) 예시
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
const s3 = new S3Client({
region: process.env.AWS_REGION,
// 표준 재시도는 기본값이 있으나, 장애 시 일시 상향 가능
maxAttempts: 5,
});
await s3.send(new PutObjectCommand({
Bucket: process.env.BUCKET!,
Key: "test.txt",
Body: "hello",
}));
여기서 중요한 건 maxAttempts 자체가 해결책이 아니라는 점입니다. 노드 시간이 정상이어야 합니다.
7.2 컨테이너에서 시간 변경은 해결이 아니다
컨테이너 내부에서 date -s로 시간을 바꾸는 방식은 보통 권한 문제로 불가능하고(또는 가능해도) 노드 커널 시간과 불일치로 더 큰 혼란을 만듭니다. 시간은 노드 레벨에서 해결하세요.
8) “IRSA 문제처럼 보이는데?” 함께 오는 혼동 정리
S3 업로드가 403이면 많은 팀이 가장 먼저 IRSA/IAM을 의심합니다. 물론 권한 문제도 흔하지만, RequestTimeTooSkewed는 결이 다릅니다.
- IRSA가 깨지면 STS AssumeRoleWithWebIdentity 단계부터 실패하거나
ExpiredToken/InvalidIdentityToken류가 나오는 경우가 많습니다. - OIDC Provider 삭제/오염으로 IRSA가 전부 실패하는 대형 사고도 있습니다. 이런 케이스는 아래 글 흐름이 더 맞습니다: EKS OIDC Provider 삭제로 IRSA 전부 실패했을 때 복구
즉, 에러 메시지에 “TimeTooSkewed”가 박혀 있으면 시간부터 보세요.
9) 운영에서 재발 방지 체크리스트
9.1 노드 부팅 표준화
- Launch Template user-data에서 chrony 설정 강제
- AMI 파이프라인에 NTP 설정 포함
9.2 모니터링
- 노드에서
chronyc tracking의 offset을 수집(노드 exporter 커스텀 스크립트 등) - 특정 임계치(예: 30초 이상)면 경보
9.3 네트워크 정책 점검
- 프라이빗 서브넷에서 NTP가 외부로 나가야 한다면 UDP 123 경로 확인
- 가능하면 AWS Time Sync Service로 내부화
10) 결론
EKS에서 Pod→S3 업로드 중 403 RequestTimeTooSkewed가 나오면, 대부분의 경우 권한이 아니라 시간 동기화 문제입니다. 진단은 “어느 노드에서 재현되는지”를 먼저 좁히고, 노드에서 timedatectl/chronyc로 동기화 상태를 확인하면 빠르게 결론이 납니다. 해결은 chrony 정상화 + NTP 경로(UDP 123) 확보, 또는 AWS Time Sync Service로 내부 동기화하는 방식이 가장 안정적입니다.
장애 복구를 급하게 해야 한다면 문제 노드를 drain하여 교체하는 것이 즉효이고, 이후 재발 방지를 위해 노드 부팅 표준화와 시간 오프셋 모니터링을 붙여두면 같은 유형의 403을 크게 줄일 수 있습니다.