Web/Spring&Spring Boot

[Maven] logback으로 로그의 개인정보관련 데이터 마스킹 처리

균지니 2022. 8. 1. 14:12

Intro

프로젝트 사업 중 서버별 로그파일 개인정보 평문 보관 여부에 대해 제가 담당하고 있는 API 모듈이 '유'로 검출이 되어 조치 안내를 받았습니다. 개인정보 관련 항목 데이터에 대해 마스킹 혹은 암호화 처리를 해야 했어요. 제가 택했던 방법은 logback을 사용해서 처리하기로 했습니다. 다행히 API 통신 이력 내역을 볼 수 있는 별도의 사이트가 존재했기 때문에 비식별화 데이터를 봐야 할 경우 해당 웹사이트를 통해 보기로 했습니다. 저의 경우 Spring MVC 환경의 logback을 사용했습니다. 이번 글은 logback을 사용하여 로그의 고객 개인정보항목 데이터를 마스킹하는 방법에 대해 공유하고자 합니다.

 

90년대 개인정보 ㅋㅋㅋㅋㅋㅋㅋ (구글링 하다가 찾음)

목차

  1. 마스킹 대상 데이터
  2. 마스킹 처리
  3. 실행
  4. 결론

 

마스킹 대상 데이터

{
    "accountNum":"987654321234567",  // 계좌번호
    "phoneNum":"01012345678",        // 핸드폰번호
    "TelNum":"026547891",            // 전화번호
    "custBirth":"19950707",          // 생년월일
    "custPNum":"14516871"            // 고객 고유번호
}

API 요청/응답 데이터 포맷은 JSON 형태입니다.  HTTP 통신을 할 때 로그파일의 요청/응답 메시지 중 개인정보에 해당하는 데이터에 마스킹 규칙을 logback 구성에 넣어 처리를 해야 했습니다. 그렇게 하려면 사용자 정의 ch.qos.logback.classic.PatternLayout을 구현해야 합니다.

 

마스킹 처리

  1. MaskingPatternLayout.class
    • 마스킹 패턴을 읽고 해당 패턴 정규식을 로그 메시지에 적용할 사용자 정의 레이아웃 클래스를 정의합니다.
  2. logback.xml
    • logback.xml 파일의 정규식 패턴을 사용하여 마스킹 패턴을 정의합니다.

 

1. MaskingPatternLayout

import ch.qos.logback.classic.PatternLayout;
import ch.qos.logback.classic.spi.ILoggingEvent;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class MaskingPatternLayout extends PatternLayout {

    private Pattern multilinePattern;
    private List<String> maskPatterns = new ArrayList<>();

    public void addMaskPattern(String maskPattern) {
        maskPatterns.add(maskPattern);
        multilinePattern = Pattern.compile(maskPatterns.stream().collect(Collectors.joining("|")), Pattern.MULTILINE);
    }

    @Override
    public String doLayout(ILoggingEvent event) {
        return maskMessage(super.doLayout(event));
    }
	
    /**
    * logback masking 처리된 문자열 반환
    *
    * @param message 로그에 찍힌 전체 메시지 라인
    * @return String 개인정보 masking 처리하여 문자열 라인 반환 
    */ 
    private String maskMessage(String message) {
        
        if (multilinePattern == null) {
            return message;
        }
        
        StringBuilder sb = new StringBuilder(message); // 로그에 찍힌 메세지 라인 StringBuilder에 담기
        Matcher matcher = multilinePattern.matcher(sb); // Matcher >> logback maskPattern 정규식 패턴
        
        while (matcher.find()) {
            IntStream.rangeClosed(1, matcher.groupCount()).forEach(group -> {
                if (matcher.group(group) != null) {
                    IntStream.range(matcher.start(group), matcher.end(group)).forEach(
                    	i -> sb.setCharAt(i, '*')
                    );
                }
            });
        }
        return sb.toString();
    }
}

