본문으로 건너뛰기

데이터 마스킹 구현 현황 및 가이드

개요​

DTx 플랫폼의 실제 데이터 마스킹 구현 현황과 개선 방안을 정리합니다. 현재 CoreLoggingModule의 HTTP 로깅 인터셉터를 통해 기본적인 마스킹이 구현되어 있으며, 추가 강화가 필요한 상황입니다.

1. 현재 구현 현황​

1.1 CoreLoggingModule 마스킹 구현​

위치: libs/core/logging/src/interceptors/http-logging.interceptor.ts

현재 마스킹 설정 (app.module.ts) - 개선됨 ✅​

// apps/dta-wide-api/src/app/app.module.ts
CoreLoggingModule.forRoot({
httpLogging: {
logRequestBody: true,
logResponseBody: true,
maskPatterns: [
{
fields: ['password', 'token', 'passwordhash', 'password_hash'],
pattern: /./g,
replacement: '*****',
},
{
fields: ['email'],
pattern: /^(.{2}).*(@.*)$/,
replacement: '$1***$2', // "bo***@weltcorp.com"
},
],
},
})

실제 구현 메서드​

// libs/core/logging/src/interceptors/http-logging.interceptor.ts

private maskSensitiveData(data: any): any {
// 재귀적으로 객체를 순회하며 민감 필드 마스킹
// fields 배열에 있는 키를 찾으면 패턴에 따라 값 치환
}

private sanitizeHeaders(headers: Record<string, any>): Record<string, any> {
const sensitiveHeaders = [
'authorization',
'cookie',
'set-cookie',
'x-api-key',
'x-auth-token',
];
// 민감한 헤더는 '[REDACTED]'로 치환
}

1.2 데이터베이스 스키마별 민감 필드 현황​

private 스키마 (환자 개인정보)​

model User {
email String @unique
passwordHash String? @map("password_hash")
mfaSecret String? @map("mfa_secret")
// ... 기타 개인정보 필드
}

operation 스키마 (운영자 정보)​

model OperationUser {
email String @unique
passwordHash String? @map("password_hash")
mfaSecret String? @map("mfa_secret")
// ... 기타 운영자 정보
}

1.3 현재 마스킹 적용 범위​

구분현재 상태설명
HTTP 요청 바디✅ 적용password, token, passwordHash, email, UUID 식별자 마스킹
HTTP 응답 바디✅ 적용password, token, passwordHash, email, UUID 식별자 마스킹
HTTP queryParams✅ 적용인증 API의 민감 파라미터 마스킹
HTTP 헤더✅ 적용authorization, cookie 등 민감 헤더 마스킹
클라이언트 IP✅ 적용IP 주소 부분 마스킹 ("192.168.1.***")
데이터베이스 로그❌ 미적용Prisma 쿼리 로그에 마스킹 없음
시스템 로그❌ 미적용일반 로거에 마스킹 없음
API 문서❌ 미적용Swagger 예시에 실제 데이터 노출 가능

2. 문제점 및 개선 필요사항​

2.1 마스킹 필드 현황​

✅ 해결된 문제들​

  • ✅ email - 부분 마스킹 적용 ("bo***@weltcorp.com")
  • ✅ passwordHash - 완전 마스킹 적용 ("*****")
  • ✅ queryParams - 인증 API의 민감 파라미터 마스킹
  • ✅ clientIp - IP 주소 부분 마스킹 적용 ("192.168.1.***")
  • ✅ verificationId - 개인 인증 관련 ID 부분 마스킹 ("bd54---64fb") 🆕
  • ✅ sessionId - 개인 세션 ID 부분 마스킹 🆕
  • ✅ userId - 사용자 ID 부분 마스킹 🆕

⚠️ 의도적으로 마스킹하지 않는 필드들 (운영상 필수)​

  • correlationId - 분산 시스템 추적용 (오류 추적 필수)
  • requestId - 요청 추적용 (성능 모니터링 필수)
  • traceId - APM 추적용 (장애 대응 필수)

❌ 아직 해결 필요한 필드들​

  • mfaSecret - MFA 시크릿 키
  • userAgent - 사용자 에이전트
  • IP 메타데이터 (지역, ISP 정보) - 향후 구현 예정

2.2 도메인별 마스킹 정책 부재​

각 도메인에서 자체적인 마스킹 구현이 없음:

  • AccessCode 도메인: 코드 값 마스킹 필요
  • Auth 도메인: 토큰, 세션 정보 마스킹 필요
  • User 도메인: 개인정보 마스킹 필요

