🔒 보안

Zero Trust

제로 트러스트 (Never Trust, Always Verify)

"절대 신뢰하지 말고, 항상 검증하라(Never Trust, Always Verify)"와 "침해를 가정하라(Assume Breach)"는 원칙을 기반으로 하는 현대 보안 모델입니다. 전통적인 경계 기반 보안(Castle-and-Moat)과 달리, 네트워크 위치와 관계없이 모든 사용자, 기기, 애플리케이션의 모든 접근을 지속적으로 검증합니다. NIST SP 800-207이 표준 프레임워크를 정의하며, 2025년 NIST SP 1800-35에서 24개 벤더와 함께 19개 실제 구현 모델을 제시했습니다.

📖 상세 설명

Zero Trust는 2010년 Forrester Research의 John Kindervag가 처음 제안한 보안 모델로, 전통적인 "경계 기반 보안(Perimeter Security)"의 한계를 극복하기 위해 등장했습니다. 기존 모델은 "내부 네트워크는 신뢰, 외부는 불신"이라는 전제였지만, 클라우드 채택, 원격 근무 확산, 내부자 위협 증가로 네트워크 경계의 의미가 사라졌습니다. Zero Trust는 위치가 아닌 신원(Identity)을 새로운 보안 경계로 정의합니다.

NIST SP 800-207은 Zero Trust Architecture(ZTA)의 핵심 원칙을 정의합니다: (1) 모든 데이터 소스와 컴퓨팅 서비스는 리소스로 간주, (2) 네트워크 위치와 관계없이 모든 통신은 보호, (3) 개별 리소스에 대한 접근은 세션별로 허용, (4) 접근은 동적 정책(사용자 신원, 애플리케이션, 기기 상태 등)에 의해 결정, (5) 모든 자산의 무결성과 보안 상태를 지속적으로 모니터링. 2025년 NIST SP 1800-35는 24개 벤더와 협력하여 19개 실제 구현 모델을 제시했습니다.

Zero Trust의 7가지 핵심 영역(Pillars)은 Identity(신원), Device(기기), Network(네트워크), Application(애플리케이션), Data(데이터), Infrastructure(인프라), Analytics/Visibility(분석/가시성)입니다. 각 영역에서 Foundation(기본), Advanced(고급), Optimal(최적) 수준으로 성숙도를 높여갑니다. 예를 들어, Identity 영역에서 기본은 SSO+MFA, 고급은 피싱 방지 MFA, 최적은 지속적 인증과 행동 분석입니다.

실제 구현에서 Zero Trust는 ZTNA(Zero Trust Network Access), SASE(Secure Access Service Edge), 마이크로세그멘테이션 등의 기술로 구현됩니다. 매 API 요청마다 JWT 토큰을 검증하고, 기기 상태(OS 버전, 보안 패치, EDR 상태)를 확인하며, 접근 컨텍스트(시간, 위치, 행동 패턴)를 분석해 동적으로 접근을 허용하거나 추가 인증을 요구합니다. Gartner는 2026년까지 75%의 미국 연방 기관이 자금과 전문성 부족으로 Zero Trust 구현에 실패할 것으로 예측했지만, 점진적 접근(Identity-first)이 권장됩니다.

💻 코드 예제

// Zero Trust API 미들웨어 - Node.js/Express
// 모든 요청을 검증: 토큰, 기기 상태, 컨텍스트
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();

// Zero Trust 컨텍스트 수집 미들웨어
function collectContext(req, res, next) {
    req.zeroTrustContext = {
        // 요청 컨텍스트
        timestamp: new Date().toISOString(),
        ip: req.ip || req.headers['x-forwarded-for'],
        userAgent: req.headers['user-agent'],

        // 기기 상태 (클라이언트가 전송)
        deviceId: req.headers['x-device-id'],
        deviceHealth: req.headers['x-device-health'],  // JSON 인코딩
        osVersion: req.headers['x-os-version'],

        // 위치 정보
        geoLocation: req.headers['x-geo-location'],

        // 세션 정보
        sessionId: req.headers['x-session-id']
    };
    next();
}

