보안 설계 원칙을 코드로 구현하기 - Python SIEM 만들기 (5편)

Minseok Jeon·2025년 11월 16일
post-thumbnail

Security Design

이 글은 "Python으로 나만의 SIEM 만들기" 시리즈의 마지막 편입니다.


들어가며

"보안은 기능이 아니라 설계입니다."

많은 개발자들이 기능 구현 후 "보안 기능"을 추가하려 합니다.

  • 로그인 기능 완성 → "보안 강화" 작업
  • API 개발 완료 → "인증 추가" 작업

하지만 이것은 틀렸습니다.

보안은 처음부터 설계에 녹아들어야 합니다.

  • "Secure by Design"
  • "Security is not a feature, it's a mindset"

이번 글에서는 제가 Mini-SIEM을 개발하면서 적용한 5가지 보안 설계 원칙을 코드와 함께 설명합니다.

모든 원칙은 Saltzer & Schroeder의 보안 설계 원칙 (1975년 MIT 논문)을 기반으로 합니다.


보안 설계 8대 원칙

Saltzer & Schroeder (1975)

  1. Economy of Mechanism (단순성)
  2. Fail-Safe Defaults (안전한 기본값)
  3. Complete Mediation (완전한 중재)
  4. Open Design (공개 설계)
  5. Separation of Privilege (권한 분리)
  6. Least Privilege (최소 권한)
  7. Least Common Mechanism (최소 공유 메커니즘)
  8. Psychological Acceptability (심리적 수용성)

이 중 5가지를 실제 코드로 구현합니다.


1️⃣ Defense in Depth (다층 방어)

개념

"단일 방어선이 뚫려도 시스템이 안전하도록 여러 계층의 방어를 구축하라"

비유:

성 (Castle) 방어 시스템
├─ Layer 1: 해자 (Moat)
├─ Layer 2: 외벽 (Outer Wall)
├─ Layer 3: 문지기 (Gatekeeper)
├─ Layer 4: 내벽 (Inner Wall)
└─ Layer 5: 성 내부 경비 (Guards)

하나가 뚫려도 다음 방어선이 존재!

실제 침해 사례

2017년 Equifax 침해 사고

공격 경로:
1. Apache Struts 취약점 (CVE-2017-5638) ❌ 패치 미적용
2. 웹 방화벽 우회 ❌ 규칙 미설정
3. 데이터베이스 접근 ❌ 네트워크 분리 없음
4. 민감 데이터 암호화 없음 ❌ 평문 저장
5. 이상 트래픽 탐지 실패 ❌ 모니터링 부재

→ 1.43억 명 개인정보 유출

만약 다층 방어가 있었다면:

1. Struts 취약점 ❌ → 2. WAF 차단 ✅ → 공격 실패!

Mini-SIEM 구현

┌─────────────────────────────────────────────────────────┐
│ Layer 1: API 인증 (Authentication)                       │
│  - X-API-Key 헤더 검증                                   │
│  - 401/403 반환                                         │
├─────────────────────────────────────────────────────────┤
│ Layer 2: 입력 검증 (Input Validation)                    │
│  - Pydantic 타입 검증                                    │
│  - 필드 범위 검증 (count >= 1)                           │
│  - 422 Unprocessable Entity 반환                        │
├─────────────────────────────────────────────────────────┤
│ Layer 3: 위협 탐지 (Threat Detection)                    │
│  - 7가지 독립적 탐지 룰                                   │
│  - SQL Injection, Brute Force 차단                      │
├─────────────────────────────────────────────────────────┤
│ Layer 4: 로그 저장 (Audit Trail)                         │
│  - 모든 요청 로깅                                        │
│  - Elasticsearch 영구 보관                               │
├─────────────────────────────────────────────────────────┤
│ Layer 5: 실시간 알림 (Alerting)                          │
│  - Slack 즉시 통보                                       │
│  - 인시던트 자동 생성                                     │
└─────────────────────────────────────────────────────────┘

코드 구현

Layer 1: API 인증

# app/utils/auth.py
from fastapi import HTTPException, Security, status
from fastapi.security import APIKeyHeader

