🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

Security & Safety Patterns 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2026. 2. 3. · 3 Views

Security & Safety Patterns 완벽 가이드

AI 에이전트와 클라우드 환경에서 필수적인 보안 패턴을 다룹니다. 격리된 실행 환경부터 민감 정보 보호까지, 초급 개발자가 알아야 할 보안의 기초를 실무 예제와 함께 배워봅니다.


목차

  1. 격리된_VM_실행_환경
  2. PII_토큰화_및_마스킹
  3. 보안_스캔_통합
  4. 권한_관리_및_샌드박싱
  5. 민감_정보_탐지
  6. 코드_실행_제한_정책

1. 격리된 VM 실행 환경

김개발 씨는 회사에서 AI 챗봇 서비스를 개발하고 있습니다. 어느 날 사용자가 입력한 코드를 실행해주는 기능을 추가해야 했습니다.

그런데 문득 불안한 생각이 들었습니다. "만약 악의적인 사용자가 서버를 해킹하는 코드를 넣으면 어떡하지?"

격리된 VM 실행 환경은 한마디로 신뢰할 수 없는 코드를 안전한 울타리 안에서 실행하는 것입니다. 마치 동물원에서 맹수를 튼튼한 우리 안에 두는 것처럼, 위험할 수 있는 코드를 별도의 가상 머신에서 실행하여 메인 시스템을 보호합니다.

이것을 제대로 이해하면 사용자 코드 실행 기능을 안전하게 제공할 수 있습니다.

다음 코드를 살펴봅시다.

import docker
from typing import Dict, Any

class IsolatedExecutor:
    def __init__(self):
        # Docker 클라이언트 초기화
        self.client = docker.from_env()

    def execute_untrusted_code(self, code: str, timeout: int = 5) -> Dict[str, Any]:
        # 격리된 컨테이너에서 코드 실행
        container = self.client.containers.run(
            image="python:3.11-slim",
            command=f"python -c '{code}'",
            mem_limit="128m",        # 메모리 제한
            cpu_period=100000,       # CPU 사용 제한
            cpu_quota=50000,
            network_disabled=True,   # 네트워크 차단
            read_only=True,          # 파일시스템 읽기 전용
            detach=True
        )
        # 실행 결과 반환 후 컨테이너 정리
        result = container.wait(timeout=timeout)
        logs = container.logs().decode('utf-8')
        container.remove(force=True)
        return {"exit_code": result["StatusCode"], "output": logs}

김개발 씨는 입사 6개월 차 백엔드 개발자입니다. 오늘 팀장님으로부터 새로운 기능 개발 요청을 받았습니다.

바로 사용자가 작성한 파이썬 코드를 웹에서 실행하고 결과를 보여주는 기능이었습니다. 처음에는 단순하게 생각했습니다.

"그냥 exec() 함수로 실행하면 되지 않을까?" 하지만 옆자리 선배 박시니어 씨가 심각한 표정으로 말했습니다. "그건 정말 위험한 생각이야.

누군가 os.system('rm -rf /')를 입력하면 어떻게 될 것 같아?" 그렇다면 격리된 실행 환경이란 정확히 무엇일까요? 쉽게 비유하자면, 이것은 마치 실험실의 안전 캐비닛과 같습니다.

위험한 바이러스를 연구할 때 과학자들은 절대로 개방된 공간에서 작업하지 않습니다. 유리로 된 특수 캐비닛 안에서만 작업하죠.

만약 문제가 생기더라도 바이러스가 외부로 퍼지지 않습니다. 격리된 VM 환경도 마찬가지입니다.

악성 코드가 실행되더라도 그 영향이 가상 머신 바깥으로 나오지 못합니다. 격리 환경이 없던 시절에는 어땠을까요?

개발자들은 사용자 입력을 직접 실행하는 것을 극도로 꺼렸습니다. 한 번의 실수로 전체 서버가 해킹당할 수 있었기 때문입니다.

코드 실행 기능이 필요한 교육용 플랫폼이나 AI 서비스들은 항상 보안 사고의 위험에 노출되어 있었습니다. 실제로 많은 온라인 코딩 플랫폼들이 이런 공격으로 서비스를 중단해야 했습니다.

바로 이런 문제를 해결하기 위해 컨테이너 기반 격리가 등장했습니다. 컨테이너를 사용하면 각 코드 실행이 완전히 독립된 환경에서 이루어집니다.

메모리 제한을 걸어 무한 루프로 인한 자원 고갈을 방지할 수 있습니다. 네트워크를 차단하면 외부 서버로의 데이터 유출도 막을 수 있습니다.

무엇보다 실행이 끝나면 컨테이너 자체가 삭제되므로 흔적도 남지 않습니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 containers.run() 메서드가 핵심입니다. 여기서 python:3.11-slim 이미지를 사용해 가벼운 파이썬 환경을 생성합니다.

**mem_limit='128m'**은 메모리를 128MB로 제한합니다. 악성 코드가 메모리를 무한히 사용하려 해도 이 이상은 불가능합니다.

