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-api의 POST /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.decode가 JWT::DecodeError: Not enough or too many segments를 던졌고, TeamFactory.signup의 rescue 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#
- 2026-06-30 10:15:22 KST — 첫 번째
POST /api/v1/teams/signup요청이 IP3.172.65.113에서 도착, 500 응답. - 2026-06-30 10:15:22 ~ 10:15:50 KST — 동일 IP에서 8회 반복 요청, 모두 동일 에러로 500.
- 2026-06-30 10:15:47 KST — 동일 시간대에
GET /api/v1/teams/find_by_domain/solutionoperations요청이 같은 시퀀스에서 발견됨 → 403ARG10002 Team not found. 도메인 존재 여부를 먼저 확인하는 클라이언트 동작으로 추정. - 2026-06-30 10:15:50 KST — 마지막 발생, 이후 동일 패턴 중단.
- 2026-06-30 이후 — 14일 retention 윈도우에서 동일 메시지 추가 발생 없음.
Error Log#
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#signup은 skip_before_action :authenticate!로 사전 인증을 우회하지만, controller 안에서 request.headers['X-CUPIX-AUTH']를 직접 읽어 TeamFactory.signup(params, access_token)으로 넘긴다. TeamFactory.signup은 params[: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.signup의 rescue 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:11—skip_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-115—Cupix::Errors::Entity/Argument외 모든 예외는 ERROR로 로깅 후SYS10000으로 래핑
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
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
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
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
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::DecodeError는Aws::CognitoIdentityProvider::Errors::NotAuthorizedException에도,Cupix::Errors::Entity/Argument에도 잡히지 않는 genericStandardError이므로SYS10000system error 경로로 들어가 500 + ERROR 로깅된다.
Log Evidence#
Datadog 쿼리:
service:cupixvista-api "Not enough or too many segments"
Request 로그(원문 발췌):
{
"@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"
}
대응되는 애플리케이션 로그(원문):
{
"@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"
}
추가 관찰 사항:
params에email도team_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:198의 JWT.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_params가 email을 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.signup의rescue 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 톤만 보임):
service:cupixvista-api status:error "Not enough or too many segments"
- 전체 signup 500 비율 (정상 가입 흐름이 깨졌는지 식별):
service:cupixvista-api @http.url_details.path:/api/v1/teams/signup @http.status_code:500
- SYS10000 system error 추세(false-positive로 잡히는 client-input 오류 비율 추적):
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으로 성공.
- 예상 복잡도: trivial —
cognito.rb한 함수에rescue JWT::DecodeError만 추가하면 즉시 system error → unauthorized로 분류가 바뀐다. spec 업데이트 포함해 변경 라인은 10줄 미만.