api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
API_KEY = os.getenv("API_KEY")

def verify_api_key(api_key: str = Security(api_key_header)) -> str:
    """Layer 1: 인증 계층"""
    if api_key is None:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="API Key is missing"
        )

    if api_key != API_KEY:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Invalid API Key"
        )

    return api_key

Layer 2: 입력 검증

# app/models/log.py
from pydantic import BaseModel, Field, validator

class LogEvent(BaseModel):
    """Layer 2: 입력 검증 계층"""
    event_type: str = Field(..., description="이벤트 타입")
    count: Optional[int] = Field(1, ge=1)  # 최소값 검증

    @validator('event_type')
    def validate_event_type(cls, v):
        """허용된 이벤트 타입만 통과"""
        allowed_types = [e.value for e in EventType]
        if v.lower() not in allowed_types:
            # 알 수 없는 타입은 UNKNOWN으로 처리 (거부하지 않음)
            return EventType.UNKNOWN.value
        return v.lower()

    @validator('count')
    def validate_count(cls, v):
        """비정상적으로 큰 값 차단"""
        if v > 10000:  # 한 번에 10,000회 이상은 비정상
            raise ValueError("count must be <= 10000")
        return v

Layer 3: 위협 탐지

# app/utils/detector.py
class ThreatDetector:
    """Layer 3: 위협 탐지 계층"""

    @classmethod
    def analyze(cls, log: NormalizedLog) -> NormalizedLog:
        """7가지 독립적 탐지 룰 실행"""
        threats = []

        # 각 탐지 룰이 독립적으로 동작
        # 하나가 우회되어도 다른 룰로 탐지 가능!
        detectors = [
            cls.detect_brute_force,
            cls.detect_suspicious_time_access,
            cls.detect_sql_injection,
            cls.detect_privilege_escalation,
            cls.detect_botnet_activity,
            cls.detect_malicious_ip,
            cls.detect_file_access_anomaly,
        ]

        for detector in detectors:
            is_threat, details = detector(log)
            if is_threat and details:
                threats.append(details)

        if threats:
            log.is_threat = True
            log.threat_details = " | ".join(threats)

        return log

Layer 4: 감사 추적 (Audit Trail)

# app/main.py
@app.post("/log")
async def receive_log(
    log_event: LogEvent,
    api_key: str = Depends(verify_api_key)
):
    # ... 처리 ...

    # Layer 4: 모든 이벤트를 로깅 (증거 보존)
    logger.info(
        f"[EVENT] {analyzed_log.event_type.value} | "
        f"IP={analyzed_log.source_ip} | "
        f"Severity={analyzed_log.severity.value} | "
        f"Threat={analyzed_log.is_threat}"
    )

    # Elasticsearch에도 영구 저장
    # (재부팅해도 데이터 유지)

Layer 5: 실시간 알림

# Layer 5: 위협 발견 시 즉시 알림
if analyzed_log.is_threat:
    # 인시던트 생성
    incident = incident_manager.create_incident(analyzed_log)

    # Slack 알림
    alert_message = (
        f"🚨 *[{analyzed_log.severity.value.upper()}]* "
        f"Security Threat Detected\n"
        f"• *Type*: {analyzed_log.event_type.value}\n"
        f"• *Source IP*: {analyzed_log.source_ip}\n"
        f"• *Details*: {analyzed_log.threat_details}\n"
        f"• *Incident ID*: {incident.id}"
    )
    send_slack_alert(alert_message)

실전 테스트

공격 시나리오: SQL Injection 시도

# 공격자 요청
curl -X POST http://localhost:8000/log \
  -H "X-API-Key: WRONG_KEY" \
  -d '{
    "event_type": "sql_injection",
    "source_ip": "203.0.113.50",
    "raw_log": "SELECT * FROM users WHERE id=1 OR 1=1--"
  }'

방어 과정:

Layer 1 (API 인증): ❌ FAILED
→ 403 Forbidden 반환
→ 공격 차단!

