데이터 마스킹 구현 현황 및 가이드
개요
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)
-
CoreLoggingModule 마스킹 필드 확장
- email, userId, ipAddress 등 추가
- 현재 password, token만 마스킹되는 문제 해결
-
환경별 마스킹 설정
- 프로덕션: 완전 마스킹
- 스테이징: 부분 마스킹
- 개발: 최소 마스킹
Phase 2: 단기 개선 (Medium Priority)
-
도메인별 마스킹 인터셉터
- AccessCode, Auth, User 도메인 우선 적용
-
Prisma 미들웨어 마스킹
- 데이터베이스 쿼리 로그 마스킹
Phase 3: 장기 개선 (Low Priority)
-
중앙 집중식 마스킹 서비스
- 모든 도메인에서 사용 가능한 공통 마스킹 서비스
-
마스킹 정책 관리 시스템
- 동적 마스킹 규칙 설정
- 역할 기반 마스킹 수준 차별화
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 관련 문서
- Platform Requirements
- Security Guidelines
- ISO 27001 암호화 컴플라이언스 현황 - 전체 암호화 정책 및 현황
- 데이터베이스 보안 구현 가이드 - DB 세션 보안 및 접근 제어
- BigQuery 마스킹된 로그 쿼리 가이드 - 실제 마스킹 적용 상태 확인 방법
8. 변경 이력
| 버전 | 날짜 | 작성자 | 변경 내용 |
|---|---|---|---|
| 0.1.0 | 2025-08-14 | bok@weltcorp.com | 실제 구현 기반 데이터 마스킹 가이드 작성 |
중요: 이 문서는 실제 구현된 코드를 기반으로 작성되었으며, 즉시 적용 가능한 개선사항을 포함하고 있습니다. Phase 1의 내용은 보안상 시급히 적용이 필요합니다.