# JWT Production Deployment Guide ## Phase 2: 프로덕션 배포 준비 ### 배포 전 필수 작업 #### 1️⃣ JWT 키 생성 (암호화 안전) ```powershell # 256비트 (32바이트) 안전한 랜덤 키 생성 # Option 1: PowerShell $bytes = New-Object Byte[] 32 [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes) $key = [Convert]::ToBase64String($bytes) Write-Host "JWT_KEY=$key" # Option 2: OpenSSL (WSL/Linux) openssl rand -hex 32 # Output: 7e3f8c9a2b1d4e6f8a3c5b7d9e1f3a5c (convert to base64 if needed) # Option 3: .NET CLI dotnet user-secrets generate ``` **결과 예시:** ``` JWT_KEY=H4sIABST2GYC/0N+JxAkLxI9XxD8kWI5E9fC3x5mJ7dP8= ``` #### 2️⃣ 데이터베이스 자격증명 검증 구현 **현재 상태**: 임시 테스트 구현 (모든 username/password 수용) **개선 사항**: Database 기반 검증 ##### Step 1: 마이그레이션 생성 (Credential 테이블) ```sql -- Migration: 0045_identity_credentials.sql BEGIN; CREATE TABLE IF NOT EXISTS public.identity_credential ( credential_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Reference to identity identity_id UUID NOT NULL UNIQUE REFERENCES public.identity(identity_id) ON DELETE CASCADE, -- Password storage (bcrypt hash) password_hash VARCHAR(255) NOT NULL, -- Credential state state VARCHAR(50) NOT NULL DEFAULT 'ACTIVE' CHECK (state IN ('ACTIVE', 'SUSPENDED', 'EXPIRED', 'REVOKED')), -- Failed login tracking failed_attempts INT DEFAULT 0, locked_until TIMESTAMP WITH TIME ZONE, -- Lifecycle created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, -- Idempotency correlation_id UUID UNIQUE ); CREATE INDEX idx_identity_credential_identity ON public.identity_credential(identity_id); CREATE INDEX idx_identity_credential_state ON public.identity_credential(state); COMMIT; ``` ##### Step 2: LoginEndpoint 수정 ```csharp public override async Task HandleAsync(LoginRequest req, CancellationToken ct) { logger.LogInformation("Login attempt for user: {User}", req.Username); if (string.IsNullOrWhiteSpace(req.Username) || string.IsNullOrWhiteSpace(req.Password)) { logger.LogWarning("Login failed: missing credentials"); ThrowError(x => x.AddError("credentials", "Username and password required")); } // FUTURE: Query database for identity by username // var identity = await sql.GetIdentityByUsernameAsync(req.Username, ct); // FUTURE: Get credential record // var credential = await sql.GetCredentialAsync(identity.Id, ct); // FUTURE: Verify password // if (!BCrypt.Net.BCrypt.Verify(req.Password, credential.PasswordHash)) // { // await sql.RecordFailedLoginAttemptAsync(credential.Id, ct); // ThrowError(x => x.AddError("credentials", "Invalid credentials")); // } // TEMPORARY: Accept any non-empty credentials var token = GenerateJwtToken(req.Username, req.Role ?? "User"); logger.LogInformation("Token issued for user: {User}", req.Username); // ... rest of implementation } ``` #### 3️⃣ 환경 변수 설정 (배포 시) **Kubernetes Secret:** ```yaml apiVersion: v1 kind: Secret metadata: name: kartsell-jwt type: Opaque data: JWT_KEY: SGg0c0lBQlNUM... # Base64 encoded ``` **Docker/.env:** ```bash JWT_KEY=H4sIABST2GYC/0N+JxAkLxI9XxD8kWI5E9fC3x5mJ7dP8= KARTSELL_POSTGRES=Host=db.production.internal;Port=5432;Database=kartselldb;Username=kartsell;Password=... ``` **AWS Systems Manager:** ```bash aws ssm put-parameter \ --name /kartsell/jwt/key \ --value "H4sIABST2GYC/0N+JxAkLxI9XxD8kWI5E9fC3x5mJ7dP8=" \ --type "SecureString" ``` #### 4️⃣ appsettings 배포 설정 **appsettings.Release.json 검증:** ```json { "Kestrel": { "Endpoints": { "Http": { "Url": "http://0.0.0.0:5002" } } }, "Authentication": { "Mode": "JWT" }, "Jwt": { "Key": "${JWT_KEY}", // ✅ Environment variable "Issuer": "KArtSell.Aegis", "Audience": "KArtSell.Aegis", "ExpirationMinutes": 60 }, "ConnectionStrings": { "Postgres": "${KARTSELL_POSTGRES}" // ✅ Environment variable }, "Serilog": { "MinimumLevel": { "Default": "Information" } } } ``` #### 5️⃣ HTTPS/TLS 설정 **Kestrel HTTPS:** ```json { "Kestrel": { "Endpoints": { "Https": { "Url": "https://0.0.0.0:443", "Certificate": { "Path": "/etc/ssl/certs/kartsell.pfx", "Password": "${CERT_PASSWORD}" } } } } } ``` **Nginx Reverse Proxy:** ```nginx upstream backend { server kartsell-host:5002; } server { listen 443 ssl http2; server_name api.kartsell.taxbaik.com; ssl_certificate /etc/nginx/ssl/kartsell.crt; ssl_certificate_key /etc/nginx/ssl/kartsell.key; ssl_protocols TLSv1.2 TLSv1.3; location /api/auth/login { proxy_pass http://backend; proxy_set_header Authorization ""; # Don't forward client auth } location /api { proxy_pass http://backend; proxy_set_header Authorization $http_authorization; proxy_pass_header Authorization; } } ``` ## 배포 체크리스트 ### 보안 - [ ] JWT_KEY 환경변수 설정 (256+ bits, cryptographically secure) - [ ] HTTPS only (HTTP → HTTPS redirect) - [ ] TLS 1.2+ enforced - [ ] HSTS header enabled (Strict-Transport-Security) - [ ] CORS properly configured (whitelist specific origins) - [ ] Rate limiting on /api/auth/login (max 5 attempts/min per IP) - [ ] Database credentials in secret manager (not hardcoded) - [ ] JWT key rotation schedule planned (annual minimum) ### 성능 - [ ] Connection pooling configured (min 10, max 50) - [ ] Caching enabled for authentication checks - [ ] Load balancer session affinity configured - [ ] CDN configured for static assets - [ ] Database query optimization verified ### 모니터링 - [ ] Authentication success/failure metrics logged - [ ] Failed login attempts alerting (>10/min = alert) - [ ] JWT validation errors tracked - [ ] Token expiration events logged - [ ] Authorization failures monitored ### 데이터베이스 - [ ] Backup schedule configured (daily minimum) - [ ] Password hashing algorithm decided (bcrypt/argon2) - [ ] Credential table indexed for fast lookups - [ ] Audit logging enabled - [ ] Database connection encryption (SSL) ### 배포 - [ ] Database migrations pre-validated - [ ] Rollback plan documented - [ ] Canary deployment configured (5% → 25% → 100%) - [ ] Health checks configured (/health/ready endpoint) - [ ] Log aggregation configured (ELK/Datadog) ## 배포 절차 ### 단계 1: 프로덕션 환경 준비 ```bash # 1. 환경 변수 설정 export JWT_KEY="H4sIABST2GYC/0N+JxAkLxI9XxD8kWI5E9fC3x5mJ7dP8=" export KARTSELL_POSTGRES="Host=prod-db;Port=5432;Database=kartselldb;Username=kartsell;Password=..." # 2. 데이터베이스 마이그레이션 실행 dotnet KArtSell.DbMigrator.dll # 3. 헬스 체크 curl https://api.kartsell.taxbaik.com/health/ready # Expected: 200 OK ``` ### 단계 2: 배포 (Blue-Green) ```bash # Blue: 현재 운영 환경 (v1.0) # Green: 새 배포 환경 (v2.0) # 1. Green 환경에 v2.0 배포 docker run -d \ -e JWT_KEY=$JWT_KEY \ -e KARTSELL_POSTGRES=$KARTSELL_POSTGRES \ -p 5002:5002 \ kartsell:v2.0 # 2. Green 환경 헬스 체크 curl http://localhost:5002/health/ready # 3. Green 환경 테스트 # - Login flow # - API requests with JWT # - Token expiration # 4. 로드 밸런서 Green으로 전환 # Blue → Green traffic switch # 5. Blue 환경 모니터링 (rollback 준비) # 30분 동안 이상 없으면 Blue 종료 ``` ### 단계 3: 배포 후 검증 ```bash # 1. JWT 토큰 발급 테스트 curl -X POST https://api.kartsell.taxbaik.com/api/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"test","password":"test","role":"Admin"}' # Expected response: # { # "accessToken": "eyJhbGc...", # "expiresIn": 3600, # "tokenType": "Bearer" # } # 2. API 엔드포인트 인증 테스트 TOKEN="eyJhbGc..." curl https://api.kartsell.taxbaik.com/api/identities \ -H "Authorization: Bearer $TOKEN" # Expected: 200 OK (또는 관련 비즈니스 응답) # 3. 모니터링 대시보드 확인 # - Authentication success rate # - API latency # - Error rates # 4. 로그 확인 # - 비정상적인 인증 실패 없음 # - 토큰 검증 오류 없음 ``` ## 롤백 절차 토큰 생성/검증 오류 발생 시: ```bash # 1. 즉시 Blue 환경으로 복구 # 로드 밸런서 Blue로 전환 # 2. 문제 분석 # - JWT_KEY 환경변수 확인 # - 데이터베이스 연결 확인 # - 로그 분석 # 3. 문제 수정 후 재배포 ``` ## 모니터링 쿼리 ### 인증 성공률 ```sql SELECT DATE_TRUNC('hour', created_at) as hour, COUNT(*) FILTER (WHERE status = 'success') as success_count, COUNT(*) FILTER (WHERE status = 'failure') as failure_count, ROUND(100.0 * COUNT(*) FILTER (WHERE status = 'success') / COUNT(*), 2) as success_rate FROM auth_logs WHERE created_at > NOW() - INTERVAL '24 hours' GROUP BY DATE_TRUNC('hour', created_at) ORDER BY hour DESC; ``` ### 토큰 검증 오류 ```sql SELECT error_message, COUNT(*) as count FROM jwt_validation_errors WHERE created_at > NOW() - INTERVAL '1 hour' GROUP BY error_message ORDER BY count DESC; ``` ## 성공 기준 배포 후 최소 24시간 모니터링: - [ ] Authentication success rate > 99% - [ ] API latency < 200ms (p95) - [ ] Token validation errors = 0 - [ ] Failed login attempts < 5/minute average - [ ] No database connection errors - [ ] User reports = 0 **이 모든 기준을 충족하면 배포 완료! ✅**