(만약 Layer 1 우회 시)
Layer 2 (입력 검증): ✅ PASS (유효한 JSON)

Layer 3 (위협 탐지): ❌ DETECTED!
→ SQL Injection 패턴 탐지
→ 인시던트 생성

Layer 4 (로그 저장): ✅ 증거 보존
→ /app/logs/app.log
→ Elasticsearch

Layer 5 (알림): ✅ Slack 즉시 알림
→ SOC 팀 인지

결과: 5개 계층 중 4개 작동 → 시스템 안전!


2️⃣ Fail-Safe Defaults (안전한 기본값)

개념

"시스템은 기본적으로 거부(Deny)하고, 명시적으로 허용(Allow)하라"

원칙:

  • Deny by default, allow by exception
  • 불확실하면 거부
  • 오류 시 안전한 쪽으로

실제 침해 사례

2019년 Capital One 침해 사고

AWS S3 버킷 설정:
{
  "public_access": "default"  // ❌ 기본값이 공개!
}

→ 1억 명 신용카드 정보 유출

만약 Fail-Safe Defaults였다면:

{
  "public_access": "deny_all"  // ✅ 기본값 거부
}

명시적으로 허용해야만 접근 가능
→ 침해 방지!

Mini-SIEM 구현

1. API 인증 기본값

# ❌ 나쁜 예: 기본값 허용
API_KEY = os.getenv("API_KEY", None)  # None이면 인증 안 함?

def verify_api_key(api_key: str = Security(api_key_header)):
    if API_KEY is None:
        return "OK"  # ⚠️ 위험! 인증 없이 통과

# ✅ 좋은 예: 기본값 거부
API_KEY = os.getenv("API_KEY")  # 설정 안 되면 None

def verify_api_key(api_key: str = Security(api_key_header)):
    if api_key is None:
        raise HTTPException(401, "API Key missing")  # ✅ 즉시 거부

    if API_KEY is None:
        raise HTTPException(500, "Server not configured")  # ✅ 서버 설정 오류

    if api_key != API_KEY:
        raise HTTPException(403, "Invalid API Key")  # ✅ 잘못된 키 거부

    return api_key  # 모든 검증 통과해야 허용

2. 이벤트 타입 정규화

# app/models/log.py
@validator('event_type', pre=True)
def normalize_event_type(cls, v):
    """알 수 없는 타입은 UNKNOWN으로 처리"""
    if isinstance(v, str):
        try:
            return EventType(v.lower())
        except ValueError:
            # ✅ Fail-Safe: 거부하지 않고 UNKNOWN으로
            # (로그는 받되, 특별 처리)
            return EventType.UNKNOWN
    return v

설계 근거:

Option A: 알 수 없는 타입 → 거부 (422 Error)
❌ 새로운 공격 유형을 탐지 못함
❌ 로그 소스 추가 시 호환성 문제

Option B: 알 수 없는 타입 → UNKNOWN으로 수용 ✅
✅ 모든 로그 수집 (증거 보존)
✅ UNKNOWN 타입만 따로 분석 가능
✅ 유연한 확장

3. 심각도 자동 할당

# app/utils/detector.py
@staticmethod
def assign_severity(log, is_threat, threat_details) -> SeverityLevel:
    """위협 심각도 자동 할당"""

    if not is_threat:
        # ✅ Fail-Safe: 위협 아니면 무조건 INFO
        return SeverityLevel.INFO

    # Critical: SQL Injection, 악성 IP
    if log.event_type in [EventType.SQL_INJECTION, EventType.MALWARE_DETECTED]:
        return SeverityLevel.CRITICAL

    # ... (중략) ...

    # ✅ Fail-Safe: 알 수 없는 위협은 LOW (보수적)
    # (과탐 > 미탐)
    return SeverityLevel.LOW

4. 환경 변수 기본값

# ❌ 나쁜 예: 위험한 기본값
DEBUG = os.getenv("DEBUG", "True")  # 프로덕션에서 디버그 모드?!

