콘텐츠로 건너뛰기

[DevOps] AI 에이전트 권한 설계 — ‘되돌릴 수 있다’는 착각

  • 기준

안녕하세요. 정리하는 개발자 워니즈입니다. AI 에이전트를 운영에 붙이기 시작하면 반드시 마주치는 질문이 있습니다. AI 에이전트 권한을 어디까지 허용할 것인가.

흔한 답은 “위험한 작업은 사람이 하고 안전한 건 맡긴다”입니다. 저도 그렇게 정했고, 다음 단계로 “되돌릴 수 있으면 맡겨도 된다” 는 기준을 세웠습니다. 그리고 이 두 번째 기준이 첫 번째보다 더 위험할 수 있다는 걸 사고 두 번을 겪고 나서야 알았습니다.

미리 밝히면 되돌림 가능성(one-way door / two-way door)이나 파급 범위(blast radius)는 운영 분야의 오래된 개념입니다. 이 글은 그 개념을 AI 에이전트 권한 설계에 적용하다가 제가 빠진 함정에 대한 기록입니다.

그러면 본격적으로 정리해보도록 하겠습니다.

1. ‘위험한 작업’만으로는 규칙이 되지 않는다

kubectl rollout restart는 위험한가요?

답이 갈립니다. 무중단 배포에 레플리카가 3개면 부담이 적고, 레플리카 1개에 초기화가 40초 걸리는 서비스면 위험합니다. 트래픽이 적은 새벽이면 영향이 작지만, 그 시간엔 대응 인력도 없어서 복구는 오히려 늦어집니다.

즉 “위험도”는 맥락마다 답이 달라집니다. 사람이 매번 판단한다면 그걸로 충분하지만, 에이전트에게 권한을 미리 나눠줘야 하는 상황에서는 사전에 분류 가능한 축이 필요합니다.

그래서 저는 이런 질문을 축으로 삼았습니다.

“이 작업이 잘못됐을 때 되돌릴 수 있는가.”

되돌릴 수 있으면 맡기고, 없으면 사람이 한다. 명확해 보였습니다. 문제는 “되돌릴 수 있다”를 제가 잘못 판정하고 있었다는 것입니다.

2. 사고 ①: 지시를 어겼는데 살았다 — 운이 좋았을 뿐

에이전트에게 작업을 위임하며 브리프 맨 위에 이렇게 적었습니다.

push / PR 생성 / merge 금지. 로컬 커밋까지만.

모호하지 않았고 맨 위에 있었고 강조도 했습니다. 그런데 작업 후 원격을 확인해보니 PR이 생성돼 있었고 이미 머지까지 되어 있었습니다.

여기서 두 가지를 배웠는데, 두 번째가 훨씬 중요했습니다.

첫째, 프롬프트의 금지는 강제가 아닙니다. AI 에이전트 권한을 브리프 문장으로 제한하려 한 것 자체가 설계 실수였습니다. 브리프는 모델이 참고하는 컨텍스트일 뿐 실행을 차단하지 않습니다. 확률적으로는 대부분 지켜지지만, 시스템이 강제하는 경계와는 성격이 다릅니다. 근본 원인도 “불복종”이 아니라 애초에 필요 없는 쓰기 자격증명이 쥐어져 있었다는 과권한입니다.

둘째, 제가 안 다친 이유는 “git이라서”가 아니었습니다. 저는 그때 “git은 revert가 있으니 되돌릴 수 있는 칸”이라고 정리했습니다. 나중에 보니 틀린 정리였습니다.

git revert가 되돌리는 것은 저장소의 코드 상태뿐입니다.

  • 머지를 트리거로 배포가 돌았다면 배포된 결과는 revert되지 않습니다
  • DB 마이그레이션이 실행됐다면 스키마는 그대로입니다
  • 외부로 나간 알림·웹훅은 회수되지 않습니다
  • 시크릿이 커밋에 포함됐다면 revert 후에도 git 이력에 남아 있어, 사실상 유출입니다

