파이썬 로그에 API 키가 남지 않게 마스킹하는 법

파이썬 로그에 API 키가 남지 않게 하려면 민감값을 로그 호출 전에 제거하는 것을 기본으로 하고, 여러 출력 경로를 공통으로 보호해야 할 때 Handler에 Filter를 적용하는 방식으로 보완하는 것이 안전합니다. logging.Filter 하나만 붙이면 모든 로그가 자동으로 안전해진다고 보기는 어렵습니다. 어떤 Logger와 Handler를 거치는지, args·예외 traceback·응답 객체가 어떻게 문자열로 바뀌는지까지 확인해야 합니다.

먼저 결정할 것: 애초에 로그에 넣지 않을 수 있는가

API 요청을 디버깅하다 보면 URL, 요청 헤더, 응답 객체를 통째로 기록하고 싶어집니다. 하지만 인증 헤더나 비밀번호처럼 로그에 없어도 업무 판단에 필요하지 않은 값은 호출부에서 제외하는 편이 가장 분명합니다.

logger.debug("요청 전송: method=%s url=%s", method, url)

다음처럼 headers 전체를 함께 넘기는 코드는 피하는 편이 좋습니다.

logger.debug("요청 전송: url=%s headers=%s", url, headers)

호출부에서 민감값을 제거하면 콘솔, 파일, 원격 수집기처럼 출력 경로가 늘어나도 보호해야 할 값 자체가 줄어듭니다. 이 방식은 로그의 의미도 선명하게 만듭니다. “인증이 붙은 요청이었다”는 사실만 필요하다면 헤더 값 대신 has_authorization=True 같은 비밀이 아닌 상태만 남길 수 있습니다.

다만 이미 여러 모듈에서 요청 정보나 예외를 로그로 기록하고 있고, 모든 호출부를 한 번에 찾기 어렵다면 공통 Filter를 추가 방어선으로 검토할 수 있습니다. Filter는 만능 비밀 탐지기가 아니므로, 호출부에서 로그를 줄이는 원칙을 대신하지는 않습니다.

Filter를 Logger보다 Handler에 붙이는 이유

파이썬 로깅에서 Logger는 이벤트를 만들고, Handler는 콘솔이나 파일 같은 출력 대상으로 보냅니다. Logger에 붙인 Filter는 그 Logger에서 처리되는 기록에 적용되지만, 하위 Logger에서 올라오는 이벤트 전체를 자동으로 포괄한다고 단정할 수 없습니다. 반면 Handler에 붙인 Filter는 해당 Handler가 출력하기 전에 적용됩니다.

따라서 보호하려는 범위가 “이 파일 Handler로 나가는 모든 로그”라면 그 Handler에 Filter를 붙이는 편이 판단하기 쉽습니다. 콘솔과 파일 Handler가 둘 다 있다면 각각에 적용됐는지 확인해야 합니다. 하위 Logger의 이벤트가 상위 Handler로 전파되는지 여부도 propagate 설정과 함께 봐야 합니다.

메시지와 args를 함께 처리하는 기본 예제

LogRecord는 msg와 args를 조합해 최종 메시지를 만듭니다. Filter에서 msg만 바꾸고 args를 그대로 두면 Formatter 단계에서 원래 인자가 다시 반영될 수 있으므로, 최종 문자열을 만든 뒤 args를 비우는 방식으로 처리할 수 있습니다.

import copy
import logging
import re


class SecretFilter(logging.Filter):
    patterns = (
        re.compile(r"(?i)(authorization\s*[:=]\s*bearer\s+)[^\s,}]+"),
        re.compile(r"(?i)(api[_-]?key\s*[:=]\s*)[^\s,}]+"),
        re.compile(r"(?i)(password\s*[:=]\s*)[^\s,}]+"),
    )

    def filter(self, record: logging.LogRecord) -> logging.LogRecord:
        masked = copy.copy(record)
        try:
            message = record.getMessage()
        except Exception:
            message = repr(record.msg)

        for pattern in self.patterns:
            message = pattern.sub(r"\1[REDACTED]", message)

        masked.msg = message
        masked.args = ()
        return masked