# ✅ 좋은 예: 안전한 기본값
DEBUG = os.getenv("DEBUG", "False").lower() == "true"
ALLOW_ORIGINS = os.getenv("ALLOW_ORIGINS", "").split(",")  # 빈 리스트 (모두 차단)
MAX_REQUEST_SIZE = int(os.getenv("MAX_REQUEST_SIZE", "1048576"))  # 1MB (작게)

5. 에러 처리

# ❌ 나쁜 예: 에러 시 계속 진행
try:
    send_slack_alert(message)
except Exception:
    pass  # ⚠️ 알림 실패해도 무시? 위험!

# ✅ 좋은 예: 에러 로깅 + Fail-Safe
try:
    send_slack_alert(message)
except Exception as e:
    logger.error(f"Slack alert failed: {e}")
    # 알림 실패해도 인시던트는 생성됨 (데이터 유실 방지)
    # 운영자가 로그에서 확인 가능

Fail-Safe 체크리스트

# 설계 시 자문 (Self-Assessment)

□ API 기본값이 "거부"인가?
□ 알 수 없는 입력을 안전하게 처리하는가?
□ 에러 발생 시 보수적으로 동작하는가?
□ 환경 변수 누락 시 안전한가?
□ 권한 부여가 명시적인가?

3️⃣ Complete Mediation (완전한 중재)

개념

"모든 접근을 매번 검증하라. 캐싱이나 우회 경로를 허용하지 마라."

원칙:

  • 모든 요청마다 인증/인가 확인
  • 이전 검증 결과를 재사용하지 않음
  • 우회 경로(Backdoor) 없음

실제 취약점 사례

IDOR (Insecure Direct Object Reference)

# ❌ 취약한 코드
@app.get("/incidents/{incident_id}")
def get_incident(incident_id: str):
    # 인증 확인 안 함!
    incident = db.get(incident_id)
    return incident

# 공격:
# GET /incidents/INC-20251111-0001  ✅ 자기 것
# GET /incidents/INC-20251111-0002  ✅ 남의 것도 조회됨!

Mini-SIEM 구현

1. 모든 요청 인증

# app/main.py

# ✅ 읽기 API: 인증 불필요 (공개 정보)
@app.get("/dashboard")
def get_dashboard():
    return stats_service.get_dashboard_stats()

@app.get("/incidents")
def list_incidents():
    return incident_manager.list_incidents()

# ✅ 쓰기 API: 인증 필수 (데이터 변경)
@app.post("/log")
async def receive_log(
    log_event: LogEvent,
    api_key: str = Depends(verify_api_key)  # 매번 검증!
):
    ...

@app.post("/incidents/{incident_id}/status", dependencies=[Depends(verify_api_key)])
def update_incident_status(...):
    # dependencies로 전역 적용
    ...

설계 철학:

읽기 (Read):
- 대시보드, 통계, 인시던트 목록 → 인증 불필요
- 이유: 내부 팀만 접근 가능한 네트워크
- 사용성 우선

쓰기 (Write):
- 로그 전송, 인시던트 변경 → 인증 필수
- 이유: 데이터 무결성 보호
- 보안 우선

2. 의존성 주입으로 우회 방지

# ❌ 나쁜 예: 함수 내부 검증 (우회 가능)
def process_log(log_event: LogEvent):
    # 함수 호출자가 검증을 건너뛸 수 있음
    if not verify_api_key():
        raise Exception("Unauthorized")
    ...

# 다른 곳에서 직접 호출 시 우회됨
process_log(malicious_log)  # 인증 건너뛰기!

# ✅ 좋은 예: FastAPI 의존성 주입 (우회 불가)
@app.post("/log")
async def receive_log(
    log_event: LogEvent,
    api_key: str = Depends(verify_api_key)  # FastAPI가 자동 실행
):
    # 이 함수에 도달했다면 이미 인증 통과
    ...

# 직접 호출 불가 (FastAPI 라우터를 통해서만 호출됨)

3. 중간자 공격 방지 (향후 개선)

# 현재: HTTP (개발 환경)
app = FastAPI()

# 프로덕션: HTTPS 강제
from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware

