AWS 기술 블로그

당근이 AWS CloudHSM으로 대규모 서명키 관리 시스템을 구축한 방법 – 3부: 서명 시스템 구현, 트러블슈팅, 무중단 키 전환

해당 포스트는 당근의 최용환님, 조승환님, 오현준님과 함께 작성했으며, AWS Summit Seoul 2026에서 발표한 세션 내용을 기반으로 합니다.

이 시리즈의 1부에서는 서명키 보안의 중요성과 당근이 하이브리드 전략을 선택한 과정을, 2부에서는 CloudHSM 아키텍처와 다계층 접근 제어 전략을 다루었습니다. 이번 마지막 글에서는 이 인프라 위에서 서명 시스템을 어떻게 구현했는지, PKCS#11 기반 시스템 운영에서 겪은 트러블슈팅, 그리고 수천만 개의 기존 토큰을 보존하며 무중단으로 키를 전환한 과정을 공유합니다.

CloudHSM과 KMS의 연동 방식 비교

서명 시스템을 구현하기에 앞서, AWS CloudHSMAWS KMS의 연동 방식 차이를 이해하는 것이 중요합니다.

CloudHSM: PKCS#11 기반 연동

HSM과 통신하기 위해서는 PKCS#11이라는 표준 인터페이스를 사용해야 합니다. PKCS#11은 C 기반으로 작성된 API 표준이라, 애플리케이션에 로드할 수 있는 Shared Object(.so) 바이너리 형태로 제공됩니다.

Go나 Java와 같은 언어에서 이 C 기반 바이너리를 사용하려면 CGO나 JNI 같은 Foreign Function Interface(FFI)를 거쳐야 합니다. 단순히 연결하는 것으로 끝이 아닙니다. PKCS#11은 상태를 유지하는(Stateful) 인터페이스이기 때문에 세션 상태 관리를 직접 해야 합니다. 따라서 로우 레벨 PKCS#11 API를 직접 다루기보다는, 검증된 고수준 래퍼 라이브러리를 통해 세션 상태를 관리하는 것이 실무적으로 권장됩니다.

당근은 Go 환경에서 crypto11 라이브러리를 사용했습니다. crypto11은 PKCS#11을 감싼 고수준 Go 래퍼로, CloudHSM이 제공하는 PKCS#11 라이브러리(libcloudhsm_pkcs11.so)를 로드하고 토큰 레이블·PIN 등의 접속 정보 설정과 세션 풀 관리를 대신 처리해 줍니다. 뒤에서 다룰 maxSessions, poolWaitTimeout 같은 튜닝 옵션도 이 라이브러리가 제공하는 설정값입니다.

PKCS#11이 일반 HTTP 클라이언트와 다른 점은, 연결이 Stateless 요청이 아니라 세션(session)이라는 상태를 유지한다는 것입니다. 그래서 스레드마다 세션을 따로 두고, 키를 매번 조회하지 않도록 키 핸들을 캐싱하는 식의 관리가 필요합니다. 실제 연동에서 겪은 함정도 몇 가지 공유하면, ① 키 생성 시 CKA_ID를 명시하지 않으면 이후 키 조회(FindKeyPair)가 실패할 수 있고, ② CloudHSM의 슬롯 번호가 매우 큰 값이라 슬롯 번호 대신 토큰 레이블로 토큰을 지정하는 편이 안전하며, ③ 단일 HSM 환경에서는 키 가용성 검사 옵션을 꺼야 정상 동작하는 경우가 있었습니다.

KMS: AWS API 기반 연동

반면 KMS는 훨씬 단순합니다. AWS SDK를 통해 kms:Sign API를 호출하면 서명이 수행되고, IRSA 기반으로 접근을 제어할 수 있습니다. CloudHSM처럼 바이너리를 로드하거나 세션 풀을 관리할 필요가 없습니다.

Active-Standby Failover 설계

서명 시스템 장애는 곧 인증 시스템 전체 장애로 이어집니다. 당근의 인증 서비스는 하루 6,500만 건 이상의 JWT 서명 요청을 처리하고 있어, 단일 서명 시스템에서 모든 것을 처리하는 구조는 위험했습니다. 실제로 운영 중 HSM 연결이 일시적으로 끊기며 토큰 서명이 실패하는 상황을 겪었고, 단일 시스템 의존성을 해결해야 했습니다. 그래서 특정 서명 백엔드 하나에 의존하지 않고 여러 서명 백엔드를 지원하는 구조를 설계했습니다.

