본문으로 건너뛰기

KServe Predict Proxy PII Masking (#1282 G-2 M3)

apps/api/src/gend_api/services/serving/inference_proxy.pyInferenceProxy.predict() 가 KServe 로 forwarding 전후로 request/response body 의 PII 를 자동 마스킹.

디자인 결정

1. 왜 fail-open?

  • Production smoke 안전성 우선 — PII masker 라이브러리 (Presidio) 장애 시에도 predict 응답 유지
  • model.predict.pii_mask_error audit emit 으로 추적 가능
  • 마스킹 실패가 inference 자체를 차단하는 것은 더 큰 가용성 손실

2. 왜 ABAC 이후에?

  • ABAC denied 케이스에서 PII masker 호출 0회 (불필요한 처리)
  • 마스킹 성능 (~100ms) 을 차단된 요청에 낭비하지 않음

3. 왜 재귀 dict 순회?

  • KServe v2 protocol body 는 임의 깊이의 dict/list 중첩 (예: inputs[0].data[0])
  • 모든 string leaf 노드에 PII 패턴 적용해야 함
  • 비-string 값 (int/float/bool/None) 은 통과 — PII 가 numeric 에는 없음

코드 핵심

async def predict(self, *, body: dict, ...) -> dict:
# 1. ABAC check (생략)
if not allowed: raise ProxyDenied(...)

# 2. PII mask request
masked_body, mask_count_req = _mask_payload_pii(body, self._text_masker)

# 3. POST KServe
resp = await http.post(url, json=masked_body)

# 4. PII mask response
masked_resp, mask_count_resp = _mask_payload_pii(resp.json(), self._text_masker)

# 5. Audit
_audit_logger.info(
"model.predict ... mask_count=%d/%d",
mask_count_req, mask_count_resp,
)
return masked_resp


def _mask_payload_pii(payload: dict, masker: TextPIIMasker) -> tuple[dict, int]:
"""재귀 traversal — dict/list/str 마스킹 + 카운트."""
count = 0
def walk(obj):
nonlocal count
if isinstance(obj, dict):
return {k: walk(v) for k, v in obj.items()}
if isinstance(obj, list):
return [walk(x) for x in obj]
if isinstance(obj, str):
try:
masked = masker.mask(obj)
if masked != obj:
count += 1
return masked
except Exception:
logger.exception("PII mask error (fail-open)")
return obj # fail-open
return obj
return walk(payload), count

회귀 가드 — 7-9 tests

apps/api/tests/test_inference_proxy_pii_masking.py:

  • request body 이메일/전화번호 마스킹 → upstream 으로 전송 검증
  • response body PII 마스킹 → caller 반환 검증
  • 중첩 dict ({"inputs": [{"data": ["alice@x.com"]}]}) 재귀
  • 비-문자열 값 통과
  • TextPIIMasker Exception → fail-open + 원본 유지
  • 마스킹 0건 → 기존 audit 라인 그대로
  • ABAC denied → 마스킹 호출 0회

트러블슈팅

증상원인해결
마스킹 0회인데 PII 누출Presidio 미배포kubectl get pods -l app=presidio
ContextGateway 에 PII_PATTERNS 정의되었으나 사용 안 됨TextPIIMasker 가 Presidio 의존regex fallback 추가 follow-up
마스킹으로 inference 실패마스킹 결과가 모델 입력 schema 깨뜨림mask_replace_value="X" 같은 동일 길이 토큰 사용

관련

  • PR #1282
  • TextPIIMasker: apps/api/src/gend_api/services/pii_masking/text_masker.py
  • ContextGateway PII_PATTERNS: apps/api/src/gend_api/services/context_gateway.py