if not DEBUG:
    app.add_middleware(HTTPSRedirectMiddleware)
    # HTTP → HTTPS 자동 리다이렉트

4. 감사 로그 (Audit Trail)

# app/main.py
@app.post("/log")
async def receive_log(
    log_event: LogEvent,
    api_key: str = Depends(verify_api_key),
    request: Request  # 요청 객체 주입
):
    # Complete Mediation: 모든 접근 기록
    logger.info(
        f"[ACCESS] "
        f"Endpoint=/log | "
        f"Client={request.client.host} | "
        f"API_Key={api_key[:8]}... | "  # 앞 8자만 로깅 (보안)
        f"Event={log_event.event_type}"
    )

    # ... 처리 ...

4️⃣ Least Privilege (최소 권한)

개념

"사용자/프로세스에게 필요한 최소한의 권한만 부여하라"

원칙:

  • 기본 권한: 없음 (No access)
  • 명시적 권한 부여
  • 불필요한 권한 제거

실제 침해 사례

2013년 Target 침해 사고

HVAC 업체 계정:
- 원래 필요한 권한: HVAC 시스템 접근
- 실제 부여된 권한: 전체 네트워크 접근 ❌

→ HVAC 업체 계정 탈취
→ POS 시스템 침투
→ 4천만 개 신용카드 정보 유출

만약 Least Privilege였다면:

HVAC 업체 계정:
- 권한: HVAC 시스템만 ✅
- POS 시스템 접근 불가 ✅

→ 침해 실패!

Mini-SIEM 구현

1. API 권한 분리

# app/main.py

# Level 0: 공개 (인증 불필요)
@app.get("/")
def home():
    return {"message": "Mini-SIEM", "version": "2.0.0"}

# Level 1: 읽기 권한 (인증 불필요)
@app.get("/dashboard")
def get_dashboard():
    return stats_service.get_dashboard_stats()

@app.get("/incidents")
def list_incidents():
    return incident_manager.list_incidents()

# Level 2: 쓰기 권한 (API 키 필수)
@app.post("/log")
async def receive_log(
    log_event: LogEvent,
    api_key: str = Depends(verify_api_key)
):
    ...

@app.post("/incidents/{incident_id}/status", dependencies=[Depends(verify_api_key)])
def update_incident_status(...):
    ...

권한 매트릭스:

엔드포인트공개읽기쓰기관리자
GET /
GET /dashboard
GET /incidents
POST /log
POST /incidents/.../status

2. 역할 기반 접근 제어 (RBAC) - 향후 개선

# 현재: 단일 API 키
API_KEY = os.getenv("API_KEY")

# 향후: 역할별 API 키
class Role(str, Enum):
    VIEWER = "viewer"      # 읽기만
    ANALYST = "analyst"    # 읽기 + 인시던트 변경
    ADMIN = "admin"        # 모든 권한

API_KEYS = {
    "viewer-key-123": Role.VIEWER,
    "analyst-key-456": Role.ANALYST,
    "admin-key-789": Role.ADMIN,
}

def verify_api_key_rbac(
    required_role: Role,
    api_key: str = Security(api_key_header)
) -> str:
    """역할 기반 인증"""
    if api_key not in API_KEYS:
        raise HTTPException(403, "Invalid API Key")

    user_role = API_KEYS[api_key]

    # 권한 계층: ADMIN > ANALYST > VIEWER
    role_hierarchy = {
        Role.VIEWER: 1,
        Role.ANALYST: 2,
        Role.ADMIN: 3,
    }

    if role_hierarchy[user_role] < role_hierarchy[required_role]:
        raise HTTPException(403, f"Requires {required_role} role")

    return api_key

# 사용 예시
@app.post("/log")
async def receive_log(
    log_event: LogEvent,
    api_key: str = Depends(lambda: verify_api_key_rbac(Role.ANALYST))
):
    # ANALYST 이상만 로그 전송 가능
    ...

@app.delete("/incidents/{id}")
async def delete_incident(
    incident_id: str,
    api_key: str = Depends(lambda: verify_api_key_rbac(Role.ADMIN))
):
    # ADMIN만 삭제 가능
    ...

