이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • bcrypt, Argon2, PBKDF2는 어떤 운영 조건에서 선택이 달라지는가?
  • 알고리즘 이름보다 중요한 cost, memory, migration 기준은 무엇인가?
  • Spring Security에서 여러 password encoder를 함께 운영하는 이유는 무엇인가?

개요

password hashing 알고리즘 선택은 “가장 유명한 이름 고르기”가 아니다. 공격 비용, 서버 지연, 라이브러리 성숙도, 규제 호환성, 기존 hash migration, 운영팀의 관측 가능성을 함께 봐야 한다.

Argon2id는 메모리 비용을 조정할 수 있어 현대 password hashing의 강한 선택지다. bcrypt는 오래 검증되었고 운영 사례가 많다. PBKDF2는 FIPS 같은 호환성 요구가 있는 환경에서 선택될 수 있지만 충분히 높은 iteration과 안전한 PRF가 필요하다.

중요한 것은 알고리즘 이름만 저장하지 않는 것이다. 저장 hash에는 알고리즘, salt, cost parameter가 함께 남아야 나중에 검증과 upgrade가 가능하다.

이 문서의 핵심은 “Argon2가 제일 좋다”가 아니라 “우리 시스템이 안전하게 운영하고 migration할 수 있는 password hashing 정책을 정한다”는 것이다.

공격 시나리오

  • 공격자는 유출된 user table에서 알고리즘과 cost를 확인한다.
  • 낮은 bcrypt cost나 낮은 PBKDF2 iteration이면 GPU로 대량 추측을 수행한다.
  • 모든 사용자가 같은 legacy SHA-256 hash면 rainbow table과 사전 대입으로 빠르게 cracked password를 얻는다.
  • 알고리즘 id가 저장되어 있지 않으면 애플리케이션은 어떤 검증기를 써야 하는지 몰라 upgrade가 지연된다.
  • 너무 높은 cost는 공격자보다 먼저 로그인 시스템을 느리게 만들어 DoS 표면이 된다.
  • migration 중 legacy hash를 무조건 허용하면 약한 계정이 장기간 남는다.

취약한 요청/응답 예시

@Bean
PasswordEncoder passwordEncoder() {
    return new MessageDigestPasswordEncoder("SHA-256");
}
users.password_hash
e3afed0047b08059d0fada10f400c1e5
8c6976e5b5410415bde908bd4dee15df

저장값만 보고는 salt, cost, algorithm version을 알 수 없다. 빠른 hash이고 migration에도 불리하다.

주제별 핵심 판단

  • Argon2id는 memory-hard 특성으로 GPU/ASIC 공격 비용을 높이는 데 유리하다.
  • bcrypt는 널리 검증되었지만 입력 길이 제한과 cost 조정 한계를 이해해야 한다.
  • PBKDF2는 호환성이 좋지만 충분히 높은 iteration과 HMAC-SHA256 이상을 고려한다.
  • scrypt도 memory-hard 선택지지만 라이브러리와 운영 표준화 상태를 확인한다.
  • 저장 hash에는 algorithm id와 parameter가 있어야 한다.
  • 신규 hash 정책과 기존 hash 검증 정책을 분리한다.
  • cost는 benchmark로 정하고 주기적으로 상향한다.

개선된 요청/응답 예시

@Bean
PasswordEncoder passwordEncoder() {
    String idForEncode = "argon2";
    Map<String, PasswordEncoder> encoders = Map.of(
        "argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8(),
        "bcrypt", new BCryptPasswordEncoder(12),
        "pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8()
    );
    return new DelegatingPasswordEncoder(idForEncode, encoders);
}
{argon2}$argon2id$v=19$m=16384,t=2,p=1$...
{bcrypt}$2a$12$...

신규 저장은 argon2로 하되 기존 {bcrypt}{pbkdf2}도 검증할 수 있게 둔다. 로그인 성공 후 upgradeEncoding으로 새 정책에 맞춰 재해시한다.

