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

  • 로그인 장애가 단순 장애인지 계정 탈취 시도인지 어떻게 나눌 수 있는가?
  • credential stuffing, password spraying, MFA 피로 공격은 어떤 로그 패턴으로 구분되는가?
  • 계정 보호 조치를 적용할 때 사용자 공지와 증거 보존을 어떻게 함께 처리하는가?

개요

로그인 장애 대응은 로그인이 안 된다는 현상을 원인별로 분해하는 작업이다.

운영자는 먼저 인증 서버 장애, OAuth 제공자 장애, CAPTCHA 장애, 메일/SMS 장애, DB 지연 같은 정상 장애 가능성을 배제해야 한다.

그 다음 credential stuffing, password spraying, 계정 enumeration, MFA push fatigue, 세션 하이재킹 같은 공격 가능성을 본다.

잘못 대응하면 정상 사용자를 대량 잠그거나, 반대로 실제 탈취 계정을 방치하게 된다.

공격 시나리오

  • 공격자는 유출된 이메일과 비밀번호 목록으로 /login에 낮은 빈도의 요청을 분산시킨다.
  • 특정 계정 하나를 반복 공격하지 않고 많은 계정에 같은 비밀번호를 시도해 잠금 정책을 피한다.
  • 로그인 실패 메시지나 응답 시간을 이용해 존재하는 계정과 존재하지 않는 계정을 구분한다.
  • MFA가 적용된 계정에는 반복 push를 보내 사용자가 실수로 승인하도록 유도한다.
  • 탈취 성공 뒤에는 이메일, 전화번호, MFA 기기, refresh token을 바꾸어 계정 회복을 어렵게 만든다.

취약한 요청/응답 예시

POST /login HTTP/1.1
Content-Type: application/json
X-Forwarded-For: 198.51.100.20
 
{
  "email": "member@example.com",
  "password": "Summer2026!"
}
HTTP/1.1 401 Unauthorized
Content-Type: application/json
 
{
  "error": "password for existing account is wrong"
}

계정 존재 여부를 알려 주는 응답은 enumeration을 돕는다.

INFO login failed email=member@example.com password=Summer2026! reason=bad_password

실패 로그에 password를 남기면 사고 대응 과정에서 로그 자체가 2차 유출 지점이 된다.

주제별 핵심 판단

  • 실패율 증가는 계정별 분포와 출처별 분포를 같이 봐야 한다.
  • password spraying은 많은 계정 + 같은 비밀번호 + 낮은 빈도 패턴으로 나타난다.
  • credential stuffing은 많은 credential pair + 알려진 자동화 user agent + 로그인 성공 후 민감 변경이 함께 보인다.
  • MFA 피로 공격은 짧은 시간 내 MFA challenge 반복 + 늦은 승인 + 생소한 기기가 핵심 신호다.
  • 계정 잠금은 강력하지만 공격자가 정상 사용자를 잠그는 DoS 수단으로 악용할 수 있다.
  • 사용자 공지는 확정 사실, 권장 조치, 아직 확인 중인 사실을 분리해야 한다.

개선된 요청/응답 예시

HTTP/1.1 401 Unauthorized
Content-Type: application/json
Cache-Control: no-store
X-Request-Id: req-7f9c2b0e
 
{
  "error": "invalid_credentials",
  "message": "인증 정보를 확인할 수 없습니다."
}
level=WARN event=auth.login.failed actor=anonymous accountHash=sha256:9f1a result=denied reason=bad_credentials ip=198.51.100.20 asn=64500 userAgentHash=sha256:c8e1 traceId=req-7f9c2b0e
level=WARN event=auth.spray.detected accountCount=238 passwordHash=sha256:masked window=PT10M action=rate_limited traceId=req-3d2a9911

응답은 계정 존재 여부를 숨기고, 로그는 검색 가능한 필드만 남긴다.

방어 설계

  • 로그인 실패 응답은 존재 계정과 미존재 계정 사이의 메시지와 상태 코드를 맞춘다.
  • rate limit은 IP 하나가 아니라 계정, IP, ASN, 기기 fingerprint, credential pair 조합으로 둔다.
  • 계정 잠금은 무조건 잠금보다 위험 기반 step-up 인증과 지연 응답을 함께 고려한다.
  • MFA 등록, 이메일 변경, 비밀번호 변경은 별도 위험 이벤트로 감사 로그를 남긴다.
  • 탈취 의심 계정은 현재 세션 종료, refresh token 폐기, 비밀번호 재설정 요구를 분리해서 적용한다.
  • 고객지원 대리 해제 절차에는 본인 확인과 감사 로그가 있어야 한다.
  • 로그인 장애 대응 중 로그 보존 기간을 단축하거나 샘플링을 켜지 않는다.
  • 외부 OAuth 장애와 내부 인증 장애를 같은 경보로 묶지 않는다.

Spring/HTTP 예시