3. 컨테이너 권한 최소화

# Dockerfile

# ❌ 나쁜 예: root로 실행
USER root
CMD ["uvicorn", "main:app"]

# ✅ 좋은 예: 전용 사용자
RUN useradd -m -u 1000 siem
USER siem

# 읽기 전용 파일 시스템 (docker-compose.yml)
services:
  fastapi_app:
    read_only: true  # 파일 시스템 읽기 전용
    tmpfs:
      - /tmp  # 임시 파일만 허용

4. 데이터베이스 권한 최소화 (향후)

# PostgreSQL 권한 설정
CREATE ROLE siem_read WITH LOGIN PASSWORD 'xxx';
CREATE ROLE siem_write WITH LOGIN PASSWORD 'yyy';

-- 읽기 전용
GRANT SELECT ON ALL TABLES IN SCHEMA public TO siem_read;

-- 쓰기 가능
GRANT SELECT, INSERT ON ALL TABLES IN SCHEMA public TO siem_write;

-- DROP, DELETE 권한 없음! (데이터 보호)

# 애플리케이션
class DBConnection:
    def __init__(self, mode: str):
        if mode == "read":
            self.user = "siem_read"
        elif mode == "write":
            self.user = "siem_write"
        else:
            raise ValueError("Invalid mode")

# 읽기 전용 연결
read_db = DBConnection("read")

# 쓰기 전용 연결
write_db = DBConnection("write")

5️⃣ OWASP Top 10 대응

OWASP Top 10 (2021)

  1. A01: Broken Access Control
  2. A02: Cryptographic Failures
  3. A03: Injection
  4. A04: Insecure Design
  5. A05: Security Misconfiguration
  6. A06: Vulnerable and Outdated Components
  7. A07: Identification and Authentication Failures
  8. A08: Software and Data Integrity Failures
  9. A09: Security Logging and Monitoring Failures
  10. A10: Server-Side Request Forgery (SSRF)

Mini-SIEM 대응 현황

A01: Broken Access Control ✅

대응:

  • API 키 인증
  • 읽기/쓰기 권한 분리
  • 의존성 주입으로 우회 방지
@app.post("/log", dependencies=[Depends(verify_api_key)])
async def receive_log(...):
    # 인증 없이 접근 불가
    ...

A03: Injection ✅

대응:

  • Pydantic 자동 검증
  • SQL Injection 탐지 룰
  • 정규식 패턴 매칭
SQL_INJECTION_PATTERNS = [
    r"(\bor\b\s+\d+\s*=\s*\d+)",
    r"(\bunion\b\s+\bselect\b)",
    r"(';?\s*drop\s+table)",
    # ...
]

def detect_sql_injection(log):
    for pattern in SQL_INJECTION_PATTERNS:
        if re.search(pattern, log.raw_log, re.IGNORECASE):
            return True, "SQL Injection detected"

A05: Security Misconfiguration ✅

대응:

  • 환경 변수로 설정 관리
  • .env 파일 (Git 제외)
  • 안전한 기본값
# .env.example (안전한 템플릿)
API_KEY=your_secure_api_key_here
SLACK_WEBHOOK_URL=https://hooks.slack.com/...
DEBUG=False  # 프로덕션 기본값

# .gitignore
.env  # 실제 설정은 Git에 커밋 안 됨

A07: Authentication Failures ✅

대응:

  • Brute Force 탐지 (5회 임계값)
  • 비정상 시간대 접속 탐지
  • 인시던트 자동 생성
def detect_brute_force(log):
    if log.event_type == EventType.LOGIN_FAILED and log.count >= 5:
        return True, f"Brute force: {log.count} attempts"

A09: Security Logging Failures ✅

대응:

  • 모든 이벤트 로깅
  • Elasticsearch 영구 저장
  • 감사 추적 (Audit Trail)
logger.info(
    f"[EVENT] {event_type} | "
    f"IP={source_ip} | "
    f"Threat={is_threat}"
)

# Filebeat → Elasticsearch (영구 보존)