본문에서 ’Failover’는 한 서명 백엔드에서 다른 백엔드로의 자동 전환을, ’Fallback’은 그 전환을 실제로 수행하는 애플리케이션 레벨 로직을 가리킵니다.

TokenSigner 인터페이스 추상화

서명 백엔드를 TokenSigner라는 인터페이스로 추상화하고, 세 가지 구현체를 Active-Standby 구조로 구성했습니다:

Signer 역할 설명
HSMSigner Active (Weight: 100%) CloudHSM PKCS#11 기반 서명
KMSSigner Standby AWS KMS API 기반 서명
LocalSigner Standby 기존 서명 시스템 (마이그레이션 중 사용)

Dynamic Config 기반 실시간 전환

Dynamic Config를 통해 각 Signer의 weight를 조절하면 서비스 재시작 없이 Active Signer를 실시간으로 변경할 수 있습니다. 또한 Active Signer가 실패하면 자동으로 Standby Signer로 전환되어 서비스 중단 없이 서명을 이어갈 수 있도록 구현했습니다. 전환 트리거는 HSM 서명 호출이 실패하거나 정해진 시간 안에 응답하지 않을 때 동작하도록 설계했습니다.

예를 들어 부하로 인한 스로틀링(CKR_FUNCTION_FAILED)에는 100ms에서 3초까지 지수 백오프(재시도 간격을 점점 늘리는 기법)로 최대 3회 재시도하고, 그래도 실패하면 Standby인 KMS로 넘깁니다.

여기서 Dynamic Config는 서비스 재시작 없이 런타임에 설정값을 갱신/반영할 있는 동적 설정 저장소를 가리킵니다. 서명 weight나 공개키 목록처럼 무중단으로 바꿔야 하는 값을 이곳에 두어, 배포 없이 즉시 반영되도록 했습니다.

실제 Failover 사례: 사용자 영향 없이 자동 전환

이 설계가 실제 운영 환경에서 빛을 발한 순간이 있었습니다.

서명 시스템 배포 중 Istio의 ServiceEntry 리소스 설정 이슈로 인해 HSM과의 통신에 latency spike가 발생했습니다. HSM 서명의 latency가 급등하면서 에러가 발생하기 시작했는데, 이때 Fallback 로직을 통해 자동으로 KMS 서명으로 전환되었습니다. 서비스 재시작 없이 즉시 전환이 이루어졌고, 에러 모니터링 지표(에러율·토큰 발급 실패율) 전 구간이 정상으로 유지되어 사용자 영향은 관측되지 않았습니다.

사용자 입장에서는 토큰 발급이 정상적으로 이루어졌고, 설정 문제를 수정하는 동안에도 서비스는 계속 정상 운영되었습니다. 단일 서명 백엔드로만 구성했다면 서비스 장애로 이어졌을 상황에서, Active-Standby 구조가 실제 운영 가치를 증명한 순간이었습니다.

서명 알고리즘 변경: RS256에서 ES256으로

CloudHSM은 인스턴스 수가 한정되어 있기 때문에, 같은 인스턴스로 많은 토큰을 서명할 있느냐가 중요한 과제였습니다. 이를 위해 기존 서명 알고리즘을 RS256(RSA 기반, 소인수 분해)에서 ES256(ECDSA 기반, 타원곡선 이산대수)으로 변경했습니다.

항목 RS256 (기존) ES256 (변경) 변화
키 크기 2,048 bit 256 bit 8x 작음
서명 크기 256 bytes 64 bytes 4x 작음
서명 속도 느림 빠름 HSM에서 효율적
보안 수준 112-bit 128-bit 동등 이상
토큰 크기 ~800 bytes ~500 bytes 약 37% 절감