network_disabled=True는 컨테이너의 모든 네트워크 연결을 차단합니다. 해커가 외부 서버와 통신하려 해도 실패하게 됩니다.

실제 현업에서는 어떻게 활용할까요? LeetCode나 프로그래머스 같은 코딩 테스트 플랫폼을 생각해보세요.

수많은 사용자가 동시에 코드를 제출하고 실행합니다. 이 모든 코드를 격리된 환경에서 실행하기 때문에 한 사용자의 악성 코드가 다른 사용자에게 영향을 주지 않습니다.

AI 에이전트 서비스에서도 LLM이 생성한 코드를 실행할 때 반드시 이런 격리 환경을 사용합니다. 하지만 주의할 점도 있습니다.

컨테이너 격리가 완벽한 것은 아닙니다. 컨테이너 탈출 취약점이 가끔 발견되기도 합니다.

따라서 보안이 매우 중요한 환경에서는 컨테이너 위에 추가로 VM 레이어를 두는 이중 격리를 사용하기도 합니다. 또한 이미지를 항상 최신 버전으로 유지하고, 불필요한 권한은 모두 제거해야 합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 조언을 듣고 Docker 기반 격리 환경을 구축한 김개발 씨는 안전하게 코드 실행 기능을 출시할 수 있었습니다.

"보안은 귀찮은 게 아니라 필수구나!"라고 깨달은 하루였습니다.

실전 팁

💡 - 컨테이너 이미지는 최소한의 패키지만 포함된 slim 버전을 사용하세요

  • 실행 시간 제한(timeout)을 반드시 설정하여 무한 루프를 방지하세요
  • 프로덕션에서는 gVisor나 Kata Containers 같은 강화된 격리 솔루션을 고려하세요

2. PII 토큰화 및 마스킹

김개발 씨는 고객 상담 로그를 분석하는 AI 시스템을 개발하고 있습니다. 그런데 로그를 살펴보니 고객의 전화번호, 이메일, 심지어 주민등록번호까지 그대로 노출되어 있었습니다.

"이걸 그대로 AI 모델에 보내도 되는 걸까?"

PII 토큰화 및 마스킹은 개인 식별 정보를 안전한 대체값으로 바꾸는 기술입니다. 마치 증인 보호 프로그램에서 신원을 숨기기 위해 가명을 사용하는 것처럼, 민감한 정보를 토큰으로 대체하여 원본 데이터를 보호합니다.

이를 통해 데이터의 유용성은 유지하면서도 개인정보를 안전하게 지킬 수 있습니다.

다음 코드를 살펴봅시다.

import re
import hashlib
from typing import Dict, Tuple

class PIIMasker:
    def __init__(self):
        self.token_map: Dict[str, str] = {}
        # PII 패턴 정의 (전화번호, 이메일, 주민번호)
        self.patterns = {
            'phone': r'01[0-9]-?\d{4}-?\d{4}',
            'email': r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}',
            'ssn': r'\d{6}-?[1-4]\d{6}'
        }

    def mask(self, text: str) -> Tuple[str, Dict[str, str]]:
        masked_text = text
        for pii_type, pattern in self.patterns.items():
            for match in re.finditer(pattern, text):
                original = match.group()
                # 해시 기반 토큰 생성 (복원 가능)
                token = f"[{pii_type.upper()}_{hashlib.md5(original.encode()).hexdigest()[:8]}]"
                self.token_map[token] = original
                masked_text = masked_text.replace(original, token)
        return masked_text, self.token_map

    def unmask(self, masked_text: str) -> str:
        result = masked_text
        for token, original in self.token_map.items():
            result = result.replace(token, original)
        return result

김개발 씨는 스타트업에서 AI 고객 상담 시스템을 개발하고 있습니다. 상담 내용을 분석해서 자동으로 답변을 생성하는 멋진 시스템을 만들었습니다.

그런데 서비스 출시 직전, 법무팀에서 연락이 왔습니다. "이거 개인정보보호법 위반이에요!" 박시니어 씨가 상황을 설명해주었습니다.

"상담 로그에 고객 전화번호가 그대로 들어있잖아. 이걸 외부 AI API에 보내면 개인정보 유출이 되는 거야." 그렇다면 PII 토큰화란 정확히 무엇일까요?

쉽게 비유하자면, 이것은 마치 비밀 코드북과 같습니다. 전쟁 영화에서 스파이들이 "작전은 내일 새벽"이라는 메시지를 "사과가 빨갛다"로 바꿔서 보내는 것을 본 적 있으신가요?

적이 메시지를 가로채도 원래 의미를 알 수 없습니다. PII 토큰화도 마찬가지입니다.

"010-1234-5678"을 "[PHONE_a1b2c3d4]"로 바꾸면, 이 데이터가 유출되어도 실제 전화번호는 알 수 없습니다. 토큰화가 없던 시절에는 어땠을까요?

