파수꾼(Watchman)은 홈랩 K3s 클러스터의 Alertmanager·Falco 경보를 받아서 읽기 전용 도구(kube_read·es_search·prom_query)로 원인을 조사하고, 텔레그램 카드로 판정(사고/의심/불명/오탐)과 조치 제안을 보내는 SecOps 에이전트입니다(설계 글). 9월 말 해커톤 제출 뒤에도 매일 운영 경보를 받고 있습니다.

이 글은 2026-09-26 ~ 10-26 한 달 동안의 작업을 정리한 것입니다. 숫자부터 보면, 머지 커밋을 뺀 커밋이 367개, 머지는 160개, 바뀐 줄은 약 +59,000 / −1,700이고, 테스트 파일은 지금 test_*.py 187개입니다. 모두 git log origin/master --since=2026-09-26으로 셌습니다.

다 묶고 보니 기능을 늘린 커밋은 많지 않았습니다. 상당수가 “모르는 걸 아는 척하지 않게” 만드는 쪽이었습니다.

내부 식별자(시크릿 이름, 감사 로그 ID, 내부 IP)는 가렸습니다. 수치는 각 커밋 메시지와 리포의 평가 문서에 적힌 값이고, 제3자가 재현한 값은 아닙니다.


1. 1~2주차: 해커톤 마감 전후 — “본선 고도화 루프”

9/26~9/30은 제출과 본선 준비 기간이었습니다. 항목에 C1~C9, D1~D7, E1~E10 같은 번호를 붙이고 하나씩 처리하는 방식으로 진행했습니다.

  • 승인 게이트 실행기. 텔레그램 버튼으로 승인하면 CronJob을 suspend하고 되돌리기도 할 수 있습니다. watchman-demo 네임스페이스에서만 동작하고, E2E 드릴은 운영 클러스터에서 1회 실측했습니다. 안전 가드 검사가 실패하면 실행 버튼은 막힙니다(fail-closed).
  • 마지막 폴백. 호스티드 NIM이 모두 응답하지 않으면 llama.cpp로 직접 띄운 Nemotron-3-Nano-4B가 판정합니다. 이 4B 모델이 길게 폭주 생성해 300초 타임아웃까지 가던 문제는 max_tokens 1024 상한과 JSON 문법 제약 디코딩으로 막았습니다.
  • 보안 하드닝. 감사 로그는 봉인 세그먼트로 회전하고, 복원 전에 해시 체인을 먼저 검증합니다. 그 외에 시간당 LLM 예산, 실행 콜백 nonce, 웹훅 JSON 깊이 상한, 다국어(일·중·스·프·포·독·이) 프롬프트 주입 탐지를 넣었습니다.
  • 사건 묶음(E4). 운영 감사 로그를 재생해 보니 실제 경보 1,554건이 사건 323건, 카드 723장으로 묶였습니다. 원래대로라면 나갔을 카드가 831장 줄었고, 그 과정에서 가장 높은 판정이 누락된 경우는 0건이었습니다.
  • OWASP ASI 커버리지 매트릭스(D6). OWASP Top 10 for Agentic Applications 2026의 ASI01~ASI10 항목마다 테스트 ID를 연결하고, CI가 그 테스트를 실제로 돌립니다.
  • 클레임-증거 추적표(C8). 제출 문서에 쓴 주장마다 그 근거가 되는 테스트나 문서를 claims.json에 묶었습니다. 이것도 CI 게이트입니다.

요약하면 이 시기에는 기능을 만드는 일과 그 기능을 쓴 문장이 사실인지 기계로 검사하는 일을 함께 했습니다.

2. 증거 도구: prom_query와 사전대조(precheck)

