ETL2025년 7월 8일4분 읽기

고객사가 내부 로그를 달라고 했다 — 폐쇄망 Splunk ETL 파이프라인 설계기

원본 로그는 줄 수 없고, 아무것도 안 줄 수도 없었다. 개인정보 마스킹 ETL + 폐쇄망 Splunk 주입까지 제약 조건 속에서 설계한 과정.

#ETL#Splunk#보안#개인정보#감사로그#Backend

고객사가 내부 로그를 달라고 했다 — 폐쇄망 Splunk ETL 파이프라인 설계기


상황

협업 SaaS에서 일하던 중, 대형 엔터프라이즈 고객사에서 특이한 요청이 왔습니다. 연간 보안 감사를 위해 자사 협업 플랫폼 내 모든 액션 로그가 필요하다는 것이었습니다. 메시지 생성·수정·삭제, 파일 접근 이력, 채널 입퇴장 등 사용자가 자사 협업 플랫폼에서 한 모든 행동의 기록이었습니다.

요구 자체는 이해할 수 있었습니다. 대기업은 내부 감사 요건이 있고, SaaS 서비스를 사용한다고 해서 감사 대상에서 제외되지 않습니다. "우리 플랫폼에서 어떤 일이 일어났는지 우리가 볼 수 있어야 한다"는 정당한 요구였습니다.

문제는 우리가 그 로그를 그대로 줄 수 없다는 것이었습니다.


제약 조건 두 가지

첫째, 원본 로그에는 개인정보가 있었습니다.

내부 로그에는 사용자 이름, 이메일, IP 주소 같은 정보가 포함되어 있었습니다. 개인정보보호법상 이걸 외부 고객사에 그대로 제공하는 건 불가능했습니다. 개인 식별 정보(PII)를 적절히 처리해야 했습니다.

둘째, 고객사 환경이 폐쇄망이었습니다.

일반적인 기업 보안 환경에서는 외부 인터넷 접근이 차단된 내부망(폐쇄망)에서 시스템을 운영합니다. 우리 서버에서 고객사 Splunk로 데이터를 직접 전송하는 게 불가능한 상황이었습니다.

이 두 제약 조건 안에서 고객의 감사 요건을 충족해야 했습니다.


설계한 구조

1단계: ETL — 원본 로그에서 감사용 데이터 추출

text
내부 로그 DB
    │
    │  Extract
    ▼
원본 로그 (이름, 이메일, IP 포함)
    │
    │  Transform (마스킹 + 포맷 변환)
    ▼
감사 로그 (고객 ID만, 개인정보 제거)
    │
    │  Load
    ▼
감사 로그 DB (고객사 전용)

마스킹 처리 기준

  • 이름 → 제거
  • 이메일 → 제거
  • IP 주소 → 제거
  • 사용자 ID → 유지 (고객사 내부에서 자신들의 ID와 매핑 가능)
  • 액션 타입, 타임스탬프, 채널 ID → 유지
javascript
function maskAuditLog(rawLog) {
  return {
    eventId: rawLog.id,
    timestamp: rawLog.createdAt,
    userId: rawLog.userId,           // 유지 (고객사 내부 매핑용)
    action: rawLog.action,           // 유지 (create, update, delete 등)
    resourceType: rawLog.resourceType, // 유지 (message, file, channel 등)
    resourceId: rawLog.resourceId,   // 유지
    // 제거: userName, userEmail, userIp
  };
}

중요한 설계 결정이 있었습니다. 사용자 이름과 이메일을 제거하면 고객사가 "이 액션이 누가 했는지"를 어떻게 알 수 있을까요? 고객사 자신의 디렉토리(AD, LDAP)에서 userId로 매핑할 수 있습니다. 내부 감사 목적이라면 고객사가 userId만으로 직원을 특정할 수 있습니다. 개인정보보호법상 민감한 PII를 우리가 외부에 제공하지 않아도 되고, 고객사는 감사 목적은 달성할 수 있는 구조입니다.