개발자들은 두 가지 선택지밖에 없었습니다. 개인정보가 포함된 데이터를 아예 사용하지 않거나, 위험을 감수하고 원본 데이터를 사용하거나.

전자를 선택하면 AI 모델의 성능이 떨어졌고, 후자를 선택하면 보안 사고의 위험이 있었습니다. 2023년에만 해도 개인정보 유출로 인한 과징금이 수백억 원에 달했습니다.

바로 이런 딜레마를 해결하기 위해 토큰화 기술이 발전했습니다. 토큰화를 사용하면 원본 데이터의 구조와 형식은 유지됩니다.

"[PHONE_a1b2c3d4]님이 문의하셨습니다"처럼 문맥이 그대로 살아있어서 AI 모델이 상담 내용을 이해하는 데 문제가 없습니다. 동시에 실제 전화번호는 노출되지 않으니 개인정보도 보호됩니다.

필요한 경우에는 토큰 맵을 사용해 원본으로 복원할 수도 있습니다. 위의 코드를 자세히 살펴보겠습니다.

patterns 딕셔너리에는 정규표현식으로 PII 패턴을 정의합니다. 한국 전화번호, 이메일, 주민등록번호를 감지할 수 있습니다.

mask() 메서드에서는 텍스트에서 PII를 찾아 해시 기반 토큰으로 대체합니다. MD5 해시를 사용해 같은 값은 항상 같은 토큰이 됩니다.

이렇게 하면 "김철수"라는 이름이 여러 번 나와도 일관된 토큰으로 처리됩니다. 실제 현업에서는 어떻게 활용할까요?

금융 기관의 사기 탐지 시스템을 생각해보세요. 거래 내역에는 계좌번호와 금액이 가득합니다.

이 데이터를 분석 시스템에 보내기 전에 계좌번호를 토큰화합니다. 분석 결과가 "계좌 [ACCOUNT_x7y8z9]에서 의심 거래 발생"이라고 나오면, 담당자만 토큰 맵을 통해 실제 계좌를 확인할 수 있습니다.

하지만 주의할 점도 있습니다. 토큰 맵 자체가 새로운 보안 자산이 됩니다.

토큰 맵이 유출되면 모든 마스킹이 무의미해집니다. 따라서 토큰 맵은 암호화된 별도 저장소에 보관하고, 접근 권한을 엄격히 제한해야 합니다.

또한 단순 해시만으로는 레인보우 테이블 공격에 취약할 수 있으므로, 솔트(salt)를 추가하는 것이 좋습니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

PII 마스킹 시스템을 도입한 후, 법무팀의 승인을 받아 무사히 서비스를 출시할 수 있었습니다. "보안과 기능, 둘 다 잡을 수 있구나!"라고 뿌듯해하는 김개발 씨였습니다.

실전 팁

💡 - 정규표현식 패턴은 지역과 서비스에 맞게 커스터마이징하세요 (한국 전화번호, 미국 SSN 등)

  • 토큰 맵은 반드시 암호화하여 별도 보안 저장소에 보관하세요
  • 가능하면 복원이 불가능한 단방향 마스킹을 기본으로 사용하세요

3. 보안 스캔 통합

김개발 씨가 열심히 개발한 코드를 배포하려는 순간, CI/CD 파이프라인이 빨간 불을 켜며 멈췄습니다. "보안 취약점 발견: SQL Injection 위험"이라는 경고 메시지가 떴습니다.

김개발 씨는 처음 보는 이 메시지에 당황했습니다. "내 코드에 보안 문제가 있다고?"

보안 스캔 통합은 코드가 배포되기 전에 자동으로 취약점을 검사하는 시스템입니다. 마치 공항 보안 검색대처럼, 모든 코드가 통과해야 하는 관문을 설치하는 것입니다.

이를 통해 개발자가 미처 발견하지 못한 보안 취약점을 사전에 차단할 수 있습니다.

다음 코드를 살펴봅시다.

import subprocess
import json
from dataclasses import dataclass
from typing import List, Optional
from enum import Enum

class Severity(Enum):
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"
    CRITICAL = "critical"

@dataclass
class Vulnerability:
    file: str
    line: int
    severity: Severity
    description: str

class SecurityScanner:
    def __init__(self, fail_threshold: Severity = Severity.HIGH):
        self.fail_threshold = fail_threshold

    def scan_dependencies(self) -> List[Vulnerability]:
        # pip-audit으로 의존성 취약점 스캔
        result = subprocess.run(
            ["pip-audit", "--format", "json"],
            capture_output=True, text=True
        )
        vulnerabilities = []
        for vuln in json.loads(result.stdout):
            vulnerabilities.append(Vulnerability(
                file="requirements.txt",
                line=0,
                severity=Severity(vuln.get("severity", "medium")),
                description=f"{vuln['name']}: {vuln['description']}"
            ))
        return vulnerabilities

    def should_block_deploy(self, vulns: List[Vulnerability]) -> bool:
        severity_order = [Severity.LOW, Severity.MEDIUM, Severity.HIGH, Severity.CRITICAL]
        threshold_idx = severity_order.index(self.fail_threshold)
        return any(severity_order.index(v.severity) >= threshold_idx for v in vulns)