10/08에 세 번째 조사 도구 prom_query를 붙였습니다(#61). 이 도구는 Prometheus HTTP API의 instant/range 쿼리를 읽기 전용으로 감쌉니다. 이후 2주 동안 같은 흐름의 작업이 계속 이어졌습니다.

  • 경보 식 해독. 경보의 generatorURL에서 PromQL 식을 꺼내 LLM에게 줍니다(#62). 그전까지는 경보 이름만 보고 무엇을 재는 경보인지 추측해야 했습니다.
  • 사전대조(precheck). LLM이 조사를 시작하기 전에 서버가 사실을 먼저 조회해서 붙입니다.
    • 지금 그 식의 값(#64)
    • node-exporter 경보의 실제 대상 노드(#65~#67)
    • 스크레이프 담당(#151)
    • 복합 식(#215)
    • 최대 8일 범위(#230)
  • 서버가 확정하는 사실. “이 경보는 지난 7일 중 몇 %의 시간 동안 울렸나”, “이미 해소됐나”, “노드가 언제 합류하거나 빠졌나” 같은 값은 LLM에게 묻지 않고 서버가 계산해서 넣습니다(#188, #189, #195, #208, #210, #211).
  • 누가 했나. Kubernetes 감사 로그의 user·verb·objectRef를 “누가 바꿨나”의 증거로 씁니다(#166, #191). 예를 들어 시크릿이 새로 생겼다는 경보가 오면, 그걸 사람이 썼는지 sops-operator가 썼는지를 판정에 넣습니다.

원칙은 하나입니다. 셀 수 있는 것은 서버가 세고, LLM은 해석만 한다. 수치를 LLM에게 맡기면 그럴듯하게 틀린 값을 지어내는 일이 생기기 때문입니다.

3. 결과 0건을 “문제없음”으로 읽지 않게

조사 도구가 결과 0건을 돌려주는 이유는 여러 가지입니다.

  • 조회 창이 짧았을 수 있습니다.
  • 필드 이름이 틀렸을 수 있습니다(fluent-bit 스키마는 kubernetes.namespace_name이지 namespace가 아닙니다).
  • 그 로그가 애초에 ES로 수집되지 않았을 수 있습니다.

그런데 LLM은 0건을 받으면 “흔적 없음 → 오탐”으로 결론 내리기 쉽습니다. 10/09~10/17 사이 커밋의 상당수가 이 문제를 다룹니다.

  • 필드 이름 교정. es_search의 필드 이름을 fluent-bit 스키마로 고쳐 쓰고, 그렇게 고쳤다는 사실을 note로 함께 돌려줍니다(#76, #77, #82, #85, #222).
  • 0건 결과 주석. 결과가 0건일 때 “이 창·이 인덱스에는 원래 이 로그가 없다”처럼 이유를 덧붙입니다(#180~#187, #204~#207).
  • 셀렉터 누락 알림. kube_read가 목록 셀렉터를 조용히 버리던 버그를 고쳤습니다(#150). 버린 것을 알려 주지 않으면, 엉뚱한 목록을 받은 LLM은 그 사실조차 모릅니다.
  • 잘린 출력 표시. 도구 출력을 자를 때는 잘렸다는 사실을 함께 표시합니다(#173~#179).
  • CI 실행 누락. 같은 시기에 CI도 같은 성격의 문제를 하나 드러냈습니다. 테스트 파일을 자동으로 찾게 바꾸자, 그동안 한 번도 실행되지 않던 테스트 25개가 나왔습니다(#63). 초록불이 사실은 빈 화면이었던 셈입니다.

4. 카드: 사람이 읽는 화면

  • 카드 첫 줄에 최종 판정(사고/의심/불명/오탐)을 표시합니다(#69).
  • 경보가 얼마나 오래 울리고 있는지 표시합니다(#149).
  • 같은 경보가 다시 울리면 재발 줄을 붙이고(#87, #88), 재발로 보는 창을 72시간으로 늘렸습니다(#157).
  • alert_domain(#156). 운영 장애(디스크·노드 다운)와 보안 사고를 구분해 표시합니다. 둘 다 ‘사고’라고 쓰면 보안 사고가 장애 카드들 사이에 묻힙니다.
  • 판정이 이전과 뒤집혔을 때 경고합니다(#226).
  • 에이전트가 꺼져 있던 공백 구간은 별도 카드로 알립니다(#229).
  • 웹훅이 413으로 잘렸을 때도 카드를 보냅니다(#231).

5. 보안: 에이전트 자신이 공격면

OWASP ASI가 경고하는 위험은 파수꾼에게도 그대로 해당합니다. 파수꾼은 공격자가 쓴 로그를 읽고 판정하는 에이전트이기 때문입니다.

  • ASI06 기억 오염 게이트(#159). 파수꾼은 사람이 👍로 확정한 판정을 서명된 기억으로 저장하고(#158), 비슷한 경보가 오면 참고합니다. ASI06(Memory & Context Poisoning)이 바로 이 기억을 오염시키는 공격입니다. 그래서 ‘오탐’으로 확정된 기억이 있더라도 코드에 박힌 판정 하한을 넘지 못하도록 막았고, 그 동작을 평가 게이트로 검사합니다. OWASP 문서의 완화책인 “에이전트가 스스로 만든 출력을 신뢰 기억으로 자동 재수집하지 말 것”과 같은 방향입니다.
  • 카드 줄바꿈 주입 차단(#119, #120). 경보 라벨에 줄바꿈을 넣으면 카드에 가짜 판정 줄이 끼어들 수 있었습니다. 이를 막고, 검증 정규식도 match에서 fullmatch로 바꿨습니다.
  • 공개 지표 축소(#60). 공개 /metrics는 합계 값(PUBLIC_TOTALS)만 내보냅니다.
  • 자기 로그 차단. 파수꾼 자신의 로그는 어떤 로그 경로로도 다시 읽지 않습니다(#72, #94). 자기가 쓴 경고문을 읽고 다시 놀라는 일을 막기 위해서입니다.

판정 하한(floor)과 가드는 이번 달에 단 한 번도 느슨하게 하지 않았습니다. 다만 예외를 하나 뒀습니다. cred-sweep 경보의 대상이 전부 SOPS 관리 항목인 경우에는 하한을 면제합니다(#152). 이 예외는 감사 로그로 그 사실을 증명할 수 있는 경우에만 적용됩니다.

6. LLM은 언제든 사라진다

호스티드 모델에 기대는 이상 장애는 예외가 아니라 일상입니다.

  • 모델 폐기 대응. 모델이 410/404(폐기)를 주면 폴백으로 넘기고 텔레그램으로 한 번 알립니다(#30, #32).
  • 가드 게이트웨이 시간초과. 10/18 기록을 보니 가드 게이트웨이를 경유한 호출 44건 중 41건이 시간초과였고, 같은 모델을 직접 호출하면 10건 중 10건이 성공했습니다. 그래서 게이트웨이가 시간초과를 내면 잠시 쉬게 하고 같은 모델을 직접 부르도록 바꿨습니다(#236). 가드 검사 자체를 건너뛰는 변경은 아닙니다.
  • 연결 방어. 연결 타임아웃을 10초로 두고 TLS 리셋을 처리합니다(#234, #235). 시간 예산을 60% 쓰면 조사를 강제로 마무리합니다(#233).
  • 응답 파싱. LLM이 JSON에 // 주석을 달거나 형식을 어기는 경우에도 판정을 읽어 내도록 관대하게 파싱합니다(#98, #243).
  • 주 모델 교체. 결정적인 사건은 10/23에 일어났습니다. 주 모델 nemotron-3-super-120b가 chat 호출에 404를 주기 시작했는데, /v1/models 목록에는 여전히 남아 있어서 겉보기로는 멀쩡해 보였습니다. 그동안 파수꾼은 폴백인 ultra로 조용히 판정하고 있었습니다. 10/26에 주 모델을 nemotron-3-ultra-550b-a55b로 바꿨습니다(#249). 모델 카드 기준으로 총 550B·활성 55B 파라미터의 MoE 모델입니다.

7. 평가: 숫자가 뛰면 먼저 의심한다

10/15에 실제 NIM으로 민감도(진양성)를 측정했습니다. 결과는 공격 시나리오 12건 중 7건 탐지, 오탐 0건이었습니다(#165). 놓친 5건은 ‘불명’으로 끝났는데, 다시 보니 증거를 다 확인하고도 불명이라고 판정한 경우였습니다(#168). 증거를 못 찾은 게 아니라 판단을 내리지 않은 것입니다.

10/25에는 “경보 자체가 공격 기법이면 ‘불명’ 대신 ‘의심’으로 판정하라”는 프롬프트를 넣었습니다(#248). 그런데 다시 측정하자 탐지가 10~12건으로 뛰었습니다. 개선 폭이 지나치게 커서 결과 행의 verdict_model을 확인했고, 판정한 모델이 super에서 ultra로 바뀌어 있었습니다. 앞 절의 10/23 404 때문입니다. 결국 ‘불명’에서 멈추던 습관은 프롬프트 문제라기보다 super 모델의 특성에 가까웠습니다.

이 일로 남은 교훈은 세 가지입니다.

  1. 평가 결과는 모델이 같은 실행끼리만 비교합니다.
  2. 프롬프트를 바꿀 때는 같은 날, 같은 모델로 수정 전과 후를 함께 돌립니다.
  3. “X로 끝내라” 같은 지시는 모델이 천장으로 읽습니다. #248의 첫 문구로 돌렸을 때 ‘사고’여야 할 6건이 ‘의심’으로 내려갔습니다. 바닥으로 쓰려면 “최소 X”라고 써야 합니다.

위 탐지 수치는 모두 이 리포 안의 12건짜리 합성 시나리오 기준입니다. 중립적인 외부 벤치마크나 다른 SecOps 에이전트와 직접 비교한 결과는 없습니다.

8. 인프라: 노드가 줄었다

  • CI의 무거운 잡은 원래 isagal 셀프호스티드 러너에서 돌았습니다(10/03). isagal이 전기 문제로 퇴역하면서 louise로 옮겼습니다(10/15).
  • 10/26 기준 클러스터는 4노드입니다. 이 과정은 앞 글에 정리했습니다.

정리

축 한 달 전 지금
조사 도구 kube_read · es_search + prom_query, 서버 사전대조·서버 확정 사실
0건 결과 그대로 전달 이유 주석·필드 교정·잘림 표시
카드 판정·근거 + 지속시간·재발·장애/보안 구분·판정 뒤집힘
기억 없음 서명 기억 + ASI06 하한 게이트
LLM 장애 폴백 + 게이트웨이 우회·시간 예산·폐기 감지, 주 모델 ultra
평가 케이스 뱅크 + 실 NIM 민감도, verdict_model 기준 비교

이 에이전트에서 가장 비싼 실패는 틀린 판정이 아닙니다. 모르는 것을 아는 것처럼 말하는 판정입니다. 틀린 판정은 사람이 의심하고 다시 보지만, 자신 있게 쓴 ‘오탐’은 아무도 다시 열어 보지 않습니다. 이번 한 달 작업의 대부분이 그 문장이 나오지 않게 막는 데 들어갔습니다.

References

  1. OWASP GenAI Security Project, OWASP Top 10 for Agentic Applications for 2026, 2025-12-09. 본문 PDF: genai.owasp.org/download/52117 (ASI06 Memory & Context Poisoning)
  2. J. Sotiropoulos, OWASP Top 10 for Agentic Applications — The Benchmark for Agentic Security, OWASP GenAI, 2025-12-09
  3. Kubernetes Docs, Auditing · kube-apiserver Audit Configuration (v1)
  4. Prometheus Docs, HTTP API
  5. NVIDIA, nemotron-3-ultra-550b-a55b model card · NIM API reference
  6. 1차 데이터: watchman-agent 리포 git log (2026-09-26 ~ 10-26), PR #30~#249 커밋 메시지, eval/ 평가 문서