handler = logging.FileHandler("automation.log", encoding="utf-8")
handler.addFilter(SecretFilter())
handler.setFormatter(logging.Formatter("%(levelname)s %(message)s"))

logger = logging.getLogger("automation")
logger.setLevel(logging.DEBUG)
logger.addHandler(handler)

이 예제는 문자열로 만들어진 메시지의 일부 패턴을 가리는 출발점입니다. 실제 API 키의 모양이나 응답 구조를 자동으로 알아내는 기능을 공식 logging 문서가 보장하는 것은 아닙니다. 패턴을 늘리기 전에 어떤 값이 실제로 로그에 들어오는지 샘플을 확인하고, 오탐으로 운영에 필요한 정보까지 가리지 않는지 검토해야 합니다.

또한 Handler Filter가 반환한 LogRecord가 다른 Handler에 어떻게 전달되는지는 구성 방식에 따라 확인해야 합니다. 콘솔과 파일에 서로 다른 Formatter를 쓰거나, 별도 Handler가 같은 이벤트를 직접 처리한다면 각 출력 경로의 필터 적용 여부를 따로 점검하는 것이 안전합니다.

예외 traceback은 별도로 점검해야 합니다

logger.exception()이나 exc_info를 사용하면 예외 정보와 traceback이 로그에 추가됩니다. 예외 객체의 문자열 표현이나 traceback에 요청 정보가 포함되어 있다면, 일반 메시지용 정규식만으로는 충분하지 않을 수 있습니다.

try:
    call_api()
except Exception:
    logger.exception("외부 API 호출 실패")

여기서 로그에 남겨야 하는 것은 실패라는 사실과 식별 가능한 작업 정보이지, 요청 headers나 응답 본문 전체가 아닐 수 있습니다. 예외를 기록하기 전에 민감한 값을 예외 메시지에 넣지 않는 설계가 우선입니다. traceback을 보존해야 한다면 필터가 가릴 수 있는 범위와 가리지 못하는 범위를 실제 출력 형식으로 확인해야 합니다.

응답 객체와 딕셔너리 로그를 줄이는 방법

응답 객체나 dict를 %s로 통째로 넘기는 습관도 누출 지점입니다. 키 이름이 token, secret, authorization처럼 명확하더라도 중첩 구조나 사용자 정의 객체의 __str__ 결과는 별도로 처리될 수 있습니다.

safe_result = {
    "status_code": response.status_code,
    "request_id": response.headers.get("x-request-id"),
}
logger.info("API 응답 요약: %s", safe_result)

운영 로그에는 상태 코드, 내부에서 만든 요청 식별자, 처리 단계처럼 장애 판단에 필요한 정보만 남기는 편이 좋습니다. 응답 본문이 꼭 필요하다면 허용할 필드만 골라 새 구조로 만든 뒤 기록하고, 인증 정보가 들어갈 수 있는 원본 객체는 로그 호출에 넘기지 않는 방식이 예측 가능합니다.

적용 후 확인할 체크 순서

  1. logger, 하위 Logger, propagate 설정을 확인합니다.
  2. 콘솔·파일·원격 전송 등 모든 Handler 목록과 Filter 부착 여부를 확인합니다.
  3. msg와 args를 각각 사용하는 호출, f-string, 예외 traceback, 응답 객체 로그를 찾아봅니다.
  4. 테스트용 가짜 토큰을 넣고 실제 Formatter 결과에 [REDACTED]가 나오는지 출력 경로별로 확인합니다.
  5. 토큰이 보이지 않는 것만 확인하지 말고, 필요한 상태 코드와 요청 식별자가 여전히 남는지도 확인합니다.

결론적으로 로그에 없어도 되는 비밀은 호출부에서 기록하지 않는 것이 첫 선택입니다. 이미 넓게 퍼진 로그 호출을 보완해야 할 때는 Handler별 Filter를 사용하되, 모든 출력 경로와 예외·객체 표현을 별도로 점검해야 합니다.

파일 로그 설정 자체가 작동하지 않는 문제라면 파이썬 로그 파일이 안 생기는 이유와 logging 설정 점검법을 이어서 확인할 수 있습니다. 예외 traceback을 남기는 기준은 파이썬 로그 파일에 traceback 남기는 법에서 다룹니다.

출처

Leave a Comment