MaskingPatternLayout는 logback의 PatternLayout을 상속받아 구현한 클래스입니다. logback.xml의 마스킹 패턴을 읽고 이를 로그 메시지에 적용을 합니다. 큰 흐름은 로그 메시지를 읽어 logback에 작성된 정규식 패턴을 찾아 특정 패턴을 확인 후 해당 문자열들을 *로 치환합니다. 정리하자면, 해당 클래스의 역할은 로그 메시지 상에서 개인정보 항목의 정규식 패턴에 부합되는 데이터를 masking(*로 치환) 처리합니다.

 

2. logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="5 seconds">

   <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
      <encoder>
         <pattern>[%d{HH:mm:ss.SSS}] [%thread] [%-5level] %-40.40logger{39} : %msg%n</pattern>
      </encoder>
   </appender>

   <appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
      <file>/fswaslog/tomcat8_api/logs/api.log</file>
      <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
         <layout class="module.util.MaskingPatternLayout">
            <maskPattern>\"accountNum\\{0,1}\"\s*:\s*\\{0,1}\"(.*?)\"</maskPattern>
            <maskPattern>\"phoneNum\\{0,1}\"\s*:\s*\\{0,1}\"(.*?)\"</maskPattern>
            <maskPattern>\"TelNum\\{0,1}\"\s*:\s*\\{0,1}\"(.*?)\"</maskPattern>
            <maskPattern>\"custBirth\\{0,1}\"\s*:\s*\\{0,1}\"(.*?)\"</maskPattern>
            <maskPattern>\"custPNum\\{0,1}\"\s*:\s*\\{0,1}\"(.*?)\"</maskPattern>
            <pattern>%-5p [%d{ISO8601,UTC}] [%thread] %c: %m%n%rootException</pattern>
         </layout>
      </encoder>
      <encoder>
         <pattern>[%d{HH:mm:ss.SSS}] [%thread] [%-5level] %-40.40logger{39} : %msg%n</pattern>
      </encoder>

      <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
         <FileNamePattern>/waslog/tomcat8_api/logs/api.%d{yyyyMMdd}.log</FileNamePattern> <!-- daily rollover -->
         <maxHistory>30</maxHistory> <!-- keep 30 days' worth of history -->
      </rollingPolicy>
   </appender>

   <!-- log file -->
   <logger name="module.api" level="debug" />

   <root level="info">
      <appender-ref ref="console" />
      <appender-ref ref="file" />
   </root>
   <typeAliases></typeAliases>
</configuration>

서비스 운영시 파일로 로그를 남기기 때문에 logback.xml의 file appender 태그에 maskPattern을 추가해야 합니다. <layout class="PatternLayout을 상속받아 구현한 클래스 경로"> 태그를 추가 후 masking 적용할 항목 및 정규식 패턴을 <maskPattern> 태그에 작성합니다.

 

2.2 JSON 데이터 형태

  1. "key":"value"
  2. "key":""
  3. \"key\”:\"value\"
  4. \"key\":\"\"

테스트를 해보니 표준 규격에 맞지 않는 특정 데이터들이 존재했습니다.^^  총 4개의 규격이고 내용은 하기와 같습니다.

 

1. JSON 기본 형태

2. 1번 + (빈 값으로 넘어온 경우)