// Zero Trust 검증 미들웨어
function zeroTrustVerify(options = {}) {
    return async (req, res, next) => {
        const ctx = req.zeroTrustContext;

        try {
            // 1. 토큰 검증 (항상 검증)
            const authHeader = req.headers.authorization;
            if (!authHeader?.startsWith('Bearer ')) {
                return res.status(401).json({
                    error: 'Missing token',
                    action: 'LOGIN_REQUIRED'
                });
            }

            const token = authHeader.substring(7);
            const decoded = jwt.verify(token, process.env.JWT_SECRET);
            req.user = decoded;

            // 2. 기기 상태 검증 (Assume Breach)
            const deviceHealth = await verifyDeviceHealth(ctx);
            if (!deviceHealth.compliant) {
                return res.status(403).json({
                    error: 'Device not compliant',
                    reason: deviceHealth.reason,
                    action: 'DEVICE_REMEDIATION_REQUIRED'
                });
            }

            // 3. 접근 컨텍스트 평가
            const riskScore = await evaluateRisk(req.user, ctx, options.resource);

            // 4. 리스크 기반 동적 접근 제어
            if (riskScore > 80) {
                // 고위험: 접근 거부
                await logSecurityEvent('HIGH_RISK_ACCESS_DENIED', req);
                return res.status(403).json({
                    error: 'Access denied due to high risk',
                    riskScore,
                    action: 'CONTACT_SECURITY'
                });
            } else if (riskScore > 50) {
                // 중위험: Step-up 인증 요구
                const stepUpCompleted = await checkStepUpAuth(req.user, ctx.sessionId);
                if (!stepUpCompleted) {
                    return res.status(403).json({
                        error: 'Additional authentication required',
                        riskScore,
                        action: 'STEP_UP_AUTH_REQUIRED',
                        stepUpMethods: ['FIDO2', 'TOTP']
                    });
                }
            }

            // 5. 권한 검증 (최소 권한 원칙)
            if (options.permission) {
                const hasAccess = await checkPermission(
                    req.user,
                    options.permission,
                    options.resource
                );
                if (!hasAccess) {
                    return res.status(403).json({
                        error: 'Insufficient permissions',
                        required: options.permission
                    });
                }
            }

            // 6. 감사 로깅 (항상)
            await logAccessEvent({
                user: req.user.sub,
                resource: options.resource,
                action: req.method,
                context: ctx,
                riskScore,
                decision: 'ALLOW'
            });

            next();

        } catch (error) {
            if (error.name === 'TokenExpiredError') {
                return res.status(401).json({
                    error: 'Token expired',
                    action: 'REFRESH_TOKEN'
                });
            }
            throw error;
        }
    };
}

// 기기 상태 검증
async function verifyDeviceHealth(ctx) {
    if (!ctx.deviceHealth) {
        return { compliant: false, reason: 'Device health not reported' };
    }

    const health = JSON.parse(ctx.deviceHealth);

    // 최소 보안 요구사항
    const requirements = {
        osUpdated: health.osLastPatched > Date.now() - 30 * 24 * 60 * 60 * 1000,
        firewallEnabled: health.firewall === true,
        antivirusActive: health.antivirus?.status === 'active',
        diskEncrypted: health.diskEncryption === true,
        edrRunning: health.edrStatus === 'running'
    };

    const compliant = Object.values(requirements).every(v => v);
    const failedChecks = Object.entries(requirements)
        .filter(([, v]) => !v)
        .map(([k]) => k);

    return { compliant, failedChecks, reason: failedChecks.join(', ') };
}

// 리스크 평가 엔진
async function evaluateRisk(user, ctx, resource) {
    let riskScore = 0;

    // 시간 기반 리스크
    const hour = new Date().getHours();
    if (hour < 6 || hour > 22) riskScore += 20;  // 비업무 시간

    // 위치 기반 리스크
    const isKnownLocation = await isUserKnownLocation(user.sub, ctx.geoLocation);
    if (!isKnownLocation) riskScore += 30;  // 새로운 위치

    // 행동 기반 리스크 (UEBA)
    const behaviorAnomaly = await detectBehaviorAnomaly(user.sub, ctx);
    riskScore += behaviorAnomaly.score;

    // 리소스 민감도
    const resourceSensitivity = await getResourceSensitivity(resource);
    riskScore += resourceSensitivity * 10;

    // 최근 실패한 인증 시도
    const recentFailures = await getRecentAuthFailures(user.sub);
    riskScore += recentFailures * 5;

    return Math.min(riskScore, 100);
}