방어 설계

  • 우선순위는 modern password hashing, 사용자별 salt, 조정 가능한 cost, migration 가능성이다.
  • 알고리즘 선택은 실제 운영 인스턴스에서 인증 지연과 CPU/메모리 사용량을 측정해 결정한다.
  • 신규 hash 알고리즘과 legacy 검증 알고리즘을 정책으로 분리한다.
  • 모든 stored hash에 id prefix 또는 version metadata를 포함한다.
  • password encoder 변경은 기능 배포가 아니라 보안 정책 변경으로 다룬다.
  • 고비용 hash 계산은 rate limit, login throttling, 계정 잠금 정책과 함께 설계한다.
  • batch rehash는 raw password가 없으므로 불가능하거나 위험하다. 로그인 성공 기반 upgrade를 기본으로 한다.
  • compliance 요구가 있으면 승인된 알고리즘과 module 요구사항을 별도로 확인한다.

Spring/HTTP 예시

public Authentication authenticate(String email, String rawPassword) {
    Member member = memberRepository.findByEmail(email)
        .orElseThrow(BadCredentialsException::new);
 
    if (!passwordEncoder.matches(rawPassword, member.passwordHash())) {
        throw new BadCredentialsException();
    }
 
    if (passwordEncoder.upgradeEncoding(member.passwordHash())) {
        member.changePasswordHash(passwordEncoder.encode(rawPassword));
        passwordPolicyAudit.recordUpgrade(member.id());
    }
 
    return authenticated(member);
}
level=INFO event=password_encoder_benchmark result=measured algorithm=argon2 p95Ms=180 memoryMiB=16 instance=c7g.large traceId=928d4e11

policy를 바꾸기 전에 실제 인스턴스에서 p50/p95 검증 시간을 측정한다. 개발자 노트북 수치만으로 결정하지 않는다.

운영 로그와 감사 지점

level=INFO event=password_hash_upgraded result=allowed memberId=1004 oldId=bcrypt newId=argon2 reason=login_success traceId=928d4e11
level=WARN event=legacy_password_hash_detected result=review_required memberId=1004 algorithm=sha256 action=force_reset traceId=928d4e11
  • hash 원문은 남기지 않는다.
  • 알고리즘 id, cost, upgrade count는 운영 지표로 남긴다.
  • legacy hash 비율을 dashboard로 추적한다.
  • cost 상향 뒤 login latency와 error rate를 함께 본다.
  • 약한 hash는 upgrade 불가하면 비밀번호 재설정 대상으로 분류한다.

실전 판단 기준

  • “bcrypt면 무조건 안전”이 아니라 cost와 migration 상태를 봐야 한다.
  • “Argon2면 무조건 도입”이 아니라 라이브러리 지원, 메모리 비용, 운영 장애 영향을 봐야 한다.
  • PBKDF2는 낮은 iteration으로 쓰면 password storage 방어력이 약하다.
  • 저장 hash에 id prefix가 없으면 장기 migration 비용이 커진다.
  • cost를 높이면 보안은 좋아질 수 있지만 로그인 서버 용량과 rate limit 설계도 함께 바뀐다.
  • password policy 변경은 보안팀, 운영팀, 고객지원이 모두 영향을 받는다.

테스트 포인트

  • {bcrypt} legacy hash와 {argon2} 신규 hash가 모두 검증되는지 확인한다.
  • 신규 가입은 현재 idForEncode 알고리즘으로 저장되는지 확인한다.
  • legacy hash 로그인 성공 후 새 hash로 바뀌는지 확인한다.
  • 낮은 cost hash가 upgradeEncoding 대상인지 확인한다.
  • benchmark 결과가 목표 p95 latency 안에 있는지 확인한다.
  • hash 원문이 로그, trace, metric label에 남지 않는지 확인한다.

위험 신호!

  • 알고리즘 이름만 정하고 cost parameter가 없다.
  • 저장 hash에 알고리즘 id가 없다.
  • legacy SHA-256 hash를 무기한 허용한다.
  • migration을 위해 raw password를 별도로 저장하자는 제안이 있다.
  • password hashing 비용을 측정하지 않고 production에 올린다.
  • 로그인 rate limit 없이 cost만 높인다.

확인 질문

확인 질문

  • 알고리즘 선택에서 이름보다 중요한 것은 무엇인가?
    • cost parameter, 운영 지연, migration 가능성, 라이브러리 안정성, compliance 요구다.
  • DelegatingPasswordEncoder는 왜 유용한가?
    • 여러 stored hash 형식을 검증하면서 신규 저장은 새 알고리즘으로 하도록 migration을 지원하기 때문이다.
  • cost를 높일 때 함께 봐야 하는 운영 지표는 무엇인가?
    • login p95 latency, CPU/메모리 사용량, 인증 실패율, rate limit 차단율이다.

참고 문서