포스트

[MCP&A2A] 14. 인증과 보안

[MCP&A2A] 14. 인증과 보안

보안 아키텍처 개요

AI 에이전트 시스템은 민감한 데이터를 처리하므로 다층 보안 전략이 필수입니다. 인증(Authentication), 인가(Authorization), 데이터 보호, 감사(Audit)를 조합하여 포괄적인 보안을 구현합니다.

보안 계층

1
2
3
4
5
6
7
8
9
10
11
┌─────────────────────────────────────┐
│  1. 네트워크 보안 (TLS, Firewall)  │
├─────────────────────────────────────┤
│  2. 인증 (JWT, OAuth2)              │
├─────────────────────────────────────┤
│  3. 인가 (RBAC, Tenant Isolation)   │
├─────────────────────────────────────┤
│  4. 데이터 보호 (Encryption, RLS)   │
├─────────────────────────────────────┤
│  5. 감사 (Audit Log, Monitoring)    │
└─────────────────────────────────────┘

JWT (JSON Web Token) 인증

JWT 구조

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
JWT = Header.Payload.Signature

Header (Base64):
{
  "alg": "RS256",
  "typ": "JWT"
}

Payload (Base64):
{
  "sub": "user_123",
  "tenant_id": "tenant_abc",
  "roles": ["admin", "user"],
  "exp": 1735689600,
  "iat": 1735686000
}

Signature:
RSASHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  privateKey
)

왜 RS256인가?

알고리즘키 타입장점단점용도
HS256대칭키빠름, 간단서버만 검증 가능단일 서비스
RS256비대칭키공개키로 검증느림, 복잡✅ 마이크로서비스

RS256 선택 이유:

  • MCP/A2A 서버는 공개키만 보유
  • 인증 서버만 개인키 보유
  • 키 유출 시 영향 최소화
  • 서비스 간 신뢰 불필요

JWT 생성 (Auth 서버)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// internal/auth/jwt_generator.go
package auth

import (
    "crypto/rsa"
    "time"
    
    "github.com/golang-jwt/jwt/v5"
)

type JWTGenerator struct {
    privateKey *rsa.PrivateKey
    issuer     string
}

type Claims struct {
    UserID   string   `json:"sub"`
    TenantID string   `json:"tenant_id"`
    Email    string   `json:"email"`
    Roles    []string `json:"roles"`
    jwt.RegisteredClaims
}

func NewJWTGenerator(privateKeyPEM string, issuer string) (*JWTGenerator, error) {
    privateKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(privateKeyPEM))
    if err != nil {
        return nil, err
    }
    
    return &JWTGenerator{
        privateKey: privateKey,
        issuer:     issuer,
    }, nil
}

func (g *JWTGenerator) Generate(userID, tenantID, email string, roles []string) (string, error) {
    now := time.Now()
    
    claims := &Claims{
        UserID:   userID,
        TenantID: tenantID,
        Email:    email,
        Roles:    roles,
        RegisteredClaims: jwt.RegisteredClaims{
            Issuer:    g.issuer,
            Subject:   userID,
            ExpiresAt: jwt.NewNumericDate(now.Add(1 * time.Hour)),
            IssuedAt:  jwt.NewNumericDate(now),
            NotBefore: jwt.NewNumericDate(now),
        },
    }
    
    token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
    
    signedToken, err := token.SignedString(g.privateKey)
    if err != nil {
        return "", err
    }
    
    return signedToken, nil
}

// Refresh Token (긴 유효기간)
func (g *JWTGenerator) GenerateRefreshToken(userID, tenantID string) (string, error) {
    now := time.Now()
    
    claims := &jwt.RegisteredClaims{
        Issuer:    g.issuer,
        Subject:   userID,
        ExpiresAt: jwt.NewNumericDate(now.Add(30 * 24 * time.Hour)), // 30일
        IssuedAt:  jwt.NewNumericDate(now),
    }
    
    token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
    return token.SignedString(g.privateKey)
}

JWT 검증 (MCP/A2A 서버)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// internal/auth/jwt_validator.go
package auth

import (
    "crypto/rsa"
    "fmt"
    
    "github.com/golang-jwt/jwt/v5"
)

type JWTValidator struct {
    publicKey *rsa.PublicKey
    issuer    string
}