김개발 씨는 새로운 기능 개발을 마치고 의기양양하게 코드를 푸시했습니다. 하지만 GitHub Actions가 실패했다는 알림이 울렸습니다.

뭐가 문제지? 로그를 확인해보니 Bandit이라는 도구가 보안 문제를 발견했다고 합니다.

박시니어 씨가 다가와 설명해주었습니다. "우리 팀은 모든 코드에 보안 스캔을 걸어놨어.

네가 작성한 SQL 쿼리에서 인젝션 가능성이 발견된 거야." 그렇다면 보안 스캔 통합이란 정확히 무엇일까요? 쉽게 비유하자면, 이것은 마치 자동차 출고 전 품질 검사와 같습니다.

자동차 공장에서는 차가 고객에게 전달되기 전에 수십 가지 검사를 거칩니다. 브레이크는 제대로 작동하는지, 에어백은 문제없는지 꼼꼼히 확인합니다.

보안 스캔도 마찬가지입니다. 코드가 프로덕션에 배포되기 전에 알려진 취약점이 있는지 자동으로 검사합니다.

보안 스캔이 없던 시절에는 어땠을까요? 개발자들은 보안 전문가가 수동으로 코드를 검토하기를 기다려야 했습니다.

시간이 오래 걸렸고, 사람인지라 실수로 놓치는 부분도 있었습니다. 더 큰 문제는 오픈소스 라이브러리의 취약점이었습니다.

개발자가 직접 작성하지 않은 코드에서 보안 문제가 터지는 경우가 허다했습니다. Log4j 사태가 대표적인 예입니다.

바로 이런 문제를 해결하기 위해 DevSecOps가 등장했습니다. 보안 스캔을 CI/CD 파이프라인에 통합하면 모든 코드 변경이 자동으로 검사됩니다.

**SAST(정적 분석)**는 코드 자체를 분석하고, **SCA(소프트웨어 구성 분석)**는 사용 중인 라이브러리의 알려진 취약점을 확인합니다. 심각도 기준을 설정해서 HIGH 이상의 취약점이 발견되면 배포를 자동으로 차단할 수 있습니다.

위의 코드를 자세히 살펴보겠습니다. Vulnerability 데이터클래스는 발견된 취약점 정보를 담습니다.

파일명, 라인 번호, 심각도, 설명을 포함합니다. scan_dependencies() 메서드는 pip-audit을 실행하여 파이썬 패키지의 알려진 취약점을 검사합니다.

should_block_deploy() 메서드는 설정된 임계값 이상의 취약점이 있으면 True를 반환하여 배포를 차단합니다. 실제 현업에서는 어떻게 활용할까요?

대부분의 테크 기업에서는 여러 보안 도구를 조합하여 사용합니다. 파이썬 코드는 Bandit으로, 자바스크립트는 ESLint 보안 플러그인으로, 컨테이너 이미지는 Trivy로 스캔합니다.

GitHub의 Dependabot이나 Snyk 같은 서비스를 연동하면 취약점이 발견됐을 때 자동으로 PR을 생성해서 패치 버전으로 업데이트해주기도 합니다. 하지만 주의할 점도 있습니다.

보안 스캔 도구는 **거짓 양성(false positive)**을 많이 발생시킬 수 있습니다. 실제로는 문제가 없는데 경고를 띄우는 경우입니다.

이런 경고가 너무 많으면 개발자들이 피로감을 느끼고 경고를 무시하게 됩니다. 따라서 팀의 상황에 맞게 규칙을 조정하고, 정말 중요한 취약점에 집중하도록 설정해야 합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 도움으로 파라미터화된 쿼리로 코드를 수정한 김개발 씨는 드디어 초록 불을 보았습니다.

"자동 검사가 있으니까 오히려 안심이 되네요!"

실전 팁

💡 - 보안 스캔은 가능한 개발 초기 단계(IDE, pre-commit)에서 실행하세요

  • 거짓 양성이 많은 규칙은 팀과 논의하여 예외 처리하거나 비활성화하세요
  • 의존성 취약점은 Dependabot이나 Renovate로 자동 업데이트를 설정하세요

4. 권한 관리 및 샌드박싱

김개발 씨는 AI 에이전트가 파일을 읽고 수정하는 기능을 개발했습니다. 그런데 테스트 중 에이전트가 실수로 시스템 설정 파일을 삭제해버렸습니다.

얼굴이 새하얗게 질린 김개발 씨. "AI한테 모든 권한을 줬더니 이런 일이..."

권한 관리 및 샌드박싱은 프로그램이 할 수 있는 일을 명확하게 제한하는 것입니다. 마치 아이에게 장난감 방에서만 놀게 하고 부엌의 칼은 만지지 못하게 하는 것처럼, AI 에이전트나 플러그인에게 필요한 최소한의 권한만 부여합니다.