제 경우는 이 부작용이 하나도 없는 변경이었습니다. 그래서 revert 하나로 끝났습니다. 되돌릴 수 있는 작업이라서가 아니라, 되돌릴 게 코드밖에 없었던 상황이라서 살았습니다.

3. 사고 ②: 파급 범위를 세지 않았다 — 이번엔 사람이 냈다

두 번째 사고의 행위자는 에이전트가 아니라 저 자신입니다. 사람도 같은 방식으로 실패한다는 점에서 오히려 중요한 사례입니다.

로컬 머신을 정리하다가 부모 프로세스가 launchd인 가상화 관련 프로세스를 발견했습니다. “부모가 launchd니까 고아 프로세스”라고 판단하고 종료시켰습니다. 결과는 이랬습니다.

  • 그 프로세스는 컨테이너 런타임의 VM이었고, 통째로 내려감
  • 그 위에서 돌던 MCP 서버 컨테이너 14개 전멸
  • 진행 중이던 다른 작업 세션 4개 중단

판단부터 틀렸습니다. macOS에서 시스템 서비스가 launchd 아래 붙는 건 정상이라, 부모가 launchd라는 것만으로는 고아인지 아닌지 알 수 없습니다.

하지만 더 중요한 건 대상을 잘못 잡았다는 점입니다. 저는 “프로세스 1개 재시작”으로 봤지만 실제 대상은 VM 전체였고, 거기 얹힌 컨테이너와 세션까지가 영향 범위였습니다. 컨테이너는 VM과 함께 되살아났지만, 끊긴 세션의 작업 맥락은 checkpoint가 없어 복구하지 못했습니다.

영향 범위는 명령이 지목한 대상이 아니라, 그 대상에 의존하는 것들로 정의된다.

4. 두 사고가 가리키는 한 가지

두 사고는 달라 보이지만 같은 실수입니다.

내가 본 것 실제
사고 ① “git이니까 revert 가능” 되돌릴 게 코드뿐이었을 때만 참
사고 ② “프로세스 1개 재시작” 대상은 VM, 영향은 컨테이너 14개 + 세션 4개

둘 다 “되돌릴 수 있다”를 실제 확인 없이 명령어 이름만 보고 판정했습니다.

되돌리는 명령이 존재한다는 것과 실제로 복구된다는 것은 다릅니다.

kubectl scale은 되돌리는 명령이 있지만 이전 replica 수를 기록해 두지 않았다면 무엇으로 되돌릴지 모릅니다. HPA가 붙어 있으면 상태가 더 복잡해집니다. ConfigMap 수정은 기본적으로 변경 이력이 없어서, 바꾸기 전 값을 따로 저장하지 않았다면 복원할 원본이 없습니다. 게다가 잘못된 설정이 이미 데이터를 망가뜨렸다면 설정을 되돌려도 데이터는 그대로입니다.

5. 그래서 표를 이렇게 바꿨습니다

처음에 저는 명령을 복구 시간에 따라 한 줄로 세웠습니다. 그 표는 버렸습니다. 복구 시간·데이터 손실·영향 범위·기밀성은 서로 환산되지 않는 별개의 축이라, 하나의 등급으로 합치는 순간 부정확해지기 때문입니다.

지금은 축별로 보고 그중 가장 엄격한 통제를 적용합니다. AI 에이전트 권한은 이 다섯 축을 모두 통과한 범위로만 열립니다.
AI 에이전트 권한을 복구 비용으로 나눈 표 — 복구 0초(읽기 전용, 단독 실행) / 분 단위(되돌리기 명령 존재, 사후 검증) / 시간 단위(재생성 필요, 사람 승인) / 복구 불가(원본 소실, 권한 없음)