func NewJWTValidator(publicKeyPEM string, issuer string) (*JWTValidator, error) {
    publicKey, err := jwt.ParseRSAPublicKeyFromPEM([]byte(publicKeyPEM))
    if err != nil {
        return nil, err
    }
    
    return &JWTValidator{
        publicKey: publicKey,
        issuer:    issuer,
    }, nil
}

func (v *JWTValidator) Validate(tokenString string) (*Claims, error) {
    token, err := jwt.ParseWithClaims(
        tokenString,
        &Claims{},
        func(token *jwt.Token) (interface{}, error) {
            // 알고리즘 확인
            if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
                return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
            }
            return v.publicKey, nil
        },
    )
    
    if err != nil {
        return nil, err
    }
    
    claims, ok := token.Claims.(*Claims)
    if !ok || !token.Valid {
        return nil, fmt.Errorf("invalid token")
    }
    
    // Issuer 확인
    if claims.Issuer != v.issuer {
        return nil, fmt.Errorf("invalid issuer")
    }
    
    return claims, nil
}

Auth Middleware

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// internal/middleware/auth.go
package middleware

import (
    "context"
    "net/http"
    "strings"
    
    "mcp-server/internal/auth"
)

type AuthMiddleware struct {
    validator *auth.JWTValidator
}

func NewAuthMiddleware(validator *auth.JWTValidator) *AuthMiddleware {
    return &AuthMiddleware{
        validator: validator,
    }
}

func (m *AuthMiddleware) Authenticate(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Authorization 헤더 추출
        authHeader := r.Header.Get("Authorization")
        if authHeader == "" {
            http.Error(w, "Missing authorization header", http.StatusUnauthorized)
            return
        }
        
        // Bearer 토큰 파싱
        parts := strings.Split(authHeader, " ")
        if len(parts) != 2 || parts[0] != "Bearer" {
            http.Error(w, "Invalid authorization header format", http.StatusUnauthorized)
            return
        }
        
        tokenString := parts[1]
        
        // JWT 검증
        claims, err := m.validator.Validate(tokenString)
        if err != nil {
            http.Error(w, fmt.Sprintf("Invalid token: %v", err), http.StatusUnauthorized)
            return
        }
        
        // 컨텍스트에 사용자 정보 추가
        ctx := context.WithValue(r.Context(), "user_id", claims.UserID)
        ctx = context.WithValue(ctx, "tenant_id", claims.TenantID)
        ctx = context.WithValue(ctx, "email", claims.Email)
        ctx = context.WithValue(ctx, "roles", claims.Roles)
        
        // 다음 핸들러 호출
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// Optional Auth (토큰 없어도 통과, 있으면 파싱)
func (m *AuthMiddleware) OptionalAuth(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        authHeader := r.Header.Get("Authorization")
        
        if authHeader != "" {
            parts := strings.Split(authHeader, " ")
            if len(parts) == 2 && parts[0] == "Bearer" {
                claims, err := m.validator.Validate(parts[1])
                if err == nil {
                    ctx := context.WithValue(r.Context(), "user_id", claims.UserID)
                    ctx = context.WithValue(ctx, "tenant_id", claims.TenantID)
                    r = r.WithContext(ctx)
                }
            }
        }
        
        next.ServeHTTP(w, r)
    })
}

Role-Based Access Control (RBAC)

역할 정의

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// internal/auth/roles.go
package auth

type Role string

const (
    RoleAdmin     Role = "admin"      // 전체 권한
    RoleUser      Role = "user"       // 기본 사용자
    RoleViewer    Role = "viewer"     // 읽기 전용
    RoleAPIClient Role = "api_client" // API 전용 (UI 접근 불가)
)

type Permission string

const (
    // 문서 권한
    PermDocumentRead   Permission = "document:read"
    PermDocumentWrite  Permission = "document:write"
    PermDocumentDelete Permission = "document:delete"
    
    // 태스크 권한
    PermTaskCreate Permission = "task:create"
    PermTaskRead   Permission = "task:read"
    PermTaskCancel Permission = "task:cancel"
    
    // 설정 권한
    PermSettingsRead  Permission = "settings:read"
    PermSettingsWrite Permission = "settings:write"
    
    // 관리 권한
    PermUserManage   Permission = "user:manage"
    PermTenantManage Permission = "tenant:manage"
)

