비밀번호를 잘못 저장한 사고는 매년 나온다. LinkedIn (2012, SHA-1 평문), Yahoo (2013, MD5), Adobe (2013, 3DES). 공통점은 속도가 빠른 해시 또는 일반 해시 + salt 없음. 이 가이드는 2026 년 기준으로 옳은 선택과 흔히 빠지는 오답을 정리한다.
왜 SHA-256 이 아닌가
SHA-256 은 훌륭한 일반 해시 함수 다. 문서 무결성, JWT 서명, 파일 체크섬 등엔 적합하다. SHA 해시 로 직접 계산할 수 있다. 하지만 비밀번호엔 부적합하다 — 이유는 단 하나,너무 빠르다.
2026 년 기준 GPU 한 장으로 SHA-256 을 초당 100 억 회 (10 GH/s) 계산할 수 있다. 8 자 영문+숫자 비밀번호의 전체 공간은 약 2.18 × 1014 — GPU 8 장이면 6 시간 안에 전수 탐색이 끝난다. salt 가 있어도 한 사용자에 대해서는 같은 시간이 걸린다. MD5 는 더 빠르고 (50 GH/s+), 충돌까지 증명됐다 — MD5 해시 는 체크섬용으로만 남겨둬야 한다.
비밀번호 해시 함수는 의도적으로 느려야 한다. 정상 로그인 (1 회) 은 100 ms 라도 사용자가 못 느끼지만, brute force (수십억 회) 는 견딜 수 없게 된다.
옳은 선택지 — 2026 기준
Argon2id (1순위)
2015 년 Password Hashing Competition 우승. RFC 9106 (2021) 표준. 메모리 하드 — GPU 의 메모리 부족을 강제하기 때문에 GPU 가속이 의미를 잃는다.
- 파라미터 3 개:
m(메모리 KiB),t(반복),p(병렬). - OWASP 권장 (2026):
m=46 MiB,t=1,p=1. 서버가 여유 있으면m=64 MiB. id변종이 side-channel + GPU 둘 다 방어.i또는d단독은 비추.
bcrypt (2순위, 호환성 우선시)
1999 년 등장. 30 년 가까이 검증됐고, 거의 모든 언어에 검증된 라이브러리 가 있다. Argon2 가 없는 환경에서 안전한 차선.
- 파라미터 1 개:
cost(= log2 반복 횟수). - OWASP 권장 (2026):
cost=12. M2 MacBook 기준 약 250 ms. - 제한: 입력 72 바이트로 잘림. 긴 비밀번호 또는 passphrase 는 SHA-256 으로 먼저 해시 후 bcrypt 적용 (단, NULL 바이트 취약점 주의 — base64 인코딩 후 넣는 것이 안전).
- 직접 시험해 보려면 Bcrypt 해시 에서 cost 별 해시 시간과 결과 형식을 볼 수 있다.
scrypt — 3순위
Argon2 보다 오래됐지만 메모리 하드. Argon2 가 가능하다면 굳이 새로 쓸 이유는 없다. 이미 scrypt 를 쓰는 시스템이면 그대로 유지해도 무방.
PBKDF2 — 권장하지 않음 (하위 호환만)
NIST FIPS 호환이 필요한 정부 시스템 외에는 추천 안 함. GPU 가속에 약함.
cost 파라미터 — 어떻게 정할까
원칙: 정상 로그인 1 회가 100~250 ms 안에 끝나는 가장 큰 값. 사용자 체감 X, 공격자 비용 ↑.
직접 측정 — 운영 환경과 같은 사양 머신에서 시간 측정.
import bcrypt from "bcryptjs";
console.time("bcrypt-12");
await bcrypt.hash("test", 12);
console.timeEnd("bcrypt-12");Argon2id 의 경우 메모리도 함께 봐야 한다. m=46 MiB 에 p=1 이면 동시 로그인 10 명이면 460 MiB 가 필요. 서버 RAM 과 동접 부하 함께 산정.
salt — 왜 필요한지
salt 는 사용자별 랜덤 바이트로, 같은 비밀번호가 다른 해시를 만들도록 강제한다. 두 가지 공격 방어:
- rainbow table — 미리 계산된 (비밀번호 → 해시) 표. salt 가 있으면 사용자마다 다른 표가 필요해 무용지물.
- 일괄 공격 — DB 가 털리면 같은 비밀번호 쓰는 사람들이 같은 해시로 묶여 보이는 문제. salt 가 있으면 일괄 비교 불가.
bcrypt/Argon2 모두 salt 를 알아서 생성하고 해시 문자열 안에 포함시킨다. 직접 만질 일이 없다.
pepper — 선택 사항
pepper 는 모든 사용자에게 같은 시크릿 (env 변수 등). DB 만 털린 상황 에서 추가 방어선. 단,
- 서버가 시크릿을 알아야 검증 가능. 분산 시스템에서는 모든 서버에 배포 필요.
- pepper 가 노출되면 salt 만 있는 상태와 동일 — 일반화된 이점은 없다.
- rotation 이 어렵다. 변경 시 모든 해시를 다시 계산해야 함.
대부분의 서비스는 Argon2id 만으로 충분하다. pepper 는 보안 요구가 매우 높은 시스템에서만 추가.
비밀번호 생성 정책 — 사용자 측
NIST 800-63B (2020 개정) 의 핵심:
- 최소 8 자, 권장 12 자 이상.
- 구성 강제 금지 — "대문자·숫자·특수문자 포함 필수" 는 실제로 보안을 떨어뜨린다. 사용자가
Password1!같은 예측 패턴으로 회피. - 주기적 만료 금지 — 사고 의심 시에만 강제 변경. 정기 변경은 더 약한 비밀번호로 이동시킬 뿐.
- 유출 DB 대조 — Have I Been Pwned API 또는 로컬 사본 으로 흔히 털린 비밀번호 차단.
사용자에게 권장할 비밀번호 자체는 비밀번호 생성기 같은 도구로 16+ 자 랜덤 또는 4~5 단어 passphrase 가 가장 안전.
알고리즘 마이그레이션
오래된 알고리즘 (MD5, SHA-1, 약한 bcrypt cost) 을 쓰고 있다면 어떻게 옮길까? 두 가지 패턴:
A. wrap-and-rotate (즉시 보호)
모든 기존 해시를 new_algo(old_hash) 로 한 번에 감싼다. 예: bcrypt(sha256(password)). 검증 시 같은 순서로 적용. 로그인 사용자가 평문을 보낼 때 점진적으로 순수 새 해시로 교체.
- 장점: DB 안의 모든 해시가 즉시 강화됨. 사용자 액션 불필요.
- 단점: 검증 비용 약간 증가. 사용자별 마이그레이션 상태 추적 필요.
B. on-login rotate (단순)
로그인 시 평문을 받으면 새 알고리즘으로 다시 해시해 저장. 아직 로그인 안 한 사용자는 옛 해시 그대로. 대부분의 사용자는 한 달 안에 마이그레 이션 완료.
- 장점: 단순. 한 컬럼에 알고리즘 prefix 만 표시.
- 단점: 비활성 사용자는 영영 남음. 사고 시 노출.
대규모 사용자라면 A + B 조합 — A 로 즉시 wrap, B 로 점진 교체.
흔히 빠지는 함정
1. 자체 구현
"SHA-256 을 1000 번 반복하면 안전" 같은 자체 설계 금지. 검증된 라이브러리만 사용. Argon2 는 직접 구현하면 side-channel 누수가 거의 확실.
2. salt 재사용
모든 사용자에 같은 salt = salt 없음과 동일. 라이브러리가 자동 생성하면 문제 없음.
3. 평문 로그
에러 로그에 request body 가 그대로 들어가면 평문 비밀번호가 로그에 남는다. 로그 마스킹 필수.
4. 클라이언트 해시
브라우저에서 해시한 결과를 서버로 보내고 "서버는 평문을 모른다" 고 주장하는 패턴. 잘못. 그 해시가 그 사용자의 사실상 비밀번호가 된다 — DB 가 털리면 그대로 로그인 가능. 클라이언트 해싱은 추가 layer 이지 대체가 아니다.
5. 길이 제한
bcrypt 72 바이트 제한 외에 임의 제한 (16/32 자) 금지. passphrase 사용자 를 막을 뿐.
요약
- 비밀번호 해시는 의도적으로 느린 함수만. SHA-256/MD5 는 금지.
- 2026 권장: Argon2id (1순위) → bcrypt cost 12 (호환성).
- 정상 로그인 1 회 100~250 ms 가 되도록 cost 조정. 실측 권장.
- salt 는 자동, pepper 는 선택. 자체 구현 절대 금지.
- NIST 800-63B: 최소 길이만, 구성 강제 X, 정기 만료 X, 유출 DB 차단 O.
- 알고리즘 교체는 wrap-and-rotate 또는 on-login rotate. 비활성 사용자 남는 점 주의.