A02: Cryptographic Failures ⚠️ (향후 개선)

현재 상태:

  • API 키 평문 전송 (HTTP)
  • 로그 암호화 없음

개선 방안:

# 1. HTTPS 강제
from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware
app.add_middleware(HTTPSRedirectMiddleware)

# 2. 민감 데이터 암호화
from cryptography.fernet import Fernet

class LogEncryption:
    def __init__(self):
        self.key = os.getenv("ENCRYPTION_KEY").encode()
        self.cipher = Fernet(self.key)

    def encrypt(self, data: str) -> str:
        return self.cipher.encrypt(data.encode()).decode()

    def decrypt(self, data: str) -> str:
        return self.cipher.decrypt(data.encode()).decode()

# 사용
encryptor = LogEncryption()
encrypted_log = encryptor.encrypt(log.raw_log)

# Elasticsearch 저장 시 암호화
POST /siem-logs/_doc
{
  "raw_log": "encrypted_data_here",
  "encrypted": true
}

A06: Vulnerable Components ✅

대응:

  • requirements.txt로 버전 고정
  • Dependabot 자동 업데이트 (GitHub)
# requirements.txt (버전 고정)
fastapi==0.115.0
uvicorn==0.32.0
pydantic==2.9.2

# 취약점 스캔
$ pip-audit
# 또는
$ safety check

실전 침해 시나리오 분석

시나리오: Brute Force → SQL Injection → 권한 상승

공격자 목표: 관리자 계정 탈취 후 데이터 유출

Step 1: Brute Force Attack

# 공격자 시도
for i in {1..100}; do
  curl -X POST http://localhost:8000/log \
    -H "X-API-Key: attacker-key" \
    -d '{
      "event_type": "login_failed",
      "source_ip": "203.0.113.100",
      "username": "admin",
      "count": 1
    }'
done

시스템 방어:

Layer 1 (인증): ✅ PASS (유효한 API 키)
Layer 2 (검증): ✅ PASS (유효한 JSON)
Layer 3 (탐지): ❌ DETECTED!

탐지 룰: detect_brute_force()
→ 5회째에서 탐지
→ 인시던트 생성: INC-20251111-0001
→ Slack 알림: "🚨 Brute Force from 203.0.113.100"

Layer 4 (로깅): ✅ 모든 시도 기록
→ /app/logs/app.log
→ Elasticsearch: siem-logs-2025.11.11

Layer 5 (알림): ✅ SOC 팀 인지
→ IP 차단 결정

Step 2: SQL Injection 시도

# 공격자 시도 (Brute Force 차단 후 다른 공격)
curl -X POST http://localhost:8000/log \
  -H "X-API-Key: attacker-key" \
  -d '{
    "event_type": "sql_injection",
    "source_ip": "203.0.113.100",
    "raw_log": "admin' OR '1'='1'--"
  }'

시스템 방어:

Layer 3 (탐지): ❌ DETECTED!

탐지 룰: detect_sql_injection()
→ 패턴 매칭: r"(\bor\b\s+\d+\s*=\s*\d+)" 근사 매칭
→ 심각도: CRITICAL
→ 인시던트: INC-20251111-0002
→ Slack: "🚨 [CRITICAL] SQL Injection detected"

→ 공격 실패!

Step 3: 권한 상승 시도

# 공격자 시도 (다른 취약점 탐색)
curl -X POST http://localhost:8000/log \
  -H "X-API-Key: attacker-key" \
  -d '{
    "event_type": "privilege_escalation",
    "source_ip": "203.0.113.100",
    "raw_log": "sudo -i",
    "username": "user123"
  }'

시스템 방어:

Layer 3 (탐지): ❌ DETECTED!

탐지 룰: detect_privilege_escalation()
→ 키워드 매칭: "sudo"
→ 심각도: HIGH
→ 인시던트: INC-20251111-0003

→ 공격 실패!

최종 결과

공격자: 3가지 공격 시도
시스템: 3건 모두 탐지 및 차단 ✅