이것이 바로 최소 권한 원칙입니다.

다음 코드를 살펴봅시다.

import os
from pathlib import Path
from typing import Set, Optional
from dataclasses import dataclass, field

@dataclass
class SandboxPolicy:
    allowed_paths: Set[Path] = field(default_factory=set)
    allowed_operations: Set[str] = field(default_factory=lambda: {"read"})
    max_file_size: int = 10 * 1024 * 1024  # 10MB

class SandboxedFileSystem:
    def __init__(self, policy: SandboxPolicy):
        self.policy = policy

    def _is_path_allowed(self, path: Path) -> bool:
        # 경로가 허용된 디렉토리 내부인지 확인
        resolved = path.resolve()
        return any(
            resolved.is_relative_to(allowed.resolve())
            for allowed in self.policy.allowed_paths
        )

    def read_file(self, path: str) -> Optional[str]:
        target = Path(path)
        if "read" not in self.policy.allowed_operations:
            raise PermissionError("읽기 권한이 없습니다")
        if not self._is_path_allowed(target):
            raise PermissionError(f"접근 불가 경로: {path}")
        if target.stat().st_size > self.policy.max_file_size:
            raise ValueError(f"파일 크기 제한 초과")
        return target.read_text()

    def write_file(self, path: str, content: str) -> None:
        target = Path(path)
        if "write" not in self.policy.allowed_operations:
            raise PermissionError("쓰기 권한이 없습니다")
        if not self._is_path_allowed(target):
            raise PermissionError(f"접근 불가 경로: {path}")
        target.write_text(content)

김개발 씨는 요즘 핫한 AI 에이전트 서비스를 개발하고 있습니다. 에이전트가 사용자의 파일을 읽고 분석해주는 기능이 핵심이었습니다.

처음에는 편의를 위해 에이전트에게 전체 파일시스템 접근 권한을 주었습니다. 그런데 문제가 터졌습니다.

에이전트가 "임시 파일 정리"를 한다면서 /etc 디렉토리의 중요한 설정 파일까지 삭제해버린 것입니다. 다행히 테스트 서버였지만, 프로덕션이었다면 대형 사고였습니다.

그렇다면 샌드박싱이란 정확히 무엇일까요? 쉽게 비유하자면, 이것은 마치 호텔 카드키와 같습니다.

호텔에 체크인하면 카드키를 받습니다. 이 카드키로는 자기 방과 피트니스센터, 수영장에만 들어갈 수 있습니다.

다른 손님의 방이나 직원 전용 구역에는 절대 들어갈 수 없습니다. 샌드박싱도 마찬가지입니다.

프로그램에게 꼭 필요한 공간과 권한만 부여하고, 나머지는 철저히 차단합니다. 권한 관리가 없던 시절에는 어땠을까요?

초기 컴퓨터 시스템에서는 모든 프로그램이 전체 시스템에 접근할 수 있었습니다. 하나의 악성 프로그램이 실행되면 전체 시스템이 감염되었습니다.

바이러스가 창궐했던 시절을 기억하시나요? 지금은 운영체제 자체가 프로세스마다 권한을 분리하지만, 애플리케이션 레벨에서도 추가 보호가 필요합니다.

바로 이런 이유로 **최소 권한 원칙(Principle of Least Privilege)**이 등장했습니다. 이 원칙에 따르면 모든 프로그램과 사용자는 작업 수행에 필요한 최소한의 권한만 가져야 합니다.

AI 에이전트가 문서를 읽기만 하면 된다면 쓰기 권한은 주지 않습니다. 특정 폴더의 파일만 접근하면 된다면 해당 폴더만 허용합니다.

이렇게 하면 에이전트가 오작동하거나 해킹당해도 피해 범위가 제한됩니다. 위의 코드를 자세히 살펴보겠습니다.

SandboxPolicy 클래스가 허용 정책을 정의합니다. allowed_paths는 접근 가능한 디렉토리 목록이고, allowed_operations는 허용된 작업(read, write) 목록입니다.

_is_path_allowed() 메서드는 요청된 경로가 허용된 디렉토리 내부인지 확인합니다. **Path.is_relative_to()**를 사용해 상위 디렉토리 탈출 공격(../../etc/passwd)도 방지합니다.

실제 현업에서는 어떻게 활용할까요? 브라우저가 대표적인 샌드박스 환경입니다.

웹사이트의 자바스크립트 코드는 사용자의 파일시스템에 직접 접근할 수 없습니다. 모바일 앱도 마찬가지입니다.

카메라나 위치 정보에 접근하려면 사용자에게 명시적으로 권한을 요청해야 합니다. AI 에이전트 프레임워크들도 이런 권한 시스템을 적극 도입하고 있습니다.

하지만 주의할 점도 있습니다. 권한을 너무 엄격하게 제한하면 정상적인 기능도 동작하지 않을 수 있습니다.

