기술 블로그
기술설명형LUNA AI-GRCMCP Gateway보안설계 원칙

통제 장치가 고장 나면, 열어야 할까 닫아야 할까

2026.09.219분 분량
통제 장치가 고장 나면 열어야 할까 닫아야 할까 — 오픈소프트랩 기술블로그
AI 정보보호 설계 (2)에서 차단과 허용 사이에 칸을 더 뒀다고 썼습니다. 그 칸들이 고장 났을 때를 다룹니다.

안녕하세요. 오픈소프트랩에서 생성형 AI 거버넌스 제품을 만드는 개발팀입니다.

저희가 만드는 기능은 차단하는 일을 합니다. 직원이 외부 AI에 회사 기밀을 전달하지 않도록, 정책에 어긋나는 요청과 응답을 중간에서 걸러냅니다.

개발을 시작할 때 답을 먼저 정해둔 질문이 하나 있었습니다.

차단하는 장치 자체가 고장 나면, 요청을 통과시켜야 할까 차단해야 할까?

답은 차단입니다. 이것을 fail-closed라고 부릅니다. 반대로 고장 났을 때 통과시키는 것이 fail-open입니다. 안전장치가 고장 났을 때 어느 쪽으로 동작할지를 정하는 설계 원칙인데, 상황에 따라 정답이 다릅니다. 지하철 개찰구는 화재 시 사람이 빠져나가야 하므로 fail-open이 맞고, 금고는 정전이 나도 잠겨 있어야 하므로 fail-closed가 맞습니다.

fail-open과 fail-closed 비교, 그리고 코드 리뷰에서 반복해서 확인하게 된 여섯 자리
fail-open과 fail-closed 비교, 그리고 코드 리뷰에서 반복해서 확인하게 된 여섯 자리

팀 전원이 동의했고 문서에도 적었습니다. 여기까지는 쉬웠습니다.

그런데 코드에는 다르게 적혀 있었습니다

원칙을 정한 뒤로 1년 동안, 코드를 읽을 때마다 원칙과 반대로 동작하는 자리가 나왔습니다. 여섯 번입니다.

처음 한두 번은 실수로 봤습니다. 세 번째부터 패턴이라고 생각했습니다. 여섯 번 모두 차단하는 코드에서 나오지 않았기 때문입니다.

차단하는 코드는 모두가 주의해서 씁니다. 리뷰도 거기에 집중됩니다. 문제는 늘 그 주변에 있었습니다.

예외를 처리하는 자리

검증을 담당하는 외부 부품을 호출하는데, 응답이 없거나 오류가 날 수 있습니다. 그 자리의 코드가 이렇게 되어 있었습니다.

try {
    result = validator.validate(content);
} catch (Exception e) {
    log.warn("검증 실패, 통과 처리", e);
    return ALLOW;   // 검증에 실패하면 통과시킨다
}

작성자를 탓할 코드가 아닙니다. 서비스가 멈추지 않게 하려는 의도로 나옵니다. 검증 부품에 문제가 생겼다고 직원들이 AI를 아예 못 쓰게 되면 업무에 지장이 생기니, 통과시키고 로그를 남겨두면 나중에 확인할 수 있다고 판단하는 것입니다.

그런데 이 코드가 실제로 의미하는 것은 이렇습니다. 검증 부품을 중단시키면 통제가 사라진다.

빈 값을 해석하는 자리

외부 연동용 API 키를 발급할 때 접속을 허용할 IP를 지정하게 만들었습니다. 이 값이 비어 있으면 무엇으로 봐야 할까요.

전체 허용으로 읽기 쉽습니다. 입력하지 않았으면 제한하지 않겠다는 뜻으로 보는 게 자연스럽고, 많은 설정 화면이 그렇게 동작합니다.

보안 설정에서는 다릅니다. 빈 값은 "제한 없음"이 아니라 "아직 정하지 않음"입니다. 아직 정하지 않은 보안 설정의 기본값이 전체 허용이면, 급하게 키를 발급한 사람은 자기가 무엇을 열었는지 모릅니다.

감사 코드를 적는 자리

위험도를 사후에 분석하는 처리가 있고, 감사 로그(Audit Log, 누가 언제 무엇을 했고 시스템이 어떻게 판정했는지 남기는 기록)에 분석 수행 여부를 코드로 남깁니다. 이 값이 항상 "수행함"으로 고정되어 있었습니다.

실제로 분석이 돌지 않는 경로가 세 가지 있었는데, 그 경우에도 로그에는 "수행함"이 남았습니다. "수행 안 함", "실패함"을 뜻하는 코드는 정의만 되어 있고 아무 데서도 쓰이지 않았습니다.

요청이 통과된 것은 아니니 통제가 뚫린 상황은 아닙니다. 그래도 감사 기록이 사실과 다른 것은 fail-open입니다. 나중에 "이 요청은 위험 분석을 거쳤는가"를 확인할 때, 로그는 거치지 않은 요청도 거쳤다고 답합니다.

