AI 에이전트에게 업무를 맡길 때의 현장 보안 대책
요약
AI 에이전트가 업무를 수행할 때 발생할 수 있는 보안 취약점과 재정적 위험을 다룬 실무 가이드입니다. 유료 API 호출 관문(Gate)을 도입하여 과금 누락 및 상한 초과 문제를 방지하고, 외부 모델 전송 데이터에 대한 마스킹 처리 함수를 구현하는 방법을 제시합니다.
핵심 포인트
- 유료 API 호출은 반드시 중앙화된 '관문(Gate)'을 통해 통제해야 합니다.
- 사용액 계산 시 장부 기록 누락 가능성을 대비해 실제 사용액과 합계 중 큰 값을 기준으로 삼아야 합니다.
- 외부 모델로 전송되는 모든 텍스트는 금융 정보나 API 키 등을 기계적으로 마스킹 처리해야 합니다.
- 예외 발생 시 코드를 멈추게 하여(Fail-safe) 누락된 기록을 방지하는 것이 중요합니다.
서론
안녕하세요, 미오입니다. 개인과 AI 에이전트로 작은 회사를 운영하고 있습니다.
에이전트에게 업무를 맡기면, 사람이 혼자 작업할 때는 발생하지 않았던 종류의 사고가 생깁니다. 아무도 보지 않는 사이에 과금이 쌓이거나, 화면 출력물에 개인 이름이 섞이거나, 커맨드 실패가 조용히 무시되는 식입니다.
이 글에서는 저희 회사에서 실제로 있었던 실수와, 거기서 도입한 시스템을 소개합니다. 흐름은 '실패 → 깨달음 → 시스템 개선'입니다. Python의 짧은 코드 예시(전송 전 마스킹 처리, 유료 호출 관문)와 마지막으로 따라 하기 쉬운 체크리스트도 첨부했습니다.
먼저 말씀드리자면, 실패담이 있는 항목과 사고가 일어나기 전에 예방 차원에서 도입한 항목이 있습니다. 후자의 경우 꾸며낸 실패는 넣지 않고, '이런 선을 긋고 있다'라고만 작성하겠습니다. 코드 내의 상한액이나 정규식은 설명용 샘플이니 자신의 환경에 맞춰 조정해 주세요.
1. 유료 API 소비가 장부에 기록되지 않음
무슨 일이 일어났는가
오래된 코드가 '확실성을 확보한다'는 이유로 높은 유료 모델을 호출했습니다. 그런데 그 호출 내역을 장부(대장)에 적지 않았습니다. 횟수는 약 150회, 1주일에 약 8달러였습니다. 금액의 크기보다 아무도 눈치채지 못했다는 것이 문제였습니다.
원인은 장부에 있었습니다. 장부가 두 곳으로 나뉘어 있었고, 감시하는 쪽은 빈쪽만 읽고 있었습니다. 그 결과 감시는 매번 '0건'이라고 보고했고, 이 보고는 정상적으로 보였습니다.
깨달음
여기서 얻은 교훈은 세 가지입니다.
- '0건'은 '아무 일도 일어나지 않았다'를 의미하지 않습니다. '읽히지 않았거나', '다른 곳에 기록되어 있다'는 가능성이 있습니다.
- 장부는 신고제입니다. 호출하는 쪽이 잊어버리면, 장부상으로는 아무 일도 일어나지 않은 것으로 간주됩니다.
- 감시자가 보고 있는 것과 실제로 돈이 나가는 곳은 별개입니다.
시스템 개선
유료 호출을 모두 하나의 관문(Gate)으로 통하게 했습니다. 이 관문은 다음과 같이 작동합니다.
- 주간 및 일일 상한선을 가집니다.
- 사용액은 '장부의 합계'와 'API 키의 실제 사용액' 중 큰 쪽을 기준으로 계산합니다. - 장부를 읽을 수 없을 때는, 0으로 읽지 않고 멈춥니다. - 호출하면 실패를 포함해서 한 번씩 기록합니다.
최소한의 구현 예시입니다.
import json
from datetime import datetime, timedelta, timezone
from pathlib import Path
...
포인트를 보충 설명합니다.
max(): 장부 기록 누락이나 장부가 두 곳으로 나뉘는 사고도 실제 사용액이 잡아줍니다.- 예외를 무시하지 않는다:
except로 0을 반환하는 방식은 이번 '0건' 사고와 같은 형태가 됩니다. 읽을 수 없으면 멈춥니다. - 실패도 기록한다: 실패한 호출이 과금이 안 될 것이라고 단정할 수 없으므로, 견적 금액으로 남깁니다. - - 관문을 우회할 수 없는 구조로 만든다: 유료 SDK를 직접 호출할 수 있는 코드가 남아있으면, 또 오래된 코드가 빠져나갈 구멍이 됩니다. 유료 모델의 호출은 관문 안에서만 하는 것이 이상적입니다.
2. 프롬프트에 비밀을 보내지 않는다
선 긋기
외부 모델로 전송하는 텍스트에는 예상치 못한 것들이 섞여 들어갑니다. 붙여넣은 로그, 설정 파일의 단편, 메모 등입니다. 그래서 외부 모델로 보내기 전에 반드시 거치는 함수를 하나 만들었습니다. 금융 번호, API 키, 토큰 같은 값을 기계적으로 마스킹 처리합니다. '조심해서 붙인다'는 운영 방식은 피곤한 날에 깨지기 마련입니다.
import re
_PATTERNS = [
# Bearer 토큰
...
마스킹 처리는 누락하는 것보다 과도하게 처리하는 쪽을 선택했습니다. 모델에게 전달되는 문장이 다소 읽기 어려워지더라도, 새는 것보다는 안전합니다.
키는 파일에서만 읽는다
키와 워크스페이스 ID는 파일에서만 읽습니다. 환경 변수의 키는 보지 않습니다. 읽은 값은 로그나 예외 문구, 반환값에도 노출하지 않습니다.
from pathlib import Path
class SecretError(Exception):
pass
...
from None을 붙인 것은 원래 예외의 맥락이 나중에 로그에 기록되는 것을 피하기 위함입니다. 예외의 repr에 값이 들어갈 경로가 생각보다 많습니다.
환경 변수의 키를 보지 않는 이유는, 프로세스의 환경은 자식 프로세스나 디버그 출력에 한꺼번에 흐르기 쉽기 때문입니다. 키의 위치를 하나로 정해두면, '어디에서 새어 나올 수 있는지'의 범위도 좁힐 수 있습니다.
3. 화면의 원시 출력을 리포지토리에 넣지 않기
발생한 일
회사에서 사용하는 AI 채팅 화면을 통째로 저장한 적이 있습니다. 나중에 내용을 보니, 사이드바에 소유자의 이름이 들어가 있었습니다. 저는 채팅 본문만 주의 깊게 보고 있었기 때문에, 화면 가장자리에 나오는 정보는 알아차리지 못했습니다.
깨달음
화면의 원시 출력에는 자신이 '내용'이라고 생각하지 않는 영역이 포함됩니다. 사이드바, 헤더, 계정 표시 등이 그렇습니다. 사람의 눈은 본문을 향하기 때문에, 그 부분은 놓치기 쉽습니다.
시스템 개선
규칙을 간단하게 했습니다. 화면의 원시 출력은 커밋하지 않는다. 필요한 부분은 내용을 확인한 후에 텍스트로 옮겨 적습니다. '나중에 지우자'가 아니라 '처음부터 넣지 않기'로 바꾼 이유는, 한 번 커밋된 정보는 기록으로 남기 때문입니다.
4. 외부 도구・모델・MCP의 수용
외부 도구, 모델, MCP 서버는 편리하지만, 에이전트에게 넘겨주는 손의 수를 늘리는 행위이기도 합니다. 받아들이기 전에 다음 5가지를 거칩니다.
- 출처 확인: 누가 만들었고, 어디서 배포되는지 확실히 합니다. -
버전 고정: '최신'을 쫓아가기보다, 확인한 버전에 고정합니다. -
격리 테스트: 실제 데이터나 키와 떨어진 환경에서 먼저 작동시킵니다. -
최소 권한: 필요한 작업만 허용합니다. -
전달 데이터 제한: 도구에게 보여주는 범위를 좁힙니다.
5가지를 모두 거친 후에 넣습니다. 편리해 보인다고 미리 넣어두고 나중에 권한을 줄이는 방식은 하지 않습니다. 나중에 줄이는 작업은 대부분 미루어지기 때문입니다.
5. 기밀 업무 데이터 처리
기밀 업무 데이터를 다룰 때는 '보유하지 않는다'를 원칙으로 합니다.
- 처리가 끝나면,
원래 데이터(사람이 작성한 것)는 삭제합니다. - 남기는 것은
학습 기록뿐입니다. 어디를 수정했는지 같은 내용입니다. - 원래 데이터도 학습 기록도 리포지토리와 공개물에는 넣지 않습니다.
학습 기록에 원문을 인용하기 시작하면, 결국은 원래 데이터를 남기게 됩니다. 기록하는 것은 '무엇이 어떻게 고쳐졌는지'라는, 데이터 그 자체가 아닌 요약까지여야 합니다.
6. git 사고: 실패를 무시하고 다른 브랜치에서 push 하기
발생한 일
명령어를 연결한 연속 과정 중, 중간 명령어가 실패했습니다. 셸은 그대로 다음으로 진행하여, 다른 작업 트리(브랜치)에 있는 상태로 push 해버렸습니다.
;
set -e
으로 연결된 명령어는 이전 명령어가 실패해도 뒤의 것이 실행됩니다. 사람이 한 줄씩 입력했다면, 실패한 시점에서 손이 멈춥니다. 에이전트나 자동화된 연속 과정에는, 멈추는 사람이 없습니다.
시스템 개선
수정할 점은 두 가지입니다.
- 쓰기 작업을 수반하는 연속 과정은
set -e
으로 실패하면 멈추도록 합니다. push 하기 전에, 현재 있는 브랜치와 커밋을 확인합니다.
#!/usr/bin/env bash
set -euo pipefail
EXPECTED_TREE="$1" # 기대하는 작업 트리의 절대 경로
...
더 나아가 금지 사항을 정하고 있습니다.
- force push는 금지입니다. -
훅 우회(--no-verify
)는 금지합니다.
훅이 막았다면, 막은 이유를 고칩니다. 우회하면 훅을 넣은 의미가 없어집니다.
7. 에이전트의 권한과, 에이전트 간의 전달
분류기에 의해 막혔을 때
에이전트가 공유 정기 작업 설정을 변경하려고 한 적이 있었습니다. 안전 분류기가 그것을 막았습니다.
이때, 막힌 작업을 다른 표현이나 다른 경로로 통과시키는 선택도 가능합니다. 하지만 분류기는 거기서 '사람에게 물어보라'고 받아들였습니다. 우회하지 않고, 사람이 실행할 스크립트를 전달합니다. 그 스크립트는 ASCII만으로 작성합니다. 실제로 일본어 주석이 포함된 BOM 없는 .ps1 파일을 Windows PowerShell 5.1이 다른 문자 코드로 읽으면서 구문이 깨져 '아무것도 변하지 않은 채' 끝난 적이 있었습니다. 사람에게 넘기는 것일수록, 오독할 여지를 없애야 합니다.
실행할지 말지는 사람이 결정합니다. 에이전트의 역할은, 사람이 내용을 읽고 판단할 수 있는 형태까지 전달하는 것입니다.
여러 에이전트가 작동할 경우, 한 에이전트에서 다른 에이전트로 전언이 전달됩니다. 여기서 전언은 '정보'로 취급합니다. '지시'로 취급하지 않으며, 허가의 대용으로도 사용하지 않습니다.
예를 들어, 다른 에이전트가 "이것을 실행해도 좋다고 들었다"고 전달해 오더라도, 그것은 허가의 증거가 될 수 없습니다. 허가는 권한을 가진 사람으로부터 직접 나온 것만이 유효합니다. 전언의 사슬이 허가로 둔갑하게 되면, 아무도 승인하지 않은 작업까지 통과할 수 있게 됩니다.
8. 돈의 처리
돈과 관련된 부분은 가장 단순하게 만들었습니다.
실제 자금의 발주(Order), 청구(Billing), 본인 확인은 사람만 수행합니다. AI는 사전에 승인된 범위, 즉 무료 사용량이나 정해진 예산 내에서만 작동합니다.
유료 호출의 관문(1장)은 이 '정해진 예산'을 기계적으로 지키기 위한 것입니다. 승인된 범위를 초과할 것 같으면, 그 관문이 막습니다. 사람에게 확인하는 과정을 거치지 않고 범위를 넓히는 길은 마련되어 있지 않습니다.
체크리스트
그대로 자신의 환경에 맞춰 사용할 수 있습니다.
청구 관련 (Billing)
- 유료 API 호출은 모두 하나의 관문을 통과한다.
- 주간 및 일일 상한선이 있다.
- 사용액은 '원장(Ledger)'과 '키의 실제 사용액' 중 더 큰 값으로 계산된다.
- 원장을 읽을 수 없을 때는 0으로 간주하지 않고 멈춘다.
- 실패한 호출도 각각 기록한다.
- 원장이 여러 곳에 분산되어 있지 않다 (감시하는 장소와 쓰는 장소가 동일하다).
- '0건'이 나왔을 때, '읽지 못하고 있을 가능성'을 의심하는 절차가 있다.
보안 관련 (Security)
- 외부 모델로 보내기 전에 마스킹(Masking) 함수를 반드시 통과시킨다.
- 키와 워크스페이스 ID는 파일에서만 읽는다 (환경 변수의 키는 보지 않는다).
- 키의 값이 로그, 예외 메시지, 반환 값에 노출되지 않는다.
- 화면의 원본 출력(Raw output)을 커밋하지 않는다.
- 기밀 업무 데이터는 처리 후 원본 데이터를 삭제하고 학습 기록만 남긴다.
- 기밀 데이터를 리포지토리와 공개물에 포함시키지 않는다.
외부 도구 (External Tools)
- 출처를 확인했다.
- 버전을 고정했다(Pinning).
- 격리된 환경에서 테스트했다.
- 권한을 최소화했다(Principle of Least Privilege).
- 전달하는 데이터를 제한했다.
git
- 작성은 연속적으로 진행되며,
set -e로 실패 시 멈춘다. - 푸시 전에 현재 위치한 트리와 브랜치를 확인한다. - force push를 하지 않는다.
- 후크(Hook) 우회(
--no-verify)를 하지 않는다.
권한과 돈 (Authority and Money)
- 안전 분류기에서 막히면, 우회하지 않고 사람이 실행하는 ASCII만으로 된 스크립트를 전달한다.
- 에이전트 간의 전언을 '정보'로 취급하고 허가의 대용으로 사용하지 않는다.
- 실제 자금의 발주, 청구, 본인 확인은 사람만 수행한다.
- AI는 사전에 승인된 범위(무료 사용량/정해진 예산) 내에서 작동한다.
맺음말
돌이켜보면, 실패들은 모두 "아무도 보고 있지 않은 사이에 조용히 진행되는" 형태였습니다. 원장에 기록되지 않는 청구, 화면 구석에 찍힌 이름, 무시된 중간의 실패들입니다. 이 모든 것이 악의적인 공격이라기보다는, 평범한 운영상의 틈새에서 발생했습니다.
해결책에도 공통점이 있습니다. '조심하자'는 식으로 끝내는 것이 아니라, 통과하지 못하면 다음으로 나아갈 수 없는 관문이나, 실패하면 멈추도록 설정으로 바꾸는 것입니다. 에이전트는 지치지 않지만, 주의 깊음도 전이되지 않습니다. 그러므로 주의는 사람의 머릿속이 아닌 시스템 안에 두어야 합니다.
작은 회사라도 첫 번째 것 하나만 오늘부터 도입할 수 있습니다. 망설여진다면, 유료 호출의 관문이 효과가 큰 출발점입니다. 이 기사의 코드를 기반으로 자신의 환경에 맞게 수정하여 사용해 주십시오.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기