// 역할별 권한 매핑
var RolePermissions = map[Role][]Permission{
    RoleAdmin: {
        PermDocumentRead, PermDocumentWrite, PermDocumentDelete,
        PermTaskCreate, PermTaskRead, PermTaskCancel,
        PermSettingsRead, PermSettingsWrite,
        PermUserManage, PermTenantManage,
    },
    RoleUser: {
        PermDocumentRead, PermDocumentWrite,
        PermTaskCreate, PermTaskRead, PermTaskCancel,
        PermSettingsRead,
    },
    RoleViewer: {
        PermDocumentRead,
        PermTaskRead,
        PermSettingsRead,
    },
    RoleAPIClient: {
        PermDocumentRead, PermDocumentWrite,
        PermTaskCreate, PermTaskRead,
    },
}

// HasPermission 권한 확인
func HasPermission(roles []string, permission Permission) bool {
    for _, roleStr := range roles {
        role := Role(roleStr)
        permissions, ok := RolePermissions[role]
        if !ok {
            continue
        }
        
        for _, p := range permissions {
            if p == permission {
                return true
            }
        }
    }
    
    return false
}

RBAC Middleware

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// internal/middleware/rbac.go
package middleware

import (
    "net/http"
    
    "mcp-server/internal/auth"
)

type RBACMiddleware struct{}

func NewRBACMiddleware() *RBACMiddleware {
    return &RBACMiddleware{}
}

func (m *RBACMiddleware) RequirePermission(permission auth.Permission) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // 컨텍스트에서 역할 추출
            roles, ok := r.Context().Value("roles").([]string)
            if !ok {
                http.Error(w, "Unauthorized", http.StatusUnauthorized)
                return
            }
            
            // 권한 확인
            if !auth.HasPermission(roles, permission) {
                http.Error(w, "Forbidden: insufficient permissions", http.StatusForbidden)
                return
            }
            
            next.ServeHTTP(w, r)
        })
    }
}

func (m *RBACMiddleware) RequireRole(requiredRole auth.Role) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            roles, ok := r.Context().Value("roles").([]string)
            if !ok {
                http.Error(w, "Unauthorized", http.StatusUnauthorized)
                return
            }
            
            // 역할 확인
            hasRole := false
            for _, role := range roles {
                if auth.Role(role) == requiredRole {
                    hasRole = true
                    break
                }
            }
            
            if !hasRole {
                http.Error(w, "Forbidden: required role not found", http.StatusForbidden)
                return
            }
            
            next.ServeHTTP(w, r)
        })
    }
}

라우터에 적용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// cmd/server/main.go
func main() {
    // 미들웨어
    authMiddleware := middleware.NewAuthMiddleware(jwtValidator)
    rbacMiddleware := middleware.NewRBACMiddleware()
    
    r := chi.NewRouter()
    
    // 공개 엔드포인트
    r.Get("/health", healthHandler)
    r.Get("/.well-known/agent-card.json", agentCardHandler)
    
    // 인증 필요
    r.Group(func(r chi.Router) {
        r.Use(authMiddleware.Authenticate)
        
        // 기본 사용자 권한
        r.Get("/mcp/documents", listDocumentsHandler)
        
        // 쓰기 권한 필요
        r.Group(func(r chi.Router) {
            r.Use(rbacMiddleware.RequirePermission(auth.PermDocumentWrite))
            r.Post("/mcp/documents", createDocumentHandler)
            r.Put("/mcp/documents/{id}", updateDocumentHandler)
        })
        
        // 삭제 권한 필요 (관리자만)
        r.Group(func(r chi.Router) {
            r.Use(rbacMiddleware.RequirePermission(auth.PermDocumentDelete))
            r.Delete("/mcp/documents/{id}", deleteDocumentHandler)
        })
        
        // 관리자 전용
        r.Group(func(r chi.Router) {
            r.Use(rbacMiddleware.RequireRole(auth.RoleAdmin))
            r.Get("/admin/users", listUsersHandler)
            r.Post("/admin/users", createUserHandler)
        })
    })
}

Rate Limiting

Token Bucket 알고리즘

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// internal/middleware/rate_limit.go
package middleware

import (
    "net/http"
    "sync"
    "time"
    
    "golang.org/x/time/rate"
)