3. 1번 +  ("앞에 역슬래시(\)가 붙는 경우, 이중 문자열 경우)

4. 3번 + (빈 값으로 넘어온 경우)

 

1번은 JSON 기본 형태, 2번은 빈 값으로 넘어온 경우, 3번은 이중 문자열 형태로 따옴표의 구분을 위해서  따옴표(") 앞에 역슬래시(\) 이스케이프를 사용한 경우입니다. 4번은 3번의 형태에 해당되지만 빈 값은 경우입니다. 그리하여 4가지 규격에 모두 맞는 정규식 패턴을 찾느라 뻘짓을 많이 했어요 ㅎㅎ 

 

후 4가지 형태를 만족하는 정규식이라..(생각 생각)

2.3 maskPattern

사용자의 개인정보를 마스킹하기 위해서 정규식 패턴을 사용할 수 있습니다.

<maskPattern>\"accountNum\\{0,1}\"\s*:\s*\\{0,1}\"(.*?)\\{0,1}\"</maskPattern>

 

마스킹 대상 항목 (key)와  정규식 패턴 (value)을 기입해줘야 합니다.  JSON형태이기 때문에 "key":"value" 기본 형태를 유지하면서 위 2.2번 4개의 데이터 규격에 맞는 패턴을 사용해야 합니다. maskPattern에 작성한 정규식 패턴을 분해해서 간단하게 살펴보겠습니다.

 

{0,1} {N,M} N~M번 사이로 올 수 있다. 범위 지정 가능
\s 공백 문자가 한개도 없거나 무수히 많을 수 있다.
. 임의의 한 문자
* 임의의 문자 앞의 문자가 하나도 없거나 무수히 많을 수 있다.
? 앞의 하나 있거나 없을 수도 있다.
() 하나의 문자로 취급

 

  1. "accountNum":"987654321234567"
  2. "accountNum":""
  3. \"accountNum\":\"987654321234567\"
  4. \"accountNum\":\"\"

자바에서는 특수문자를 이스케이프 문자라고 합니다. 이스케이프 문자들은 문자열로 인식할 수 없는 자바의 키워드이기 때문에 역슬래시(\)를 이용해서 문자열 처리를 합니다. 역슬래시와 함께 특수문자를 사용하면 문자열처럼 사용할 수 있습니다.  정규 표현식은 주어진 문자열 속에서 특정 패턴을 가진 문자열을 찾을 때 사용합니다.  정규 표현식 패턴에서 특수문자를  찾아야 하는 경우 특수문자 앞에 역슬래시 (\) 기입해줍니다. \\{0,1}는 역 슬래시가 0~1번 사이 올 수 있다는 뜻입니다. 여기서 역슬래시를 두 개 사용한 이유는  스트링 리터럴에서는 역슬래시 2개(\\)가 스트링 1개의  역슬래시를 표현합니다. \\에서 첫번째 \는 특수문자 구분을 위한 것이고 두번째 \는 실제 역슬래시 특수문자를 찾기 위함입니다.  \s* 는 key와 value 사이에 공백이 존재할 수도 있어 공백이 없거나 많을 수 있다는 뜻이며 (.*?)는 임의의 한 문자가 없을 수도 있고, 많을 수도 있고 없을 수도 있다는 뜻입니다.

 

빌드 후 서버 실행

마스킹 처리를 프로젝트에 적용 후 서버에 적용합니다. 마스킹된 데이터를 보기 위해서 간단한 API 응답 결과를 출력해 봅시다. JSON형태의 데이터 중 개인정보에 해당하는 데이터는 마스킹되었음이 보입니다.

INFO  [2022-07-31 19:41:12,059] [main] module.util.MaskingPatternLayout: JSON response:
{"accountNum":"***************","phoneNum":"***********","user_id":"87656","city":"Seoul","Country":"Korea"}

이 접근 방식을 사용하면 logback.xml의 maskPattern에 정규 표현식을 정의한 로그 파일의 데이터만 마스킹됩니다. 또한, logback.xml의 package의 하위의 모든 logging을 하는 행위에 대해서 마스킹 대상 항목 및 정규식 패턴과 일치한다면 masking 처리되는 것을 확인할 수 있습니다.

 

결론

애플리케이션 로그에서 민감한 데이터를 마스킹하기 위해 사용자 지정 PatternLayout을 만드는 방법에 대해서 설명드렸습니다. 어렵지 않게 관리할 수 있으며 로그에서 데이터 마스킹을 할 때 암호화/복호화 방식이 아닌 logback을 활용한 마스킹 처리 방법도 있다는 점을 공유드렸습니다.  이상 글 마무리 하도록 하겠습니다. 감사합니다 :)