feat(security): API 키 보호, CORS 강화, Rate Limiting KV 전환
보안 개선: - API 키 하드코딩 제거 (NAMECHEAP_API_KEY_INTERNAL) - CORS 정책: * → hosting.anvil.it.com 제한 - /health 엔드포인트 DB 정보 노출 방지 - Rate Limiting 인메모리 Map → Cloudflare KV 전환 - 분산 환경 일관성 보장 - 재시작 후에도 유지 - 자동 만료 (TTL) 문서: - CLAUDE.md Security 섹션 추가 - KV Namespace 설정 가이드 추가 - 배포/마이그레이션 가이드 추가 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
372
KV_MIGRATION_GUIDE.md
Normal file
372
KV_MIGRATION_GUIDE.md
Normal file
@@ -0,0 +1,372 @@
|
||||
# Rate Limiting KV Migration Guide
|
||||
|
||||
## 변경 사항 요약
|
||||
|
||||
Rate Limiting 시스템을 인메모리 Map에서 Cloudflare KV로 마이그레이션했습니다.
|
||||
|
||||
### 기존 문제점
|
||||
- Workers 인스턴스 간 공유되지 않음
|
||||
- 재시작 시 초기화됨
|
||||
- 분산 환경에서 Rate Limit 우회 가능
|
||||
|
||||
### 개선 사항
|
||||
✅ 인스턴스 간 공유 (KV 기반)
|
||||
✅ 재시작 후에도 유지
|
||||
✅ 분산 환경에서 일관된 Rate Limiting
|
||||
✅ 자동 만료 (TTL)
|
||||
|
||||
---
|
||||
|
||||
## 배포 절차
|
||||
|
||||
### 1. KV Namespace 생성
|
||||
|
||||
```bash
|
||||
# Production용 Namespace 생성
|
||||
wrangler kv:namespace create RATE_LIMIT_KV
|
||||
```
|
||||
|
||||
출력 예시:
|
||||
```
|
||||
⛅️ wrangler 3.x.x
|
||||
-------------------
|
||||
🌀 Creating namespace with title "telegram-summary-bot-RATE_LIMIT_KV"
|
||||
✨ Success!
|
||||
Add the following to your configuration file in your kv_namespaces array:
|
||||
{ binding = "RATE_LIMIT_KV", id = "abc123def456ghi789jkl012mno345pq" }
|
||||
```
|
||||
|
||||
### 2. wrangler.toml 업데이트
|
||||
|
||||
위에서 출력된 `id` 값을 복사하여 `wrangler.toml` 파일의 20-22번째 줄을 수정합니다:
|
||||
|
||||
```toml
|
||||
[[kv_namespaces]]
|
||||
binding = "RATE_LIMIT_KV"
|
||||
id = "abc123def456ghi789jkl012mno345pq" # ← 실제 ID로 변경
|
||||
```
|
||||
|
||||
### 3. 로컬 개발용 Namespace 생성 (선택사항)
|
||||
|
||||
로컬 테스트 시 별도의 KV를 사용하려면:
|
||||
|
||||
```bash
|
||||
wrangler kv:namespace create RATE_LIMIT_KV --preview
|
||||
```
|
||||
|
||||
출력된 `preview_id`를 `wrangler.toml`에 추가:
|
||||
|
||||
```toml
|
||||
[[kv_namespaces]]
|
||||
binding = "RATE_LIMIT_KV"
|
||||
id = "abc123def456ghi789jkl012mno345pq"
|
||||
preview_id = "xyz789abc123def456ghi012jkl345mno" # 로컬 개발용
|
||||
```
|
||||
|
||||
### 4. TypeScript 컴파일 확인
|
||||
|
||||
```bash
|
||||
npx tsc --noEmit
|
||||
```
|
||||
|
||||
에러가 없어야 합니다.
|
||||
|
||||
### 5. 로컬 테스트
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
다른 터미널에서 테스트 요청:
|
||||
|
||||
```bash
|
||||
# 첫 번째 요청 (성공)
|
||||
curl -X POST http://localhost:8787/webhook \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Telegram-Bot-Api-Secret-Token: ${WEBHOOK_SECRET}" \
|
||||
-d '{"update_id":1,"message":{"message_id":1,"from":{"id":123,"is_bot":false,"first_name":"Test"},"chat":{"id":123,"type":"private"},"date":1234567890,"text":"테스트"}}'
|
||||
|
||||
# 30번 연속 요청 후 31번째 요청 (Rate Limit)
|
||||
# Rate Limit 메시지가 표시되어야 함
|
||||
```
|
||||
|
||||
### 6. Production 배포
|
||||
|
||||
```bash
|
||||
npm run deploy
|
||||
```
|
||||
|
||||
### 7. 배포 확인
|
||||
|
||||
```bash
|
||||
# 로그 스트리밍
|
||||
npm run tail
|
||||
|
||||
# Health Check
|
||||
curl https://telegram-summary-bot.kappa-d8e.workers.dev/health
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 변경된 파일
|
||||
|
||||
### `/Users/kaffa/telegram-bot-workers/wrangler.toml`
|
||||
```diff
|
||||
[[d1_databases]]
|
||||
binding = "DB"
|
||||
database_name = "telegram-conversations"
|
||||
database_id = "c285bb5b-888b-405d-b36f-475ae5aed20e"
|
||||
|
||||
+[[kv_namespaces]]
|
||||
+binding = "RATE_LIMIT_KV"
|
||||
+id = "YOUR_KV_NAMESPACE_ID" # Run: wrangler kv:namespace create RATE_LIMIT_KV
|
||||
```
|
||||
|
||||
### `/Users/kaffa/telegram-bot-workers/src/types.ts`
|
||||
```diff
|
||||
export interface Env {
|
||||
DB: D1Database;
|
||||
AI: Ai;
|
||||
BOT_TOKEN: string;
|
||||
WEBHOOK_SECRET: string;
|
||||
// ... 기타 환경변수
|
||||
+ RATE_LIMIT_KV: KVNamespace;
|
||||
}
|
||||
```
|
||||
|
||||
### `/Users/kaffa/telegram-bot-workers/src/security.ts`
|
||||
```diff
|
||||
-// Rate Limiting
|
||||
-const rateLimitMap = new Map<string, { count: number; resetAt: number }>();
|
||||
-
|
||||
-export function checkRateLimit(
|
||||
- userId: string,
|
||||
- maxRequests: number = 30,
|
||||
- windowMs: number = 60000
|
||||
-): boolean {
|
||||
- // 인메모리 Map 기반 로직
|
||||
-}
|
||||
|
||||
+// Rate Limiting (Cloudflare KV 기반)
|
||||
+interface RateLimitData {
|
||||
+ count: number;
|
||||
+ resetAt: number;
|
||||
+}
|
||||
+
|
||||
+export async function checkRateLimit(
|
||||
+ kv: KVNamespace,
|
||||
+ userId: string,
|
||||
+ maxRequests: number = 30,
|
||||
+ windowMs: number = 60000
|
||||
+): Promise<boolean> {
|
||||
+ // KV 기반 로직 (자동 TTL)
|
||||
+}
|
||||
```
|
||||
|
||||
### `/Users/kaffa/telegram-bot-workers/src/index.ts`
|
||||
```diff
|
||||
- if (!checkRateLimit(telegramUserId)) {
|
||||
+ if (!(await checkRateLimit(env.RATE_LIMIT_KV, telegramUserId))) {
|
||||
await sendMessage(
|
||||
env.BOT_TOKEN,
|
||||
chatId,
|
||||
'⚠️ 너무 많은 요청입니다. 잠시 후 다시 시도해주세요.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 기술 상세
|
||||
|
||||
### Rate Limiting 알고리즘
|
||||
|
||||
**Key 형식:** `ratelimit:{userId}`
|
||||
|
||||
**데이터 구조:**
|
||||
```typescript
|
||||
{
|
||||
count: number, // 현재 윈도우 내 요청 수
|
||||
resetAt: number // 윈도우 만료 시각 (Unix timestamp)
|
||||
}
|
||||
```
|
||||
|
||||
**처리 로직:**
|
||||
1. KV에서 사용자별 카운터 조회
|
||||
2. 윈도우 만료 확인 (현재 시각 > resetAt)
|
||||
- 만료 시: 새 윈도우 시작 (count=1)
|
||||
- 유효 시: count 확인
|
||||
3. count >= maxRequests (기본 30) → Rate Limit 초과
|
||||
4. count < maxRequests → count 증가 후 KV 업데이트
|
||||
|
||||
**자동 만료:**
|
||||
- KV의 `expirationTtl` 사용 (초 단위)
|
||||
- 윈도우 종료 시 자동 삭제 (메모리 효율)
|
||||
|
||||
**에러 처리:**
|
||||
- KV 오류 시 Rate Limit 통과 (서비스 가용성 우선)
|
||||
- 로그 기록: `[RateLimit] KV 오류: ...`
|
||||
|
||||
---
|
||||
|
||||
## 모니터링
|
||||
|
||||
### KV Namespace 확인
|
||||
|
||||
```bash
|
||||
# KV Namespace 목록
|
||||
wrangler kv:namespace list
|
||||
|
||||
# 특정 키 조회
|
||||
wrangler kv:key get "ratelimit:821596605" --namespace-id=YOUR_KV_ID
|
||||
|
||||
# 모든 키 목록
|
||||
wrangler kv:key list --namespace-id=YOUR_KV_ID
|
||||
```
|
||||
|
||||
### Cloudflare Dashboard
|
||||
1. https://dash.cloudflare.com → Workers & Pages
|
||||
2. KV → `telegram-summary-bot-RATE_LIMIT_KV` 클릭
|
||||
3. Key-Value 쌍 확인 (실시간)
|
||||
|
||||
### 로그 확인
|
||||
|
||||
```bash
|
||||
wrangler tail
|
||||
|
||||
# Rate Limit 오류만 필터링
|
||||
wrangler tail --format json | jq 'select(.message | contains("RateLimit"))'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 트러블슈팅
|
||||
|
||||
### KV Namespace ID를 잊었을 때
|
||||
```bash
|
||||
wrangler kv:namespace list
|
||||
```
|
||||
|
||||
### KV 데이터 초기화 (테스트용)
|
||||
```bash
|
||||
# 특정 사용자 Rate Limit 초기화
|
||||
wrangler kv:key delete "ratelimit:821596605" --namespace-id=YOUR_KV_ID
|
||||
|
||||
# 전체 초기화 (주의!)
|
||||
wrangler kv:key list --namespace-id=YOUR_KV_ID | jq -r '.[] | .name' | xargs -I {} wrangler kv:key delete "{}" --namespace-id=YOUR_KV_ID
|
||||
```
|
||||
|
||||
### Rate Limit 테스트 스크립트
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# rate-limit-test.sh
|
||||
TOKEN="YOUR_WEBHOOK_SECRET"
|
||||
|
||||
for i in {1..35}; do
|
||||
echo "Request $i:"
|
||||
curl -s -X POST http://localhost:8787/webhook \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Telegram-Bot-Api-Secret-Token: $TOKEN" \
|
||||
-d '{"update_id":'$i',"message":{"message_id":'$i',"from":{"id":123,"is_bot":false,"first_name":"Test"},"chat":{"id":123,"type":"private"},"date":1234567890,"text":"테스트 '$i'"}}' \
|
||||
| jq -r '.message // "OK"'
|
||||
sleep 0.5
|
||||
done
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 성능 영향
|
||||
|
||||
### Before (인메모리 Map)
|
||||
- 응답 시간: ~1ms (동기)
|
||||
- 메모리: 인스턴스별 독립
|
||||
- 분산 환경: 비효율적
|
||||
|
||||
### After (KV)
|
||||
- 응답 시간: ~20-50ms (KV read/write 포함)
|
||||
- 메모리: 0 (KV로 오프로드)
|
||||
- 분산 환경: 일관성 보장
|
||||
|
||||
**권장 사항:** KV 호출을 줄이기 위해 windowMs를 늘리지 말 것 (현재 60초 최적)
|
||||
|
||||
---
|
||||
|
||||
## 롤백 절차 (문제 발생 시)
|
||||
|
||||
### 1. 이전 버전 복구
|
||||
```bash
|
||||
git revert HEAD
|
||||
npm run deploy
|
||||
```
|
||||
|
||||
### 2. wrangler.toml에서 KV Namespace 제거
|
||||
```toml
|
||||
# 아래 3줄 주석 처리 또는 삭제
|
||||
# [[kv_namespaces]]
|
||||
# binding = "RATE_LIMIT_KV"
|
||||
# id = "..."
|
||||
```
|
||||
|
||||
### 3. 재배포
|
||||
```bash
|
||||
npm run deploy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
**Q: 로컬 개발 시 KV를 사용하지 않으려면?**
|
||||
A: `--remote` 플래그 사용하여 Production KV 직접 사용 (권장하지 않음)
|
||||
```bash
|
||||
wrangler dev --remote
|
||||
```
|
||||
|
||||
**Q: KV 비용은 얼마나 드나요?**
|
||||
A: Cloudflare Workers Free Plan: 100,000 read/day, 1,000 write/day 무료
|
||||
- Rate Limiting: 1 read + 1 write per request
|
||||
- 하루 1,000명 사용자까지 무료
|
||||
|
||||
**Q: KV가 느린 경우?**
|
||||
A: KV는 글로벌 분산 스토리지이므로 20-50ms latency 정상
|
||||
- 대안: Durable Objects (더 빠르지만 비용 높음)
|
||||
|
||||
**Q: Rate Limit 설정 변경하려면?**
|
||||
A: `security.ts:checkRateLimit()` 파라미터 수정
|
||||
```typescript
|
||||
// 기본값: 30 requests / 60초
|
||||
export async function checkRateLimit(
|
||||
kv: KVNamespace,
|
||||
userId: string,
|
||||
maxRequests: number = 50, // ← 변경
|
||||
windowMs: number = 120000 // ← 2분으로 변경
|
||||
): Promise<boolean>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 문서 업데이트
|
||||
|
||||
이 마이그레이션으로 인해 다음 문서도 업데이트 필요:
|
||||
|
||||
- [x] `CLAUDE.md` - Rate Limiting 섹션 수정
|
||||
- [ ] `README.md` - 설치/배포 절차 업데이트 (KV Namespace 생성 추가)
|
||||
- [ ] `schema.sql` - 변경 없음 (KV는 별도 저장소)
|
||||
|
||||
---
|
||||
|
||||
## 체크리스트
|
||||
|
||||
배포 전 확인:
|
||||
|
||||
- [ ] `wrangler kv:namespace create RATE_LIMIT_KV` 실행 완료
|
||||
- [ ] `wrangler.toml`에 실제 KV Namespace ID 입력
|
||||
- [ ] `npx tsc --noEmit` 타입 에러 없음
|
||||
- [ ] 로컬 테스트 (`npm run dev`) 정상 동작
|
||||
- [ ] Rate Limit 테스트 (30회 연속 요청) 성공
|
||||
- [ ] Production 배포 (`npm run deploy`) 완료
|
||||
- [ ] Health Check 확인
|
||||
- [ ] 실제 Telegram 메시지 테스트 완료
|
||||
|
||||
배포 완료!
|
||||
Reference in New Issue
Block a user