// Zero Trust 적용된 라우트
app.use(collectContext);

app.get('/api/documents/:id',
    zeroTrustVerify({ permission: 'documents:read', resource: 'documents' }),
    async (req, res) => {
        const doc = await getDocument(req.params.id);
        res.json(doc);
    }
);

app.delete('/api/users/:id',
    zeroTrustVerify({ permission: 'users:delete', resource: 'users' }),
    async (req, res) => {
        await deleteUser(req.params.id);
        res.json({ success: true });
    }
);

app.listen(3000);
# Zero Trust Policy Decision Point (PDP) - Python
# 중앙화된 정책 엔진으로 모든 접근 결정
from dataclasses import dataclass
from typing import Optional, List, Dict
from datetime import datetime
from enum import Enum
import asyncio

class Decision(Enum):
    ALLOW = "allow"
    DENY = "deny"
    STEP_UP = "step_up"  # 추가 인증 필요

@dataclass
class Subject:
    """접근 주체 (사용자/서비스)"""
    id: str
    type: str  # 'user' | 'service'
    roles: List[str]
    department: str
    auth_level: int  # 1=password, 2=MFA, 3=FIDO2
    session_start: datetime

@dataclass
class Device:
    """기기 상태"""
    id: str
    compliant: bool
    os_version: str
    last_seen: datetime
    risk_score: int

@dataclass
class Resource:
    """접근 대상 리소스"""
    id: str
    type: str
    sensitivity: int  # 1-5
    owner: str
    department: str

@dataclass
class Environment:
    """환경 컨텍스트"""
    timestamp: datetime
    ip_address: str
    geo_location: str
    is_corporate_network: bool

@dataclass
class AccessRequest:
    """접근 요청"""
    subject: Subject
    device: Device
    resource: Resource
    action: str
    environment: Environment

@dataclass
class AccessDecision:
    """접근 결정"""
    decision: Decision
    reason: str
    risk_score: int
    required_auth_level: Optional[int] = None
    obligations: Optional[List[str]] = None

class ZeroTrustPDP:
    """Zero Trust Policy Decision Point"""

    def __init__(self):
        self.policies = []

    def evaluate(self, request: AccessRequest) -> AccessDecision:
        """모든 정책을 평가하여 접근 결정"""
        risk_score = self._calculate_risk(request)

        # 정책 체인 평가
        for policy in self.policies:
            result = policy.evaluate(request, risk_score)
            if result.decision == Decision.DENY:
                return result

        # 리스크 기반 최종 결정
        if risk_score > 80:
            return AccessDecision(
                decision=Decision.DENY,
                reason="Risk score too high",
                risk_score=risk_score
            )
        elif risk_score > 50:
            required_level = 3 if risk_score > 70 else 2
            if request.subject.auth_level < required_level:
                return AccessDecision(
                    decision=Decision.STEP_UP,
                    reason="Additional authentication required",
                    risk_score=risk_score,
                    required_auth_level=required_level
                )

        return AccessDecision(
            decision=Decision.ALLOW,
            reason="All policies passed",
            risk_score=risk_score,
            obligations=["LOG_ACCESS", "ENCRYPT_RESPONSE"]
        )

    def _calculate_risk(self, request: AccessRequest) -> int:
        """컨텍스트 기반 리스크 점수 계산"""
        score = 0

        # 기기 리스크
        if not request.device.compliant:
            score += 40
        score += request.device.risk_score

        # 시간 기반 리스크
        hour = request.environment.timestamp.hour
        if hour < 6 or hour > 22:
            score += 15

        # 위치 기반 리스크
        if not request.environment.is_corporate_network:
            score += 20

        # 리소스 민감도
        score += request.resource.sensitivity * 5

        # 인증 강도 (낮으면 리스크 증가)
        score += (3 - request.subject.auth_level) * 10

        return min(score, 100)


class DeviceCompliancePolicy:
    """기기 준수 정책"""
    def evaluate(self, request: AccessRequest, risk_score: int) -> AccessDecision:
        if not request.device.compliant:
            return AccessDecision(
                decision=Decision.DENY,
                reason="Device not compliant with security policy",
                risk_score=risk_score
            )
        return AccessDecision(Decision.ALLOW, "Device compliant", risk_score)


