라떼군 이야기


AWS S3 업로드 시 403 Signature Mismatch 에러 해결기: Pinpoint APM이 원인이었다

서론

Spring Boot 애플리케이션에서 AWS S3에 파일을 업로드하는 기능을 개발하던 중, 예상치 못한 문제에 직면했습니다. 분명히 올바른 Access Key와 Secret Key를 사용하고 있고, IAM 권한도 제대로 설정되어 있는데 계속해서 다음과 같은 에러가 발생했습니다.

Pinpoint APM이란?

문제를 설명하기 전에 먼저 Pinpoint에 대해 간단히 알아보겠습니다. Pinpoint는 네이버에서 개발한 오픈소스 APM(Application Performance Monitoring) 도구입니다. 대규모 분산 시스템에서 애플리케이션의 성능을 모니터링하고, 서비스 간의 호출 관계를 추적하는 데 사용됩니다.

Pinpoint의 주요 특징:

  • 분산 트랜잭션 추적: 마이크로서비스 간의 호출 흐름을 시각화
  • 실시간 애플리케이션 모니터링: 응답 시간, 처리량, 에러율 등을 실시간 모니터링
  • 코드 레벨 가시성: 메소드 단위까지 성능 병목 지점 파악
  • 무침투적 설치: 기존 코드 수정 없이 Java Agent만 추가하면 적용 가능

Pinpoint의 동작 원리

Pinpoint는 Java Agent를 통해 애플리케이션에 설치됩니다. Tomcat 실행 시 다음과 같이 설정합니다:

-javaagent:/opt/pinpoint-agent/pinpoint-bootstrap.jar
-Dpinpoint.agentId=web-app-01
-Dpinpoint.applicationName=my-application

중요한 점은 Pinpoint Agent가 설치되면, 애플리케이션에서 발생하는 모든 외부 HTTP 요청에 자동으로 추적 헤더들이 추가된다는 것입니다. 이는 개발자가 의식하지 못하는 사이에 일어나며, AWS SDK를 통한 S3 요청도 예외가 아닙니다.

software.amazon.awssdk.services.s3.model.S3Exception: The request signature we calculated does not match the signature you provided.
Check your key and signing method.

일반적으로 이런 에러는 잘못된 자격 증명이나 권한 문제로 인해 발생하는데, 아무리 확인해도 설정에는 문제가 없었습니다. 심지어 새로운 IAM 사용자를 만들고 새로운 Access Key를 발급받아도 같은 문제가 지속되었습니다.

본론

Pinpoint가 HTTP 요청에 헤더를 추가하는 메커니즘

Pinpoint Agent는 바이트코드 조작(Bytecode Instrumentation) 기술을 사용하여 다음과 같은 방식으로 동작합니다:

  1. HTTP 클라이언트 라이브러리 인터셉트: Apache HttpClient, OkHttp, URLConnection 등의 HTTP 클라이언트 라이브러리들을 모니터링 대상으로 설정
  2. 자동 헤더 삽입: 외부로 나가는 모든 HTTP 요청에 추적을 위한 헤더들을 자동으로 추가
  3. 트랜잭션 컨텍스트 전파: 서비스 간 호출 추적을 위해 TraceId, SpanId 등의 정보를 헤더에 포함

즉, 개발자가 별도의 코드 수정 없이도 Tomcat에 Pinpoint Agent만 설정하면, 해당 애플리케이션에서 발생하는 모든 외부 HTTP 요청(AWS S3 포함)에 자동으로 다음과 같은 헤더들이 추가됩니다:

pinpoint-flags: 0
pinpoint-host: s3.ap-northeast-2.amazonaws.com
pinpoint-pappname: myapp.tomcat
pinpoint-papptype: 1010
pinpoint-pspanid: 1234567890123456789
pinpoint-spanid: 9876543210987654321
pinpoint-traceid: myapp^1234567890123^45

이는 개발자가 의도하지 않았음에도 불구하고 투명하게 발생하는 현상입니다.

문제 상황 분석

@Service
public class FileService {

