WebAuthn이 쉬워 보이는 이유 — 실제로 배포하려고 하면
Source: Dev.to
Every WebAuthn demo works. Production is where things quietly fall apart.
WebAuthn 데모는 위험할 정도로 설득력 있습니다. 튜토리얼을 따라가고, 패스키를 등록하고, 인증에 성공하면 모든 것이… 해결된 느낌입니다.
No passwords. No OTPs. No friction.
Then you ship it. And suddenly:
- 사용자가 인증해서는 안 될 때 인증함
- 카운터가 이상하게 동작함
- 브라우저마다 의견이 다름
- 엔터프라이즈 고객이 데모에서는 전혀 대비되지 않은 질문을 함
This gap — between “it works” and “it survives production” — is where most WebAuthn implementations fail.
After watching teams repeat the same mistakes for years, three gaps show up again and again.
Gap #1: 데모 수준 데이터베이스 접근
대부분의 WebAuthn 데모는 다음과 같은 코드를 포함합니다:
async function getUser(username) {
const query = `SELECT * FROM users WHERE username = '${username}'`;
return db.query(query);
}
개발, 테스트, 심지어 스테이징에서도 동작합니다—하지만 실제 사용자 이름에 따옴표가 포함되거나 누군가 ' OR '1'='1 와 같은 공격을 시도하면 문제가 발생합니다.
이것은 WebAuthn 문제를 의미하는 것이 아니라, 데모가 간과하는 프로덕션 엔지니어링 문제입니다. 프로덕션 시스템은 동적 IN 절을 포함한 모든 것을 파라미터화합니다:
function buildInClause(values) {
if (!Array.isArray(values) || values.length === 0) {
return { clause: '(NULL)', params: [] };
}
const placeholders = values.map(() => '?').join(',');
return {
clause: `(${placeholders})`,
params: Array.from(values)
};
}
차이점은 우아함이 아니라 — 생존성입니다.
Gap #2: “One-Call” Verification Illusions
Demo verification often looks like this:
await fido2.verify(response);
데모 검증은 보통 다음과 같이 보입니다:
Clean, minimal, but completely insufficient. In production, this single call hides multiple attack surfaces:
깨끗하고 최소하지만 전혀 충분하지 않습니다. 실제 운영 환경에서는 이 단일 호출이 여러 공격 표면을 숨깁니다:
-
Replay attacks
-
Counter rollback (cloned authenticators)
-
Origin spoofing
-
Challenge reuse across sessions
-
Native app origin edge cases
-
재생 공격
-
카운터 롤백(복제된 인증기)
-
출처 위조
-
세션 간 챌린지 재사용
-
네이티브 앱 출처 엣지 케이스
Real verification logic validates every layer:
실제 검증 로직은 모든 계층을 검증합니다:
if (counter <= prevCounter && counterSupported) {
throw new Error("counter rollback detected");
}
if (origin !== expectedOrigin) {
throw new Error("origin mismatch");
}
Counters, origins, encoding, challenge binding, RP ID — none of these are optional in production. If you skip one, authentication still “works” — until it doesn’t.
카운터, 출처, 인코딩, 챌린지 바인딩, RP ID — 이들은 운영 환경에서 선택 사항이 아닙니다. 하나라도 생략하면 인증이 “작동”하는 것처럼 보이지만 — 결국 작동하지 않게 됩니다.
Gap #3: “단일 도메인” WebAuthn의 신화
데모는 다음을 가정합니다:
- 하나의 RP ID
- 하나의 도메인
- 하나의 정책
프로덕션 환경은 그렇지 않습니다. 실제 시스템은 다음을 처리해야 합니다:
- 여러 서브도메인
- 와일드카드 RP ID
- 엔터프라이즈 인증기 허용 목록 (AAGUID‑based)
- 사용자당 디바이스 제한
- 테넌트별 타임아웃
- 디바이스 바인딩
구성은 상수가 아니라 데이터가 됩니다:
{
"domain": ".example.com",
"device_limit": 2,
"registration_session_timeout": 999
}
이는 복잡함을 위한 복잡함이 아니라, 실제 조직을 지원하기 위해 최소한으로 필요한 것입니다.
What “Production‑Ready” Actually Means
| 영역 | 데모 현실 | 프로덕션 현실 |
|---|---|---|
| 데이터베이스 | 문자열 쿼리 | 전역 파라미터화 |
| 검증 | 단일 함수 | 다계층 검증 |
| 도메인 | localhost | 와일드카드 및 서브도메인 |
| 카운터 | 무시됨 | 롤백 감지 |
| 정책 | 하드코딩 | 테넌트별 구성 |
이러한 실패는 데모에서는 명확히 드러나지 않습니다. 모두 사용자가 시스템을 신뢰한 후에 나타납니다.
WebAuthn에 대한 냉정한 진실
WebAuthn은 시연하기는 쉽지만 안전하게 운영하기는 어렵다. 문제는 표준이 아니라, 시연이 통과했다고 해서 배포 가능한 시스템이라고 착각하는 것이다.
프로토타입을 넘어 패스키를 배포하려는 경우, 시연을 교육 도구로 여기고, 설계 참고 자료로 여기지 말라. 인증에서는 실패가 오류처럼 보이는 경우는 드물고, 오히려 잘못된 사용자가 성공하는 형태로 나타난다.
나는 실제 트래픽, 브라우저, 기업 제약을 견뎌야 하는 WebAuthn 시스템을 작업한다. 위의 교훈 대부분은 “잘 작동한다”는 것이 실제로는 그렇지 않을 때까지 고친 경험에서 나온 것이다.