생성된 인시던트:
- INC-20251111-0001: Brute Force (MEDIUM)
- INC-20251111-0002: SQL Injection (CRITICAL)
- INC-20251111-0003: Privilege Escalation (HIGH)

SOC 팀 조치:
1. IP 203.0.113.100 방화벽 차단
2. 관련 계정 비밀번호 재설정
3. 침해 지표 (IOC) 공유

보안 설계 체크리스트

설계 단계

□ Defense in Depth
  □ 최소 3개 이상의 방어 계층?
  □ 각 계층이 독립적으로 동작?
  □ 하나 뚫려도 시스템 안전?

□ Fail-Safe Defaults
  □ 기본값이 "거부"?
  □ 환경 변수 누락 시 안전?
  □ 에러 발생 시 보수적?

□ Complete Mediation
  □ 모든 요청마다 인증?
  □ 캐싱으로 우회 불가?
  □ 감사 로그 기록?

□ Least Privilege
  □ 필요한 최소 권한만?
  □ 역할 기반 접근 제어?
  □ 기본 권한은 없음?

□ OWASP Top 10
  □ Injection 대응?
  □ 인증 실패 탐지?
  □ 보안 로깅?

구현 단계

□ 코드 리뷰
  □ API 인증 확인
  □ 입력 검증 확인
  □ 에러 처리 확인

□ 테스트
  □ 인증 우회 시도 (401/403 확인)
  □ SQL Injection 테스트
  □ Brute Force 시뮬레이션

□ 문서화
  □ 보안 설계 문서
  □ 인증 방법 가이드
  □ 침해 대응 플레이북

운영 단계

□ 모니터링
  □ 실시간 위협 탐지
  □ 인시던트 대시보드
  □ 알림 정상 작동

□ 정기 점검
  □ 의존성 취약점 스캔 (weekly)
  □ API 키 교체 (monthly)
  □ 침투 테스트 (quarterly)

□ 사고 대응
  □ 인시던트 대응 절차
  □ 백업 및 복구 계획
  □ 침해 지표 (IOC) 수집

마치며

시리즈 회고

이 시리즈를 통해 우리는:

  1. SIEM의 본질을 이해했습니다

    • 단순한 로그 저장소가 아님
    • 실시간 위협 탐지 시스템
  2. 실무 수준의 기술을 적용했습니다

    • MITRE ATT&CK Framework
    • OWASP Top 10
    • Saltzer & Schroeder 보안 원칙
  3. 오픈소스로 구현했습니다

    • FastAPI: 고성능 API
    • Elasticsearch: 대용량 검색
    • Pydantic: 타입 안전성
  4. 포트폴리오를 완성했습니다

    • GitHub 공개
    • 설계 문서화
    • 블로그 시리즈

핵심 교훈

"보안은 기능이 아니라 설계다"

  • 처음부터 보안 고려
  • 다층 방어 구축
  • 안전한 기본값
  • 최소 권한 원칙

다음 단계

학습:

  • CISSP 자격증
  • Certified Ethical Hacker (CEH)
  • 침투 테스트 실습

프로젝트 확장:

  • 머신러닝 이상 탐지
  • 위협 인텔리전스 연동
  • SOAR 자동화
  • 웹 UI 대시보드

커리어:

  • SOC Analyst 지원
  • Security Engineer 지원
  • 보안 관제 직무

감사 인사

이 시리즈를 끝까지 읽어주신 여러분께 감사드립니다.

질문, 피드백, 개선 아이디어가 있다면:

  • GitHub Issues
  • 블로그 댓글
  • 이메일

함께 더 안전한 세상을 만들어갑시다! 🛡️


참고 자료

보안 원칙

침해 사고 사례

기술 문서


프로젝트 정보

  • GitHub: mini-siem-log-monitoring
  • 시리즈: Python SIEM 만들기 (5/5편 - 완결)
  • 전체 코드: app/ 디렉토리 참조

⭐ GitHub Star와 좋아요 부탁드립니다!


💡 "보안은 마라톤입니다. 끝이 없습니다. 하지만 포기하지 마세요."

여러분의 보안 여정을 응원합니다! 🚀

0개의 댓글