02 · 탐지 방식
모든 탐지기는 같은 질문에 답합니다. 얼마나 확실하고, 왜 그렇게 판단했는가?
‘왜’에 해당하는 근거는 정확히 다섯 종류이고, 순위가 매겨져 있습니다. 이후 내용 대부분이 이 순위에서 나옵니다.
근거 등급
다섯 단계의 근거
각 탐지기는 찾은 항목에 이 중 하나를 붙입니다. 목록은 고정되어 있어서 여섯 번째 등급을 만들 수 없고, 탐지기가 스스로 등급을 올릴 수도 없습니다.
- 5 · Private key스스로 정체를 드러냅니다시작 배너와 끝 배너 사이의 전부. 기본 동작이 block인 유일한 등급입니다.-----BEGIN …
- 4 · Provider회사가 자기 키에 표식을 붙입니다고정된 접두사, 정해진 문자 집합, 알려진 길이.ghp_ · AKIA · sk-
- 3 · Structural고유한 형식이 있습니다특정 벤더에 속하지 않는 문법: 헤더, URL, JWT.Bearer …
- 2 · Contextual이름이 시크릿이라고 알려 줍니다자격 증명 같은 이름 뒤에 무작위처럼 보이는 값이 옵니다.api_key = …
- 1 · Entropy그냥 무작위로 보입니다이것만으로는 절대 부족합니다. 보조 신호로만 씁니다.x8Kd92mQz1
↑ 더 확실함가장 약함 ↓
Tier 4
접두사, 문자 집합, 길이
53개 제공자, 108개 자격 증명 계열은 키 앞부분에 알아볼 수 있는 표식을 붙입니다. 이 탐지기들은 모두 같은 네 부분 구조이고, 이 등급 전체가 이 작은 언어로 작성됩니다.
- Prefix
- ghp_
- 고정 문자열 하나
- Alphabet
- A–Z a–z 0–9
- 고정된 바이트 클래스 11개 중 하나
- Run
- exactly 36
- 정확히 n자, 또는 n자 이상
- Validator
- none
- 추가 검사(선택)
- 경계
- 바로 앞뒤 문자는 해당 문자 집합에 속하지 않아야 합니다. 그래야 더 긴 문자열의 일부를 온전한 키로 오인하지 않습니다.
- 대소문자
- 문서상 소문자 16진수인 벤더 형식에는 소문자 전용 클래스를 씁니다. 대소문자가 섞인 비슷한 문자열은 매칭하지 않고 걸러 냅니다.
- 검증기
- Confluent 키 끝에는 실제 CRC-32 체크섬이 붙어 있고, 탐지기가 이를 다시 계산합니다. Cloudflare 키는 소문자 16진수인지만 확인합니다.
이 구조로는 표현할 수 없을 만큼 특이한 벤더 두 곳은 문법을 직접 작성했습니다:
sk-proj-⟨74자⟩T3BlbkFJ⟨74자⟩가운데 표식은 base64("OpenAI")입니다. 발급되는 모든 키에 이 표식이 들어 있으므로, 탐지기는 sk-만 믿지 않고 이 표식을 기준으로 삼습니다.
Tier 3
누구의 것도 아닌 형식
특정 벤더의 패턴이 아닌 범용 형식입니다. 모두 6개이며, 작은 common 프로파일에 남는 탐지기는 이것뿐입니다.
- JWT
- 점으로 구분된 세 덩어리이고, 앞의 두 덩어리는
eyJ로 시작해야 합니다. base64로 인코딩한 JSON의 특징입니다. - Bearer
Bearer뒤에 16자 이상이 와야 합니다. 토큰만 잡고 헤더 단어는 그대로 둡니다.- 연결 URL
postgres://user:pw@host에서 비밀번호만 골라냅니다. 호스트는 그대로 읽을 수 있습니다.- otpauth URI
- 스킴과
secret=파라미터를 봅니다. 라벨과 발급자는 건드리지 않습니다.
의도적인 예외가 하나 있습니다. JWT 페이로드에 Supabase anon 키라고 적혀 있으면 제외합니다. 이 키는 원래 공개용이라 브라우저 번들에 그대로 들어가기 때문입니다. 토큰 내용을 읽는 곳은 여기뿐입니다.
Tier 2
이름이 값을 보증합니다
정해진 문법이 없는 자격 증명을 잡는 등급입니다. 두 조건을 모두 만족해야 합니다. 이름이 시크릿처럼 들리고, 값도 시크릿처럼 보여야 합니다.
- 이름
- 대소문자와 구두점을 먼저 정규화하므로
apiKey,API-KEY,api.key는 같은 이름으로 봅니다. - 강한 이름
api_key,password,client_secret,access_token: 값의 무작위성 점수가 3.0 이상이어야 합니다.- 애매한 이름
auth,credential,signing_key: 기준이 3.5로 올라갑니다. 단어가 약한 만큼 더 강한 근거가 필요합니다.- 길이
- 최소 8자입니다. 무작위성은 16자를 넘을 때만 측정하고, 4 KB를 넘는 값은 전용 탐지기에 맡깁니다.
- token 단독
token이라는 단어만 있는 경우는 일부러 무시합니다. 너무 흔해서 의미가 없습니다.
그다음 대부분을 걸러 냅니다
이 탐지기가 하는 일의 상당 부분은 시크릿이 아닌 것을 알아보는 일입니다. 아래 값은 모두 인식해서 건너뜁니다:
- changeme
- placeholder
- redacted
- <your-key-here>
- ${process.env.KEY}
- $[variables.x]
- `date +%s`
- op://vault/item/field
- :bind_param
- /etc/ssl/key.pem
- true
- 12345
모두 시크릿 자체가 아니라 시크릿을 가리키는 값입니다. 이런 값까지 잡아내면 사람들은 금세 스캐너를 꺼 버립니다.
Tier 1
무작위성은 증거가 아닙니다
탐지 안 함
a8f3k29dj4ms91x만 텍스트에 단독으로 있을 때. 커밋 해시, ID, nonce, UUID일 수 있습니다.
탐지함
password=a8f3k29dj4ms91x. 같은 문자열이지만 이번에는 앞에 근거가 붙어 있습니다.
엔트로피만으로는 어떤 것도 등급이 오르지 않습니다.
엔트로피는 위 등급의 기준을 올리거나 내리는 데만 쓰입니다. 오탐이 적은 이유도, 이름도 형식도 없는 고엔트로피 시크릿을 그대로 통과시키는 이유도 이 결정 하나에 있습니다.
등급이 충돌할 때
다섯 가지 판정 기준, 항상 이 순서로
두 탐지기가 겹치는 텍스트를 동시에 잡는 경우는 흔합니다. 승자는 어느 쪽이 먼저 실행됐는지와 상관없이 정해집니다. 고정된 비교를 위에서부터 차례로 적용해, 처음 차이가 나는 기준에서 결정합니다.
- 1 · 심각도
- 경고만 하는 후보는 차단하는 후보를 밀어낼 수 없습니다. 등급보다도 우선합니다.
- 2 · 등급
- 위 등급 순서입니다. private key > provider > structural > contextual > entropy.
- 3 · 확신도
- high, medium, low.
- 4 · 범위 폭
- 좁은 범위가 이깁니다. 주변 문단이 아니라 키만 가립니다.
- 5 · 순서
- 등록 순서, 그다음 결과가 나온 순서입니다. 내장 탐지기가 사용자 정의 탐지기보다 먼저 등록되므로 답은 항상 하나로 정해집니다.
입력 전체를 볼 때도 탐욕(greedy) 방식으로 승자를 고르지 않습니다. 같은 기준으로 가중치를 매겨 서로 겹치지 않는 탐지 결과 중 근거의 합이 가장 큰 조합을 고릅니다.
작은 언어
정규식 엔진이 없습니다
코어는 외부 의존성을 하나도 허용하지 않으므로, 쓸 수 있는 정규식 엔진 자체가 없습니다. 대신 Tier 4의 네 부분 구조를 한 번 정의해 두었고, 모든 제공자 탐지기가 그 구조로 작성됩니다.
얻는 것
백트래킹이 없으므로, 악성 입력 하나로 서버를 멈추게 하는 고전적인 공격이 통할 여지가 없습니다. 리뷰로 막는 것이 아니라 구조상 불가능합니다. 스캔마다 한 번 계산하는 룩업 테이블 덕분에 악의적인 입력에서도 전체 처리가 선형 시간에 끝납니다.
대가
“접두사 다음에 길이가 제한된 문자열” 꼴이 아니면 코드를 직접 작성해야 합니다. 벤더 두 곳의 형식이 그랬습니다.
11개 문자 집합, 그 외에는 없습니다
- alnum
- alnum-dash
- alnum-dash-dot
- alnum-underscore
- upper-alnum
- lower-alnum
- digit
- hex
- hex-or-dash
- lower-hex
- base64-body
직접 만드는 규칙
형식은 직접, 엔진은 코어가
출시됨 · Rust · JavaScript · Python · CLI
회사마다 자체 키 형식이 있습니다. 예전에는 탐지기를 추가하려면 Rust 코드를 작성해야 했고, 그래서 다들 이 라이브러리 옆에 별도 스캔을 하나 더 돌렸습니다. 이 라이브러리가 막으려던 바로 그 불일치입니다. 이제는 같은 네 부분 구조를 일반 텍스트로 넘기면 됩니다. 콜백이 아니라 데이터로 넘깁니다.
ruleset-revision: 1
detector: acme-internal-token
specificity: contextual
prefix: "ACME_"
alphabet: alnum-dash
run: at-least 20
validator: none콜백은 여전히 받지 않습니다
콜백을 쓰면 여러분의 코드가 후보마다 실행됩니다. 판단하려면 평문이 필요하니 평문이 경계를 넘게 되고, Node, 브라우저, Python이 서로 다른 답을 낼 수도 있습니다.
룰셋: 실제로 출시된 방식
문법은 처음에 한 번만 경계를 넘습니다. 매칭은 Rust 코어가 기존 엔진으로 직접 합니다. 실행되는 코드가 없으니 텍스트를 가로챌 수도 없습니다.
- 등급 상한
- 룰은 entropy 또는 contextual 등급만 지정할 수 있습니다. 상위 세 등급은 내장 탐지기 전용이라, 사용자 룰은 탐지 결과를 추가할 수는 있어도 기존 결과를 뒤집을 수는 없습니다.
- 검증기
- 정해진 목록에서 이름으로 고릅니다. 직접 작성한 함수, 표현식, 코드는 쓸 수 없습니다.
- 확신도
- 항상 medium입니다. 올릴 수 있는 필드가 없습니다.
- 크기
- UTF-8 기준 64 KiB이며, 처음에 한 번 파싱합니다. 실패하면 막습니다. 한 줄만 잘못돼도 파일 전체를 거부합니다.
개인정보
여섯 번째 탐지 대상은 따로 관리합니다
개인정보는 다섯 등급과 별도로 자체 영역을 둡니다. 이메일, IBAN, 결제 카드, 전화번호, 네트워크 주소, 미국 사회보장번호(SSN)가 여기에 속합니다. 모든 사용 환경에서 선택 사항이고 기본값은 꺼짐이며, 명시적으로 켜야 동작합니다.
Luhn
결제 카드는 카드 업계가 이미 쓰는 체크섬으로 검증합니다. 이름과 버전이 붙은 검증기로 제공됩니다.
IBAN mod-97
계좌번호는 형식만 보지 않고 ISO 나머지 검사로 확인합니다.
SSN 할당 규칙
미국 기관이 공개한 구조상 제외 규칙만 적용합니다. 발급 여부나 신원은 조회하지 않습니다.
이 세 가지는 엔진에 처음 들어온 산술 검사입니다. 그전에는 코어 전체의 숫자 검사가 제공자 한 곳의 키에 쓰는 CRC-32 하나뿐이었습니다.