이걸 고치려다 구조까지 손댔습니다. 분석을 결과를 받지 않는 비동기 호출로 던져두면, 실제로 돌았는지를 감사 코드로 되돌리는 게 원천적으로 불가능합니다.

기록해야 할 사실이 있다면, 그 사실을 알 수 없는 구조로는 만들 수 없습니다.

판정 근거를 가져오는 자리

권한을 확인할 때 세션(Session, 로그인한 사용자별로 서버가 기억해두는 정보)에 저장된 값을 근거로 삼는 경우가 있습니다. 이때 확인해야 할 것은 그 값을 누가 언제 바꾸는가입니다.

화면 진입 시점에 갱신되는 값이라면, 사용자가 직전에 연 메뉴에 따라 매번 덮어써질 수 있습니다. 그러면 특정 기능에 대한 권한이 없어도 권한 있는 다른 메뉴를 한 번 열고 오는 것만으로 판정이 바뀝니다. 권한 검사 코드는 멀쩡히 있습니다. 검사하는 값이 의도한 의미가 아닐 뿐입니다.

여기에 자주 겹치는 문제가 하나 더 있습니다. 신규 API의 주소 형식이 기존과 다르면 공통 검사에 아예 걸리지 않습니다.

지금은 안 터지는 자리

정책을 조회하는 SQL에 결과를 한 건으로 확정하는 처리가 빠져 있었습니다. 조건이 맞으면 여러 건을 반환하면서 DB 오류가 나고, 요청 전체가 실패합니다.

그런데 지금은 문제가 없었습니다. 현재 데이터에 그런 조건이 없었기 때문입니다.

설계 문서에는 "이 조회는 한 건으로 확정해 반환한다"고 적혀 있었습니다. 사실이 아니었습니다. 문서는 의도를 적고, 스키마는 제약을 겁니다. 스키마가 막고 있지 않으면 언젠가 그 조건은 성립합니다.

반대로 동작한 자리

마지막은 방향이 반대라 함께 적습니다.

추적 로그를 기록하는 코드가, 로그 저장에 실패하면 요청 처리 전체를 실패시킬 수 있는 위치에 있었습니다. fail-open이 아니라 과도한 fail-closed입니다.

원칙은 fail-closed지만 무엇이 통제이고 무엇이 부가 기능인지는 구분해야 합니다. 검증은 통제라 실패하면 차단해야 하고, 로그 적재는 관측이라 실패해도 요청을 중단시키면 안 됩니다. 이 구분을 안 하면 안전하게 만들었는데 서비스가 안 도는 상태가 됩니다.

그래서 읽는 방법을 바꿨습니다

여섯 번을 겪고 나서 코드 리뷰에서 던지는 질문을 바꿨습니다. "제대로 차단하는가"는 이미 다들 보고 있었습니다.

  • 이 함수가 예외를 던지면 요청은 어디로 가는가
  • 이 설정값이 비어 있으면 무엇으로 해석되는가
  • 이 로그에 기록되는 값은 실제로 일어난 일인가, 일어났을 것으로 가정한 일인가
  • 이 판정에 쓰는 값은 누가 언제 바꾸는가
  • 이 주소는 기존 공통 처리에 실제로 걸리는가
  • 지금 문제가 없는 이유가 코드 때문인가, 현재 데이터가 우연히 그런 것인가

여섯 건 중 네 건이 이 질문들을 들고 코드를 다시 읽는 과정에서 나왔습니다. 새 도구를 넣은 게 아니라 읽는 각도를 바꾼 것뿐입니다.

얻은 것과 포기한 것이 있습니다. 얻은 것은 찾는 속도입니다. 포기한 것은 리뷰 시간입니다. 질문 여섯 개를 다 들고 읽으면 한 번에 보던 분량이 줄어듭니다. 그래서 지금은 보안 판정에 관여하는 코드에만 이 질문을 적용하고 있습니다.

아직 못 찾은 것이 있을 겁니다

여섯 건을 찾았다는 말은 여섯 건이 있었다는 뜻이지, 이제 없다는 뜻이 아닙니다.

특히 다섯 번째 유형이 남습니다. 지금 데이터에서 안 터지는 결함은 코드를 읽어야 보입니다. 테스트가 잡아준 게 아닙니다.

지금은 보안 판정에 관여하는 조회를 목록으로 관리하면서, 스키마가 실제로 단건을 보장하는지 하나씩 확인하고 있습니다. 사람이 하는 일이라 느립니다.

다음으로는 스키마 제약과 운영 데이터 분포를 대조해 후보를 뽑는 쪽을 보려고 합니다. 자동으로 판정까지 하기는 어렵겠지만, 읽어볼 순서를 정해주는 정도는 될 것 같습니다.


오픈소프트랩 개발팀이 작성합니다. 생성형 AI 사용 통제와 AI 에이전트의 도구 실행 통제를 만들고 있습니다.