2.3 환경별 차별화 부족​

개발/스테이징/프로덕션 환경별 마스킹 수준 차이가 없음

3. 실무적 차별 마스킹 전략​

3.1 권한 기반 차등 마스킹​

실무에서는 모든 데이터를 획일적으로 마스킹하면 운영 효율성이 떨어집니다. 따라서 용도별 차별적 접근이 필요합니다.

API 응답: 권한에 따른 차등 마스킹​

// apps/dta-wide-api/src/app/user/services/user.service.ts
async getUserProfile(requesterId: string, targetUserId: string) {
const user = await this.userRepository.findById(targetUserId);

// 본인 조회: 원본 데이터 (모바일 앱 정상 동작)
if (requesterId === targetUserId) {
return {
email: user.email, // ✅ "user@example.com" - 원본 제공
phone: user.phone, // ✅ "010-1234-5678" - 앱에서 사용
name: user.name // ✅ "홍길동" - 정상 표시
};
}

// 관리자 조회: 부분 마스킹
if (await this.isAdmin(requesterId)) {
return {
email: this.maskEmail(user.email), // "us***@ex******.com"
phone: this.maskPhone(user.phone), // "010-****-5678"
name: this.maskName(user.name) // "홍**"
};
}

// 일반 사용자: 공개 정보만
return {
publicId: user.publicId,
displayName: user.displayName
};
}

로그: 항상 마스킹/해시화​

// libs/core/logging/src/interceptors/http-logging.interceptor.ts
private processLogData(data: any): any {
return {
user_hash: this.hashUserId(data.userId), // ✅ 해시화
email_masked: this.maskEmail(data.email), // ✅ 부분 마스킹
response_body: '[REDACTED]', // ✅ 완전 마스킹
client_ip_info: this.processClientIp(data.clientIp) // ✅ 메타데이터 변환
};
}

3.2 IP 주소 처리: 부분 마스킹 + 메타데이터​

문제: 완전 해시화 시 역추적 불가능 해결: 부분 마스킹 + 지역/ISP 정보 보존

// libs/core/logging/src/services/ip-processing.service.ts
interface IpProcessingResult {
string; // "192.168.1.***" - 부분 마스킹
region: string; // "Seoul, KR" - 지역 정보
isp: string; // "KT Corporation" - ISP 정보
network_class: string; // "192.168.0.0/16" - 네트워크 대역
// 원본 IP는 저장하지 않음
}

@Injectable()
export class IpProcessingService {
processClientIp(ip: string): IpProcessingResult {
return {
masked_ip: this.maskIpAddress(ip),
region: this.getIpRegion(ip),
isp: this.getIpIsp(ip),
network_class: this.getNetworkClass(ip)
};
}

private maskIpAddress(ip: string): string {
const parts = ip.split('.');
return `${parts[0]}.${parts[1]}.${parts[2]}.***`;
}

private getIpRegion(ip: string): string {
// GeoIP 서비스를 통한 지역 정보 조회
return this.geoIpService.getRegion(ip);
}

private getIpIsp(ip: string): string {
// ISP 정보 조회
return this.geoIpService.getISP(ip);
}
}

3.3 환경별 차별화된 마스킹 정책​

// libs/core/config/src/lib/masking-config.service.ts
@Injectable()
export class MaskingConfigService {
getMaskingStrategy(environment: string, dataType: 'api' | 'log' | 'external') {
const strategies = {
production: {
api: { level: 'permission-based' }, // 권한 기반 차등
log: { level: 'strict' }, // 완전 마스킹
external: { level: 'hash-only' } // 해시만 전송
},
development: {
api: { level: 'minimal' }, // 최소 마스킹 (디버깅 편의)
log: { level: 'moderate' }, // 부분 마스킹
external: { level: 'strict' } // 외부는 항상 엄격
}
};

return strategies[environment]?.[dataType] || strategies.production[dataType];
}
}

4. 개선 방안​

4.1 마스킹 필드 확장​

