본문으로 건너뛰기

브로커 401

거래소(Binance / Bybit / OKX / Alpaca / KIS) API가 401 Unauthorized를 반환하는 상황입니다. 거래 자체가 멈출 수 있는 P1 장애 — 즉시 진단 후 사용자 영향 범위를 결정합니다.

사전 요구 사항

  • broker_order_events 테이블에 최근 5xx/401 이벤트가 있는지 조회 가능
  • 영향 사용자 ID 또는 자산군이 알려져 있어야 우선순위 부여 가능

1. 원인 가설 (빈도순)

원인영향 범위검증
placeholder 값 (your_api_key_here)단일 사용자 (env 폴백 사용 시 전체).env 또는 exchange_keys 행 확인
API 키 만료 / 무효화 (사용자가 거래소 측에서 revoke)단일 사용자거래소 콘솔
권한 부족 (read-only / IP 제한)단일 사용자거래소 키 권한 확인
ENCRYPTION_SALT 회전 후 미재등록전체 사용자DB 복호화 실패 로그
ALPACA_BASE_URL 라이브/페이퍼 미스매치Alpaca 단독URL 점검
시계 드리프트 (Binance HMAC)단일 사용자ntpq 또는 timedatectl

2. 진단

단일 사용자 단위

# 1) 가장 최근 401/403 이벤트 추출
docker compose exec timescaledb psql -U quant -d quantai -c "
SELECT received_at, user_id, asset_class,
event_type, payload->>'http_status' AS code,
payload->>'broker' AS broker,
LEFT(payload->>'error', 120) AS err
FROM broker_order_events
WHERE event_type IN ('error_4xx','broker_4xx','http_4xx','auth_failed')
AND received_at > NOW() - INTERVAL '6 hours'
ORDER BY received_at DESC LIMIT 20;
"

# 2) 해당 사용자의 활성 키 상태
docker compose exec timescaledb psql -U quant -d quantai -c "
SELECT id, exchange, asset_class, is_active, created_at, last_used_at,
(api_key_encrypted IS NOT NULL) AS has_key
FROM exchange_keys
WHERE user_id = <USER_ID>;
"

# 3) Fernet 복호화가 정상인지 (placeholder 점검 포함)
docker compose exec api python -c "
from server.auth.crypto import decrypt
from server.db.session import SessionLocal
from server.models.exchange_keys import ExchangeKey
with SessionLocal() as s:
for k in s.query(ExchangeKey).filter_by(user_id=<USER_ID>):
try:
api = decrypt(k.api_key_encrypted)
bad = api in {'', 'your_api_key_here'} or api.startswith('your_')
print(k.id, k.exchange, 'BAD' if bad else 'ok', f'len={len(api)}')
except Exception as e:
print(k.id, 'DECRYPT_FAIL', e)
"

전체 사용자 광역 (SALT 회전 의심)

# 모든 키가 동시에 401이라면 SALT 회전을 의심
docker compose exec timescaledb psql -U quant -d quantai -c "
SELECT COUNT(DISTINCT user_id) AS users_failing
FROM broker_order_events
WHERE event_type = 'auth_failed'
AND received_at > NOW() - INTERVAL '15 minutes';
"

20명 이상 동시 실패면 거의 확실히 SALT 회전이 원인입니다 → 시크릿 회전 — ENCRYPTION_SALT 참고.

3. 조치

Case A: placeholder 값

# .env에서 더미 값 발견
grep -E "(your_api_key_here|your_secret|changeme)" .env
# 발견 시 진짜 값으로 교체 또는 삭제
docker compose restart api

사용자가 UI에서 등록한 키가 placeholder인 경우, 사용자에게 재등록 안내.

Case B: 만료된 키

사용자에게 거래소 콘솔에서 새 키 발급 안내. 운영자가 직접 새 키를 등록할 필요는 없음 (거래소 키는 사용자 소유).

# 사용자 키 비활성 처리 (로그인 시 재등록 유도)
docker compose exec timescaledb psql -U quant -d quantai -c "
UPDATE exchange_keys SET is_active = false
WHERE user_id = <USER_ID> AND exchange = '<binance|alpaca|kis>';
"

Case C: 권한 부족

거래소 콘솔에서 다음 권한 ON 필요:

거래소필수 권한
Binance / Bybit / OKXSpot Trade (또는 Futures Trade), Read
AlpacaTrading + Market Data
KIS거래용 + 조회용 (모의투자는 별도 발급)

IP 제한이 켜져 있다면 운영 VM의 outbound IP를 화이트리스트에 추가.

Case D: SALT 회전 후 미재등록

시크릿 회전 — ENCRYPTION_SALT의 재등록 절차 따르기. 단일 페이지로 처리 — 본 페이지에서 중복하지 않음.

Case E: Alpaca URL 미스매치

# 페이퍼 키로 라이브 URL을 호출하면 401
grep ALPACA_BASE_URL .env
# 페이퍼: https://paper-api.alpaca.markets
# 라이브: https://api.alpaca.markets

라이브 승급 시 사용자가 페이퍼 키를 그대로 둔 케이스가 흔함. UI에서 "라이브 키 등록" 강제.

Case F: 시계 드리프트 (Binance)

# VM 시계 동기화
ssh azureuser@<vm-ip> 'sudo timedatectl set-ntp true; timedatectl status'

5초 이상 어긋나면 Binance가 401 / Timestamp for this request was 1000ms ahead.

4. 검증

# 조치 후 다음 사이클에서 reconciler audit가 정상으로
docker compose exec timescaledb psql -U quant -d quantai -c "
SELECT received_at, user_id, payload->>'severity' AS sev
FROM broker_order_events
WHERE user_id = <USER_ID>
AND event_type = 'reconcile_pass'
ORDER BY received_at DESC LIMIT 3;
"
# 가장 최근 row의 severity가 'info' 또는 'warning' (이전 'critical'에서 회복)

트러블슈팅

증상원인 / 조치
사용자가 키 재등록 후에도 401캐시된 토큰 (KIS) 또는 BotManager 메모리 캐시. compose restart api
401 → 200 후 다시 401거래소 측 IP 제한 / 일시 rate limit. broker 콘솔 / 상태 페이지 확인
401인데 단일 사용자 / 단일 자산군만거의 확실히 사용자 측 키 문제. 다른 사용자가 같은 거래소에서 정상이면 광역 원인 배제
401인데 거래는 들어감 (간헐적)다른 사용자 keys로 우연히 동작? 권한 분리 사고 의심 → 멀티유저 격리 사고

관련 페이지