또한 권한 우회 취약점이 존재할 수 있습니다. 예를 들어 심볼릭 링크를 통해 허용되지 않은 파일에 접근하는 공격이 있습니다.

따라서 실제 경로(resolved path)를 기준으로 검사하고, 정기적으로 정책을 점검해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

샌드박스 정책을 도입한 후, 에이전트는 지정된 작업 디렉토리 안에서만 활동하게 되었습니다. "AI한테도 적절한 울타리가 필요하구나!"라고 깨달은 김개발 씨였습니다.

실전 팁

💡 - 경로 검증 시 반드시 정규화된 절대 경로(resolved path)를 사용하세요

  • 권한 정책은 화이트리스트 방식으로 설계하세요 (기본 거부, 명시적 허용)
  • 정기적으로 권한 설정을 감사(audit)하여 불필요한 권한을 제거하세요

5. 민감 정보 탐지

김개발 씨가 코드 리뷰를 하다가 깜짝 놀랐습니다. 동료가 올린 PR에 AWS 액세스 키가 하드코딩되어 있었던 것입니다.

"이거 그대로 푸시됐으면 큰일 날 뻔했네!" 가슴을 쓸어내린 김개발 씨는 이런 실수를 자동으로 잡을 방법이 없을까 고민했습니다.

민감 정보 탐지는 코드나 로그에서 비밀번호, API 키, 인증 토큰 같은 민감한 데이터를 자동으로 찾아내는 기술입니다. 마치 공항 보안 검색대의 X-ray 기계처럼, 겉으로는 평범해 보이는 코드 속에 숨어있는 위험한 정보를 탐지합니다.

이를 통해 보안 사고를 사전에 예방할 수 있습니다.

다음 코드를 살펴봅시다.

import re
from dataclasses import dataclass
from typing import List, Dict, Pattern
from enum import Enum

class SecretType(Enum):
    AWS_KEY = "AWS Access Key"
    GITHUB_TOKEN = "GitHub Token"
    PRIVATE_KEY = "Private Key"
    DATABASE_URL = "Database Connection String"

@dataclass
class DetectedSecret:
    secret_type: SecretType
    file_path: str
    line_number: int
    matched_text: str  # 일부만 표시

class SecretDetector:
    def __init__(self):
        # 각 비밀 유형별 정규표현식 패턴
        self.patterns: Dict[SecretType, Pattern] = {
            SecretType.AWS_KEY: re.compile(r'AKIA[0-9A-Z]{16}'),
            SecretType.GITHUB_TOKEN: re.compile(r'ghp_[a-zA-Z0-9]{36}'),
            SecretType.PRIVATE_KEY: re.compile(r'-----BEGIN (?:RSA |EC )?PRIVATE KEY-----'),
            SecretType.DATABASE_URL: re.compile(
                r'(?:mysql|postgresql|mongodb)://[^:]+:[^@]+@[^\s]+'
            ),
        }

    def scan_text(self, content: str, file_path: str = "") -> List[DetectedSecret]:
        findings = []
        lines = content.split('\n')
        for line_num, line in enumerate(lines, 1):
            for secret_type, pattern in self.patterns.items():
                if match := pattern.search(line):
                    # 발견된 비밀의 일부만 기록 (전체 노출 방지)
                    masked = match.group()[:8] + "..." + match.group()[-4:]
                    findings.append(DetectedSecret(
                        secret_type=secret_type,
                        file_path=file_path,
                        line_number=line_num,
                        matched_text=masked
                    ))
        return findings

김개발 씨의 회사에서 충격적인 사건이 일어났습니다. 인턴 직원이 실수로 GitHub public 저장소에 데이터베이스 비밀번호를 올려버린 것입니다.

불과 30분 만에 해커가 접근하여 고객 데이터가 유출되었습니다. 회사는 큰 손해를 입었고, 인턴은 자책감에 빠졌습니다.

박시니어 씨가 팀 회의에서 말했습니다. "사람은 실수를 합니다.

중요한 건 시스템으로 이런 실수를 막는 거예요. 우리도 비밀 탐지 시스템을 도입합시다." 그렇다면 민감 정보 탐지란 정확히 무엇일까요?

쉽게 비유하자면, 이것은 마치 금속 탐지기와 같습니다. 공항 보안 검색대에서 가방을 X-ray에 통과시키면 날카로운 물체나 금지 품목을 자동으로 찾아냅니다.

민감 정보 탐지기도 마찬가지입니다. 코드 파일을 스캔하면서 API 키, 비밀번호, 인증서 같은 "위험한 물건"을 자동으로 찾아냅니다.

민감 정보 탐지가 없던 시절에는 어땠을까요? 개발자들은 코드 리뷰 때 눈으로 직접 확인해야 했습니다.

하지만 수천 줄의 코드에서 20자리 문자열이 API 키인지 일반 변수인지 구분하기는 쉽지 않습니다. 특히 바쁜 마감 시즌에는 리뷰가 부실해지기 쉽습니다.