type RateLimiter struct {
    limiters map[string]*rate.Limiter
    mu       sync.RWMutex
    
    rate  rate.Limit // requests per second
    burst int        // bucket size
}

func NewRateLimiter(rps float64, burst int) *RateLimiter {
    return &RateLimiter{
        limiters: make(map[string]*rate.Limiter),
        rate:     rate.Limit(rps),
        burst:    burst,
    }
}

func (rl *RateLimiter) getLimiter(key string) *rate.Limiter {
    rl.mu.Lock()
    defer rl.mu.Unlock()
    
    limiter, exists := rl.limiters[key]
    if !exists {
        limiter = rate.NewLimiter(rl.rate, rl.burst)
        rl.limiters[key] = limiter
    }
    
    return limiter
}

func (rl *RateLimiter) Limit(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 테넌트별 제한
        tenantID := r.Context().Value("tenant_id")
        if tenantID == nil {
            tenantID = "anonymous"
        }
        
        key := tenantID.(string)
        limiter := rl.getLimiter(key)
        
        if !limiter.Allow() {
            w.Header().Set("X-RateLimit-Limit", fmt.Sprintf("%.0f", rl.rate))
            w.Header().Set("X-RateLimit-Remaining", "0")
            w.Header().Set("Retry-After", "1")
            
            http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
            return
        }
        
        next.ServeHTTP(w, r)
    })
}

// Cleanup 주기적으로 사용하지 않는 limiter 제거
func (rl *RateLimiter) Cleanup(interval time.Duration) {
    ticker := time.NewTicker(interval)
    defer ticker.Stop()
    
    for range ticker.C {
        rl.mu.Lock()
        
        now := time.Now()
        for key, limiter := range rl.limiters {
            // 1시간 동안 사용 안 한 limiter 제거
            if limiter.Tokens() == float64(rl.burst) {
                delete(rl.limiters, key)
            }
        }
        
        rl.mu.Unlock()
    }
}

계층적 Rate Limiting

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// internal/middleware/tiered_rate_limit.go
package middleware

type TieredRateLimiter struct {
    tiers map[string]*RateLimiter
}

func NewTieredRateLimiter() *TieredRateLimiter {
    return &TieredRateLimiter{
        tiers: map[string]*RateLimiter{
            "free":       NewRateLimiter(10, 20),    // 10 req/s, burst 20
            "basic":      NewRateLimiter(50, 100),   // 50 req/s, burst 100
            "pro":        NewRateLimiter(100, 200),  // 100 req/s, burst 200
            "enterprise": NewRateLimiter(500, 1000), // 500 req/s, burst 1000
        },
    }
}

func (trl *TieredRateLimiter) Limit(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 사용자의 플랜 확인
        plan := r.Context().Value("plan")
        if plan == nil {
            plan = "free"
        }
        
        limiter, ok := trl.tiers[plan.(string)]
        if !ok {
            limiter = trl.tiers["free"]
        }
        
        limiter.Limit(next).ServeHTTP(w, r)
    })
}

데이터 암호화

저장 데이터 암호화 (Encryption at Rest)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// internal/crypto/encryption.go
package crypto

import (
    "crypto/aes"
    "crypto/cipher"
    "crypto/rand"
    "encoding/base64"
    "errors"
    "io"
)

type Encryptor struct {
    key []byte // 32 bytes for AES-256
}

func NewEncryptor(key string) (*Encryptor, error) {
    // 키는 32바이트여야 함 (AES-256)
    keyBytes := []byte(key)
    if len(keyBytes) != 32 {
        return nil, errors.New("key must be 32 bytes")
    }
    
    return &Encryptor{key: keyBytes}, nil
}

func (e *Encryptor) Encrypt(plaintext string) (string, error) {
    block, err := aes.NewCipher(e.key)
    if err != nil {
        return "", err
    }
    
    // GCM (Galois/Counter Mode)
    gcm, err := cipher.NewGCM(block)
    if err != nil {
        return "", err
    }
    
    // Nonce 생성
    nonce := make([]byte, gcm.NonceSize())
    if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
        return "", err
    }
    
    // 암호화
    ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
    
    // Base64 인코딩
    return base64.StdEncoding.EncodeToString(ciphertext), nil
}