평가 축 물어볼 것
복구 가능성 되돌리는 명령이 아니라 되돌릴 원본이 있는가 (이전 값 캡처·스냅샷·PITR)
부작용 배포·마이그레이션·외부 알림 등 되돌릴 수 없는 파생 동작이 있는가
영향 범위 이 대상에 의존하는 것이 무엇인가
기밀성 되돌려도 이미 나간 정보(로그의 토큰·개인정보)가 있는가
검증 여부 그 복구 절차를 실제로 해본 적 있는가

어느 한 축이라도 걸리면 등급을 올립니다. “복구 명령이 있으니 괜찮다”는 판단은 여기서 걸러집니다.

에이전트 권한은 이 평가를 통과한 범위로만 줍니다.

  • 무변경 조회 — 단독 실행. 단 로그에 토큰·개인정보가 섞일 수 있으므로 마스킹과 네임스페이스 제한이 전제입니다. 읽기라고 무조건 안전한 게 아닙니다
  • 되돌릴 원본이 확보된 변경 — 사전 조건 검사 + 실행 후 검증. 이전 값 캡처가 없으면 이 칸에 넣지 않습니다
  • 부작용이 딸린 변경(배포·마이그레이션·외부 통신) — 사람이 실행
  • 검증된 백업이 없는 삭제 — 권한 자체를 주지 않습니다

6. 이 판정을 시스템으로 강제하기

사고 ①이 알려준 건 프롬프트로는 경계를 만들 수 없다는 것이었습니다. AI 에이전트 권한은 판정만으로 좁혀지지 않고, 실제 인증·인가 계층에 반영돼야 합니다. 판정 결과는 권한 설정으로 옮겨야 합니다.

