404를 포함한 모든 것을 통과시킨 안전 게이트
한 콘텐츠 필터가 몇 달 동안 깨끗하다고 보고했습니다. 그것은 아무것도 읽고 있지 않았습니다. 검사가 조용히 승인으로 뒤바뀌는 네 가지 방법과 실패를 명확하게 만드는 방법.
한 게시 파이프라인에는 배포할 때마다 게시된 페이지를 가져와 금지된 용어를 grep으로 찾고, 일치하는 것이 있으면 게시물을 철회하는 검사가 있었습니다. 이 검사는 매번 문제가 없다고 보고했습니다. 또한, 존재하는 내내 다른 어떤 것도 보고할 수 없었습니다. 페이지 가져오기는 조용히 아무것도 반환하지 않았고, 빈 내용에 대한 grep은 아무것도 찾지 못하며, '찾지 못함'은 "안전함"을 의미하도록 설정되어 있었습니다.
버그는 가져오기(fetch)가 아닙니다. 버그는 "문제를 찾지 못함"과 "찾아볼 수 없음"이 동일한 결과를 냈고, 그중 하나만이 사실이었다는 점입니다.
반전하는 게이트의 형태
검사는 세 가지 가능한 결과를 가지지만 대부분의 구현은 두 가지만 인코딩합니다. 문제를 발견하거나, 문제가 없음을 확인하거나, 또는 검사에 실패할 수 있습니다. 세 번째 경우에서 안전성이 새어 나가게 되는데, 이는 검사를 작성하는 가장 쉬운 방법이 "검사 실패"를 "문제 없음"과 구별할 수 없게 만들기 때문입니다.
보통 이러한 검사들이 시작되는 형태인 셸 버전을 살펴봅시다.
curl -s "$URL" | grep -iEf patterns.txt && echo "BLOCKED"
해당 라인에서 발생하는 모든 실패는 아무런 표시 없이 조용히 처리됩니다. 404 오류가 발생하면 curl은 빈 본문과 종료 코드 0을 반환하는데, 이는 -s가 진행 상황만 표시하지 않기 때문입니다. DNS 실패 시에도 빈 본문이 반환됩니다. 패턴 파일이 비어 있으면 grep은 일치하는 것을 찾지 못합니다. 파이프라인 종료 상태는 마지막 명령어만 반영하므로 curl이 완전히 실패해도 grep은 "일치 항목 없음"을 보고하게 됩니다. 네 가지 서로 다른 고장이 하나의 구별할 수 없는 결과를 낳고, 바로 그 결과 때문에 게시물이 통과됩니다.
이러한 형태는 콘텐츠 필터에만 국한되지 않습니다. 연결 오류를 "보고된 오류 없음"으로 처리하는 상태 확인. 아무것도 일치하지 않는 glob에서 호출된 린터. 규칙 세트 다운로드에 실패한 보안 스캐너. 각각의 경우에 메커니즘은 정상이고, 보고서는 녹색이며, 그 녹색은 대시보드가 암시하는 것의 정반대를 의미합니다.
동일한 검사가 실패한 네 가지 방식
문제의 검사는 네 가지 독립적인 방식으로 실패했습니다. 각 방식 하나만으로도 실패하기에 충분했고, 또 각 방식 하나만으로는 알아차리기 어려웠기 때문에 이 점을 명확히 밝힐 가치가 있습니다.
검사가 다른 문서를 읽고 있었습니다
원래 검사는 HTML을 마크다운으로 변환한 후 반환하는 헬퍼를 통해 페이지를 가져왔습니다. 이는 읽기에는 편리했지만, 패턴이 마크업을 기준으로 작성되었기 때문에 매칭에는 치명적이었습니다. 속성, 메타 태그, 구조화된 데이터 블록은 변환 과정에서 살아남지 못합니다. 검사는 실행되었지만 아무것도 찾지 못했고, 의도치 않은 식별자가 포함될 가능성이 가장 높은 필드를 전혀 보지 못했습니다.
이 교훈은 이 헬퍼 하나에 국한되지 않고 일반화될 수 있습니다. 필터는 특정 표현형을 기준으로 작성됩니다. 소스와 필터 사이의 무언가가 그 표현형을 다시 작성한다면, 필터는 이제 프로덕션 어디에도 존재하지 않는 문서를 검사하게 됩니다. 사용자가 받는 바이트를 그대로 가져오십시오.
노이즈가 너무 커서 아무도 신호를 읽지 않았습니다
동일한 검사가 모든 페이지에서 7~8개의 일치 항목을 보고했는데, 이는 항상 동일한 것들이었습니다. 바로 광고 스크립트에 포함되어 의도적으로 사이트의 모든 페이지에 존재하는 게시자 ID였습니다.
실행할 때마다 '양치기 소년'처럼 구는 검사는 더 이상 확인하지 않게 됩니다. 더 나쁜 것은, 운영자가 포괄적인 예외 처리를 추가하도록 훈련시킨다는 점이며, 포괄적인 예외 처리는 실제 일치 항목이 무시되는 곳입니다.
본능적으로 검색 영역을 기사 본문으로 좁히려고 합니다. 이를 측정해 본 결과, 더 위험한 방향으로 잘못된 접근임이 드러났습니다. 광고도 기사 요소 내부에 렌더링되므로 범위를 좁히면 오탐(false positive)의 절반만 줄어들 뿐이며, 그러면서도 작성자의 말에서 파생되어 식별자가 위치할 바로 그곳인 제목, 메타 설명, 구조화된 데이터는 새로 제외됩니다.
해결책은 범위를 줄이는 것이 아니라 노이즈를 제거하는 것입니다. script, style, iframe, noscript 블록을 제거하고, 광고 컨테이너 태그는 텍스트를 삭제하지 않고 감싸는 태그만 해제하며, 구조화된 데이터는 스크립트 제거 전에 추출했다가 나중에 다시 추가하여 유지합니다. 동일한 페이지에서 측정한 결과는 다음과 같습니다. 이전에는 7개 일치, 범위를 좁혔을 때 4개, 노이즈를 제거하고 전체를 커버했을 때 0개.
패턴 파일이 비어 있도록 허용되었습니다
거부 목록(denylist) 로드에 실패하면 패턴 집합은 비어 있게 되고, 빈 패턴 집합은 아무것도 매칭하지 않습니다. 이는 진짜 깨끗한 페이지와 동일하게 "깨끗함"으로 나타납니다.
이 문제는 저렴한 비용으로 해결할 수 있지만 거의 실행되지 않습니다.
PAT="$(grep -vE '^\s*#|^\s*$' denylist.txt)"
[ -n "$PAT" ] || { echo "empty denylist, cannot verify"; exit 1; }
검사가 의존하는 모든 입력은 동일한 취급을 받아야 합니다. "나쁜 것"을 정의하는 요소가 누락될 수 있다면, 그 부재는 판정이 아니라 오류여야 합니다.
누구도 게이트가 실패할 수 있다는 사실을 테스트하지 않았다
가장 근본적인 문제는 이 모든 것이 가상적인 이야기가 아니라는 점입니다. 존재하지 않는 URL에 검사를 실행시켜 보고 문제가 없다고 보고하는 것을 관찰함으로써 약 1분 만에 발견할 수 있는 문제였습니다.
게이트는 통과하는 방향으로만 테스트됩니다. 누군가 게시물을 작성하고, 검사를 실행하고, 통과하는 것을 보고, 배포합니다. 실제 위반 사항을 만들어내는 것이 일처럼 느껴지고 약간 위험하게 느껴지기 때문에 실패하는 방향은 거의 실행되지 않습니다. 그래서 중요한 분기, 즉 릴리스를 막아야 하는 분기는 결코 실행되지 않는 유일한 분기가 됩니다.
이것이 부주의해서가 아니라 체계적인 문제인 데에는 이유가 있습니다. 테스트를 위해 실제 위반 사항을 만들어낸다는 것은 실제 비밀 정보가 포함된 텍스트를 작성하는 것을 의미하며, 아무도 그것을 커밋하고 싶어 하지 않습니다. 망가진 대상을 검사 대상으로 지정하는 것은 의도적으로 무언가를 망가뜨리는 것을 의미합니다. 두 가지 모두 일부러 문제를 만드는 것처럼 느껴지고, 검사는 작동하는 것처럼 보이므로 그 작업은 영원히 미뤄집니다.
두 가지 습관이 이 문제를 영구적으로 해결하며, 둘 다 이러한 불편함을 피하게 해줍니다.
CI에서 의도적으로 문제를 포함한 픽스처(fixture)로 게이트를 테스트하십시오. 거부 목록에 있는 용어 하나를 포함하는 작은 파일과, 그 파일에 대해 검사가 0이 아닌 값으로 종료된다는 단언(assertion)을 추가합니다. 이 픽스처는 절대 배포되지 않고, 실제 페이지에 닿지 않으며, 누군가 패턴 로딩이나 종료 코드를 망가뜨리면 큰 소리로 실패를 알립니다. 실제 용어를 커밋하는 것이 꺼려진다면, 순전히 이 목적을 위해 거부 목록에 존재하는 감시용 항목을 사용하십시오.
의도적으로 도달할 수 없는 대상으로 게이트를 테스트하십시오. 404 오류를 보장하는 URL을 대상으로 지정하고, 문제가 없다고 보고하는 것이 아니라 실패를 보고하는지 단언하십시오. 그 단언 하나만으로도 첫날에 원래 버그를 잡을 수 있었을 것이며, 단 한 줄이면 충분합니다.
두 테스트 모두 이름을 붙일 만한 공통된 속성을 공유합니다. 바로 실제 콘텐츠 없이 게이트를 실행한다는 점입니다. 실패 방향 테스트가 생략되는 이유는 보통 사람들이 실제와 같은 위반 사항이 필요하다고 생각하기 때문인데, 픽스처와 잘못된 URL은 그 요구사항을 완전히 제거해 줍니다.
실패를 명확하게 알리기
세 가지 결과를 분리하도록 검사를 다시 작성하는 것은, 주로 파이프라인이 그 결과들을 하나로 합치도록 내버려 두지 않는 것입니다.
BAD=0
for L in "${LANGS[@]}"; do
HTML="$(curl -fsSL "$BASE/$L/$SLUG")" \
|| { echo "fetch failed: $L" >&2; BAD=1; continue; }
[ -n "$HTML" ] || { echo "empty body: $L" >&2; BAD=1; continue; }
CLEANED="$(printf '%s' "$HTML" | strip_noise)" \
|| { echo "cleanup failed: $L" >&2; BAD=1; continue; }
if printf '%s\n' "$CLEANED" | grep -qiEf <(printf '%s\n' "$PAT"); then
echo "match: $L" >&2; BAD=1
fi
done
[ "$BAD" -eq 0 ] || retract
실패할 수 있는 모든 단계는 실제 일치(match)가 할당하는 것과 동일한 실패 변수를 할당합니다. curl -f는 HTTP 오류를 빈 본문(body) 대신 종료 코드로 만듭니다. 각 단계는 자체 명령 치환(command substitution) 내에서 실행되므로, 실패는 파이프라인 종료 상태(exit status)에 의해 무시되는 대신 포착될 수 있습니다. '문제가 발생했다'에서 '깨끗한 상태'로 가는 경로는 없습니다.
여기서 파생되는 일반적인 규칙은 다음과 같습니다. 검사는 '해롭다는 증거를 찾지 못함'이 아니라 '안전함이 검증됨'을 반환해야 합니다. 이 둘은 비슷하게 들리지만, 메커니즘이 고장 났을 때는 정반대로 작동합니다. 전자는 검사가 실제로 실행되고, 올바른 문서를 확인하고, 비교 대상인 '나쁨'에 대한 정의를 가지고 있어야 합니다. 후자는 빈 문자열이 영원히, 그리고 무료로 제공하는 것입니다.
fail-closed는 공짜가 아니므로 이에 대한 한 가지 주의 사항이 있습니다. 모든 일시적인 오류에 대해 차단하는 게이트는 네트워크 순간 장애(blip)에도 차단될 것이며, 만약 차단 비용이 비싸다면 팀은 이를 우회할 것입니다. 두 가지 사항이 이를 관리 가능하게 합니다. 가져오기(fetch)와 같은 일시적인 부분을 실패로 선언하기 전에 재시도하여, 단일 연결 끊김이 릴리스를 중단시키지 않도록 합니다. 그리고 작업을 보존하고 어떤 단계가 실패했는지 정확하게 보고함으로써 차단된 상태에서 저렴한 비용으로 복구할 수 있도록 만듭니다. fail-closed는 잘못된 차단(false block)의 비용이 1분간의 혼란일 때는 지속 가능하지만, 한 시간의 재구성 작업일 때는 지속 불가능합니다.
오늘 실행할 수 있는 간단한 감사
여러분이 소유한 자동화된 게이트를 하나 골라 네 가지 질문을 해보세요.
검사 대상이 없거나 접근할 수 없는 경우 어떻게 동작하나요? 만약 답이 "통과"라면, 여러분은 이 버그를 가지고 있는 것입니다.
사용자가 받는 것과 동일한 표현을 검사하나요, 아니면 중간에 헬퍼가 형식을 바꾼 무언가를 검사하나요?
설정, 거부 목록 또는 규칙 집합을 불러오지 못하면 어떻게 되나요? 빈 규칙은 반드시 오류여야 합니다.
마지막으로 의도적으로 실패한 적이 언제인가요? 만약 답이 "없다"라면, 여러분은 그것이 실패할 수 있다는 사실을 모르는 것입니다.
이 이야기 속 파이프라인은 이제 네 가지 항목 모두에서 안전한 방향으로 실패하며, 그 작업은 반나절도 채 걸리지 않았습니다. 비싼 부분은 수정 작업이 아니었습니다. 아무 의미 없었던 몇 달간의 녹색 체크 표시가 바로 그것이었습니다.
관련 글
당신의 에이전트가 당신 없이 게시하는 텍스트를 위한 프라이버시 게이트
스스로 게시물을 작성하고 게시하는 에이전트는 설득으로 차단 기능을 해제할 수 없는 게이트가 필요합니다. 두 개의 레이어가 있는데, 하나는 로컬 및 결정적 레이어이고 다른 하나는 격리된 레이어입니다.
코드를 커밋하고 배포하는 CI 봇을 위한 최소 권한
광범위한 쓰기 권한과 무제한 배포 권한을 가진 자동화 봇은 한 번의 잘못된 실행을 전체 시스템 장애로 만듭니다. 영향 범위를 줄이는 방법은 다음과 같습니다.
Declarative Firewall Sync가 규칙을 정리하고 액세스를 차단할 때
방화벽 동기화가 수동으로만 존재하던 규칙을 정리하여 라이브 트래픽을 차단했습니다. 여기에 사후 분석, 근본 원인, 그리고 정리(prune) 작업을 안전하게 만드는 방법이 있습니다.
유출을 확인하는 모델에 거부 목록을 절대 보내지 마십시오
유출 확인 프롬프트에 비밀 용어 목록을 붙여넣으면 보호하는 모든 항목의 색인이 내보내집니다. 목록이 머신을 벗어나지 않도록 작업을 분할하십시오.
AI 에이전트는 Sudo를 실행할 수 없으며, 그것이 바로 여러분을 위한 경계선입니다
AI 에이전트가 프로덕션 서버를 정리하도록 하는 것은 `sudo`를 실행할 수 없다는 것을 알아차리기 전까지는 위험하게 들립니다. 권한 경계는 작업을 에이전트에게 안전한 작업과 사람만 할 수 있는 작업으로 저절로 나눕니다.
효과적인 AI 페어 프로그래밍을 위한 결정적 파이프라인
AI 코더는 강력하지만 게이트가 없으면 방향을 잃습니다. 상태 파일과 계획 동결 기능을 갖추고 이를 감싸는 계획, 빌드, 검증, 배포 파이프라인이 있습니다.