func (e *Encryptor) Decrypt(ciphertext string) (string, error) {
    // Base64 디코딩
    data, err := base64.StdEncoding.DecodeString(ciphertext)
    if err != nil {
        return "", err
    }
    
    block, err := aes.NewCipher(e.key)
    if err != nil {
        return "", err
    }
    
    gcm, err := cipher.NewGCM(block)
    if err != nil {
        return "", err
    }
    
    nonceSize := gcm.NonceSize()
    if len(data) < nonceSize {
        return "", errors.New("ciphertext too short")
    }
    
    nonce, ciphertext := data[:nonceSize], data[nonceSize:]
    
    // 복호화
    plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
    if err != nil {
        return "", err
    }
    
    return string(plaintext), nil
}

필드 레벨 암호화

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// internal/models/user.go
package models

import (
    "mcp-server/internal/crypto"
)

type User struct {
    ID        string
    Email     string
    Name      string
    
    // 암호화된 필드
    APIKey    string `json:"-"` // JSON 응답에서 제외
    SecretKey string `json:"-"`
}

func (u *User) EncryptSecrets(encryptor *crypto.Encryptor) error {
    if u.APIKey != "" {
        encrypted, err := encryptor.Encrypt(u.APIKey)
        if err != nil {
            return err
        }
        u.APIKey = encrypted
    }
    
    if u.SecretKey != "" {
        encrypted, err := encryptor.Encrypt(u.SecretKey)
        if err != nil {
            return err
        }
        u.SecretKey = encrypted
    }
    
    return nil
}

func (u *User) DecryptSecrets(encryptor *crypto.Encryptor) error {
    if u.APIKey != "" {
        decrypted, err := encryptor.Decrypt(u.APIKey)
        if err != nil {
            return err
        }
        u.APIKey = decrypted
    }
    
    if u.SecretKey != "" {
        decrypted, err := encryptor.Decrypt(u.SecretKey)
        if err != nil {
            return err
        }
        u.SecretKey = decrypted
    }
    
    return nil
}

감사 로그 (Audit Log)

Audit Log 구조

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// internal/audit/logger.go
package audit

import (
    "context"
    "encoding/json"
    "time"
    
    "github.com/google/uuid"
)

type EventType string

const (
    EventLogin          EventType = "auth.login"
    EventLogout         EventType = "auth.logout"
    EventDocumentCreate EventType = "document.create"
    EventDocumentRead   EventType = "document.read"
    EventDocumentUpdate EventType = "document.update"
    EventDocumentDelete EventType = "document.delete"
    EventTaskCreate     EventType = "task.create"
    EventTaskCancel     EventType = "task.cancel"
    EventSettingsChange EventType = "settings.change"
)

type AuditEvent struct {
    ID        string                 `json:"id"`
    Timestamp time.Time              `json:"timestamp"`
    Type      EventType              `json:"type"`
    TenantID  string                 `json:"tenant_id"`
    UserID    string                 `json:"user_id"`
    IP        string                 `json:"ip"`
    UserAgent string                 `json:"user_agent"`
    Resource  string                 `json:"resource"`
    Action    string                 `json:"action"`
    Result    string                 `json:"result"` // success, failure
    Metadata  map[string]interface{} `json:"metadata"`
}

type AuditLogger struct {
    store AuditStore
}

func NewAuditLogger(store AuditStore) *AuditLogger {
    return &AuditLogger{store: store}
}

func (al *AuditLogger) Log(ctx context.Context, eventType EventType, resource, action, result string, metadata map[string]interface{}) error {
    event := &AuditEvent{
        ID:        uuid.New().String(),
        Timestamp: time.Now(),
        Type:      eventType,
        TenantID:  getStringFromContext(ctx, "tenant_id"),
        UserID:    getStringFromContext(ctx, "user_id"),
        IP:        getStringFromContext(ctx, "ip"),
        UserAgent: getStringFromContext(ctx, "user_agent"),
        Resource:  resource,
        Action:    action,
        Result:    result,
        Metadata:  metadata,
    }
    
    return al.store.Save(event)
}

func getStringFromContext(ctx context.Context, key string) string {
    val := ctx.Value(key)
    if val == nil {
        return ""
    }
    
    str, _ := val.(string)
    return str
}

Audit Middleware

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
// internal/middleware/audit.go
package middleware

import (
    "bytes"
    "io"
    "net/http"
    "time"
    
    "mcp-server/internal/audit"
)