타원곡선 기반 알고리즘이 훨씬 가볍기 때문에, 같은 HSM 인스턴스에서 처리할 수 있는 RPS를 크게 끌어올릴 수 있었습니다. 키 크기가 8배, 서명 크기가 4배 줄어들면서 JWT 토큰 자체가 가벼워졌고, 네트워크 대역폭 절약 효과도 있었습니다. 한정된 HSM 인스턴스를 더 효율적으로 활용할 수 있게 된 것입니다.

다만 알고리즘 전환은 단순한 성능 최적화가 아니라 호환성 작업이기도 합니다. 토큰을 검증하는 모든 클라이언트·서비스가 ES256(ECDSA)을 지원하는지, 검증 측의 허용 알고리즘 목록(allowlist)에 ES256이 포함되는지, ECDSA 서명 인코딩을 올바르게 처리하는지 사전에 확인해야 합니다. 문제가 생겼을 때 되돌릴 수 있도록 롤백 경로도 함께 준비하는 것이 안전합니다.

트러블슈팅: PKCS#11 실전 경험

설계가 끝나면 바로 프로덕션에 적용하는 것이 아니라, 충분한 테스트를 거쳐야 합니다. 프로덕션 적용 전 다양한 테스트를 진행하면서 PKCS#11 기반 시스템에서 겪을 수 있는, 공식 문서에는 나오지 않는 실전 경험들을 얻었습니다.

1. Scale-out 세션 고정 문제

증상: HSM 인스턴스를 Scale-out 했는데 전체 처리량(TPS)이 전혀 올라가지 않았습니다.

원인: PKCS#11 세션이 애플리케이션 시작 시점에 한 번 생성되고 나면, 이후로는 기존 인스턴스에 고정(pinning)되는 현상이 있었습니다. 새로 추가된 HSM 인스턴스는 세션이 하나도 연결되지 않은 채로 유휴 상태였습니다.

해결: HSM 인스턴스가 Scale-out 될 때마다 애플리케이션을 재시작하여 세션을 재수립하도록 했습니다. 재시작된 애플리케이션은 새 HSM 인스턴스를 포함한 전체 클러스터에 세션을 분산 연결하게 됩니다. 이 방식은 PKCS#11 세션의 정적 바인딩 특성에 따른 현실적인 대응이며, 향후에는 세션 풀을 주기적으로 재생성하거나 새 인스턴스 감지 시 자동 재연결하는 방식을 검토하고 있습니다.

2. Scale-in 중 inflight 요청 실패

증상: HSM 인스턴스 종료 시 진행 중이던 일부 요청이 실패했습니다.

원인: PKCS#11 클라이언트는 제거된 인스턴스를 감지하고 커넥션을 끊는 처리는 잘 해주고 있었습니다. 하지만 인스턴스가 제거되는 순간에 이미 진행 중이던 in-flight 요청들이 CKR_DEVICE_REMOVED나 Resource Pool Timeout 에러를 만나면서 약 0.5%의 에러율이 발생했습니다.

대응: 이 부분은 앞서 설계한 Fallback 로직이 있었기 때문에, 사용자 관점에서의 영향은 없었습니다. HSM 서명 실패 시 자동으로 KMS Signer로 전환되어 서명이 정상 처리되었습니다.

교훈: Scale-out과 Scale-in 모두 PKCS#11의 세션 기반 특성에서 오는 문제입니다. 공식 문서에도 잘 나와 있지 않아서, 직접 테스트하면서 알게 된 부분들이었습니다.

3. MaxSessions 튜닝: 실패보다 대기가 낫다

증상: PKCS#11 래퍼 라이브러리의 maxSessions 값을 높게 설정했더니, 오히려 HSM 에러가 발생하고 latency가 급등했습니다.

원인: 세션을 너무 많이 열면 HSM 내부 큐가 과부하되면서 “HSM could not Queue” 에러가 발생했습니다. Flow Control 없이 대량의 요청이 HSM에 동시에 밀려들어간 것입니다.

해결: 직관과 반대로, 세션 수를 낮추고 poolWaitTimeout설정하는 것이 정답이었습니다. 클라이언트 쪽에서 세션 획득을 기다리게 하면, HSM 내부 큐가 안정적으로 유지되며 에러는 발생하지 않고 latency만 소폭 증가했습니다.