2단계: 폐쇄망 Splunk 전송 구조

폐쇄망 환경에서 외부 데이터를 받는 일반적인 방법은 프록시 서버입니다.

text
[우리 서버]
    │
    │  HTTPS 전송
    ▼
[고객사 DMZ의 프록시 서버]
    │
    │  내부망 경유
    ▼
[폐쇄망 내부 Splunk]

Splunk는 HEC(HTTP Event Collector)라는 토큰 기반 데이터 수신 인터페이스를 제공합니다. 고객사는 DMZ에 Splunk Forwarder를 두고, 우리가 전송한 데이터를 내부 Splunk로 포워딩하는 구조로 구성했습니다.

javascript
// Splunk HEC로 데이터 전송
async function sendToSplunk(events, config) {
  const payload = {
    events: events.map(event => ({
      time: new Date(event.timestamp).getTime() / 1000,
      source: 'swit-audit',
      sourcetype: 'swit:audit:log',
      event: event,
    })),
  };
 
  await axios.post(config.hecEndpoint, payload, {
    headers: {
      'Authorization': `Splunk ${config.hecToken}`,
      'Content-Type': 'application/json',
    },
    httpsAgent: new https.Agent({
      rejectUnauthorized: true, // 인증서 검증 필수
    }),
  });
}

배치 처리와 재전송 처리

감사 로그는 실시간이 아니라 일 단위 배치로 전송했습니다.

실시간 전송의 문제점:

  • 폐쇄망 프록시의 일시적인 다운으로 로그가 유실될 수 있음
  • 네트워크 불안정 시 스파이크 발생

배치 처리의 장점:

  • 실패 시 해당 배치를 재전송 가능
  • 전송 시간을 새벽 저트래픽 시간대로 고정 가능
  • 전송 이력을 DB에 남겨 어디까지 전송했는지 추적 가능
javascript
// 마지막으로 전송한 시점 이후의 로그만 가져오기
async function fetchUnsentLogs(lastSentAt) {
  return auditLogRepo.findAll({
    where: {
      createdAt: { $gt: lastSentAt },
    },
    orderBy: { createdAt: 'asc' },
    take: 1000, // 한 번에 최대 1000건
  });
}
 
// 전송 완료 후 체크포인트 업데이트
async function updateSendCheckpoint(lastLogId, lastLogTime) {
  await checkpointRepo.upsert({
    key: 'splunk-audit-checkpoint',
    lastSentId: lastLogId,
    lastSentAt: lastLogTime,
  });
}

결과

항목해결 방법
원본 PII 노출마스킹 ETL로 이름·이메일·IP 제거, userId만 전달
폐쇄망 전송DMZ 프록시 + Splunk HEC 토큰 인증
전송 안정성일 단위 배치 + 체크포인트 기반 재전송
법적 준수개인정보보호법 준수 (PII 미전달), 고객사 감사 요건 충족

고객사는 자신들의 Splunk 대시보드에서 자사 협업 플랫폼 내 모든 액션 이력을 조회할 수 있게 됐고, 연간 보안 감사를 통과했습니다.


마치며

이 프로젝트에서 배운 것은 제약 조건이 곧 설계의 방향이라는 것입니다.

"원본 로그를 그대로 줄 수 없다"는 제약이 ETL + 마스킹이라는 해결책을 만들었습니다. "폐쇄망이라 직접 전송이 안 된다"는 제약이 프록시 + HEC라는 구조를 만들었습니다.

제약이 없었다면 더 간단한 방법을 썼을 것이고, 그랬다면 개인정보 문제나 보안 문제가 뒤에 터졌을 겁니다. 제약이 오히려 더 견고한 설계로 이끌었습니다.

"왜 이렇게 복잡하게 만들었냐"는 질문에 대한 답은 "제약 조건이 그 복잡함을 요구했기 때문"입니다.

#ETL#Splunk#보안#개인정보#감사로그#Backend

황호민

Backend Engineer · Java/Kotlin · Spring Boot · Next.js