// apps/dta-wide-api/src/app/app.module.ts
CoreLoggingModule.forRoot({
httpLogging: {
logRequestBody: true,
logResponseBody: true,
maskPatterns: [
// 인증 관련
{
fields: ['password', 'passwordHash', 'oldPassword', 'newPassword'],
pattern: /./g,
replacement: '*****',
},
{
fields: ['token', 'accessToken', 'refreshToken', 'idToken'],
pattern: /./g,
replacement: '*****',
},
{
fields: ['mfaSecret', 'secret', 'apiKey'],
pattern: /./g,
replacement: '*****',
},
// 개인정보
{
fields: ['email'],
pattern: /^(.{2}).*(@.*)$/,
replacement: '$1***$2', // te***@example.com
},
{
fields: ['phoneNumber', 'phone'],
pattern: /^(\d{3}).*(\d{4})$/,
replacement: '$1****$2', // 010****1234
},
// 식별자
{
fields: ['userId', 'user_id'],
pattern: /^(.{8}).*$/,
replacement: '$1-****', // 부분 마스킹
},
// 네트워크 정보
{
fields: ['ipAddress', 'ip'],
pattern: /\d+\.\d+\.\d+\.(\d+)/,
replacement: '***.***.***.$ 1', // 마지막 옥텟만 표시
},
// 액세스 코드
{
fields: ['code', 'accessCode'],
pattern: /^(.{4}).*(.{4})$/,
replacement: '$1****$2', // 앞뒤 4자리만 표시
},
],
},
})

3.2 도메인별 마스킹 인터셉터 구현​

AccessCode 도메인 마스킹 인터셉터​

// apps/dta-wide-api/src/app/access-code/interceptors/access-code-masking.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable()
export class AccessCodeMaskingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map(data => this.maskAccessCodeData(data))
);
}

private maskAccessCodeData(data: any): any {
if (!data) return data;

// 재귀적으로 객체 순회하며 code 필드 마스킹
if (typeof data === 'object') {
if (Array.isArray(data)) {
return data.map(item => this.maskAccessCodeData(item));
}

const masked = { ...data };

// 액세스 코드 마스킹
if (masked.code && typeof masked.code === 'string') {
const codeLength = masked.code.length;
if (codeLength > 8) {
// 앞 4자리와 뒤 4자리만 표시
masked.code = masked.code.substring(0, 4) +
'*'.repeat(codeLength - 8) +
masked.code.substring(codeLength - 4);
} else if (codeLength > 4) {
// 앞 2자리만 표시
masked.code = masked.code.substring(0, 2) +
'*'.repeat(codeLength - 2);
}
}

// 이메일 마스킹 (액세스 코드와 연관된 경우)
if (masked.email && typeof masked.email === 'string') {
const [localPart, domain] = masked.email.split('@');
if (localPart && domain) {
masked.email = localPart.substring(0, 2) + '***@' + domain;
}
}

// 중첩 객체 처리
Object.keys(masked).forEach(key => {
if (typeof masked[key] === 'object') {
masked[key] = this.maskAccessCodeData(masked[key]);
}
});

return masked;
}

return data;
}
}

3.3 Prisma 미들웨어 마스킹​

// libs/core/database/src/lib/prisma.service.ts
import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect();

// 쿼리 로깅 미들웨어
this.$use(async (params, next) => {
const before = Date.now();

// 쿼리 파라미터 마스킹
const maskedParams = this.maskQueryParams(params);

// 로깅 (마스킹된 파라미터로)
console.log('Query:', {
model: params.model,
action: params.action,
args: maskedParams.args,
});

const result = await next(params);

const after = Date.now();
console.log(`Query took ${after - before}ms`);

// 결과 마스킹
return this.maskQueryResult(result);
});
}

private maskQueryParams(params: any): any {
const masked = { ...params };

if (masked.args?.data) {
masked.args.data = this.maskSensitiveFields(masked.args.data);
}

if (masked.args?.where) {
masked.args.where = this.maskSensitiveFields(masked.args.where);
}

return masked;
}

private maskQueryResult(result: any): any {
if (!result) return result;

if (Array.isArray(result)) {
return result.map(item => this.maskSensitiveFields(item));
}

return this.maskSensitiveFields(result);
}

private maskSensitiveFields(data: any): any {
if (!data || typeof data !== 'object') return data;

const masked = { ...data };
const sensitiveFields = [
'email', 'passwordHash', 'mfaSecret',
'token', 'apiKey', 'ipAddress'
];

sensitiveFields.forEach(field => {
if (field in masked) {
masked[field] = '[MASKED]';
}
});

return masked;
}
}

3.4 환경별 마스킹 설정​