class SensitiveResourcePolicy:
    """민감한 리소스 접근 정책"""
    def evaluate(self, request: AccessRequest, risk_score: int) -> AccessDecision:
        if request.resource.sensitivity >= 4:
            # 고민감도 리소스는 FIDO2 필수
            if request.subject.auth_level < 3:
                return AccessDecision(
                    decision=Decision.STEP_UP,
                    reason="High-sensitivity resource requires FIDO2",
                    risk_score=risk_score,
                    required_auth_level=3
                )
            # 같은 부서만 접근 가능
            if request.subject.department != request.resource.department:
                return AccessDecision(
                    decision=Decision.DENY,
                    reason="Cross-department access denied for sensitive resource",
                    risk_score=risk_score
                )
        return AccessDecision(Decision.ALLOW, "Resource access allowed", risk_score)


# 사용 예시
pdp = ZeroTrustPDP()
pdp.policies = [
    DeviceCompliancePolicy(),
    SensitiveResourcePolicy()
]

# 접근 요청 평가
request = AccessRequest(
    subject=Subject(
        id="user_123",
        type="user",
        roles=["developer"],
        department="engineering",
        auth_level=2,  # MFA 완료
        session_start=datetime.now()
    ),
    device=Device(
        id="device_456",
        compliant=True,
        os_version="macOS 14.2",
        last_seen=datetime.now(),
        risk_score=10
    ),
    resource=Resource(
        id="doc_789",
        type="document",
        sensitivity=3,
        owner="user_111",
        department="engineering"
    ),
    action="read",
    environment=Environment(
        timestamp=datetime.now(),
        ip_address="10.0.0.1",
        geo_location="Seoul, KR",
        is_corporate_network=True
    )
)

decision = pdp.evaluate(request)
print(f"Decision: {decision.decision.value}")
print(f"Reason: {decision.reason}")
print(f"Risk Score: {decision.risk_score}")
# Zero Trust Policy - Open Policy Agent (OPA)
# 중앙화된 정책 관리로 마이크로서비스 전체에 일관된 Zero Trust 적용

package zerotrust

import future.keywords.if
import future.keywords.in

# 기본값: 접근 거부 (Deny by default)
default allow := false
default decision := {"allowed": false, "reason": "No matching policy"}

# 리스크 점수 계산
risk_score := score if {
    device_risk := device_risk_score
    time_risk := time_risk_score
    location_risk := location_risk_score
    resource_risk := resource_risk_score
    auth_risk := auth_risk_score

    score := min([device_risk + time_risk + location_risk + resource_risk + auth_risk, 100])
}

# 기기 리스크
device_risk_score := 40 if {
    not input.device.compliant
} else := input.device.risk_score

# 시간 기반 리스크 (비업무 시간)
time_risk_score := 15 if {
    hour := time.clock(time.now_ns())[0]
    hour < 6
} else := 15 if {
    hour := time.clock(time.now_ns())[0]
    hour > 22
} else := 0

# 위치 기반 리스크
location_risk_score := 20 if {
    not input.environment.is_corporate_network
} else := 0

# 리소스 민감도 리스크
resource_risk_score := input.resource.sensitivity * 5

# 인증 강도 리스크
auth_risk_score := (3 - input.subject.auth_level) * 10

# ===== Zero Trust 정책 =====

# 정책 1: 기기 준수 필수
decision := {"allowed": false, "reason": "Device not compliant", "action": "REMEDIATE_DEVICE"} if {
    not input.device.compliant
}

# 정책 2: 고위험 접근 차단
decision := {"allowed": false, "reason": "Risk score too high", "risk_score": risk_score} if {
    risk_score > 80
}

# 정책 3: 중위험 시 Step-up 인증 요구
decision := {
    "allowed": false,
    "reason": "Step-up authentication required",
    "risk_score": risk_score,
    "required_auth_level": required_level,
    "action": "STEP_UP_AUTH"
} if {
    risk_score > 50
    risk_score <= 80
    required_level := 3 if risk_score > 70 else 2
    input.subject.auth_level < required_level
}

# 정책 4: 민감한 리소스는 같은 부서만
decision := {
    "allowed": false,
    "reason": "Cross-department access denied for sensitive resource"
} if {
    input.resource.sensitivity >= 4
    input.subject.department != input.resource.department
}

