ES /docs

team signup failed: Not enough or too many segments

RCA: team signup failed: Not enough or too many segments

Overview#

What Happened#

2026-06-30 10:15 KST 무렵, cupixvista-apiPOST /api/v1/teams/signup 엔드포인트에서 28초 동안 8건의 500 에러가 발생했다. 모든 요청이 동일 IP(3.172.65.113)에서 들어왔고, email 파라미터 없이 team_domain=solutionoperations만 포함된 채 X-CUPIX-AUTH 헤더에 비정상 토큰(JWT segment 수 ≠ 3)을 실어 보냈다. 그 결과 Cupix::Aws::Cognito.get_user_by_access_token이 호출한 JWT.decodeJWT::DecodeError: Not enough or too many segments를 던졌고, TeamFactory.signuprescue StandardError 블록이 이를 SYS10000 system error 로 변환해 ERROR 레벨로 로깅했다.

Quick Facts#

Field Value
exception.class Cupix::Errors::System (wraps JWT::DecodeError)
exception.message Not enough or too many segments
top_frame lib/cupix/aws/cognito.rb:198 (JWT.decode)
error_code SYS10000
endpoint POST /api/v1/teams/signup (Api::V1::TeamsController#signup)
http_status 500
deploy production-us-west-2-20260629T1555Z0-bfdc5ebd-cupixvista
env production, us-west-2
remote_ip 3.172.65.113 (단일 source)
user_agent Mozilla/5.0 (Windows NT 10.0; Win64; x64) ... Chrome/149.0.0.0 Safari/537.36

Affected Teams#

Team / Domain Error Count Impact
cupixvista-api (signup 엔드포인트) 8 외부에서 들어온 비정상 가입 시도 1건이 500 에러 8건으로 알람을 발생시킴. 실제 정상 사용자 가입 흐름은 영향 없음(동일 시간대 다른 IP의 200 응답 확인).

Timeline#

  1. 2026-06-30 10:15:22 KST — 첫 번째 POST /api/v1/teams/signup 요청이 IP 3.172.65.113에서 도착, 500 응답.
  2. 2026-06-30 10:15:22 ~ 10:15:50 KST — 동일 IP에서 8회 반복 요청, 모두 동일 에러로 500.
  3. 2026-06-30 10:15:47 KST — 동일 시간대에 GET /api/v1/teams/find_by_domain/solutionoperations 요청이 같은 시퀀스에서 발견됨 → 403 ARG10002 Team not found. 도메인 존재 여부를 먼저 확인하는 클라이언트 동작으로 추정.
  4. 2026-06-30 10:15:50 KST — 마지막 발생, 이후 동일 패턴 중단.
  5. 2026-06-30 이후 — 14일 retention 윈도우에서 동일 메시지 추가 발생 없음.

Error Log#

Datadog Logs

text
team signup failed: Not enough or too many segments

Impact#

  • Service: cupixvista-api
  • 발생 횟수: 8
  • 최초 발생: 2026-06-30 10:15 KST
  • 최근 발생: 2026-06-30 10:15 KST
  • 사용자 영향: 없음(공격/오설정 클라이언트 1건이 자기 자신의 가입에만 실패). 정상 가입 트래픽은 동일 시간대에 200으로 성공 응답.
  • 운영 영향: 28초 동안 ERROR 레벨 로그 8건 발생 → 알림 임계치를 자극할 수 있음.

Root Cause Summary#

Api::V1::TeamsController#signupskip_before_action :authenticate!로 사전 인증을 우회하지만, controller 안에서 request.headers['X-CUPIX-AUTH']를 직접 읽어 TeamFactory.signup(params, access_token)으로 넘긴다. TeamFactory.signupparams[:email]이 비어 있을 때 Cupix::Aws::Cognito.get_user_by_access_token을 호출하고, 이 함수는 무조건 ::JWT.decode(access_token, nil, false)sub 클레임을 꺼내려 한다. 클라이언트가 점(.)으로 구분된 3-segment JWT 형식이 아닌 토큰(빈 문자열, 임의 문자열, 잘려나간 토큰 등)을 보내면 jwt-2.3.0의 validate_segment_count!JWT::DecodeError: 'Not enough or too many segments'를 던진다. 이 예외는 Cupix::Errors::Entity 또는 Cupix::Errors::Argument가 아니라 일반 StandardError이므로 TeamFactory.signuprescue StandardError 분기가 ERROR 레벨로 로깅한 뒤 Cupix::Errors::System(SYS10000)으로 감싸 500을 반환한다. 즉 사용자 입력(클라이언트가 보낸 깨진 토큰) 때문에 system error 경로를 타고 있어, 본질적으로는 400대 응답이어야 할 상황을 500 + ERROR 로그로 처리하는 것이 root cause다.

Technical Analysis#

Code Path#

  • Entry point: app/controllers/api/v1/teams_controller.rb:20-29 (Api::V1::TeamsController#signup)
  • 인증 우회: app/controllers/api/v1/teams_controller.rb:11skip_before_action :authenticate!, only: %i[find_by_domain signup]
  • Factory dispatch: app/factories/team_factory.rb:87-116 (TeamFactory.signup)
  • 이메일 확보 분기: app/factories/team_factory.rb:148-154 (find_user_email) — params[:email]이 blank이면 access_token을 Cognito로 보냄
  • Failure point: lib/cupix/aws/cognito.rb:198::JWT.decode(access_token, nil, false).first['sub']
  • 예외 변환: app/factories/team_factory.rb:107-115Cupix::Errors::Entity/Argument 외 모든 예외는 ERROR로 로깅 후 SYS10000으로 래핑
app/controllers/api/v1/teams_controller.rb:10-29ruby
  before_action :set_team, except: %i[create invitation find_by_domain untrash purge mock signup]
  skip_before_action :authenticate!, only: %i[find_by_domain signup]

  # ...

  def signup
    access_token = request.headers['X-CUPIX-AUTH']
    access_token = access_token.split(' ').last if access_token.present? && access_token.include?(' ')
    team = TeamFactory.signup(params, access_token)

    render_api Renderable.new({
      contents: team,
      serializer_option: @serializer_option
    })
  end
app/factories/team_factory.rb:87-116ruby
    def signup(params, access_token = nil)
      find_user_email(params, access_token)
      validate_required_signup_params(params)

      validate_team_domain(params[:team_domain])

      # ...

    rescue Cupix::Errors::Entity, Cupix::Errors::Argument => e
      # Validation errors already logged at WARN by ApplicationRecord - don't re-log
      raise e
    rescue StandardError => e
      # System errors should be logged at ERROR
      Cupix::Logger.error("team signup failed: #{e.message}", class: self.class.name, function: __method__, error: e)
      raise e if e.is_a?(Cupix::Errors::BaseError)

      raise Cupix::Errors::System.new(code: 'SYS10000', reason: e.to_s, message: e.message)
    end
app/factories/team_factory.rb:148-154ruby
    def find_user_email(params, access_token)
      if params[:email].blank?
        access_token = access_token || params['X-CUPIX-AUTH'] || params['x-cupix-auth']
        cognito_user = Cupix::Aws::Cognito.get_user_by_access_token(access_token: access_token)
        params.merge!(email: cognito_user.email)
      end
    end
lib/cupix/aws/cognito.rb:197-211ruby
        def get_user_by_access_token(access_token: nil, sub: nil)
          sub ||= ::JWT.decode(access_token, nil, false).first['sub']

          Rails.cache.fetch(user_cache_key(sub), expires_in: 1.hour) do
            Cupix::Logger.info("Fetching an user from Cognito: #{sub}", function: __method__, module: 'Cupix::Aws', class: 'Cognito')

            response = client.get_user(access_token: access_token)
          rescue ::Aws::CognitoIdentityProvider::Errors::NotAuthorizedException
            raise Cupix::Errors::Unauthorized.new(code: 'AUTH20013', reason: 'Invalid access token')
          else
            Cupix::Aws::Cognito::UserResponse.new(
              user_response_hash(response)
            )
          end
        end
jwt-2.3.0/lib/jwt/decode.rb:77-83 (gem)ruby
    def validate_segment_count!
      return if segment_length == 3
      return if !@verify && segment_length == 2 # If no verifying required, the signature is not needed
      return if segment_length == 2 && header['alg'] == 'none'

      raise(JWT::DecodeError, 'Not enough or too many segments')
    end

기대 동작 vs 실제 동작

  • 기대: 클라이언트가 깨진 토큰을 보내면 400 Bad Request 또는 401 Unauthorized(AUTH20013-Invalid access token 등)로 응답하고, WARN 레벨 이하로만 기록되어 운영 알림을 발생시키지 않아야 한다.
  • 실제: JWT::DecodeErrorAws::CognitoIdentityProvider::Errors::NotAuthorizedException에도, Cupix::Errors::Entity/Argument에도 잡히지 않는 generic StandardError이므로 SYS10000 system error 경로로 들어가 500 + ERROR 로깅된다.

Log Evidence#

Datadog 쿼리:

text
service:cupixvista-api "Not enough or too many segments"

Request 로그(원문 발췌):

json
{
  "@timestamp": "2026-06-30T01:15:51.316Z",
  "message": "[500] POST /api/v1/teams/signup (Api::V1::TeamsController#signup)",
  "status": "info",
  "log_type": "request",
  "controller": "Api::V1::TeamsController",
  "action": "signup",
  "http": {
    "url_details": { "path": "/api/v1/teams/signup" },
    "status_code": 500,
    "method": "POST"
  },
  "params": {
    "team_domain": "solutionoperations",
    "fields": ["id", "name", "domain"]
  },
  "remote_ip": "3.172.65.113",
  "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ... Chrome/149.0.0.0 Safari/537.36",
  "cupix_auth_method": "COGNITO",
  "error": {
    "reason": "Not enough or too many segments",
    "code": "SYS10000",
    "message": "Not enough or too many segments",
    "class": "Cupix::Errors::System"
  },
  "request_id": "5c0d5d5e-04bb-49b5-abc4-faa604463071"
}

대응되는 애플리케이션 로그(원문):

json
{
  "@timestamp": "2026-06-30T01:15:50.697Z",
  "message": "team signup failed: Not enough or too many segments",
  "status": "error",
  "level": "error",
  "class": "Class",
  "function": "signup",
  "error": { "msg": "Not enough or too many segments" },
  "si_trace_id": "5c0d5d5e-04bb-49b5-abc4-faa604463071",
  "si_trace_origin": "api_request"
}

추가 관찰 사항:

  • paramsemailteam_name도 없음 → find_user_email 분기로 진입(이메일 blank) → JWT.decode 도달.
  • cupix_auth_method: COGNITO 태그가 붙어 있음 → 클라이언트는 Cognito 토큰을 실어 보낸 것으로 인식되었지만 실제 token segment 수가 3이 아니었음(빈 문자열, 잘린 토큰, prefix-only 등).
  • 동일 IP 3.172.65.113에서 8회만 발생, 14일 retention 내 다른 IP/시간대에서 동일 메시지 0건.
  • 같은 IP에서 직전에 GET /api/v1/teams/find_by_domain/solutionoperations도 호출했고 403(ARG10002 Team not found)을 받았다. 즉 도메인 사전 확인 → 가입 시도 흐름이지만 토큰이 깨진 상태로 8회 재시도한 것.
  • 동일 시간대(2026-06-30 KST)에 다른 IP의 POST /api/v1/teams/signup은 200으로 성공 → 정상 가입 경로 자체는 건강함.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 클라이언트가 잘못된(3-segment JWT가 아닌) access_token을 X-CUPIX-AUTH로 보내서 cognito.rb:198JWT.decode가 실패했고, system error 경로로 변환되어 500/ERROR 로그가 생성됨 메시지가 jwt-2.3.0 decode.rb:82의 정확한 문구; 단일 IP에서 28초 8회 burst; params.email 누락 → find_user_email이 JWT 경로로 진입; cupix_auth_method: COGNITO로 태그됨; 정상 IP의 동시간대 가입은 200 Confirmed
H2 jwt gem 또는 Cognito SDK 업그레이드 회귀(regression) 14일 retention 내 동일 메시지가 이 8건뿐이고 모두 단일 IP. deploy SHA bfdc5ebd-cupixvista는 같은 날 다른 정상 200 응답에서도 활성. Rejected
H3 Cognito 서비스 장애로 인한 토큰 검증 실패 JWT.decode는 Cognito와 통신하지 않는 로컬 디코딩 함수(verify=false). 또한 같은 시각 다른 가입은 정상 200. status-board에 dep:cognito 같은 active incident 없음. Rejected
H4 params[:email]이 누락된 채로 들어오는 비정상 클라이언트 경로 자체가 입력 검증 누락 버그 validate_required_signup_paramsemail을 required로 선언하지만, find_user_email(이메일을 채워주는 함수)이 그 검증보다 먼저 호출됨 — 즉 잘못된 토큰일 때 검증보다 외부 호출이 선행됨 검증 순서 문제는 fix 권장 사항으로 다뤄야 하나, 직접적인 root cause 메시지 자체는 jwt decoding 실패 Confirmed (보조 원인)

Fix Recommendation#

즉시 조치 (Critical)#

  • 파일: lib/cupix/aws/cognito.rb:197-211 (get_user_by_access_token)

  • 방향: ::JWT.decode 호출을 rescue JWT::DecodeError 로 감싸 Cupix::Errors::Unauthorized.new(code: 'AUTH20013', reason: 'Invalid access token')(이미 같은 함수가 NotAuthorizedException에 대해 사용하는 코드)로 변환한다. 이렇게 하면 클라이언트 입력 오류가 Cupix::Errors::Unauthorized로 분류되어 TeamFactory.signuprescue Cupix::Errors::Entity, Cupix::Errors::Argument 또는 base error 경로에서 WARN 이하로 처리되도록 후속 분기와 합쳐질 수 있다.

  • 근거: jwt-2.3.0의 JWT::DecodeError는 클라이언트가 제어 가능한 입력 형식 오류이며, 운영자가 알림을 받아야 할 system error가 아니다. NotAuthorizedException과 동일한 의미(Invalid access token)로 통합 처리해야 한다.

  • 파일: app/factories/team_factory.rb:107-115 (rescue 블록)

  • 방향: JWT::DecodeError도 위에서 Cupix::Errors::Unauthorized로 변환되면 자동으로 base error 경로에 포함되지만, 변환을 미루는 경우에는 rescue JWT::DecodeError => e 분기를 추가해 WARN으로 로깅한 뒤 Cupix::Errors::Argument(ARG10001-Invalid access token 등 적절한 코드)로 다시 던지는 방안도 고려한다. 코드 변경 범위가 더 작은 것은 cognito.rb 한 곳을 고치는 것.

단기 개선 (1주 이내)#

  • 파일: app/factories/team_factory.rb:87-92

  • 방향: find_user_email 호출 전에 validate_required_signup_params(params)validate_team_domain(params[:team_domain])을 먼저 실행하도록 순서를 바꾼다. 즉시 검증 가능한 입력(이메일이 명시적으로 비어 있고 토큰도 없는/깨진 경우)을 외부 호출 이전에 400으로 거절하면, 비정상 클라이언트 burst가 Cognito 의존 경로까지 도달하지 않아 로그 잡음과 외부 호출 비용 모두 줄어든다.

  • 근거: 현재는 params[:email]이 비어 있고 token도 비어 있을 때 JWT.decode(nil, ...)JWT::DecodeError: Nil JSON web token을 던지는 또 다른 변형(decode.rb:12)으로 빠질 수 있다. 검증 우선 순서 변경이 일반화된 가드 역할을 한다.

  • Datadog monitor 임계치: @error.code:SYS10000 + @error.message:"Not enough or too many segments"를 별도 그룹으로 잡아 단일 IP burst가 운영 알림을 트리거하지 않도록 분리한다.

장기 개선 (재발 방지)#

  • 토큰 입력 검증을 TeamsController#signup 진입 직후로 끌어올려 before_action/concern 형태로 표준화한다. signup뿐 아니라 향후 skip_before_action :authenticate! 라우트가 늘어날 때 동일한 입력 정규화/검증을 재사용할 수 있다.
  • WAF/CloudFront 레벨에서 /api/v1/teams/signup에 대한 IP-당 분당 요청 수 제한(rate limiting) 도입 — 동일 IP의 8회 burst가 28초에 들어오는 패턴은 application 계층 이전에 차단되는 것이 바람직하다.

Monitoring#

추가/조정해야 할 알림:

  • 동일 메시지 단일 IP burst 감지(현재는 한 클러스터로 묶여 SYS10000 톤만 보임):
text
service:cupixvista-api status:error "Not enough or too many segments"
  • 전체 signup 500 비율 (정상 가입 흐름이 깨졌는지 식별):
text
service:cupixvista-api @http.url_details.path:/api/v1/teams/signup @http.status_code:500
  • SYS10000 system error 추세(false-positive로 잡히는 client-input 오류 비율 추적):
text
service:cupixvista-api status:error @error.code:SYS10000

위 쿼리들은 monitor-only 문법(| stats, count by(...))을 사용하지 않고 dashboard timeseries widget에 그대로 사용 가능한 facet 검색 형식이다.

Risk Assessment#

  • Risk level: low — 사용자 영향은 없고, 외부 IP 1건의 28초 burst가 ERROR 로그를 발생시켜 운영 알림 잡음을 만든 사건. 정상 가입 흐름은 동일 시간대에 200으로 성공.
  • 예상 복잡도: trivialcognito.rb 한 함수에 rescue JWT::DecodeError만 추가하면 즉시 system error → unauthorized로 분류가 바뀐다. spec 업데이트 포함해 변경 라인은 10줄 미만.