type AuditMiddleware struct {
    logger *audit.AuditLogger
}

func NewAuditMiddleware(logger *audit.AuditLogger) *AuditMiddleware {
    return &AuditMiddleware{logger: logger}
}

type responseWriter struct {
    http.ResponseWriter
    statusCode int
    body       *bytes.Buffer
}

func (rw *responseWriter) WriteHeader(code int) {
    rw.statusCode = code
    rw.ResponseWriter.WriteHeader(code)
}

func (rw *responseWriter) Write(b []byte) (int, error) {
    rw.body.Write(b)
    return rw.ResponseWriter.Write(b)
}

func (am *AuditMiddleware) Log(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        
        // IP 추출
        ip := r.Header.Get("X-Real-IP")
        if ip == "" {
            ip = r.Header.Get("X-Forwarded-For")
        }
        if ip == "" {
            ip = r.RemoteAddr
        }
        
        // 컨텍스트에 IP, User-Agent 추가
        ctx := context.WithValue(r.Context(), "ip", ip)
        ctx = context.WithValue(ctx, "user_agent", r.Header.Get("User-Agent"))
        
        // Response writer 래핑
        rw := &responseWriter{
            ResponseWriter: w,
            statusCode:     http.StatusOK,
            body:           &bytes.Buffer{},
        }
        
        // 다음 핸들러 실행
        next.ServeHTTP(rw, r.WithContext(ctx))
        
        // 감사 로그 기록
        duration := time.Since(start)
        
        result := "success"
        if rw.statusCode >= 400 {
            result = "failure"
        }
        
        am.logger.Log(ctx, mapMethodToEvent(r.Method, r.URL.Path), r.URL.Path, r.Method, result, map[string]interface{}{
            "status_code": rw.statusCode,
            "duration_ms": duration.Milliseconds(),
        })
    })
}

func mapMethodToEvent(method, path string) audit.EventType {
    // 경로와 메서드로 이벤트 타입 매핑
    if strings.Contains(path, "/documents") {
        switch method {
        case "POST":
            return audit.EventDocumentCreate
        case "GET":
            return audit.EventDocumentRead
        case "PUT", "PATCH":
            return audit.EventDocumentUpdate
        case "DELETE":
            return audit.EventDocumentDelete
        }
    }
    
    return audit.EventType(fmt.Sprintf("%s.%s", method, path))
}

CORS 설정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// internal/middleware/cors.go
package middleware

import (
    "net/http"
    
    "github.com/go-chi/cors"
)

func CORSMiddleware(allowedOrigins []string) func(http.Handler) http.Handler {
    return cors.Handler(cors.Options{
        AllowedOrigins:   allowedOrigins,
        AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"},
        AllowedHeaders:   []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
        ExposedHeaders:   []string{"Link"},
        AllowCredentials: true,
        MaxAge:           300, // 5분
    })
}

보안 헤더

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// internal/middleware/security_headers.go
package middleware

import (
    "net/http"
)

func SecurityHeaders(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // HSTS
        w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
        
        // XSS Protection
        w.Header().Set("X-Content-Type-Options", "nosniff")
        w.Header().Set("X-Frame-Options", "DENY")
        w.Header().Set("X-XSS-Protection", "1; mode=block")
        
        // CSP
        w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'")
        
        // Referrer Policy
        w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
        
        // Permissions Policy
        w.Header().Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()")
        
        next.ServeHTTP(w, r)
    })
}

핵심 요약

보안 체크리스트

인증

  • JWT RS256 사용
  • 공개키/개인키 분리
  • 토큰 만료 (1시간)
  • Refresh Token (30일)

인가

  • RBAC 구현
  • 역할별 권한 매핑
  • 권한 체크 미들웨어
  • 최소 권한 원칙

Rate Limiting

  • Token Bucket 알고리즘
  • 테넌트별 제한
  • 계층적 제한 (플랜별)
  • 429 응답

데이터 보호

  • AES-256 GCM 암호화
  • 필드 레벨 암호화
  • TLS 통신
  • RLS (Row-Level Security)

감사

  • 모든 액션 로그
  • IP, User-Agent 기록
  • 성공/실패 구분
  • 장기 보관

보안 헤더

  • HSTS, CSP, X-Frame-Options
  • CORS 설정
  • Referrer Policy

작성일: 2024-12-13

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.