    @Value("${aws.s3.bucket-name}")
    private String bucketName;

    @Value("${aws.s3.access-key}")
    private String accessKey;

    @Value("${aws.s3.secret-key}")
    private String secretKey;

    private final S3Client s3Client;

    public FileService() {
        this.s3Client = S3Client.builder()
                .region(Region.AP_NORTHEAST_2)
                .credentialsProvider(StaticCredentialsProvider.create(
                    AwsBasicCredentials.create(accessKey, secretKey)
                ))
                .build();
    }

    public String uploadFile(MultipartFile file) {
        String key = generateKey(file.getOriginalFilename());

        PutObjectRequest request = PutObjectRequest.builder()
                .bucket(bucketName)
                .key(key)
                .contentType(file.getContentType())
                .build();

        s3Client.putObject(request, RequestBody.fromBytes(file.getBytes()));
        return key;
    }

    private String generateKey(String originalFilename) {
        LocalDateTime now = LocalDateTime.now();
        String uuid = UUID.randomUUID().toString();
        return String.format("files/%d/%02d/%02d/%s_%s",
            now.getYear(), now.getMonthValue(), now.getDayOfMonth(), uuid, originalFilename);
    }
}

로그 분석을 통한 단서 발견

AWS SDK의 DEBUG 로그를 활성화하여 자세히 살펴보니, 흥미로운 점을 발견했습니다:

DEBUG s.a.awssdk.auth.signer.Aws4Signer - AWS4 Canonical Request: PUT
/files/2025/10/18/abc123-def4-56789-ghij-klmnopqrstuv.png

amz-sdk-invocation-id:abc12345-6789-abcd-efgh-ijklmnopqrst
amz-sdk-request:attempt=1; max=4
content-length:2048
content-type:image/png
host:my-bucket-name.s3.ap-northeast-2.amazonaws.com
pinpoint-flags:0
pinpoint-host:s3.ap-northeast-2.amazonaws.com
pinpoint-pappname:myapp.tomcat
pinpoint-papptype:1010
pinpoint-pspanid:1234567890123456789
pinpoint-spanid:9876543210987654321
pinpoint-traceid:myapp^1234567890123^45
x-amz-content-sha256:UNSIGNED-PAYLOAD
x-amz-date:20251018T120000Z

여기서 핵심은 Canonical Requestpinpoint-* 헤더들이 포함되어 있다는 점이었습니다.

AWS 서명 메커니즘 이해

AWS의 Signature Version 4는 요청의 모든 헤더를 포함하여 서명을 계산합니다. 즉, 요청에 포함된 모든 헤더가 서명 계산에 영향을 미칩니다.

  1. Canonical Request 생성: HTTP 메소드, URI, 쿼리 파라미터, 헤더, 페이로드를 정규화
  2. String to Sign 생성: 알고리즘, 타임스탬프, 범위, Canonical Request 해시를 결합
  3. 서명 계산: Secret Key와 String to Sign을 이용해 HMAC-SHA256 계산

문제는 Pinpoint APM이 추가하는 커스텀 헤더들이 이 과정에 포함되면서, AWS S3가 예상하지 못한 서명이 생성되는 것이었습니다.

시도했던 해결 방법들

  1. 새로운 IAM 사용자 생성: 효과 없음
  2. 새로운 Access Key 발급: 효과 없음
  3. 권한 재설정: 효과 없음
  4. 하드코딩으로 자격 증명 직접 입력: 효과 없음
  5. TLS 버전 다운그레이드: 효과 없음

진짜 해결책: Pinpoint 비활성화

문제의 원인을 파악한 후, 다음과 같은 방법들로 해결할 수 있었습니다.

방법 1: JVM 옵션으로 Pinpoint 비활성화

-Dpinpoint.enable=false
-Dprofiler.enable=false

방법 2: 코드 레벨에서 Pinpoint 헤더 제거

@Configuration
public class S3Config {