# 정책 5: 민감한 리소스는 FIDO2 필수
decision := {
    "allowed": false,
    "reason": "Sensitive resource requires FIDO2",
    "required_auth_level": 3,
    "action": "STEP_UP_AUTH"
} if {
    input.resource.sensitivity >= 4
    input.subject.auth_level < 3
}

# 모든 정책 통과 시 허용
allow if {
    input.device.compliant
    risk_score <= 80
    not (risk_score > 50; input.subject.auth_level < 2)
    not (input.resource.sensitivity >= 4; input.subject.department != input.resource.department)
    not (input.resource.sensitivity >= 4; input.subject.auth_level < 3)
}

decision := {
    "allowed": true,
    "reason": "All policies passed",
    "risk_score": risk_score,
    "obligations": ["LOG_ACCESS", "ENCRYPT_RESPONSE"]
} if {
    allow
}

# ===== API 호출 예시 =====
# curl -X POST http://localhost:8181/v1/data/zerotrust/decision \
#   -H "Content-Type: application/json" \
#   -d '{
#     "input": {
#       "subject": {"id": "user_123", "auth_level": 2, "department": "engineering"},
#       "device": {"compliant": true, "risk_score": 10},
#       "resource": {"id": "doc_456", "sensitivity": 3, "department": "engineering"},
#       "action": "read",
#       "environment": {"is_corporate_network": true}
#     }
#   }'

🗣️ 실무에서 이렇게 말하세요

💬 보안 아키텍처 회의에서
"VPN 기반 접근은 Zero Trust 관점에서 부족합니다. VPN 연결만으로 내부 리소스를 전부 신뢰하는 건 'Castle-and-Moat' 모델이에요. ZTNA로 전환해서 리소스별로 접근을 제어하고, 매 요청마다 사용자 신원과 기기 상태를 검증해야 합니다. Identity-first로 시작해서 점진적으로 확대하는 게 현실적입니다."
💬 기술 리뷰에서
"API 게이트웨이에서 토큰 검증만 하는 건 Zero Trust가 아닙니다. 기기 상태 체크, 접근 컨텍스트(시간, 위치) 평가, 리스크 기반 동적 인가까지 해야 해요. 리스크 점수가 높으면 Step-up 인증을 요구하고, 매우 높으면 접근을 차단하는 로직을 추가합시다. OPA로 정책을 중앙화하면 서비스마다 일관되게 적용할 수 있습니다."
💬 경영진 보고에서
"Zero Trust 도입은 1년 이상의 여정입니다. NIST는 Identity, Device, Network, Application, Data 5개 영역에서 기본-고급-최적 단계로 성숙도를 높이라고 권장합니다. 우선 Identity 영역에서 모든 사용자에게 피싱 방지 MFA를 적용하고, 그 다음 기기 상태 검증으로 확장하는 로드맵을 제안합니다."

⚠️ 주의사항 & 베스트 프랙티스

빅뱅 방식의 전환 시도

Zero Trust를 한 번에 완전히 구현하려는 시도는 실패 확률이 높습니다. Gartner는 2026년까지 75%의 기관이 자금과 전문성 부족으로 실패할 것으로 예측합니다. Identity-first(신원 관리 우선) 접근으로 점진적으로 구현하세요.

기기 상태 검증 누락

"Assume Breach" 원칙에서 기기는 항상 잠재적 위협입니다. 토큰 검증만으로는 부족합니다. OS 버전, 보안 패치, 디스크 암호화, EDR 상태 등을 검증하고, 비준수 기기는 민감한 리소스 접근을 차단하세요.

정적 정책만 사용

역할만으로 접근을 허용하는 건 Zero Trust가 아닙니다. 컨텍스트(시간, 위치, 행동 패턴, 기기 상태)를 종합한 동적 리스크 평가가 필요합니다. 리스크가 높으면 추가 인증을 요구하거나 접근을 거부하세요.

Zero Trust 베스트 프랙티스

NIST SP 800-207과 CISA ZTMM을 참조하세요. Identity부터 시작해 MFA를 전사 적용하고, 점진적으로 Device, Network, Application, Data 영역으로 확대하세요. 중앙화된 정책 엔진(OPA 등)으로 일관성을 유지하고, 모든 접근을 로깅하여 가시성을 확보하세요.

🔗 관련 용어

📚 더 배우기