// libs/core/config/src/lib/config.service.ts
export class ConfigService {
getMaskingConfig() {
const env = this.get('NODE_ENV');

switch (env) {
case 'production':
return {
enabled: true,
level: 'strict', // 모든 민감 정보 마스킹
logLevel: 'error', // 에러만 로깅
};

case 'stage':
return {
enabled: true,
level: 'moderate', // 주요 민감 정보만 마스킹
logLevel: 'warn',
};

case 'development':
default:
return {
enabled: false, // 개발 환경에서는 마스킹 비활성화 (선택적)
level: 'minimal',
logLevel: 'debug',
};
}
}
}

4. 구현 우선순위​

Phase 1: 즉시 적용 필요 (High Priority)​

  1. CoreLoggingModule 마스킹 필드 확장

    • email, userId, ipAddress 등 추가
    • 현재 password, token만 마스킹되는 문제 해결
  2. 환경별 마스킹 설정

    • 프로덕션: 완전 마스킹
    • 스테이징: 부분 마스킹
    • 개발: 최소 마스킹

Phase 2: 단기 개선 (Medium Priority)​

  1. 도메인별 마스킹 인터셉터

    • AccessCode, Auth, User 도메인 우선 적용
  2. Prisma 미들웨어 마스킹

    • 데이터베이스 쿼리 로그 마스킹

Phase 3: 장기 개선 (Low Priority)​

  1. 중앙 집중식 마스킹 서비스

    • 모든 도메인에서 사용 가능한 공통 마스킹 서비스
  2. 마스킹 정책 관리 시스템

    • 동적 마스킹 규칙 설정
    • 역할 기반 마스킹 수준 차별화

5. 테스트 및 검증​

5.1 단위 테스트​

// libs/core/logging/src/interceptors/http-logging.interceptor.spec.ts
describe('HttpRequestLoggingInterceptor', () => {
describe('maskSensitiveData', () => {
it('should mask email addresses', () => {
const data = { email: 'test@example.com' };
const masked = interceptor.maskSensitiveData(data);
expect(masked.email).toBe('te***@example.com');
});

it('should mask passwords completely', () => {
const data = { password: 'secretPassword123' };
const masked = interceptor.maskSensitiveData(data);
expect(masked.password).toBe('*****');
});

it('should mask nested objects', () => {
const data = {
user: {
email: 'nested@example.com',
profile: {
phone: '01012345678'
}
}
};
const masked = interceptor.maskSensitiveData(data);
expect(masked.user.email).toBe('ne***@example.com');
expect(masked.user.profile.phone).toBe('010****5678');
});
});
});

5.2 통합 테스트​

// apps/dta-wide-api-e2e/src/masking.spec.ts
describe('Data Masking E2E', () => {
it('should mask sensitive data in API responses', async () => {
const response = await request(app.getHttpServer())
.get('/users/profile')
.expect(200);

// 이메일이 마스킹되었는지 확인
expect(response.body.email).toMatch(/^.{2}\*+@.+$/);

// 비밀번호 해시가 응답에 포함되지 않았는지 확인
expect(response.body.passwordHash).toBeUndefined();
});
});

6. 모니터링 및 알림​

6.1 마스킹 실패 모니터링​

// 마스킹 실패 시 알림
if (maskingFailed) {
this.logger.error('Masking failed for sensitive data', {
context: 'HttpLoggingInterceptor',
field: fieldName,
error: error.message,
});

// 메트릭 수집
this.metricsService.increment('masking.failure', {
field: fieldName,
interceptor: 'http-logging',
});
}

6.2 대시보드 메트릭​

  • 마스킹 성공률
  • 마스킹 처리 시간
  • 민감 데이터 노출 시도 횟수
  • 환경별 마스킹 적용 현황

7. 관련 파일 및 문서​

7.1 핵심 구현 파일​

  • libs/core/logging/src/interceptors/http-logging.interceptor.ts - HTTP 로깅 인터셉터
  • apps/dta-wide-api/src/app/app.module.ts - 메인 모듈 설정
  • libs/core/database/prisma/schema.prisma - 데이터베이스 스키마

7.2 관련 문서​

8. 변경 이력​

버전날짜작성자변경 내용
0.1.02025-08-14bok@weltcorp.com실제 구현 기반 데이터 마스킹 가이드 작성

중요: 이 문서는 실제 구현된 코드를 기반으로 작성되었으며, 즉시 적용 가능한 개선사항을 포함하고 있습니다. Phase 1의 내용은 보안상 시급히 적용이 필요합니다.