GitHub에 따르면 매년 수백만 개의 비밀이 public 저장소에 노출된다고 합니다. 바로 이런 문제를 해결하기 위해 Secret Scanning 도구들이 등장했습니다.

이 도구들은 알려진 비밀 형식의 패턴을 가지고 있습니다. AWS 액세스 키는 항상 'AKIA'로 시작하고 20자입니다.

GitHub 토큰은 'ghp_'로 시작합니다. 이런 패턴을 정규표현식으로 정의하고, 코드 전체를 스캔합니다.

패턴이 매칭되면 즉시 경고를 발생시킵니다. 위의 코드를 자세히 살펴보겠습니다.

patterns 딕셔너리에 각 비밀 유형별 정규표현식을 정의합니다. AWS 키의 경우 'AKIA'로 시작하고 그 뒤에 16자의 대문자와 숫자가 옵니다.

scan_text() 메서드는 텍스트의 각 줄을 검사합니다. 비밀이 발견되면 전체를 기록하지 않고 일부만 마스킹하여 저장합니다.

탐지 로그 자체가 유출되어도 전체 비밀은 노출되지 않도록 하는 것입니다. 실제 현업에서는 어떻게 활용할까요?

GitHub는 자체적으로 Secret Scanning 기능을 제공합니다. public 저장소에 알려진 형식의 비밀이 푸시되면 자동으로 감지하고, 해당 서비스 제공자(AWS, Stripe 등)에게 알려서 토큰을 무효화시키기도 합니다.

pre-commit 훅에 detect-secrets나 gitleaks 같은 도구를 연동하면 커밋 자체를 차단할 수도 있습니다. 하지만 주의할 점도 있습니다.

정규표현식만으로는 모든 비밀을 탐지할 수 없습니다. 개발자가 만든 커스텀 형식의 비밀이나, 패턴이 없는 단순 문자열 비밀번호는 놓칠 수 있습니다.

또한 엔트로피 분석(무작위성이 높은 문자열 탐지)을 병행하면 더 많은 비밀을 찾을 수 있습니다. 하지만 거짓 양성이 증가할 수 있으니 적절한 균형이 필요합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. pre-commit 훅에 비밀 탐지 도구를 설치한 후, 실수로 API 키를 커밋하려 할 때마다 경고가 뜹니다.

"시스템이 나 대신 실수를 잡아주니까 안심이 되네요!"

실전 팁

💡 - pre-commit 단계에서 비밀 탐지를 실행하여 커밋 자체를 차단하세요

  • 정규표현식 패턴과 엔트로피 분석을 함께 사용하면 탐지율이 높아집니다
  • 발견된 비밀은 즉시 무효화하고 새로 발급받으세요 (회전이 아닌 교체)

6. 코드 실행 제한 정책

김개발 씨는 AI 코딩 어시스턴트 서비스를 운영하고 있습니다. 어느 날 모니터링 대시보드를 보니 CPU 사용량이 100%에 고정되어 있었습니다.

확인해보니 누군가 while True: pass 같은 무한 루프 코드를 실행시킨 것입니다. "이런 악의적인 코드를 어떻게 막지?"

코드 실행 제한 정책은 실행되는 코드에 시간, 메모리, 시스템 호출 등의 제한을 거는 것입니다. 마치 놀이공원에서 키 제한이 있는 것처럼, 위험할 수 있는 코드에 명확한 경계선을 설정합니다.

이를 통해 서비스의 안정성을 유지하면서도 코드 실행 기능을 제공할 수 있습니다.

다음 코드를 살펴봅시다.

import signal
import resource
from typing import Callable, Any, Optional
from dataclasses import dataclass
from functools import wraps

@dataclass
class ExecutionPolicy:
    max_time_seconds: int = 5        # 최대 실행 시간
    max_memory_mb: int = 128          # 최대 메모리 사용량
    max_output_size: int = 10000      # 최대 출력 크기
    allow_network: bool = False       # 네트워크 허용 여부
    allow_file_write: bool = False    # 파일 쓰기 허용 여부

class ExecutionLimiter:
    def __init__(self, policy: ExecutionPolicy):
        self.policy = policy

    def _timeout_handler(self, signum, frame):
        raise TimeoutError(f"실행 시간 {self.policy.max_time_seconds}초 초과")

    def limited_exec(self, func: Callable) -> Callable:
        @wraps(func)
        def wrapper(*args, **kwargs) -> Any:
            # 메모리 제한 설정 (Linux)
            mem_bytes = self.policy.max_memory_mb * 1024 * 1024
            resource.setrlimit(resource.RLIMIT_AS, (mem_bytes, mem_bytes))
            # 시간 제한 설정
            signal.signal(signal.SIGALRM, self._timeout_handler)
            signal.alarm(self.policy.max_time_seconds)
            try:
                result = func(*args, **kwargs)
                return result
            except MemoryError:
                raise MemoryError(f"메모리 {self.policy.max_memory_mb}MB 초과")
            finally:
                signal.alarm(0)  # 타이머 해제
        return wrapper