@Component
class AuthenticationAuditListener {
 
    @EventListener
    void onFailure(AbstractAuthenticationFailureEvent event) {
        var auth = event.getAuthentication();
        audit.warn("auth.login.failed",
            Map.of(
                "actor", "anonymous",
                "accountHash", hash(auth.getName()),
                "result", "denied",
                "reason", event.getException().getClass().getSimpleName()
            ));
    }
 
    @EventListener
    void onSuccess(AuthenticationSuccessEvent event) {
        var auth = event.getAuthentication();
        audit.info("auth.login.succeeded",
            Map.of(
                "actor", "member:" + auth.getName(),
                "result", "allowed",
                "mfaRequired", requiresStepUp(auth)
            ));
    }
}

Spring Security 인증 이벤트는 로그인 성공/실패의 기술적 사실을 제공한다.

계정 잠금, MFA 재요구, refresh token 폐기는 애플리케이션 정책으로 연결해야 한다.

운영 로그와 감사 지점

level=INFO event=auth.account.protected actor=system target=member:1004 action=require_password_reset reason=credential_stuffing_suspected evidenceWindow=PT30M traceId=ir-20260701-001
level=WARN event=auth.mfa.push_fatigue actor=member:1004 challengeCount=12 approved=false window=PT5M action=challenge_blocked traceId=ir-20260701-001
  • 로그인 성공, 실패, 잠금, 잠금 해제, MFA challenge, MFA 승인, 비밀번호 변경을 서로 다른 event 이름으로 남긴다.
  • email 원문 대신 검색 가능한 해시를 쓰고, 고객지원에 필요한 화면에서만 복호화 가능한 식별자를 조회한다.
  • 계정 탈취 의심 시 마지막 정상 로그인, 첫 의심 로그인, 민감 변경 시각을 타임라인으로 묶는다.
  • OAuth social login이면 provider, client registration, redirect result를 함께 남긴다.
  • 경보는 실패 수만 보지 말고 실패 후 성공, 성공 후 민감 변경, 새 기기 등록을 연결한다.

실전 판단 기준

  • 장애라면 여러 사용자가 같은 시각에 같은 내부 오류를 겪는다.
  • 공격이라면 출처가 분산되어도 credential, user agent, 실패 후 성공 패턴이 남는다.
  • 잠금은 피해 확산을 줄이지만 정상 사용자와 고객지원 부하를 만든다.
  • 사용자 공지 전에는 영향 계정 기준과 권장 조치가 내부적으로 합의되어야 한다.
  • 계정 탈취가 의심되면 비밀번호 변경만 요구하지 말고 기존 세션과 refresh token을 같이 다룬다.
  • 공격 출처 차단은 임시 완화책이며 credential 유출 여부 확인을 대체하지 않는다.
  • 대응 완료 기준은 로그인 성공률 회복이 아니라 의심 계정 보호와 재발 탐지 규칙 배포다.

테스트 포인트

  • 존재 계정과 미존재 계정의 로그인 실패 응답이 구분되지 않는지 확인한다.
  • 같은 IP, 여러 IP, 같은 계정, 여러 계정 조합으로 rate limit이 동작하는지 확인한다.
  • MFA challenge 반복 시 차단, 지연, 추가 확인 절차가 작동하는지 확인한다.
  • 탈취 의심 계정 보호 후 기존 세션과 refresh token이 실제로 거부되는지 확인한다.
  • 고객지원 잠금 해제 경로가 감사 로그를 남기는지 확인한다.
  • 로그인 경보가 실패 수뿐 아니라 실패 후 성공 패턴을 잡는지 확인한다.

위험 신호!

  • 로그인 실패 로그에 password, OTP, recovery code가 남는다.
  • 401 응답이 계정 존재 여부나 MFA 설정 여부를 알려 준다.
  • 계정 잠금 해제를 고객지원이 로그 없이 수동 처리한다.
  • 로그인 성공 이벤트와 비밀번호/MFA 변경 이벤트를 연결할 trace id가 없다.
  • 탈취 의심 계정의 기존 refresh token이 계속 사용할 수 있다.
  • 공격 대응 중 정상 장애 여부를 확인하지 않고 모든 원인을 공격으로 단정한다.

확인 질문

확인 질문

  • 로그인 장애와 계정 탈취 의심을 나누는 첫 기준은 무엇인가?
    • 실패가 특정 내부 의존성에 몰리는지, 계정/출처/credential 조합 패턴으로 분산되는지다.
  • 계정 탈취 의심 시 비밀번호 재설정만으로 부족한 이유는 무엇인가?
    • 이미 발급된 세션과 refresh token이 남아 있으면 공격자가 계속 접근할 수 있기 때문이다.
  • 로그에서 반드시 숨겨야 하는 값은 무엇인가?
    • password, OTP, recovery code, token 원문, 주민번호, 카드번호, secret이다.

참고 문서