설정 maxSessions 높음 maxSessions 낮음 + poolWaitTimeout
Flow Control 없음 세션 풀에서 대기
HSM 내부 큐 과부하 안정
Latency 급등 소폭 증가
에러 “HSM could not Queue” 발생 0건

실제 튜닝에서도 세션 수를 제한하지 않으면 처리량은 높지만 스로틀링이 빈번했고, Pod당 maxSessions를 단계적으로(20 → 50 → 80) 올리자 스로틀링 에러가 사라지면서 처리량도 안정적으로 회복됐습니다. 당근은 처리량과 안정성의 균형점으로 Pod당 maxSessions를 80 수준으로 채택했습니다.

교훈: HSM에 동시에 보내는 요청 수를 제한하고, 초과분은 애플리케이션 쪽에서 대기하게 하는 것이 전체 시스템 안정성 측면에서 훨씬 낫습니다. 실패보다 대기가 낫다”는 것이 핵심입니다.

4. 로깅 체계 구축: .so 바이너리 로그 캡처

문제: PKCS#11은 바이너리 형태(.so)로 제공되기 때문에, 이 바이너리가 남기는 로그를 stdout으로 캡처할 수 없었습니다. 앞서 소개한 Scale-in이나 MaxSessions 문제도 처음에는 HSM 쪽에서 무슨 에러가 나는 건지 파악이 쉽지 않았습니다.

해결: Shared Object 파일이 Pod 내부에 쓰는 로그 파일(/opt/cloudhsm/run/cloudhsm-pkcs11.log)을 실시간으로 tail하는 LogTail 컴포넌트를 구현했습니다. Regex로 로그를 파싱하여 ERROR 레벨은 Sentry로 알림을 보내고, 메트릭은 Datadog으로 수집하도록 했습니다.

이 로깅 체계를 갖춘 이후로는 HSM 쪽 이슈를 빠르게 감지하고 원인을 파악할 수 있게 되었습니다. 앞서 소개한 모든 트러블슈팅들도 이 체계가 있었기에 원인을 정확히 짚어낼 수 있었습니다.

모니터링은 두 층위로 구성했습니다. 애플리케이션 레벨에서는 앞서 설명한 LogTail로 PKCS#11 에러를 수집해 Sentry·Datadog으로 알림과 메트릭을 보내고, HSM 인프라 레벨에서는 CloudWatch 지표와 CloudTrail 감사 로그를 활용했습니다. 특히 HSM의 키 토큰 점유량처럼 한계가 정해진 지표(예: HsmKeysTokenOccupied)는 한계에 도달하기 전에 알림이 오도록 임계치를 미리 설정해 두는 것이 중요했습니다. 다만 hsm2m.medium 등 일부 인스턴스 타입은 제공되는 CloudWatch 지표가 제한적이라, 부족한 부분은 HSM 감사 로그(로그인/로그아웃 이벤트 등)로 보완했습니다.

서명키 무중단 전환

시스템을 구축하고 운영 이슈도 해결한 후, 가장 중요한 작업이 남았습니다. 운영 중인 서비스의 서명키를 신규 시스템으로 무중단 전환하는 것입니다. 핵심 과제는 “기존에 발급된 수천만 개의 토큰을 깨뜨리지 않으면서, 어떻게 새 키로 넘어갈 것인가”였습니다.

kid 기반 멀티키 구조

키 전환의 핵심은 JWT 헤더에 들어가는 kid(Key ID) 필드입니다. JWT 표준에 정의된 이 필드를 활용하면, 토큰을 서명할 때 JWT 헤더에 kid를 포함시켜 어떤 키로 서명했는지 식별할 수 있습니다.

{
    "alg": "ES256",
    "kid": "hsm-key-002"
}

검증할 때는 이 kid 값을 가지고 Dynamic Config(공개키 저장소)에서 해당 공개키를 조회하여 검증합니다. 이 구조 덕분에 HSM 키, KMS 키, 그리고 기존 Local 키까지 동시에 공존할 수 있습니다.

5단계 무중단 키 전환 프로세스

키 로테이션에서 반드시 지켜야 할 핵심 원칙은 서명을 먼저 전환하고, 검증을 나중에 정리한다”는 것입니다. 어느 시점에서도 유효한 토큰의 검증이 실패하는 일이 없도록 다음 5단계를 순서대로 진행합니다.