    @Bean
    public S3Client s3Client() {
        return S3Client.builder()
                .region(Region.AP_NORTHEAST_2)
                .credentialsProvider(StaticCredentialsProvider.create(
                    AwsBasicCredentials.create(accessKey, secretKey)
                ))
                .overrideConfiguration(ClientOverrideConfiguration.builder()
                        .addExecutionInterceptor(new RemovePinpointHeadersInterceptor())
                        .build())
                .build();
    }

    public static class RemovePinpointHeadersInterceptor implements ExecutionInterceptor {
        @Override
        public SdkHttpRequest modifyHttpRequest(
                Context.ModifyHttpRequest context,
                ExecutionAttributes executionAttributes) {

            SdkHttpRequest.Builder requestBuilder = context.httpRequest().toBuilder();

            // Pinpoint 헤더들 제거
            requestBuilder.removeHeader("pinpoint-flags");
            requestBuilder.removeHeader("pinpoint-host");
            requestBuilder.removeHeader("pinpoint-pappname");
            requestBuilder.removeHeader("pinpoint-papptype");
            requestBuilder.removeHeader("pinpoint-pspanid");
            requestBuilder.removeHeader("pinpoint-spanid");
            requestBuilder.removeHeader("pinpoint-traceid");

            return requestBuilder.build();
        }
    }
}

방법 3: application.properties 설정

pinpoint.enable=false
profiler.enable=false

결론

이번 문제를 통해 몇 가지 중요한 교훈을 얻었습니다:

1. APM 도구와 AWS SDK의 상호작용 주의

Pinpoint와 같은 APM(Application Performance Monitoring) 도구들은 HTTP 요청에 추가적인 헤더를 삽입하여 트레이싱을 수행합니다. 하지만 이런 헤더들이 AWS의 서명 메커니즘과 충돌할 수 있다는 점을 간과하기 쉽습니다.

2. 로그 분석의 중요성

단순히 에러 메시지만 보고 판단하지 말고, DEBUG 레벨의 상세 로그를 통해 실제 요청이 어떻게 구성되는지 확인하는 것이 중요합니다.

3. 근본 원인 파악의 필요성

403 Signature Mismatch 에러가 발생하면 대부분 자격 증명 문제라고 생각하기 쉽지만, 실제로는 더 깊은 곳에 원인이 있을 수 있습니다.

4. 개발 환경과 운영 환경의 차이

개발 환경에서는 APM을 사용하지 않다가 운영 환경에서 갑자기 이런 문제가 발생할 수 있으므로, 환경 간 차이점을 미리 파악하고 대비하는 것이 중요합니다.

5. APM과 클라우드 서비스의 호환성 고려

Pinpoint 외에도 New Relic, DataDog, AppDynamics 등 다양한 APM 도구들이 유사한 방식으로 HTTP 헤더를 추가할 수 있습니다. 클라우드 서비스(AWS, GCP, Azure)와 APM 도구를 함께 사용할 때는 미리 호환성을 검증하는 것이 중요합니다. 개발 환경에서는 APM을 사용하지 않다가 운영 환경에서 갑자기 이런 문제가 발생할 수 있으므로, 환경 간 차이점을 미리 파악하고 대비하는 것이 중요합니다.

최종 권장사항

  • AWS SDK와 APM 도구를 함께 사용할 때는 호환성을 미리 검증하세요
  • APM 도구가 자동으로 추가하는 헤더들이 클라우드 서비스의 인증에 영향을 줄 수 있음을 인지하세요
  • 특정 서비스에 대해서만 APM 헤더를 제외하는 설정을 고려하세요
  • 상세한 로깅을 통해 실제 요청 내용을 확인하는 습관을 기르세요
  • 개발 환경과 운영 환경의 APM 설정을 일치시켜 예상치 못한 문제를 방지하세요

이런 숨겨진 이슈들은 예상치 못한 곳에서 발생할 수 있으므로, 항상 열린 마음으로 다양한 가능성을 고려하며 문제를 해결하는 것이 중요합니다. 특히 Java Agent 기반의 APM 도구들은 개발자가 의식하지 못하는 사이에 애플리케이션의 동작을 변경할 수 있다는 점을 항상 염두에 두어야 합니다.