mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-14 22:13:41 +08:00
docs(ko-KR): add Korean translation for skills
- 15 skill categories (17 files): coding-standards, tdd-workflow, frontend-patterns, backend-patterns, security-review (2 files), postgres-patterns, verification-loop, continuous-learning, continuous-learning-v2, eval-harness, iterative-retrieval, strategic-compact, golang-patterns, golang-testing, clickhouse-io, project-guidelines-example
This commit is contained in:
495
docs/ko-KR/skills/security-review/SKILL.md
Normal file
495
docs/ko-KR/skills/security-review/SKILL.md
Normal file
@@ -0,0 +1,495 @@
|
||||
---
|
||||
name: security-review
|
||||
description: 인증 추가, 사용자 입력 처리, 시크릿 관리, API 엔드포인트 생성, 결제/민감한 기능 구현 시 이 스킬을 사용하세요. 포괄적인 보안 체크리스트와 패턴을 제공합니다.
|
||||
origin: ECC
|
||||
---
|
||||
|
||||
# 보안 리뷰 스킬
|
||||
|
||||
이 스킬은 모든 코드가 보안 모범 사례를 따르고 잠재적 취약점을 식별하도록 보장합니다.
|
||||
|
||||
## 활성화 시점
|
||||
|
||||
- 인증 또는 권한 부여 구현 시
|
||||
- 사용자 입력 또는 파일 업로드 처리 시
|
||||
- 새로운 API 엔드포인트 생성 시
|
||||
- 시크릿 또는 자격 증명 작업 시
|
||||
- 결제 기능 구현 시
|
||||
- 민감한 데이터 저장 또는 전송 시
|
||||
- 서드파티 API 통합 시
|
||||
|
||||
## 보안 체크리스트
|
||||
|
||||
### 1. 시크릿 관리
|
||||
|
||||
#### 절대 하지 말아야 할 것
|
||||
```typescript
|
||||
const apiKey = "sk-proj-xxxxx" // Hardcoded secret
|
||||
const dbPassword = "password123" // In source code
|
||||
```
|
||||
|
||||
#### 반드시 해야 할 것
|
||||
```typescript
|
||||
const apiKey = process.env.OPENAI_API_KEY
|
||||
const dbUrl = process.env.DATABASE_URL
|
||||
|
||||
// Verify secrets exist
|
||||
if (!apiKey) {
|
||||
throw new Error('OPENAI_API_KEY not configured')
|
||||
}
|
||||
```
|
||||
|
||||
#### 확인 단계
|
||||
- [ ] 하드코딩된 API 키, 토큰, 비밀번호 없음
|
||||
- [ ] 모든 시크릿이 환경 변수에 저장됨
|
||||
- [ ] `.env.local`이 .gitignore에 포함됨
|
||||
- [ ] git 히스토리에 시크릿 없음
|
||||
- [ ] 프로덕션 시크릿이 호스팅 플랫폼(Vercel, Railway)에 저장됨
|
||||
|
||||
### 2. 입력 유효성 검사
|
||||
|
||||
#### 항상 사용자 입력을 검증할 것
|
||||
```typescript
|
||||
import { z } from 'zod'
|
||||
|
||||
// Define validation schema
|
||||
const CreateUserSchema = z.object({
|
||||
email: z.string().email(),
|
||||
name: z.string().min(1).max(100),
|
||||
age: z.number().int().min(0).max(150)
|
||||
})
|
||||
|
||||
// Validate before processing
|
||||
export async function createUser(input: unknown) {
|
||||
try {
|
||||
const validated = CreateUserSchema.parse(input)
|
||||
return await db.users.create(validated)
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return { success: false, errors: error.errors }
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 파일 업로드 유효성 검사
|
||||
```typescript
|
||||
function validateFileUpload(file: File) {
|
||||
// Size check (5MB max)
|
||||
const maxSize = 5 * 1024 * 1024
|
||||
if (file.size > maxSize) {
|
||||
throw new Error('File too large (max 5MB)')
|
||||
}
|
||||
|
||||
// Type check
|
||||
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif']
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
throw new Error('Invalid file type')
|
||||
}
|
||||
|
||||
// Extension check
|
||||
const allowedExtensions = ['.jpg', '.jpeg', '.png', '.gif']
|
||||
const extension = file.name.toLowerCase().match(/\.[^.]+$/)?.[0]
|
||||
if (!extension || !allowedExtensions.includes(extension)) {
|
||||
throw new Error('Invalid file extension')
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
```
|
||||
|
||||
#### 확인 단계
|
||||
- [ ] 모든 사용자 입력이 스키마로 검증됨
|
||||
- [ ] 파일 업로드가 제한됨 (크기, 타입, 확장자)
|
||||
- [ ] 사용자 입력이 쿼리에 직접 사용되지 않음
|
||||
- [ ] 화이트리스트 검증 사용 (블랙리스트가 아닌)
|
||||
- [ ] 에러 메시지가 민감한 정보를 노출하지 않음
|
||||
|
||||
### 3. SQL Injection 방지
|
||||
|
||||
#### 절대 SQL을 연결하지 말 것
|
||||
```typescript
|
||||
// DANGEROUS - SQL Injection vulnerability
|
||||
const query = `SELECT * FROM users WHERE email = '${userEmail}'`
|
||||
await db.query(query)
|
||||
```
|
||||
|
||||
#### 반드시 파라미터화된 쿼리를 사용할 것
|
||||
```typescript
|
||||
// Safe - parameterized query
|
||||
const { data } = await supabase
|
||||
.from('users')
|
||||
.select('*')
|
||||
.eq('email', userEmail)
|
||||
|
||||
// Or with raw SQL
|
||||
await db.query(
|
||||
'SELECT * FROM users WHERE email = $1',
|
||||
[userEmail]
|
||||
)
|
||||
```
|
||||
|
||||
#### 확인 단계
|
||||
- [ ] 모든 데이터베이스 쿼리가 파라미터화된 쿼리 사용
|
||||
- [ ] SQL에서 문자열 연결 없음
|
||||
- [ ] ORM/쿼리 빌더가 올바르게 사용됨
|
||||
- [ ] Supabase 쿼리가 적절히 새니타이징됨
|
||||
|
||||
### 4. 인증 및 권한 부여
|
||||
|
||||
#### JWT 토큰 처리
|
||||
```typescript
|
||||
// ❌ WRONG: localStorage (vulnerable to XSS)
|
||||
localStorage.setItem('token', token)
|
||||
|
||||
// ✅ CORRECT: httpOnly cookies
|
||||
res.setHeader('Set-Cookie',
|
||||
`token=${token}; HttpOnly; Secure; SameSite=Strict; Max-Age=3600`)
|
||||
```
|
||||
|
||||
#### 권한 부여 확인
|
||||
```typescript
|
||||
export async function deleteUser(userId: string, requesterId: string) {
|
||||
// ALWAYS verify authorization first
|
||||
const requester = await db.users.findUnique({
|
||||
where: { id: requesterId }
|
||||
})
|
||||
|
||||
if (requester.role !== 'admin') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized' },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
||||
// Proceed with deletion
|
||||
await db.users.delete({ where: { id: userId } })
|
||||
}
|
||||
```
|
||||
|
||||
#### Row Level Security (Supabase)
|
||||
```sql
|
||||
-- Enable RLS on all tables
|
||||
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Users can only view their own data
|
||||
CREATE POLICY "Users view own data"
|
||||
ON users FOR SELECT
|
||||
USING (auth.uid() = id);
|
||||
|
||||
-- Users can only update their own data
|
||||
CREATE POLICY "Users update own data"
|
||||
ON users FOR UPDATE
|
||||
USING (auth.uid() = id);
|
||||
```
|
||||
|
||||
#### 확인 단계
|
||||
- [ ] 토큰이 httpOnly 쿠키에 저장됨 (localStorage가 아닌)
|
||||
- [ ] 민감한 작업 전에 권한 부여 확인
|
||||
- [ ] Supabase에서 Row Level Security 활성화됨
|
||||
- [ ] 역할 기반 접근 제어 구현됨
|
||||
- [ ] 세션 관리가 안전함
|
||||
|
||||
### 5. XSS 방지
|
||||
|
||||
#### HTML 새니타이징
|
||||
```typescript
|
||||
import DOMPurify from 'isomorphic-dompurify'
|
||||
|
||||
// ALWAYS sanitize user-provided HTML
|
||||
function renderUserContent(html: string) {
|
||||
const clean = DOMPurify.sanitize(html, {
|
||||
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p'],
|
||||
ALLOWED_ATTR: []
|
||||
})
|
||||
return <div dangerouslySetInnerHTML={{ __html: clean }} />
|
||||
}
|
||||
```
|
||||
|
||||
#### Content Security Policy
|
||||
```typescript
|
||||
// next.config.js
|
||||
const securityHeaders = [
|
||||
{
|
||||
key: 'Content-Security-Policy',
|
||||
value: `
|
||||
default-src 'self';
|
||||
script-src 'self' 'unsafe-eval' 'unsafe-inline';
|
||||
style-src 'self' 'unsafe-inline';
|
||||
img-src 'self' data: https:;
|
||||
font-src 'self';
|
||||
connect-src 'self' https://api.example.com;
|
||||
`.replace(/\s{2,}/g, ' ').trim()
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### 확인 단계
|
||||
- [ ] 사용자 제공 HTML이 새니타이징됨
|
||||
- [ ] CSP 헤더가 구성됨
|
||||
- [ ] 검증되지 않은 동적 콘텐츠 렌더링 없음
|
||||
- [ ] React의 내장 XSS 보호가 사용됨
|
||||
|
||||
### 6. CSRF 보호
|
||||
|
||||
#### CSRF 토큰
|
||||
```typescript
|
||||
import { csrf } from '@/lib/csrf'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const token = request.headers.get('X-CSRF-Token')
|
||||
|
||||
if (!csrf.verify(token)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid CSRF token' },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
||||
// Process request
|
||||
}
|
||||
```
|
||||
|
||||
#### SameSite 쿠키
|
||||
```typescript
|
||||
res.setHeader('Set-Cookie',
|
||||
`session=${sessionId}; HttpOnly; Secure; SameSite=Strict`)
|
||||
```
|
||||
|
||||
#### 확인 단계
|
||||
- [ ] 상태 변경 작업에 CSRF 토큰 적용
|
||||
- [ ] 모든 쿠키에 SameSite=Strict 설정
|
||||
- [ ] Double-submit 쿠키 패턴 구현
|
||||
|
||||
### 7. 속도 제한
|
||||
|
||||
#### API 속도 제한
|
||||
```typescript
|
||||
import rateLimit from 'express-rate-limit'
|
||||
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100, // 100 requests per window
|
||||
message: 'Too many requests'
|
||||
})
|
||||
|
||||
// Apply to routes
|
||||
app.use('/api/', limiter)
|
||||
```
|
||||
|
||||
#### 비용이 높은 작업
|
||||
```typescript
|
||||
// Aggressive rate limiting for searches
|
||||
const searchLimiter = rateLimit({
|
||||
windowMs: 60 * 1000, // 1 minute
|
||||
max: 10, // 10 requests per minute
|
||||
message: 'Too many search requests'
|
||||
})
|
||||
|
||||
app.use('/api/search', searchLimiter)
|
||||
```
|
||||
|
||||
#### 확인 단계
|
||||
- [ ] 모든 API 엔드포인트에 속도 제한 적용
|
||||
- [ ] 비용이 높은 작업에 더 엄격한 제한
|
||||
- [ ] IP 기반 속도 제한
|
||||
- [ ] 사용자 기반 속도 제한 (인증된 사용자)
|
||||
|
||||
### 8. 민감한 데이터 노출
|
||||
|
||||
#### 로깅
|
||||
```typescript
|
||||
// ❌ WRONG: Logging sensitive data
|
||||
console.log('User login:', { email, password })
|
||||
console.log('Payment:', { cardNumber, cvv })
|
||||
|
||||
// ✅ CORRECT: Redact sensitive data
|
||||
console.log('User login:', { email, userId })
|
||||
console.log('Payment:', { last4: card.last4, userId })
|
||||
```
|
||||
|
||||
#### 에러 메시지
|
||||
```typescript
|
||||
// ❌ WRONG: Exposing internal details
|
||||
catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: error.message, stack: error.stack },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
// ✅ CORRECT: Generic error messages
|
||||
catch (error) {
|
||||
console.error('Internal error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'An error occurred. Please try again.' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
#### 확인 단계
|
||||
- [ ] 로그에 비밀번호, 토큰, 시크릿 없음
|
||||
- [ ] 사용자에게 표시되는 에러 메시지가 일반적임
|
||||
- [ ] 상세 에러는 서버 로그에만 기록
|
||||
- [ ] 사용자에게 스택 트레이스가 노출되지 않음
|
||||
|
||||
### 9. 블록체인 보안 (Solana)
|
||||
|
||||
#### 지갑 검증
|
||||
```typescript
|
||||
import { verify } from '@solana/web3.js'
|
||||
|
||||
async function verifyWalletOwnership(
|
||||
publicKey: string,
|
||||
signature: string,
|
||||
message: string
|
||||
) {
|
||||
try {
|
||||
const isValid = verify(
|
||||
Buffer.from(message),
|
||||
Buffer.from(signature, 'base64'),
|
||||
Buffer.from(publicKey, 'base64')
|
||||
)
|
||||
return isValid
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 트랜잭션 검증
|
||||
```typescript
|
||||
async function verifyTransaction(transaction: Transaction) {
|
||||
// Verify recipient
|
||||
if (transaction.to !== expectedRecipient) {
|
||||
throw new Error('Invalid recipient')
|
||||
}
|
||||
|
||||
// Verify amount
|
||||
if (transaction.amount > maxAmount) {
|
||||
throw new Error('Amount exceeds limit')
|
||||
}
|
||||
|
||||
// Verify user has sufficient balance
|
||||
const balance = await getBalance(transaction.from)
|
||||
if (balance < transaction.amount) {
|
||||
throw new Error('Insufficient balance')
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
```
|
||||
|
||||
#### 확인 단계
|
||||
- [ ] 지갑 서명 검증됨
|
||||
- [ ] 트랜잭션 세부 정보 유효성 검사됨
|
||||
- [ ] 트랜잭션 전 잔액 확인
|
||||
- [ ] 블라인드 트랜잭션 서명 없음
|
||||
|
||||
### 10. 의존성 보안
|
||||
|
||||
#### 정기 업데이트
|
||||
```bash
|
||||
# Check for vulnerabilities
|
||||
npm audit
|
||||
|
||||
# Fix automatically fixable issues
|
||||
npm audit fix
|
||||
|
||||
# Update dependencies
|
||||
npm update
|
||||
|
||||
# Check for outdated packages
|
||||
npm outdated
|
||||
```
|
||||
|
||||
#### 잠금 파일
|
||||
```bash
|
||||
# ALWAYS commit lock files
|
||||
git add package-lock.json
|
||||
|
||||
# Use in CI/CD for reproducible builds
|
||||
npm ci # Instead of npm install
|
||||
```
|
||||
|
||||
#### 확인 단계
|
||||
- [ ] 의존성이 최신 상태
|
||||
- [ ] 알려진 취약점 없음 (npm audit 클린)
|
||||
- [ ] 잠금 파일 커밋됨
|
||||
- [ ] GitHub에서 Dependabot 활성화됨
|
||||
- [ ] 정기적인 보안 업데이트
|
||||
|
||||
## 보안 테스트
|
||||
|
||||
### 자동화된 보안 테스트
|
||||
```typescript
|
||||
// Test authentication
|
||||
test('requires authentication', async () => {
|
||||
const response = await fetch('/api/protected')
|
||||
expect(response.status).toBe(401)
|
||||
})
|
||||
|
||||
// Test authorization
|
||||
test('requires admin role', async () => {
|
||||
const response = await fetch('/api/admin', {
|
||||
headers: { Authorization: `Bearer ${userToken}` }
|
||||
})
|
||||
expect(response.status).toBe(403)
|
||||
})
|
||||
|
||||
// Test input validation
|
||||
test('rejects invalid input', async () => {
|
||||
const response = await fetch('/api/users', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ email: 'not-an-email' })
|
||||
})
|
||||
expect(response.status).toBe(400)
|
||||
})
|
||||
|
||||
// Test rate limiting
|
||||
test('enforces rate limits', async () => {
|
||||
const requests = Array(101).fill(null).map(() =>
|
||||
fetch('/api/endpoint')
|
||||
)
|
||||
|
||||
const responses = await Promise.all(requests)
|
||||
const tooManyRequests = responses.filter(r => r.status === 429)
|
||||
|
||||
expect(tooManyRequests.length).toBeGreaterThan(0)
|
||||
})
|
||||
```
|
||||
|
||||
## 배포 전 보안 체크리스트
|
||||
|
||||
모든 프로덕션 배포 전:
|
||||
|
||||
- [ ] **시크릿**: 하드코딩된 시크릿 없음, 모두 환경 변수에 저장
|
||||
- [ ] **입력 유효성 검사**: 모든 사용자 입력 검증됨
|
||||
- [ ] **SQL Injection**: 모든 쿼리 파라미터화됨
|
||||
- [ ] **XSS**: 사용자 콘텐츠 새니타이징됨
|
||||
- [ ] **CSRF**: 보호 활성화됨
|
||||
- [ ] **인증**: 적절한 토큰 처리
|
||||
- [ ] **권한 부여**: 역할 확인 적용됨
|
||||
- [ ] **속도 제한**: 모든 엔드포인트에서 활성화됨
|
||||
- [ ] **HTTPS**: 프로덕션에서 강제 적용
|
||||
- [ ] **보안 헤더**: CSP, X-Frame-Options 구성됨
|
||||
- [ ] **에러 처리**: 에러에 민감한 데이터 없음
|
||||
- [ ] **로깅**: 민감한 데이터가 로그에 없음
|
||||
- [ ] **의존성**: 최신 상태, 취약점 없음
|
||||
- [ ] **Row Level Security**: Supabase에서 활성화됨
|
||||
- [ ] **CORS**: 적절히 구성됨
|
||||
- [ ] **파일 업로드**: 유효성 검사됨 (크기, 타입)
|
||||
- [ ] **지갑 서명**: 검증됨 (블록체인인 경우)
|
||||
|
||||
## 참고 자료
|
||||
|
||||
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
|
||||
- [Next.js Security](https://nextjs.org/docs/security)
|
||||
- [Supabase Security](https://supabase.com/docs/guides/auth)
|
||||
- [Web Security Academy](https://portswigger.net/web-security)
|
||||
|
||||
---
|
||||
|
||||
**기억하세요**: 보안은 선택 사항이 아닙니다. 하나의 취약점이 전체 플랫폼을 침해할 수 있습니다. 의심스러울 때는 보수적으로 대응하세요.
|
||||
@@ -0,0 +1,361 @@
|
||||
| name | description |
|
||||
|------|-------------|
|
||||
| cloud-infrastructure-security | 클라우드 플랫폼 배포, 인프라 구성, IAM 정책 관리, 로깅/모니터링 설정, CI/CD 파이프라인 구현 시 이 스킬을 사용하세요. 모범 사례에 맞춘 클라우드 보안 체크리스트를 제공합니다. |
|
||||
|
||||
# 클라우드 및 인프라 보안 스킬
|
||||
|
||||
이 스킬은 클라우드 인프라, CI/CD 파이프라인, 배포 구성이 보안 모범 사례를 따르고 업계 표준을 준수하도록 보장합니다.
|
||||
|
||||
## 활성화 시점
|
||||
|
||||
- 클라우드 플랫폼(AWS, Vercel, Railway, Cloudflare)에 애플리케이션 배포 시
|
||||
- IAM 역할 및 권한 구성 시
|
||||
- CI/CD 파이프라인 설정 시
|
||||
- Infrastructure as Code(Terraform, CloudFormation) 구현 시
|
||||
- 로깅 및 모니터링 구성 시
|
||||
- 클라우드 환경에서 시크릿 관리 시
|
||||
- CDN 및 엣지 보안 설정 시
|
||||
- 재해 복구 및 백업 전략 구현 시
|
||||
|
||||
## 클라우드 보안 체크리스트
|
||||
|
||||
### 1. IAM 및 접근 제어
|
||||
|
||||
#### 최소 권한 원칙
|
||||
|
||||
```yaml
|
||||
# ✅ CORRECT: Minimal permissions
|
||||
iam_role:
|
||||
permissions:
|
||||
- s3:GetObject # Only read access
|
||||
- s3:ListBucket
|
||||
resources:
|
||||
- arn:aws:s3:::my-bucket/* # Specific bucket only
|
||||
|
||||
# ❌ WRONG: Overly broad permissions
|
||||
iam_role:
|
||||
permissions:
|
||||
- s3:* # All S3 actions
|
||||
resources:
|
||||
- "*" # All resources
|
||||
```
|
||||
|
||||
#### 다중 인증 (MFA)
|
||||
|
||||
```bash
|
||||
# ALWAYS enable MFA for root/admin accounts
|
||||
aws iam enable-mfa-device \
|
||||
--user-name admin \
|
||||
--serial-number arn:aws:iam::123456789:mfa/admin \
|
||||
--authentication-code1 123456 \
|
||||
--authentication-code2 789012
|
||||
```
|
||||
|
||||
#### 확인 단계
|
||||
|
||||
- [ ] 프로덕션에서 루트 계정 사용 없음
|
||||
- [ ] 모든 권한 있는 계정에 MFA 활성화됨
|
||||
- [ ] 서비스 계정이 장기 자격 증명이 아닌 역할을 사용
|
||||
- [ ] IAM 정책이 최소 권한을 따름
|
||||
- [ ] 정기적인 접근 검토 수행
|
||||
- [ ] 사용하지 않는 자격 증명 교체 또는 제거
|
||||
|
||||
### 2. 시크릿 관리
|
||||
|
||||
#### 클라우드 시크릿 매니저
|
||||
|
||||
```typescript
|
||||
// ✅ CORRECT: Use cloud secrets manager
|
||||
import { SecretsManager } from '@aws-sdk/client-secrets-manager';
|
||||
|
||||
const client = new SecretsManager({ region: 'us-east-1' });
|
||||
const secret = await client.getSecretValue({ SecretId: 'prod/api-key' });
|
||||
const apiKey = JSON.parse(secret.SecretString).key;
|
||||
|
||||
// ❌ WRONG: Hardcoded or in environment variables only
|
||||
const apiKey = process.env.API_KEY; // Not rotated, not audited
|
||||
```
|
||||
|
||||
#### 시크릿 교체
|
||||
|
||||
```bash
|
||||
# Set up automatic rotation for database credentials
|
||||
aws secretsmanager rotate-secret \
|
||||
--secret-id prod/db-password \
|
||||
--rotation-lambda-arn arn:aws:lambda:region:account:function:rotate \
|
||||
--rotation-rules AutomaticallyAfterDays=30
|
||||
```
|
||||
|
||||
#### 확인 단계
|
||||
|
||||
- [ ] 모든 시크릿이 클라우드 시크릿 매니저에 저장됨 (AWS Secrets Manager, Vercel Secrets)
|
||||
- [ ] 데이터베이스 자격 증명에 대한 자동 교체 활성화됨
|
||||
- [ ] API 키가 최소 분기별로 교체됨
|
||||
- [ ] 코드, 로그, 에러 메시지에 시크릿 없음
|
||||
- [ ] 시크릿 접근에 대한 감사 로깅 활성화됨
|
||||
|
||||
### 3. 네트워크 보안
|
||||
|
||||
#### VPC 및 방화벽 구성
|
||||
|
||||
```terraform
|
||||
# ✅ CORRECT: Restricted security group
|
||||
resource "aws_security_group" "app" {
|
||||
name = "app-sg"
|
||||
|
||||
ingress {
|
||||
from_port = 443
|
||||
to_port = 443
|
||||
protocol = "tcp"
|
||||
cidr_blocks = ["10.0.0.0/16"] # Internal VPC only
|
||||
}
|
||||
|
||||
egress {
|
||||
from_port = 443
|
||||
to_port = 443
|
||||
protocol = "tcp"
|
||||
cidr_blocks = ["0.0.0.0/0"] # Only HTTPS outbound
|
||||
}
|
||||
}
|
||||
|
||||
# ❌ WRONG: Open to the internet
|
||||
resource "aws_security_group" "bad" {
|
||||
ingress {
|
||||
from_port = 0
|
||||
to_port = 65535
|
||||
protocol = "tcp"
|
||||
cidr_blocks = ["0.0.0.0/0"] # All ports, all IPs!
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 확인 단계
|
||||
|
||||
- [ ] 데이터베이스가 공개적으로 접근 불가
|
||||
- [ ] SSH/RDP 포트가 VPN/배스천에만 제한됨
|
||||
- [ ] 보안 그룹이 최소 권한을 따름
|
||||
- [ ] 네트워크 ACL이 구성됨
|
||||
- [ ] VPC 플로우 로그가 활성화됨
|
||||
|
||||
### 4. 로깅 및 모니터링
|
||||
|
||||
#### CloudWatch/로깅 구성
|
||||
|
||||
```typescript
|
||||
// ✅ CORRECT: Comprehensive logging
|
||||
import { CloudWatchLogsClient, CreateLogStreamCommand } from '@aws-sdk/client-cloudwatch-logs';
|
||||
|
||||
const logSecurityEvent = async (event: SecurityEvent) => {
|
||||
await cloudwatch.putLogEvents({
|
||||
logGroupName: '/aws/security/events',
|
||||
logStreamName: 'authentication',
|
||||
logEvents: [{
|
||||
timestamp: Date.now(),
|
||||
message: JSON.stringify({
|
||||
type: event.type,
|
||||
userId: event.userId,
|
||||
ip: event.ip,
|
||||
result: event.result,
|
||||
// Never log sensitive data
|
||||
})
|
||||
}]
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
#### 확인 단계
|
||||
|
||||
- [ ] 모든 서비스에 CloudWatch/로깅 활성화됨
|
||||
- [ ] 실패한 인증 시도가 로깅됨
|
||||
- [ ] 관리자 작업이 감사됨
|
||||
- [ ] 로그 보존 기간이 구성됨 (규정 준수를 위해 90일 이상)
|
||||
- [ ] 의심스러운 활동에 대한 알림 구성됨
|
||||
- [ ] 로그가 중앙 집중화되고 변조 방지됨
|
||||
|
||||
### 5. CI/CD 파이프라인 보안
|
||||
|
||||
#### 보안 파이프라인 구성
|
||||
|
||||
```yaml
|
||||
# ✅ CORRECT: Secure GitHub Actions workflow
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read # Minimal permissions
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Scan for secrets
|
||||
- name: Secret scanning
|
||||
uses: trufflesecurity/trufflehog@main
|
||||
|
||||
# Dependency audit
|
||||
- name: Audit dependencies
|
||||
run: npm audit --audit-level=high
|
||||
|
||||
# Use OIDC, not long-lived tokens
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::123456789:role/GitHubActionsRole
|
||||
aws-region: us-east-1
|
||||
```
|
||||
|
||||
#### 공급망 보안
|
||||
|
||||
```json
|
||||
// package.json - Use lock files and integrity checks
|
||||
{
|
||||
"scripts": {
|
||||
"install": "npm ci", // Use ci for reproducible builds
|
||||
"audit": "npm audit --audit-level=moderate",
|
||||
"check": "npm outdated"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 확인 단계
|
||||
|
||||
- [ ] 장기 자격 증명 대신 OIDC 사용
|
||||
- [ ] 파이프라인에서 시크릿 스캐닝
|
||||
- [ ] 의존성 취약점 스캐닝
|
||||
- [ ] 컨테이너 이미지 스캐닝 (해당하는 경우)
|
||||
- [ ] 브랜치 보호 규칙 적용됨
|
||||
- [ ] 병합 전 코드 리뷰 필수
|
||||
- [ ] 서명된 커밋 적용
|
||||
|
||||
### 6. Cloudflare 및 CDN 보안
|
||||
|
||||
#### Cloudflare 보안 구성
|
||||
|
||||
```typescript
|
||||
// ✅ CORRECT: Cloudflare Workers with security headers
|
||||
export default {
|
||||
async fetch(request: Request): Promise<Response> {
|
||||
const response = await fetch(request);
|
||||
|
||||
// Add security headers
|
||||
const headers = new Headers(response.headers);
|
||||
headers.set('X-Frame-Options', 'DENY');
|
||||
headers.set('X-Content-Type-Options', 'nosniff');
|
||||
headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||
headers.set('Permissions-Policy', 'geolocation=(), microphone=()');
|
||||
|
||||
return new Response(response.body, {
|
||||
status: response.status,
|
||||
headers
|
||||
});
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
#### WAF 규칙
|
||||
|
||||
```bash
|
||||
# Enable Cloudflare WAF managed rules
|
||||
# - OWASP Core Ruleset
|
||||
# - Cloudflare Managed Ruleset
|
||||
# - Rate limiting rules
|
||||
# - Bot protection
|
||||
```
|
||||
|
||||
#### 확인 단계
|
||||
|
||||
- [ ] OWASP 규칙으로 WAF 활성화됨
|
||||
- [ ] 속도 제한 구성됨
|
||||
- [ ] 봇 보호 활성화됨
|
||||
- [ ] DDoS 보호 활성화됨
|
||||
- [ ] 보안 헤더 구성됨
|
||||
- [ ] SSL/TLS 엄격 모드 활성화됨
|
||||
|
||||
### 7. 백업 및 재해 복구
|
||||
|
||||
#### 자동 백업
|
||||
|
||||
```terraform
|
||||
# ✅ CORRECT: Automated RDS backups
|
||||
resource "aws_db_instance" "main" {
|
||||
allocated_storage = 20
|
||||
engine = "postgres"
|
||||
|
||||
backup_retention_period = 30 # 30 days retention
|
||||
backup_window = "03:00-04:00"
|
||||
maintenance_window = "mon:04:00-mon:05:00"
|
||||
|
||||
enabled_cloudwatch_logs_exports = ["postgresql"]
|
||||
|
||||
deletion_protection = true # Prevent accidental deletion
|
||||
}
|
||||
```
|
||||
|
||||
#### 확인 단계
|
||||
|
||||
- [ ] 자동 일일 백업 구성됨
|
||||
- [ ] 백업 보존 기간이 규정 준수 요구사항을 충족
|
||||
- [ ] 특정 시점 복구 활성화됨
|
||||
- [ ] 분기별 백업 테스트 수행
|
||||
- [ ] 재해 복구 계획 문서화됨
|
||||
- [ ] RPO 및 RTO가 정의되고 테스트됨
|
||||
|
||||
## 배포 전 클라우드 보안 체크리스트
|
||||
|
||||
모든 프로덕션 클라우드 배포 전:
|
||||
|
||||
- [ ] **IAM**: 루트 계정 미사용, MFA 활성화, 최소 권한 정책
|
||||
- [ ] **시크릿**: 모든 시크릿이 클라우드 시크릿 매니저에 교체와 함께 저장됨
|
||||
- [ ] **네트워크**: 보안 그룹 제한됨, 공개 데이터베이스 없음
|
||||
- [ ] **로깅**: CloudWatch/로깅이 보존 기간과 함께 활성화됨
|
||||
- [ ] **모니터링**: 이상 징후에 대한 알림 구성됨
|
||||
- [ ] **CI/CD**: OIDC 인증, 시크릿 스캐닝, 의존성 감사
|
||||
- [ ] **CDN/WAF**: OWASP 규칙으로 Cloudflare WAF 활성화됨
|
||||
- [ ] **암호화**: 저장 및 전송 중 데이터 암호화
|
||||
- [ ] **백업**: 테스트된 복구와 함께 자동 백업
|
||||
- [ ] **규정 준수**: GDPR/HIPAA 요구사항 충족 (해당하는 경우)
|
||||
- [ ] **문서화**: 인프라 문서화, 런북 작성됨
|
||||
- [ ] **인시던트 대응**: 보안 인시던트 계획 마련
|
||||
|
||||
## 일반적인 클라우드 보안 잘못된 구성
|
||||
|
||||
### S3 버킷 노출
|
||||
|
||||
```bash
|
||||
# ❌ WRONG: Public bucket
|
||||
aws s3api put-bucket-acl --bucket my-bucket --acl public-read
|
||||
|
||||
# ✅ CORRECT: Private bucket with specific access
|
||||
aws s3api put-bucket-acl --bucket my-bucket --acl private
|
||||
aws s3api put-bucket-policy --bucket my-bucket --policy file://policy.json
|
||||
```
|
||||
|
||||
### RDS 공개 접근
|
||||
|
||||
```terraform
|
||||
# ❌ WRONG
|
||||
resource "aws_db_instance" "bad" {
|
||||
publicly_accessible = true # NEVER do this!
|
||||
}
|
||||
|
||||
# ✅ CORRECT
|
||||
resource "aws_db_instance" "good" {
|
||||
publicly_accessible = false
|
||||
vpc_security_group_ids = [aws_security_group.db.id]
|
||||
}
|
||||
```
|
||||
|
||||
## 참고 자료
|
||||
|
||||
- [AWS Security Best Practices](https://aws.amazon.com/security/best-practices/)
|
||||
- [CIS AWS Foundations Benchmark](https://www.cisecurity.org/benchmark/amazon_web_services)
|
||||
- [Cloudflare Security Documentation](https://developers.cloudflare.com/security/)
|
||||
- [OWASP Cloud Security](https://owasp.org/www-project-cloud-security/)
|
||||
- [Terraform Security Best Practices](https://www.terraform.io/docs/cloud/guides/recommended-practices/)
|
||||
|
||||
**기억하세요**: 클라우드 잘못된 구성은 데이터 유출의 주요 원인입니다. 하나의 노출된 S3 버킷이나 과도하게 허용적인 IAM 정책이 전체 인프라를 침해할 수 있습니다. 항상 최소 권한 원칙과 심층 방어를 따르세요.
|
||||
Reference in New Issue
Block a user