특히 주의할 부분은 검증 측의 공개키 캐시입니다. 검증 서비스나 JWKS(JSON Web Key Set, 검증 측이 공개키 목록을 가져가는 표준 형식) 캐시가 새 공개키를 즉시 반영하지 못하면 전환 직후 일시적으로 검증이 실패할 수 있습니다. 그래서 새 공개키를 충분히 전파한 뒤(캐시 TTL을 고려한 대기 시간 확보) 서명 비율을 올렸고, 클라이언트 간 시계 오차(clock skew)나 구버전 앱이 남아있을 가능성까지 고려해 단계마다 지표를 확인하며 진행했습니다.

Step 1: 새 키 생성 및 공개키 등록

CloudHSM과 KMS에 각각 새로운 키를 생성하고, 해당 키의 공개키를 Dynamic Config에 등록합니다. 이 시점에서 서명은 아직 기존 Local 키로만 진행됩니다. 아무것도 바뀌지 않은 상태이지만, 검증 측에서 키의 공개키를 미리 알고 있어야 나중에 전환할 때 검증 실패가 발생하지 않기 때문에 공개키 등록을 가장 먼저 수행합니다.

서명 (쓰기) 검증 (읽기)
local-key-001 Active Active
hsm-key-002 등록 완료 (아직 서명 없음)

Step 2: Weight 기반 점진적 전환

Dynamic Config에서 local-key의 weight를 줄이고 hsm-key의 weight를 올리면, 서비스 재시작 없이 실시간으로 서명 트래픽이 새 키로 옮겨갑니다. 이 구간에서는 두 키로 서명된 토큰이 동시에 존재하게 되지만, 검증 측에서는 kid를 보고 각각의 공개키로 검증하기 때문에 문제가 없습니다. 각 단계마다 Datadog에서 에러율과 latency를 확인한 뒤 다음 단계로 넘어갔습니다.

서명 (쓰기) 검증 (읽기)
local-key-001 Weight 감소 중 Active
hsm-key-002 Weight 증가 Active

Step 3: 기존 서명 중단

기존 키로의 서명을 완전히 중단하고 기존 서명키를 제거합니다. 이 시점 이후로 모든 새로 발급되는 토큰은 hsm-key로만 서명됩니다.

중요: 기존에 local-key로 발급된 토큰은 아직 사용자 기기에 남아있을 수 있으므로, 검증 측에서는 local-key의 공개키를 아직 제거하면 안 됩니다.

서명 (쓰기) 검증 (읽기)
local-key-001 중단 Active (기존 토큰 검증용)
hsm-key-002 Active (100%) Active

Step 4: 토큰 만료 대기

기존 키로 서명된 토큰의 TTL이 자연 만료될 때까지 대기합니다. 토큰의 TTL이 모두 소진되면, local-key로 검증해야 할 토큰이 더 이상 존재하지 않게 됩니다.

Step 5: 기존 키의 공개키 제거

기존 서명키의 공개키를 Dynamic Config에서 제거합니다. 이 시점부터 서명도 검증도 모두 HSM 키로만 동작하게 되며, 전환이 완전히 마무리됩니다.

서명 (쓰기) 검증 (읽기)
hsm-key-002 Active Active

전환 결과

5단계를 거쳐 실제로 기존 서명키를 새로운 시스템으로 전환을 완료했습니다:

  • 기존 발급 토큰 100% 보존 — 수천만 개 토큰의 유효성을 그대로 유지했습니다.
  • 전환 에러율 서비스 영향 없음 — 에러 모니터링 지표 전 구간이 정상이었고, 사용자 경험 변화 없이 무중단으로 전환했습니다.
  • 기존 서명키 안전하게 제거 — 기존 서명키 리소스를 완전히 정리하고, 신규 서명 시스템만 사용하는 상태가 되었습니다.

Wrap-Up: 적용 효과와 하이브리드 전략

당근의 실측 기준 CloudHSM과 KMS는 어느 한쪽이 절대적으로 우월한 서비스가 아닙니다. 각 서비스의 강점과 한계를 이해하고, 워크로드에 맞게 조합하는 것이 핵심입니다.