⚠️ 예시 구성입니다. 환경에 맞게 리소스를 조정하세요.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: agent-readonly
rules:
  - apiGroups: [""]                # core
    resources: ["pods", "events"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["pods/log"]
    verbs: ["get"]                 # 로그 조회는 get 이면 충분
  - apiGroups: ["events.k8s.io"]   # 클라이언트에 따라 이쪽 Event 도 필요
    resources: ["events"]
    verbs: ["get", "list", "watch"]
  - apiGroups: ["apps"]
    resources: ["deployments", "replicasets"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding                  # ClusterRoleBinding 이 아니라 네임스페이스 한정
metadata:
  name: agent-readonly
  namespace: my-service
subjects:
  - kind: ServiceAccount
    name: agent-sa
    namespace: my-service
roleRef:
  kind: ClusterRole
  name: agent-readonly
  apiGroup: rbac.authorization.k8s.io

API 그룹별로 rule을 나눠야 하고(core의 podsappsdeployments를 한 줄에 섞으면 존재하지 않는 조합이 됩니다), Role 정의만으로는 아무 권한도 생기지 않으므로 바인딩까지가 한 세트입니다. 동사 단위로 권한을 끊는 방식은 Kubernetes RBAC 공식 문서의 기본 개념 그대로입니다.

다만 RBAC 하나로는 부족합니다. 권한은 여러 바인딩의 합집합이라 다른 곳에서 쓰기를 주고 있으면 소용없고, 에이전트가 GitOps 저장소나 클라우드 자격증명 같은 다른 경로로 같은 결과를 만들 수 있으면 그 경로도 막아야 합니다. 사고 ①이 정확히 이 경우였습니다 — k8s가 아니라 git 자격증명이 열려 있었습니다.

파괴적 작업 앞에 영향 범위를 세는 절차를 둔다

사고 ②의 교훈은 코드보다 순서에 가깝습니다. 무엇을 지우기 전에 거기 의존하는 것을 먼저 센다. 브랜치라면 아직 반영되지 않은 커밋을, 프로세스라면 그 위에 얹힌 컨테이너와 세션을 셉니다.

이때 기준은 로컬 상태가 아니라 원격·실제 상태여야 합니다. 로컬 main이 오래됐으면 “미반영 커밋 0건”이라는 잘못된 안심을 얻게 됩니다. git fetch 후 원격 ref로 비교하고, cherry-pick으로 이미 반영된 커밋이 있는 워크플로라면 patch 단위 비교까지 필요합니다.

보고가 아니라 상태를 확인한다

사고 ①에서 에이전트는 “로컬 커밋까지 완료했습니다”라고 보고했습니다. 사실이 아니었습니다.

에이전트의 보고는 자기 행동에 대한 서술이지 시스템 상태가 아닙니다. 원격 브랜치와 PR 목록을 직접 조회해야 하고, 이때도 이름 일부만 맞춰 찾으면 다른 브랜치를 잡을 수 있으니 전체 ref로 정확히 질의합니다.

7. 그래서 자동화를 포기했나

아닙니다. AI 에이전트 권한을 무변경 조회로 좁혀도 얻을 게 많습니다.

장애 대응에서 시간이 걸리는 건 엔터를 치는 순간이 아니라 진단과 커맨드 작성입니다. 에이전트가 로그를 긁고 이벤트를 모으고 원인을 정리한 뒤 조치 커맨드를 문자열로 출력해주면 그 부분이 사라집니다.

단계 주체 권한
이상 감지 에이전트 조회
원인 분석 에이전트 조회
조치 커맨드 생성 에이전트 없음 (텍스트 출력)
검토·실행 사람 실행
결과 검증 에이전트 조회

사람의 역할이 “복사해서 붙여넣기”로 줄어드는 건 아닙니다. 에이전트가 만든 커맨드 자체가 틀릴 수 있어 대상과 예상 결과를 확인하는 검토는 남습니다. 다만 진단부터 직접 하는 것보다는 훨씬 짧습니다.

권한을 네트워크 레벨에서도 좁히고 싶다면 Kubernetes NetworkPolicy 적용하기에서 다룬 접근이 같은 사고방식의 연장입니다. 상태를 바꾸는 작업을 어디까지 자동화할지는 Kubernetes HPA 실전 적용에서 오토스케일링 사례로 정리한 적이 있습니다.
복구 비용은 파급 범위까지 더해서 센다 — 직접 대상(프로세스 1개, 복구 분 단위) → 의존 대상(컨테이너 14개, 세션 4개) → 실제 합계(장애 범위 확대, 사람 승인 구간)

8. AI 에이전트 권한 설계 요약

  • “위험한 작업 금지”는 출발점일 뿐, 맥락마다 답이 달라 사전 분류가 안 된다
  • 되돌리는 명령의 존재가 복구 가능성을 보장하지 않는다. 되돌릴 원본이 있는지, 되돌릴 수 없는 부작용이 딸렸는지를 본다
  • 복구 가능성·부작용·영향 범위·기밀성·검증 여부는 별개 축이다. 하나로 합치지 말고 가장 엄격한 통제를 적용한다
  • 영향 범위는 명령이 지목한 대상이 아니라 거기 의존하는 것들로 정의된다
  • 경계는 프롬프트가 아니라 RBAC + 불필요한 자격증명 회수 + 우회 경로 차단으로 만든다
  • 에이전트의 완료 보고는 상태 실측으로 확인한다

당장 해보실 수 있는 것 하나를 남기겠습니다. 에이전트가 쓰는 ServiceAccount의 권한을 확인해보세요.

NS=my-service; SA=agent-sa
kubectl auth can-i --list \
  --as="system:serviceaccount:${NS}:${SA}" --namespace="$NS"

이 출력은 해당 네임스페이스에서 Kubernetes API 서버가 허용하는 범위만 보여줍니다. admission 정책이나 다른 자격증명 경로는 따로 봐야 합니다. 그래도 여기에 deletepatch가 보인다면, 최소한 그 부분은 시스템이 아니라 프롬프트로 막고 있다는 뜻입니다.

다음 글에서는 “되돌릴 원본이 확보된 변경” 칸을 실제로 어떻게 운영했는지, 사전 조건 검사를 어디서 실패했는지 정리해보겠습니다. 읽어주셔서 감사합니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다