# 사용 예시
limiter = ExecutionLimiter(ExecutionPolicy(max_time_seconds=3))

@limiter.limited_exec
def run_user_code(code: str) -> str:
    exec(compile(code, '<user>', 'exec'))
    return "실행 완료"

김개발 씨의 AI 코딩 어시스턴트는 사용자가 작성한 코드를 실행하고 결과를 보여주는 기능이 있습니다. 대부분의 사용자는 정상적인 코드를 실행하지만, 가끔 악의적인 사용자가 있습니다.

어떤 사람은 while True: pass로 CPU를 독점하려 하고, 어떤 사람은 a = [0] * (10**9)로 메모리를 고갈시키려 합니다. 박시니어 씨가 조언했습니다.

"사용자 코드를 신뢰하면 안 돼. 항상 최악의 상황을 가정하고 제한을 걸어야 해." 그렇다면 코드 실행 제한 정책이란 정확히 무엇일까요?

쉽게 비유하자면, 이것은 마치 시험 시간 제한과 같습니다. 시험에는 항상 종료 시간이 있습니다.

아무리 열심히 풀고 싶어도 시간이 되면 연필을 놓아야 합니다. 코드 실행도 마찬가지입니다.

아무리 복잡한 계산이라도 정해진 시간 안에 끝나지 않으면 강제로 중단됩니다. 이렇게 해야 다른 사용자들도 공평하게 서비스를 이용할 수 있습니다.

실행 제한이 없던 시절에는 어땠을까요? 온라인 저지 사이트들이 초기에 많이 겪었던 문제입니다.

한 사용자의 무한 루프 코드가 서버 전체를 멈추게 만들었습니다. 서비스를 재시작해야 했고, 다른 사용자들의 제출도 모두 날아갔습니다.

메모리 폭탄 공격으로 서버가 OOM(Out Of Memory)으로 죽는 경우도 허다했습니다. 바로 이런 문제를 해결하기 위해 **리소스 제한(Resource Limiting)**이 발전했습니다.

리눅스의 resource 모듈을 사용하면 프로세스가 사용할 수 있는 메모리, CPU 시간, 파일 디스크립터 수 등을 제한할 수 있습니다. signal 모듈의 알람을 이용하면 정해진 시간이 지나면 프로세스에 신호를 보내 중단시킬 수 있습니다.

이 두 가지를 조합하면 꽤 견고한 실행 환경을 만들 수 있습니다. 위의 코드를 자세히 살펴보겠습니다.

ExecutionPolicy 클래스에서 각종 제한 값을 정의합니다. 기본값으로 5초, 128MB를 설정했습니다.

limited_exec 데코레이터가 핵심입니다. 먼저 **resource.setrlimit()**로 프로세스의 최대 가상 메모리를 제한합니다.

그 다음 **signal.alarm()**으로 타이머를 설정합니다. 시간이 초과되면 SIGALRM 신호가 발생하고, 우리가 정의한 핸들러가 TimeoutError를 발생시킵니다.

실제 현업에서는 어떻게 활용할까요? LeetCode, 백준, 프로그래머스 같은 코딩 테스트 플랫폼은 모두 이런 제한을 사용합니다.

문제마다 시간 제한과 메모리 제한이 다르게 설정됩니다. AWS Lambda 같은 서버리스 환경도 함수 실행 시간과 메모리에 제한이 있습니다.

AI 에이전트가 코드를 생성하고 실행하는 서비스에서도 필수적으로 적용해야 하는 패턴입니다. 하지만 주의할 점도 있습니다.

파이썬의 signal.alarm()은 메인 스레드에서만 작동합니다. 멀티스레드 환경에서는 다른 방법을 사용해야 합니다.

또한 resource.setrlimit()은 리눅스 전용입니다. Windows에서는 다른 API를 사용해야 합니다.

가장 확실한 방법은 앞서 배운 컨테이너 격리와 함께 사용하는 것입니다. 컨테이너 레벨에서 cgroup으로 리소스를 제한하면 더 안전합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 실행 제한 정책을 도입한 후, 무한 루프 코드는 3초 만에 자동으로 종료됩니다.

메모리를 과도하게 사용하려는 코드도 MemoryError로 차단됩니다. "이제 마음 놓고 서비스를 운영할 수 있겠어!"

실전 팁

💡 - 시간 제한과 메모리 제한은 서비스 특성에 맞게 적절히 조정하세요

  • 컨테이너의 cgroup 제한과 애플리케이션 레벨 제한을 함께 사용하면 더 안전합니다
  • 제한 초과 시 사용자에게 명확한 에러 메시지를 제공하세요

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Security#VM-Isolation#PII-Masking#Sandboxing#Code-Execution-Policy#Sensitive-Data-Detection#AI,Agent,Security

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.

함께 보면 좋은 카드 뉴스