구분 AWS KMS AWS CloudHSM
성능 p50: 5ms, p99: 30ms p50: 2ms, p99: 5ms
Throughput 쿼터 기반 (증설 가능) 인스턴스 추가로 확장 (4대 약 7,000 RPS)
운영 복잡도 낮음 (IAM + IRSA) 높음 (PKCS#11, credential)
인프라 AWS Managed 전용 NodeGroup, HSM 클러스터
적합 케이스 저트래픽, 간편 운영 고트래픽, 비용 최적화

당근은 이 두 서비스를 Active-Standby로 조합하여, 보안·성능·안정성을 균형 있게 확보했습니다.

Key Takeaways

이번 프로젝트에서 당근이 얻은 핵심 성과는 세 가지입니다:

1. Private Key의 안전한 격리 — 모든 서명은 HSM과 KMS 안에서 수행되고, Private Key는 애플리케이션 밖으로 나오지 않는 구조를 실현했습니다.
2. 장애에 대비한 안정성 확보 — Failover 구조를 통해 단일 백엔드에 대한 SPOF(Single Point of Failure) 를 제거했고, 실제 운영 중 발생한 이슈에서도 사용자 영향 없이 서비스를 이어갈 수 있었습니다.
3. 더욱 안전한 서비스 제공 — 하루 6,500만 건 이상의 JWT 토큰 서명이 이전보다 견고한 기반 위에서 처리되고 있습니다.

서명키 보안은 서비스 규모와 관계없이 모든 시스템의 신뢰 기반입니다. 이 블로그에서 공유한 경험이 여러분의 서명키 관리 시스템 설계에 도움이 되기를 바랍니다. CloudHSM과 KMS에 대한 더 자세한 내용은 AWS CloudHSM 사용 설명서AWS KMS 개발자 안내서를 참고하시기 바랍니다.

조승환

조승환

조승환(Josh.cho)은 당근의 가입·로그인, 본인인증, 토큰 발급·검증 등 사용자 식별과 인증의 흐름을 다루는 소프트웨어 엔지니어입니다. 수천만 건의 인증 요청이 단 한 번의 실패도 없이 처리되도록 시스템을 설계하고, 진화하는 보안 위협과 트래픽 규모 속에서 안정적인 계정 경험을 지켜내는 일을 하고 있습니다.

오현준

오현준

오현준(Mandy.oh)는 당근의 모든 서비스가 공통으로 사용하는 통합 계정,인증/인가 시스템을 개발 및 운영하는 소프트웨어 엔지니어입니다. 모든 서비스의 관문이 되는 만큼, 보안·고가용성·고성능을 모두 만족시키기 위한 다양한 엔지니어링 과제를 해결하고 있습니다.

최용환

최용환

최용환(Yany)은 당근 및 당근페이의 서비스 안정성 및 클라우드 리소스를 관리하는 Site Reliability Engineer입니다.
당근의 사용자들이 더 매끄럽고 안전한 경험을 느낄 수 있도록 클라우드 아키텍처를 개선하고, 트래픽을 안정적으로 처리할 수 있는 시스템을 만들기 위해 여러 엔지니어링 과제를 해결하고 있습니다.

Jinhyun Park

Jinhyun Park

Jinhyun Park helps customers navigate their cloud journey, leveraging my experience in web application development and operations. I collaborate with Digital Native Business (DNB) customers to optimize architectures and implement efficient solutions that drive business value. I provide technical guidance focused on maximizing the potential of data and AI initiatives, supporting customers in their successful cloud transformation.

Kwanjin Jung

Kwanjin Jung

보안 스페셜리스트로 AWS 환경에서 안전한 클라우드 보안 환경을 만들기 위해 고민하고 있는 솔루션 아키텍트 입니다. 시스템 및 네트워크 보안부터 악성코드,취약점 분석가의 길을 걷다 클라우드의 매력에 빠져 다양한 클라우드 서비스에 빠져 살고 있습니다.

Nak-Kwon Choi

Nak-Kwon Choi

Nak-Kwon Choi serves as a partner in AWS customers' cloud journey and business growth, helping them address their challenges and enabling their digital transformation.