SSL Inspection

VPP 기반 SSL/TLS Inspection(TLS 가시성) 아키텍처와 실전 구현 가이드입니다. 이중 TLS 세션 관리, 동적 leaf 인증서 발급, CA 인프라 구성, VCL/VLS/LD_PRELOAD/memif 선택, SNI 바이패스, TLS 1.3/ECH 고려사항, 운영 트러블슈팅 런북, 거버넌스 통합을 다룹니다.

선행 문서: 본 페이지는 TCP/TLS 프록시 · SSL Inspection의 TCP 프록시·TPROXY 기본 원리와 호스트 스택 개요의 VCL/VLS 세션 레이어를 전제로 합니다. DPU 오프로드 관점은 VPP on DPU를 함께 확인하시기 바랍니다.

SSL Inspection (TLS 가시성 확보)

SSL Inspection(TLS Inspection)은 암호화된 트래픽의 내용을 중간에서 복호화하여 보안 검사(IDS/IPS, DLP, 악성코드 탐지)를 수행한 후 재암호화하여 전달하는 기술입니다. 엔터프라이즈 방화벽, NGFW, 보안 웹 게이트웨이(SWG)의 핵심 기능이며, VPP의 유저스페이스 TLS 스택은 이 기능을 고성능으로 구현하기에 최적의 플랫폼입니다.

역할 분담 안내: "SSL Inspection이 개념적으로 무엇인지·왜 필요한지·원리상 어떤 한계가 있는지" 같은 기초 개념은 VPP on DPU 문서의 0번 소절(SSL Inspection 기초 — 개념·필요성·최소 구현)에서 교육용 Python 예시와 함께 정리되어 있습니다. 이 페이지는 그 개념을 전제로 VPP host stack·TLS 플러그인·세션 계층 위에 실제로 배선하는 방법에 집중합니다. 따라서 아래 소절들은 이중 세션 관리, 동적 leaf 인증서 엔진, VCL/VLS/memif 선택, 핸드셰이크 MSC, 성능 최적화, 배치 토폴로지를 다룹니다. DPU 위에서 이것을 라인레이트로 구현하는 관점은 VPP on DPU 문서의 "DPU에서 VPP 구현" 절에서 이어집니다.

SSL Inspection 아키텍처 개요

"두 개의 TLS 세션으로 나누어 중간에서 평문을 본다"는 개념 자체는 운영편 0.1절에서 이미 다루었습니다. 이 소절은 그 이중 세션 모델을 VPP의 내부 구성요소로 옮겨 놓았을 때 어떤 모듈이 어떤 역할을 맡는지를 한 장의 그림으로 정리합니다. 핵심은 "TLS 종단 ① / 평문 검사 영역 / TLS 개시 ② / 동적 인증서 엔진 / 바이패스 정책 엔진" 다섯 블록이 한 프로세스(VPP) 안에서 세션 계층을 통해 맞물린다는 점입니다.

SSL Inspection (MITM 프록시) 아키텍처 클라이언트 원본 서버의 인증서를 기대하지만 프록시 CA 수신 TLS 세션 ① 프록시 CA가 서명한 동적 인증서 사용 VPP SSL Inspection 프록시 TLS 종단 ① 클라이언트 측 복호화 평문 검사 영역 IDS/IPS 엔진 DLP / 악성코드 URL 필터링 · 프로토콜 분석 · 로깅 TLS 개시 ② 서버 측 재암호화 동적 인증서 생성 엔진 프록시 CA 키로 원본 서버 도메인의 인증서를 실시간 서명 바이패스 정책 엔진 금융·의료·인증서 피닝 트래픽 선별 우회 TLS 세션 ② 원본 서버의 실제 인증서 검증 원본 서버 실제 서버 인증서 제공 핵심: 클라이언트에는 프록시 CA로 서명한 인증서 제공 → 프록시가 평문 접근 → 원본 서버와는 실제 TLS 연결 클라이언트 시스템에 프록시 CA를 신뢰 저장소에 사전 설치 필요 (GPO, MDM 등으로 배포)

SSL Inspection의 두 가지 배포 모드가 있습니다:

모드동작 방식클라이언트 설정적용 시나리오
투명 프록시(Transparent)네트워크 경로 상에서 패킷을 가로채어 검사합니다. 클라이언트는 프록시 존재를 인식하지 못합니다프록시 CA 인증서만 설치엔터프라이즈 방화벽, ISP 보안 장비
명시적 프록시(Explicit)클라이언트가 HTTP CONNECT 메서드로 프록시에 터널(Tunnel) 요청을 보냅니다프록시 주소 + CA 인증서 설정SWG(Secure Web Gateway), 클라우드 프록시

이중 TLS 세션 구조

SSL Inspection의 핵심은 이중 TLS 세션(Dual TLS Session) 관리입니다. VPP의 TLS 추상 계층이 이미 이중 세션 모델(TCP 세션 + App 세션)을 사용하므로, SSL Inspection은 이를 확장하여 4개의 세션을 동시에 관리합니다.

SSL Inspection 이중 TLS 세션 데이터 흐름 클라이언트 측 (TLS 세션 ①) 클라이언트 TCP ① TLS 복호화 SSL_read() 평문 검사 IDS · DLP · URL 필터 정책 판정: 허용/차단/로깅 서버 측 (TLS 세션 ②) TLS 암호화 SSL_write() TCP ② 원본 서버 세션 / FIFO 상세 매핑 클라이언트 TCP 세션 RX FIFO (암호문) TX FIFO (암호문) TLS 컨텍스트 ① (서버 역할) App RX (평문) App TX (평문) 검사 애플리케이션 평문 RX → 검사 → 평문 TX 포워딩 TLS 컨텍스트 ② (클라이언트 역할) App RX (평문) App TX (평문) SSL Inspection 세션 구성 요약 ① 클라이언트 TCP 세션 (암호문 FIFO) ↔ TLS 컨텍스트 ① (프록시가 서버 역할, 동적 인증서 사용) ② 검사 애플리케이션: TLS ①의 평문 → 보안 검사 → TLS ②의 평문으로 전달 ③ TLS 컨텍스트 ② (프록시가 클라이언트 역할, 원본 서버 인증서 검증) ↔ 서버 TCP 세션 (암호문 FIFO) 성능 영향: 이중 TLS 오버헤드 • 암호화 연산 2배 (복호화 + 재암호화) • 핸드셰이크 2회 (클라이언트 + 서버) • FIFO 4쌍 (TCP×2 + App×2) 메모리 사용 • VPP 이점: 유저스페이스 FIFO 연결로 복사 최소화 • QAT 오프로드로 이중 암호화 부담 경감 • 벡터 처리로 배치 암복호화 효율 극대화
/* SSL Inspection 프록시의 핵심 데이터 구조 (개념적 설계) */
typedef struct ssl_inspect_session_ {
    /** 클라이언트 측 TLS 세션 (프록시가 서버 역할) */
    u32 client_tls_session_handle;
    tls_ctx_t *client_tls_ctx;      /* 동적 생성 인증서 사용 */

    /** 서버 측 TLS 세션 (프록시가 클라이언트 역할) */
    u32 server_tls_session_handle;
    tls_ctx_t *server_tls_ctx;      /* 원본 서버 인증서 검증 */

    /** 검사 정책 결과 */
    ssl_inspect_action_t action;    /* ALLOW / BLOCK / LOG / BYPASS */

    /** 원본 서버 정보 (SNI에서 추출) */
    u8 *server_name;                /* ClientHello SNI 값 */
    u8 *original_cert_subject;      /* 원본 인증서 CN/SAN */

    /** 동적 인증서 캐시 인덱스 */
    u32 cert_cache_index;

    /** 검사 통계 */
    u64 bytes_inspected;
    u64 threats_detected;
} ssl_inspect_session_t;
코드 설명
  • 3~5행 client_tls_ctx는 프록시가 서버 역할을 수행하는 TLS 컨텍스트입니다. 클라이언트가 요청한 SNI 도메인에 맞는 동적 인증서(프록시 CA로 서명)를 사용합니다.
  • 7~9행 server_tls_ctx는 프록시가 클라이언트 역할을 수행하는 TLS 컨텍스트입니다. 원본 서버의 실제 인증서를 CA 체인으로 검증하여 중간자 공격(Man-in-the-Middle)이 아닌 정당한 서버임을 확인합니다.
  • 12행 action 필드는 보안 검사 엔진의 판정 결과를 저장합니다. ALLOW(통과), BLOCK(차단), LOG(기록 후 통과), BYPASS(검사 생략) 중 하나입니다.
  • 18행 동적 인증서 생성은 비용이 높으므로 cert_cache_index로 동일 도메인의 인증서를 캐싱합니다. 캐시 히트 시 RSA/ECDSA 서명 연산을 생략할 수 있습니다.

핸드셰이크 MSC — 시간축으로 본 이중 세션

이중 세션 구조는 "어떤 컴포넌트가 있는가"까지는 블록도로 표현할 수 있지만, 언제 무엇을 기다려야 하는가는 시간축 다이어그램(Message Sequence Chart, MSC)으로만 드러납니다. 이 소절은 TLS 1.3 한 세션이 SSL Inspection을 거칠 때 클라이언트·VPP 중간자·원서버 사이에 오가는 메시지를 시간 순으로 정렬합니다. 이어지는 동적 인증서 엔진·세션 FIFO 설명은 이 MSC 위의 한 점(SignLeaf 호출 직후)에서 벌어지는 일을 확대해 보는 관점입니다.

TLS 1.3 SSL Inspection 핸드셰이크 MSC (일반 경로 · 세션 재개 · HRR) Client VPP Middlebox (TLS-A / TLS-B) Origin Server TLS-A server role TLS-B client role Phase 1 — 공통 경로 (신규 세션) ① ClientHello (SNI=example.com, key_share, ALPN) ② SNI 파싱 · 정책 조회 ③ upstream ClientHello (TLS-B 개시) ④ ServerHello · Cert · Finished ⑤ SignLeaf(SAN 복사 → CA 서명) ⑥ ServerHello · (기업CA 서명) Cert · Finished ⑦ Client Finished → TLS-A 완료 ⑧ Client Finished → TLS-B 완료 ⑨ 이후 application_data: Client ↔ TLS-A 복호 → 검사 → TLS-B 암호 ↔ Origin Phase 2 — 세션 재개 경로 (PSK / NewSessionTicket) ① ClientHello (pre_shared_key 확장 + early_data?) ② PSK 캐시 확인 early_data → 거부·대기 ③ upstream CH (PSK 재사용 or 신규) ④ ServerHello(+NewSessionTicket) ⑤ ServerHello + 자체 NewSessionTicket(TLS-A) Phase 3 — HRR 경로 (key_share 불일치) ③' upstream CH (x25519 시도) ④' HelloRetryRequest(group=secp384r1) ③'' upstream CH2 (secp384r1) → 이후 ④~⑧은 공통 경로와 동일

이 MSC에서 짚어야 할 다섯 가지 포인트는 다음과 같습니다. 각 포인트는 뒤이어 나오는 소절들이 "왜 그런 구조로 구현되는가"에 대한 답을 제공합니다.

  1. ② SNI 파싱 이전에는 업스트림을 열 수 없다 — ①의 ClientHello 안의 server_name 확장을 파싱해야만 ③의 upstream ClientHello에 목적지가 정해집니다. VPP에서는 tls-async-handshake 계열 플러그인이 ClientHello를 먼저 본 뒤 accept 콜백을 미룬다는 점이 여기서 기인합니다. 자세한 배선은 운영편 11번 소절에서 다룹니다.
  2. ⑤ SignLeaf은 ④ 직후에만 수행된다 — 원서버 leaf의 Subject/SAN을 복사해야 하므로, ⑤의 서명은 반드시 ④ 이후에 일어납니다. 이 시점이 TLS 1.3 핸드셰이크에서 가장 지연에 민감한 구간이며, cert cache 히트 여부에 따라 평균 지연이 크게 갈립니다(다음 소절).
  3. ⑦과 ⑧의 순서는 서로 독립 — Client Finished(⑦)와 upstream Finished(⑧)는 상호 의존이 없어서 병렬로 처리됩니다. VPP에서는 이 병렬성을 살리기 위해 양 방향 FIFO 쌍을 따로 두며, 둘이 모두 끝나야만 application_data 구간이 열립니다.
  4. Phase 2의 early_data는 기본 차단 — TLS 1.3의 0-RTT는 클라이언트가 ServerHello를 받기 전에 데이터를 흘려 보내는 구조이지만, SSL Inspection은 아직 TLS-B가 완성되기 전이라 이 데이터를 검사할 수 없습니다. 대부분의 구현은 early_data를 거부하거나 버퍼링하는 쪽을 기본값으로 둡니다. 이것이 "0-RTT 미지원"이라는 운영 고지의 실제 이유입니다.
  5. Phase 3의 HRR은 두 핸드셰이크에서 따로 발생할 수 있다 — TLS-B 쪽에서 key_share group이 맞지 않아 HelloRetryRequest를 받으면, VPP는 업스트림 CH를 재전송해야 합니다. TLS-A 쪽은 별개로 자체 HRR을 낼 수도 있어서, 네 번의 ClientHello가 한 세션 안에서 오갈 수 있습니다. 세션 상태 머신이 "CH 카운터"를 분리해서 들고 있어야 하는 이유가 이것입니다.
관측 팁: 위 MSC 각 단계는 VPP의 event-logger와 TLS 플러그인 내부 트레이스에 거의 그대로 나타납니다. show event-logger로 ①~⑧이 순서대로 찍히는지, 특히 ⑤ signleaf와 ④ upstream_finished 사이의 지연(ms 단위)이 장시간 튀지 않는지를 보는 것이, HSM/공개키 가속기의 포화 여부를 빨리 잡아내는 가장 싼 방법입니다. 상세는 SSL Inspection 모니터링 및 디버깅 소절에서 다룹니다.

동적 인증서 생성 엔진

SSL Inspection의 핵심 컴포넌트는 동적 인증서 생성 엔진입니다. 클라이언트가 ClientHello에 포함한 SNI(Server Name Indication) 값을 기반으로, 프록시 CA 개인 키로 해당 도메인의 인증서를 실시간(Real-time)으로 서명합니다.

SSL Inspection 동적 인증서 생성 흐름 ① ClientHello 수신 SNI: api.example.com ② 바이패스 정책 확인 금융/의료/피닝 도메인? → 바이패스 or 검사 진행 검사 ③ 인증서 캐시 조회 api.example.com 캐시? TTL + 원본 만료일 확인 히트 캐시된 인증서 사용 미스 ④ 원본 서버 TLS 연결 실제 서버 인증서 수신 CN, SAN, 유효기간 추출 ⑤ 동적 인증서 생성 X.509 인증서 구성 Subject: 원본과 동일한 CN SAN: 원본의 SAN 복사 키 생성 / 재사용 RSA-2048 또는 ECDSA P-256 도메인별 키 캐싱 가능 프록시 CA로 서명 Issuer: Enterprise Proxy CA | 유효기간: 원본과 동일 or 단축 X509_sign(proxy_ca_key, EVP_sha256()) — RSA: ~0.5ms, ECDSA: ~0.1ms, QAT: ~0.02ms ⑥ 인증서 캐시 저장 + TLS 응답 ServerHello + 동적 인증서 → 클라이언트 성능 핵심: 인증서 캐시 히트율이 SSL Inspection CPS를 결정합니다 캐시 미스 → RSA 서명 ~0.5ms 추가 | 캐시 히트 → 서명 생략 | 일반적 히트율: 95%+ (동일 도메인 반복 접속)
/* 동적 인증서 생성 구현 (OpenSSL 기반, 간략화) */
static X509 *
ssl_inspect_generate_cert (ssl_inspect_ctx_t *ctx,
                           const char *server_name,
                           X509 *original_cert)
{
    X509 *cert = X509_new ();
    EVP_PKEY *pkey;
    X509_NAME *subj;

    /* 1. 키 쌍 생성 또는 캐시에서 재사용 */
    pkey = ssl_inspect_get_or_create_key (server_name);

    /* 2. 원본 인증서의 Subject/SAN 복사 */
    subj = X509_get_subject_name (original_cert);
    X509_set_subject_name (cert, subj);

    /* 3. Issuer를 프록시 CA로 설정 */
    X509_set_issuer_name (cert,
        X509_get_subject_name (ctx->proxy_ca_cert));

    /* 4. SAN(Subject Alternative Name) 복사 */
    ssl_inspect_copy_san (cert, original_cert);

    /* 5. 유효기간 설정 (원본과 동일 또는 단축) */
    X509_set_notBefore (cert, X509_get_notBefore (original_cert));
    X509_set_notAfter (cert, X509_get_notAfter (original_cert));

    /* 6. 시리얼 번호 (고유) */
    ASN1_INTEGER_set (X509_get_serialNumber (cert),
                      ssl_inspect_next_serial ());

    /* 7. 공개 키 설정 */
    X509_set_pubkey (cert, pkey);

    /* 8. 프록시 CA 개인 키로 서명 (비용이 큰 연산) */
    X509_sign (cert, ctx->proxy_ca_key, EVP_sha256 ());

    /* 9. 캐시에 저장 (TTL: 인증서 유효기간 또는 설정값) */
    ssl_inspect_cache_cert (server_name, cert, pkey);

    return cert;
}

/* 인증서 캐시 구조 */
typedef struct {
    u8 *domain;             /* 해시 키: 서버 도메인 */
    X509 *cert;             /* 동적 생성된 인증서 */
    EVP_PKEY *pkey;         /* 대응하는 개인 키 */
    f64 created_at;         /* 생성 시각 (VPP 시간) */
    f64 ttl;                /* Time-to-Live (초) */
    u32 hit_count;          /* 캐시 히트 횟수 */
} ssl_inspect_cert_cache_entry_t;
코드 설명
  • 11행 도메인별 키 쌍을 캐싱하여 재사용합니다. 매 연결마다 RSA 키를 생성하면 ~2ms가 추가되므로, 키 재사용은 성능에 핵심적입니다.
  • 14~16행 원본 인증서의 Subject(CN)를 그대로 복사합니다. 클라이언트의 인증서 검증 로직이 CN을 확인하므로, 원본과 동일해야 브라우저 경고가 발생하지 않습니다(프록시 CA를 신뢰하는 경우).
  • 22행 SAN(Subject Alternative Name) 확장을 원본에서 복사합니다. 현대 브라우저는 CN보다 SAN을 우선하므로, SAN 누락 시 ERR_CERT_COMMON_NAME_INVALID 오류가 발생합니다.
  • 34행 X509_sign()이 전체 흐름에서 가장 비용이 큰 연산입니다. RSA-2048 서명은 CPU에서 ~0.5ms, QAT 오프로드 시 ~0.02ms가 소요됩니다. 이 연산을 줄이기 위해 인증서 캐싱이 필수적입니다.

LRU 캐시 오버플로와 지연 시간 스파이크(Latency Spike)

동적 인증서 캐시는 해시맵 + LRU 연결 리스트(Linked List) 조합으로 구현하는 것이 표준입니다. 그런데 운영 환경에서 가장 흔히 관찰되는 장애는 "평상시 1ms 미만이던 SSL_accept 지연이 특정 시간대에 10~50ms로 급격히 튀는" 현상입니다. 근본 원인은 세 가지로 요약됩니다. ① 캐시 용량 초과에 따른 대규모 eviction, ② TTL 동시 만료(Expiry Thundering Herd), ③ 프록시 CA 개인 키 경합(RSA 서명 직렬화(Serialization))입니다.

첫 번째 원인은 크롤러 트래픽이나 봇 공격이 수천 개의 1회성 도메인을 SNI로 보낼 때 발생합니다. 캐시 상한(예: 2048개)을 초과하면 LRU 정책이 가장 오래된 엔트리를 대량으로 퇴출시키는데, 이 작업은 연결 리스트 조작·X509/EVP_PKEY 객체 free·해시맵 rehash를 수반해 단일 세션 처리 경로를 수 ms 동안 블록합니다. 두 번째 원인은 TTL을 동일 값(예: 3600초)으로 설정했을 때 발생합니다. 한 시점에 대량 생성된 인증서가 정확히 1시간 뒤 동시에 만료되면, 다음 ClientHello 배치가 전부 캐시 미스로 떨어져 RSA 서명 폭주가 일어납니다.

/* LRU 캐시 안정화 — jittered TTL + 연성 제한(soft limit) */
static void
cert_cache_insert (cert_cache_t *c, const char *sni,
                    X509 *cert, EVP_PKEY *k)
{
  /* 1. TTL에 ±15% 지터(jitter) 추가 — thundering herd 방지 */
  f64 base_ttl = c->default_ttl;
  f64 jitter   = ((f64) (random_u32 () % 30) - 15.0) / 100.0;
  f64 ttl      = base_ttl * (1.0 + jitter);

  /* 2. 연성 제한 도달 시 백그라운드 evict 큐에 예약 (인라인 free 금지) */
  if (hash_elts (c->map) > c->soft_limit) {
    evict_queue_push (&c->evict_q, lru_tail (c));
    /* 본 스레드는 즉시 insert 진행 — 정리는 백그라운드 워커가 수행 */
  }

  /* 3. 경성 제한(hard limit) 도달 시에만 동기적 eviction */
  if (hash_elts (c->map) > c->hard_limit) {
    cert_cache_entry_t *victim = lru_pop_tail (c);
    hash_unset (c->map, victim->sni);
    X509_free (victim->cert);
    EVP_PKEY_free (victim->pkey);
    clib_mem_free (victim);
  }

  /* 4. 새 엔트리 삽입 + LRU head에 연결 */
  cert_cache_entry_t *e = clib_mem_alloc (sizeof (*e));
  e->sni = format (0, "%s", sni);
  e->cert = cert;
  e->pkey = k;
  e->created_at = vlib_time_now (vm);
  e->ttl = ttl;
  hash_set_mem (c->map, e->sni, e);
  lru_push_head (c, e);
}

/* 5. 부정 캐시(Negative cache) — 1회성 SNI 공격 완화 */
static int
cert_cache_lookup_with_neg (cert_cache_t *c, const char *sni)
{
  if (bloom_filter_test (&c->singleton_bloom, sni))
    return CERT_CACHE_BYPASS;  /* 이전에 1회만 본 SNI — 서명 생략, 원본 통과 */
  ... 정상 조회 ... */
}
시나리오캐시 상태SSL_accept p50p99완화책
정상 브라우징히트율 96%0.3 ms1.2 ms해당 없음
크롤러 폭주히트율 12%, LRU 빈번1.8 ms47 ms부정 캐시 + Bloom
동시 TTL 만료히트율 3% (배치 만료)2.4 ms68 ms±15% TTL 지터
CA 키 경합QAT 미사용4.1 ms120 msper-worker CA + QAT
모든 완화책 적용히트율 88%0.5 ms3.8 ms

운영 권고: 프록시 CA 개인 키는 워커별로 사본을 두고, QAT(또는 AES-NI 이상의 비대칭 가속기)를 장착한 장비에서는 ENGINE_set_default_RSA로 서명을 오프로드하세요. CA 키의 RSA-2048 서명은 x86 단일 코어에서 ~500μs이지만 QAT 오프로드 시 ~20μs로 떨어져 p99 개선 효과가 극적입니다. 단, QAT 큐 오버플로 시 폴백 경로가 CPU이므로 queue depth 모니터링이 필수입니다.

TLS 1.3 0-RTT 재사용과 Replay 위험

TLS 1.3은 핸드셰이크 RTT를 줄이기 위해 0-RTT 조기 데이터(Early Data)PSK(Pre-Shared Key) 기반 재개를 도입했습니다. SSL Inspection 프록시가 PSK/session ticket을 캐싱하면 클라이언트는 동일 프록시로 재연결 시 1-RTT 혹은 0-RTT로 빠르게 복귀할 수 있습니다. 그러나 0-RTT 데이터는 forward secrecy가 약하고 재생(replay) 공격이 가능하다는 RFC 8446 §2.3/§8의 근본적 제약이 있어, 프록시가 이를 중계할 때는 멱등(idempotent) 요청(HTTP GET 등)만 허용해야 합니다.

ECH(Encrypted ClientHello)와 "우회 불가" 한계

TLS 1.3의 후속 확장인 ECH(Encrypted ClientHello, draft-ietf-tls-esni)는 클라이언트가 실제 SNI를 ECH 공개 키로 암호화하여 외부 SNI("public name")만 노출시킵니다. ECH를 지원하는 클라이언트(Chrome 117+, Firefox 118+)와 HTTPS 레코드가 DNS에 게시된 서버 사이 통신은 다음과 같은 이유로 기존 SSL Inspection 아키텍처로 복호화가 구조적으로 불가능합니다.

  1. ECH 공개 키 미보유: 프록시가 ECH를 풀려면 해당 서비스의 ECH 개인 키가 필요하지만, 이는 오직 진짜 서버만 보유합니다. 중간자가 임의로 "가짜 ECH 공개 키"를 DNS에 주입해도 DNSSEC/DoH를 사용하는 클라이언트는 이를 거부합니다.
  2. GREASE ECH 차단의 역효과: 일부 기업은 ECH ClientHello를 아예 drop해 "ECH 미지원으로 다운그레이드"를 유도했지만, ECH draft-15 이상은 이 다운그레이드를 탐지해 클라이언트가 연결을 중단(fatal alert)합니다.
  3. Outer SNI 기반 정책의 과매칭: 외부 SNI가 cloudflare-ech.com처럼 공용 앵커 도메인이면 "바이패스 or 차단" 둘 중 하나만 선택할 수 있어, 기존의 도메인 단위 정책이 전부 무력화됩니다.

현실적 대응: ECH를 사용하는 서비스에 대해 SSL Inspection을 강제하려면 (a) 조직 장비에 ECH 클라이언트 비활성화 정책(Chrome: EncryptedClientHelloEnabled=false) 배포, (b) 엔드포인트 에이전트에서 복호화 후 재발사, (c) 해당 도메인 자체 차단 중 하나를 택해야 합니다. "투명 프록시만으로 ECH 트래픽을 해독할 수 있다"고 약속하는 벤더 주장은 항상 검증이 필요합니다.

IDS 연동 memif 큐 크기 계산식

VPP가 복호화한 평문을 Suricata/Snort memif 인터페이스로 전달할 때, 큐가 너무 작으면 IDS가 잠시 뒤처질 때 VPP가 drop을 발생시키고, 너무 크면 메모리 낭비와 지연 증가를 초래합니다. 적정 큐 크기는 IDS 처리 지터(jitter)와 VPP 순간 입력 폭주를 모두 수용하도록 계산합니다.

기본 공식은 다음과 같습니다:

queue_depth ≥ (peak_pps × max_ids_stall_ms / 1000) × safety_factor

예: peak_pps         = 200,000 (200K pps 복호화 평문)
    max_ids_stall_ms = 8 (Suricata GC/rule reload 최악 지연)
    safety_factor    = 2 (워커 수 증가·순간 폭주 대비)

→ queue_depth ≥ 200000 × 0.008 × 2 = 3200 descriptor
→ 2의 거듭제곱으로 올림: ring_size = 4096
# VPP 측 memif 생성 — 계산된 ring_size 적용
vpp# create memif socket id 1 filename /run/vpp/memif-suricata.sock
vpp# create interface memif id 0 master ring-size 4096 buffer-size 2048
vpp# set interface state memif0/0 up
vpp# set interface l2 xconnect TenGigabitEthernet0/0/0 memif0/0

# Suricata 측 — 동일 ring-size로 slave 생성
# suricata.yaml
af-packet:
  - interface: memif0/0
    memif:
      role: slave
      ring-size: 4096
      buffer-size: 2048
      zero-copy: true
트래픽 프로파일peak_ppsstall 가정최소 queue권장 ring_size
사내 웹 트래픽50 kpps4 ms400512
데이터센터 북-남200 kpps8 ms3,2004,096
CDN 엣지800 kpps10 ms16,00016,384
백본 미러링2.5 Mpps15 ms75,000131,072

측정 기반 튜닝: show memif 출력의 rx/tx ring full 카운터가 지속적으로 증가하면 queue가 부족한 것입니다. 반대로 avg ring occupancy가 평균 10% 미만이면 과대 설정입니다. Suricata의 suricata.log에서 kernel drops 증가도 동시 모니터링해, 두 쪽 카운터가 함께 0에 가까운 크기가 최적입니다.

VCL + VPP-TLS 기반 SSL Inspection 구현

앞 절까지는 VPP 내부(그래프 노드, svm_fifo, tls_ctx_t) 관점에서 SSL Inspection을 설명했습니다. 실제로는 VPP 본체를 수정하지 않고도 VCL 유저스페이스 애플리케이션 + VPP-TLS 엔진 + VPP CLI로 등록한 cert-key pair만으로 MITM(Man-in-the-Middle) 복호화 프록시를 구현할 수 있습니다. 이 방식은 프로덕션에서 가장 많이 쓰이는 형태이며, 동적 인증서 생성기·정책 엔진·IDS 연동을 모두 평범한 리눅스 프로세스에서 다룰 수 있다는 장점이 있습니다.

전체 아키텍처

구현은 다섯 개의 협력 단계로 나뉩니다. 각 단계가 어떤 VCL API 혹은 VPP CLI 명령과 1:1로 대응되는지를 먼저 잡아두면 코드 흐름을 이해하기가 훨씬 쉬워집니다.

단계역할사용 API / 명령
① 프록시 CA 준비루트 CA 생성, 클라이언트 신뢰 저장소에 설치openssl req/x509, OS별 CA 등록
② VCL 리스너 바인드443에서 raw TCP로 들어오는 ClientHello를 먼저 받음vppcom_session_create(VPPCOM_PROTO_TCP)
③ SNI 피킹핸드셰이크 전에 ClientHello를 엿보아 바이패스 판정과 동적 인증서 발급 대상 결정vppcom_session_read + MSG_PEEK 파싱
④ 동적 ckpair 등록SNI용 leaf 인증서를 생성해 VPP에 cert-key pair로 등록VPP binary API app_tls_cert_key_pair_add_del 또는 CLI
⑤ 이중 TLS 터널링클라이언트 쪽 TLS 세션을 업그레이드하고, 업스트림으로 별도 TLS 연결 수립, 평문 릴레이VPPCOM_ATTR_SET_CKPAIR, VPPCOM_PROTO_TLS, vppcom_epoll_wait
VCL + VPP-TLS SSL Inspection 프로세스 구조 클라이언트 프록시 CA 신뢰 VCL SSL-Inspection 프로세스 (유저스페이스) ② TCP 리스너 (VCL) vppcom_session_listen(:443) ③ SNI Peek 파서 read(peek) → ClientHello 분석 ④ 동적 인증서 캐시 hash(SNI) → ckpair_index miss → 서명 → VPP API 등록 ⑤a 클라이언트 TLS 세션 PROTO_TLS + SET_CKPAIR accept → 평문 read/write 정책/검사 엔진 URL / IDS / DLP / 로깅 ALLOW / BLOCK / BYPASS ⑤b 업스트림 TLS 세션 PROTO_TLS + SET_SNI connect(real server:443) vppcom_epoll_wait() — 양방향 평문 릴레이 루프 원본 서버 진짜 인증서 VPP 프로세스 TLS 엔진 cert-key pair 저장소 FIFO 공유 메모리 DPDK NIC VCL 프로세스는 VPP와 공유 메모리로 통신하고, cert-key pair는 binary API로 등록해 인덱스만 세션 속성으로 지정합니다. 핵심 트릭: PROTO_TCP로 먼저 받아 SNI를 보고 바이패스/인증서를 결정한 뒤, 같은 소켓을 PROTO_TLS로 "업그레이드"해 handshake를 위임

① 프록시 CA와 ckpair 사전 등록

VPP-TLS는 애플리케이션이 직접 PEM을 들고 있지 않고, cert-key pair index를 사용합니다. 프록시 CA 자체는 리프 인증서 서명용이므로 VPP에 넣을 필요가 없고, 부팅 시점에 "빈 템플릿"이나 기본 leaf를 한 번 등록해 두면 이후 동적으로 교체할 수 있습니다.

# 1. 프록시 CA 생성 (오프라인, 한 번만)
$ openssl genrsa -out proxy-ca.key 4096
$ openssl req -x509 -new -nodes -key proxy-ca.key -sha256 -days 3650 \
      -subj "/CN=VPP SSL Inspection CA" \
      -out proxy-ca.crt

# 2. 클라이언트 장비의 신뢰 저장소에 proxy-ca.crt 설치 (OS/브라우저별 방식)
#    — 이 단계 없이는 브라우저가 즉시 경고합니다.

# 3. VPP에 "기본" cert-key pair 한 개를 미리 등록해 인덱스 확보
$ vppctl tls cert-key-pair add \
      cert /etc/vpp/certs/default-leaf.crt \
      key  /etc/vpp/certs/default-leaf.key
cert-key pair added with index 0

# 4. VCL 프로세스용 vcl.conf
$ cat /etc/vpp/vcl-inspect.conf
vcl {
    rx-fifo-size 4000000
    tx-fifo-size 4000000
    app-scope-local
    app-scope-global
    api-socket-name /run/vpp/api.sock
    use-mq-eventfd
    event-queue-size 512
}

② ClientHello에서 SNI 피킹하기

VCL은 MSG_PEEK을 직접 지원하지 않지만, vppcom_session_read_segments()로 RX FIFO 안을 소모 없이 들여다볼 수 있습니다. TLS 레코드 헤더(5바이트)와 ClientHello 고정 구조를 파싱하여 SNI를 꺼냅니다. 이 작업은 아직 PROTO_TCP 상태의 세션에서 수행한다는 점이 중요합니다.

/* VCL 세션에서 ClientHello를 소모 없이 들여다보고 SNI를 추출합니다.
   PROTO_TCP로 먼저 accept한 세션에만 의미가 있습니다. */
static int
peek_sni (int sh, char *out, size_t out_sz)
{
    vppcom_data_segment_t segs[2];
    int n = vppcom_session_read_segments (sh, segs, 2, 0);
    if (n <= 0) return -1;

    /* 여러 세그먼트를 하나의 선형 버퍼로 합칩니다 */
    uint8_t buf[2048];
    uint32_t total = 0;
    for (int i = 0; i < n && total < sizeof (buf); i++) {
        uint32_t take = segs[i].len;
        if (total + take > sizeof (buf)) take = sizeof(buf) - total;
        memcpy (buf + total, segs[i].data, take);
        total += take;
    }
    /* 중요: 아직 데이터를 소모하지 않았다고 VPP에 알립니다 */
    vppcom_session_free_segments (sh, 0);

    /* TLS Record: 0x16(Handshake) + version(2) + length(2) */
    if (total < 43 || buf[0] != 0x16) return -1;
    uint8_t *p = buf + 5;
    uint32_t left = total - 5;

    /* Handshake: HandshakeType(1)=0x01 ClientHello + length(3) */
    if (left < 4 || p[0] != 0x01) return -1;
    p += 4; left -= 4;

    /* version(2) + random(32) */
    if (left < 34) return -1;
    p += 34; left -= 34;

    /* session_id */
    if (left < 1) return -1;
    uint32_t sid = p[0]; p += 1 + sid; left -= 1 + sid;

    /* cipher_suites */
    if (left < 2) return -1;
    uint32_t cs = (p[0] << 8) | p[1];
    p += 2 + cs; left -= 2 + cs;

    /* compression_methods */
    if (left < 1) return -1;
    uint32_t cm = p[0]; p += 1 + cm; left -= 1 + cm;

    /* extensions 총 길이 */
    if (left < 2) return -1;
    uint32_t ext_total = (p[0] << 8) | p[1];
    p += 2; left -= 2;
    if (ext_total > left) return -1;

    /* extensions 순회하면서 server_name(0x0000) 찾기 */
    while (ext_total >= 4) {
        uint16_t etype = (p[0] << 8) | p[1];
        uint16_t elen  = (p[2] << 8) | p[3];
        p += 4; ext_total -= 4;
        if (elen > ext_total) return -1;

        if (etype == 0x0000 && elen >= 5) {
            /* server_name_list: list_len(2) + type(1)=0 + name_len(2) + name */
            uint16_t name_len = (p[3] << 8) | p[4];
            if (name_len + 5u > elen || name_len + 1u > out_sz)
                return -1;
            memcpy (out, p + 5, name_len);
            out[name_len] = 0;
            return 0;
        }
        p += elen; ext_total -= elen;
    }
    return -1;
}
free_segments(sh, 0)인가: 두 번째 인자는 "소비된 바이트 수"입니다. 0을 주면 데이터는 FIFO에 그대로 남아 있어서, 곧이어 세션을 TLS로 승격할 때 VPP-TLS 엔진이 같은 ClientHello를 다시 읽어 핸드셰이크를 정상적으로 수행할 수 있습니다. 이 트릭이 없으면 승격 후 TLS 엔진은 ClientHello를 못 받고 무한 대기하게 됩니다.

③ 동적 leaf 인증서 → VPP cert-key pair 등록

리프 인증서는 유저스페이스에서 OpenSSL로 서명하고, 결과 PEM을 VPP binary API로 넘겨 새 ckpair index를 받습니다. 받은 index를 SNI→index 해시에 캐싱하면 동일 도메인 재접속 시 RSA 서명을 생략할 수 있습니다.

/* 도메인별 ckpair index 캐시 */
typedef struct {
    char     sni[256];
    uint32_t ckpair_index;   /* vppcom_add_cert_key_pair 반환값 */
    time_t   expires_at;
} ckpair_entry_t;

static ckpair_entry_t ckpair_cache[4096];

static uint32_t
get_or_mint_ckpair (const char *sni)
{
    ckpair_entry_t *e = ckpair_lookup (sni);
    if (e && e->expires_at > time (NULL))
        return e->ckpair_index;            /* 캐시 히트 — 서명 생략 */

    /* 1) 프록시 CA로 SNI용 leaf 서명 (OpenSSL) */
    uint8_t *cert_pem, *key_pem;
    uint32_t cert_len, key_len;
    mint_leaf_cert (sni, &cert_pem, &cert_len, &key_pem, &key_len);

    /* 2) VPP에 등록 — 새 cert-key pair index 획득
       내부적으로 binary API app_tls_cert_key_pair_add_del을 호출합니다. */
    vppcom_cert_key_pair_t ck = {
        .cert     = cert_pem,
        .cert_len = cert_len,
        .key      = key_pem,
        .key_len  = key_len,
    };
    uint32_t idx = vppcom_add_cert_key_pair (&ck);

    free (cert_pem); free (key_pem);

    /* 3) 캐시에 기록 */
    ckpair_store (sni, idx, time (NULL) + 3600);
    return idx;
}
ckpair 누수 주의: vppcom_add_cert_key_pair로 등록한 인덱스는 해제하지 않으면 VPP 메모리에 누적됩니다. 캐시 엔트리가 퇴거(evict)될 때 반드시 vppcom_del_cert_key_pair(idx)를 호출해 해제하고, 프록시 프로세스 종료 시에도 모든 인덱스를 회수해야 합니다. 장기 운용 시 show app의 ckpair 개수를 모니터링하십시오.

④ 메인 루프: accept → SNI peek → TLS 승격 → 업스트림 연결 → 평문 릴레이

아래 예제는 단일 스레드 이벤트 루프(Event Loop) 구조입니다. 핵심은 같은 원격 피어에 대해 두 개의 VCL 세션을 쓴다는 점입니다. 첫 번째는 PROTO_TCP로 SNI를 엿보기 위한 세션, 두 번째는 SNI와 ckpair를 확정한 뒤 새로 생성하는 PROTO_TLS 세션입니다. PROTO_TCP 세션은 SNI를 얻는 즉시 닫지 않고 그대로 TLS 핸드셰이크에 사용할 수 없기 때문에, 실전에서는 accept 자체를 PROTO_TLS 리스너에서 받고, SNI는 VPP의 VPPCOM_ATTR_GET_SERVER_NAME(TLS 엔진이 핸드셰이크 시작 단계에 제공)을 통해 가져오는 방식과 병행합니다. 여기서는 교육 목적으로 가장 평범한 흐름을 보여줍니다.

/* vcl-ssl-inspect.c — VCL + VPP-TLS SSL Inspection 메인 루프 (핵심 부분) */
#include <vcl/vppcom.h>
#include <sys/epoll.h>

typedef struct {
    int  c_sh;        /* 클라이언트 TLS 세션 (프록시=서버) */
    int  u_sh;        /* 업스트림 TLS 세션 (프록시=클라이언트) */
    char sni[256];
    int  blocked;     /* 정책 판정 결과 */
} insp_pair_t;

static int epfd;

static int
connect_upstream_tls (const char *sni, uint8_t ip[4])
{
    int u = vppcom_session_create (VPPCOM_PROTO_TLS, 1);
    uint32_t slen = strlen (sni);
    vppcom_session_attr (u, VPPCOM_ATTR_SET_SNI_HOSTNAME,
                         (void *) sni, &slen);

    vppcom_endpt_t ep = { 0 };
    ep.is_ip4 = 1; ep.ip = ip; ep.port = htons (443);
    int rv = vppcom_session_connect (u, &ep);
    if (rv < 0 && rv != VPPCOM_EINPROGRESS) {
        vppcom_session_close (u);
        return -1;
    }
    return u;
}

static void
on_client_accept (int c_sh, const char *sni_from_handshake,
                  uint8_t real_ip[4])
{
    /* 1) 바이패스 정책 */
    if (policy_should_bypass (sni_from_handshake)) {
        /* 바이패스: TLS 세션을 닫고 raw TCP로 스플라이스하거나,
           애초에 PROTO_TCP 리스너에서 처리합니다. */
        splice_tcp_passthrough (c_sh, real_ip);
        return;
    }

    /* 2) 업스트림 TLS 연결 개시 (CONNECTED 이벤트는 epoll로 받음) */
    int u_sh = connect_upstream_tls (sni_from_handshake, real_ip);
    if (u_sh < 0) {
        send_block_page (c_sh, "upstream unreachable");
        vppcom_session_close (c_sh);
        return;
    }

    /* 3) 상태 구조 할당 + epoll 등록 */
    insp_pair_t *pp = calloc (1, sizeof (*pp));
    pp->c_sh = c_sh; pp->u_sh = u_sh;
    strncpy (pp->sni, sni_from_handshake, sizeof (pp->sni) - 1);

    struct epoll_event ev;
    ev.events = EPOLLIN | EPOLLRDHUP;
    ev.data.ptr = pp;                         /* 양쪽 모두 같은 pp를 가리킴 */
    vppcom_epoll_ctl (epfd, EPOLL_CTL_ADD, c_sh, &ev);
    vppcom_epoll_ctl (epfd, EPOLL_CTL_ADD, u_sh, &ev);
}

static void
relay_one_direction (insp_pair_t *pp, int from, int to)
{
    uint8_t buf[32768];
    int n = vppcom_session_read (from, buf, sizeof (buf));
    if (n <= 0) {
        if (n == VPPCOM_EAGAIN) return;
        teardown_pair (pp);
        return;
    }

    /* 정책 엔진: 평문 검사 후 허용/차단 */
    if (inspect_plaintext (pp, buf, n) == INSPECT_BLOCK) {
        send_block_page (pp->c_sh, "blocked by policy");
        teardown_pair (pp);
        return;
    }

    int off = 0;
    while (off < n) {
        int w = vppcom_session_write (to, buf + off, n - off);
        if (w == VPPCOM_EAGAIN) { /* TX FIFO full — EPOLLOUT 등록 권장 */ continue; }
        if (w < 0) { teardown_pair (pp); return; }
        off += w;
    }
}

int
main (void)
{
    vppcom_app_create ("vcl-ssl-inspect");
    epfd = vppcom_epoll_create ();

    /* 기본 ckpair(index 0)로 시작하는 TLS 리스너 —
       SNI callback 안에서 ckpair를 새 인덱스로 교체합니다. */
    int lsh = vppcom_session_create (VPPCOM_PROTO_TLS, 1);
    uint32_t idx = 0;
    uint32_t ilen = sizeof (idx);
    vppcom_session_attr (lsh, VPPCOM_ATTR_SET_CKPAIR, &idx, &ilen);

    vppcom_endpt_t ep = { 0 };
    uint8_t any[4] = { 0, 0, 0, 0 };
    ep.is_ip4 = 1; ep.ip = any; ep.port = htons (443);
    vppcom_session_bind   (lsh, &ep);
    vppcom_session_listen (lsh, 2048);

    struct epoll_event ev = { .events = EPOLLIN, .data.u32 = (uint32_t) lsh };
    vppcom_epoll_ctl (epfd, EPOLL_CTL_ADD, lsh, &ev);

    struct epoll_event events[256];
    for (;;) {
        int rv = vppcom_epoll_wait (epfd, events, 256, 1.0);
        for (int i = 0; i < rv; i++) {
            if (events[i].data.u32 == (uint32_t) lsh) {
                vppcom_endpt_t peer = { 0 };
                uint8_t pip[16]; peer.ip = pip;
                int c_sh = vppcom_session_accept (lsh, &peer, 0);

                /* accept는 TLS 핸드셰이크 완료된 세션을 반환 — SNI는 속성으로 질의 */
                char sni[256] = { 0 };
                uint32_t slen = sizeof (sni);
                vppcom_session_attr (c_sh, VPPCOM_ATTR_GET_SERVER_NAME, sni, &slen);

                /* 원래 목적지 IP는 TPROXY 또는 명시적 매핑 테이블에서 조회 */
                uint8_t real_ip[4];
                resolve_original_dst (&peer, sni, real_ip);

                on_client_accept (c_sh, sni, real_ip);
                continue;
            }

            insp_pair_t *pp = events[i].data.ptr;
            int sh = event_to_sh (pp, &events[i]);
            if (events[i].events & (EPOLLHUP | EPOLLRDHUP | EPOLLERR)) {
                teardown_pair (pp);
                continue;
            }
            if (events[i].events & EPOLLIN) {
                if (sh == pp->c_sh)
                    relay_one_direction (pp, pp->c_sh, pp->u_sh);
                else
                    relay_one_direction (pp, pp->u_sh, pp->c_sh);
            }
        }
    }
}
코드 설명
  • connect_upstream_tls() 업스트림 방향은 프록시가 클라이언트 역할이므로 VPPCOM_ATTR_SET_SNI_HOSTNAME으로 원본 SNI를 넣어야 합니다. 이 값이 없거나 프록시의 외부 IP로 대체되면 상대 서버가 잘못된 인증서를 내려보내거나 연결을 거절합니다. 논블로킹이므로 VPPCOM_EINPROGRESS는 정상입니다.
  • on_client_accept()의 ckpair 동적 교체 리스너는 "기본" ckpair index 0으로 만들어 두지만, 실제 리프 인증서는 SNI를 본 뒤 get_or_mint_ckpair(sni)로 새 인덱스를 만들어 VPPCOM_ATTR_SET_CKPAIR로 세션에 다시 꽂아줍니다. 리스너 자체의 ckpair를 바꾸지 않는 이유는 서로 다른 SNI를 동시에 받기 때문입니다.
  • 양방향 epoll data.ptr c_shu_sh를 같은 insp_pair_t*에 묶어 등록하면, 어느 방향에서 이벤트가 와도 한 번의 포인터 역참조(Dereference)로 상대 세션을 찾을 수 있습니다. event_to_sh()는 events 구조체(Struct)에서 어느 핸들인지 복원하는 헬퍼입니다 — 실전 구현은 data.u64에 포인터와 방향 비트를 같이 담는 패턴을 씁니다.
  • inspect_plaintext() 이 시점의 buf는 이미 TLS 엔진이 복호화한 평문입니다. URL 필터, Suricata 매칭, DLP 정규식, 로깅을 여기서 수행합니다. 차단 판정이 나오면 클라이언트에만 블록 페이지를 보내고 업스트림 세션은 조용히 닫습니다.
  • resolve_original_dst() 투명 모드에서 원본 목적지 IP는 커널 TPROXY나 VPP 쪽의 session 네임스페이스(Namespace) 매핑(Mapping)에서 가져와야 합니다. 명시적 프록시(HTTP CONNECT)를 쓰는 경우에는 CONNECT 헤더에서 호스트를 파싱해 DNS로 해석합니다.

⑤ 실행 및 점검

# 빌드
$ gcc -O2 -o vcl-ssl-inspect vcl-ssl-inspect.c sni_peek.c cert_mint.c \
      -I/usr/include/vpp -lvppcom -lvlibmemoryclient -lsvm \
      -lssl -lcrypto -lpthread

# 실행 (VPP가 이미 동작 중이어야 합니다)
$ VCL_CONFIG=/etc/vpp/vcl-inspect.conf ./vcl-ssl-inspect &

# 클라이언트 트래픽을 443/tcp 리스너로 유도 — TPROXY 규칙 예
$ iptables -t mangle -A PREROUTING -p tcp --dport 443 \
      -j TPROXY --tproxy-mark 0x1/0x1 --on-port 443

# 동작 확인: 프록시 CA를 신뢰시킨 클라이언트에서
$ curl -v https://api.example.com/
# → 인증서 Issuer가 "VPP SSL Inspection CA"로 보이면 성공

# VPP 측 상태 점검
$ vppctl show app
$ vppctl show session verbose
$ vppctl show tls                 # 등록된 cert-key pair 수
$ vppctl show errors | grep tls
성능/안정성 팁: ① ckpair 캐시는 도메인 단위로 유지하되 TTL을 leaf 유효기간보다 짧게 두어 누수를 방지합니다. ② 차단 시 send_block_page는 반드시 close 전에 write를 완료해야 하므로, TX FIFO가 비워질 때까지 EPOLLOUT을 기다리고 닫아야 합니다. ③ 정책 엔진은 호출당 지연이 누적되므로 호출 횟수를 제한하거나 별도 스레드·ring buffer로 옮기는 편이 안정적입니다. ④ QUIC(HTTP/3) 트래픽은 같은 구조로 복호화할 수 없으므로, UDP 443을 방화벽에서 차단해 TCP 443으로 폴백(Fallback)을 강제하는 정책이 실무에서 자주 병행됩니다.
법적/윤리적 경계: SSL Inspection은 본질적으로 MITM 복호화이며, 조직 내부 보안 장비로 쓰일 때도 사용자 고지와 접근 통제가 필수입니다. 프록시 CA 개인 키가 유출되면 네트워크 전체가 위조 인증서에 노출되므로 HSM 보관, 키 로테이션, 감사 로그를 반드시 설계에 포함해야 합니다.

투명 SSL Inspection 구현

투명(Transparent) 모드의 SSL Inspection은 네트워크 경로 상에서 패킷을 가로채는 방식입니다. VPP의 그래프 노드 아키텍처를 활용하여, ip4-input 이후 특정 조건(목적지 포트 443)의 트래픽을 검사 노드로 리다이렉트합니다.

/* 투명 SSL Inspection 그래프 노드 흐름 */
/*
 * 일반 트래픽:
 *   dpdk-input → ip4-input → ip4-lookup → ip4-rewrite → dpdk-output
 *
 * SSL Inspection 대상:
 *   dpdk-input → ip4-input → ip4-lookup
 *       → ssl-inspect-classify (dst port 443 검출)
 *       → tcp-input (TCP 연결 수립)
 *       → tls-input (TLS 세션 ① 핸드셰이크)
 *       → ssl-inspect-engine (평문 검사)
 *       → tls-output (TLS 세션 ② 재암호화)
 *       → tcp-output → ip4-rewrite → dpdk-output
 */

/* ssl-inspect-classify 노드: 검사 대상 분류 */
static uword
ssl_inspect_classify_fn (vlib_main_t *vm,
                         vlib_node_runtime_t *node,
                         vlib_frame_t *frame)
{
    u32 *from = vlib_frame_vector_args (frame);
    u32 n_left = frame->n_vectors;
    u32 next_index;

    while (n_left > 0) {
        vlib_buffer_t *b = vlib_get_buffer (vm, from[0]);
        ip4_header_t *ip = vlib_buffer_get_current (b);
        tcp_header_t *tcp;

        if (ip->protocol == IP_PROTOCOL_TCP) {
            tcp = (tcp_header_t *) ip4_next_header (ip);

            if (clib_net_to_host_u16 (tcp->dst_port) == 443) {
                /* 바이패스 정책 확인 */
                if (ssl_inspect_should_bypass (b, ip, tcp))
                    next_index = SSL_INSPECT_NEXT_BYPASS;
                else
                    next_index = SSL_INSPECT_NEXT_INTERCEPT;
            } else {
                next_index = SSL_INSPECT_NEXT_PASSTHROUGH;
            }
        } else {
            next_index = SSL_INSPECT_NEXT_PASSTHROUGH;
        }

        vlib_validate_buffer_enqueue_x1 (vm, node, next_index,
                                          to_next, n_left_to_next,
                                          from[0], next_index);
        from += 1;
        n_left -= 1;
    }
    return frame->n_vectors;
}
코드 설명
  • 1~14행 VPP 그래프 노드 체인에서 SSL Inspection을 투명하게 삽입하는 구조를 보여줍니다. ssl-inspect-classify 노드가 ip4-lookup 이후에 위치하여, 목적지 포트 443인 패킷만 검사 경로로 분기합니다.
  • 31~38행 목적지 포트 443(HTTPS) 트래픽을 감지하면, 먼저 ssl_inspect_should_bypass()로 바이패스 정책을 확인합니다. 금융, 의료, 인증서 피닝 도메인 등은 검사를 건너뛰어야 합니다.
  • 35~38행 바이패스가 아닌 경우 SSL_INSPECT_NEXT_INTERCEPT로 분기하여 TLS 종단 → 검사 → 재암호화 경로로 진입합니다. 바이패스 트래픽은 원래 경로(ip4-rewrite)로 직접 전달됩니다.
/* ssl-inspect-engine 노드: 평문 검사 핵심 로직 */
static uword
ssl_inspect_engine_fn (vlib_main_t *vm,
                       vlib_node_runtime_t *node,
                       vlib_frame_t *frame)
{
    ssl_inspect_main_t *sim = &ssl_inspect_main;
    u32 *from = vlib_frame_vector_args (frame);
    u32 n_left = frame->n_vectors;

    while (n_left > 0) {
        ssl_inspect_session_t *sis;
        session_t *app_session;
        u8 *plaintext;
        u32 plaintext_len;
        ssl_inspect_action_t action;

        /* 클라이언트 측 TLS에서 복호화된 평문 읽기 */
        app_session = session_get (sis->client_tls_session_handle);
        plaintext_len = svm_fifo_max_dequeue (app_session->rx_fifo);

        if (plaintext_len == 0)
            goto next;

        plaintext = svm_fifo_peek (app_session->rx_fifo,
                                    plaintext_len);

        /* ─── 보안 검사 파이프라인 ─── */

        /* 1단계: URL/도메인 필터링 (HTTP Host 헤더 검사) */
        action = ssl_inspect_url_filter (sim, plaintext,
                                         plaintext_len);
        if (action == SSL_INSPECT_BLOCK)
            goto block;

        /* 2단계: IDS/IPS 패턴 매칭 (Suricata 룰셋) */
        action = ssl_inspect_ids_check (sim, plaintext,
                                         plaintext_len);
        if (action == SSL_INSPECT_BLOCK)
            goto block;

        /* 3단계: DLP (데이터 유출 방지) 검사 */
        action = ssl_inspect_dlp_check (sim, plaintext,
                                         plaintext_len);
        if (action == SSL_INSPECT_BLOCK)
            goto block;

        /* 4단계: 악성코드 시그니처 스캔 */
        action = ssl_inspect_malware_scan (sim, plaintext,
                                            plaintext_len);
        if (action == SSL_INSPECT_BLOCK)
            goto block;

        /* 모든 검사 통과 → 서버 측 TLS로 포워딩 */
        ssl_inspect_forward_to_server (sis, plaintext,
                                       plaintext_len);
        sis->bytes_inspected += plaintext_len;
        goto next;

    block:
        /* 차단: 클라이언트에 에러 응답 + 연결 종료 */
        ssl_inspect_send_block_page (sis);
        ssl_inspect_close_session (sis);
        sis->threats_detected++;

    next:
        from += 1;
        n_left -= 1;
    }
    return frame->n_vectors;
}
코드 설명
  • 19~21행 클라이언트 측 TLS 세션(프록시가 서버 역할)의 App RX FIFO에서 복호화된 평문을 읽습니다. 이 평문은 클라이언트가 보낸 원본 HTTP 요청(또는 다른 프로토콜 데이터)입니다.
  • 30~53행 4단계 보안 검사 파이프라인을 순차적으로 적용합니다. URL 필터링 → IDS/IPS → DLP → 악성코드 순서로 진행하며, 어느 단계에서든 차단 판정이 나오면 즉시 중단합니다. 이 순서는 비용이 낮은 검사(URL 필터)를 먼저 수행하여 불필요한 비용을 줄이는 최적화입니다.
  • 56~57행 모든 검사를 통과하면 평문을 서버 측 TLS 세션(프록시가 클라이언트 역할)의 App TX FIFO에 기록합니다. TLS 엔진이 이를 재암호화하여 원본 서버로 전송합니다.
  • 62행 차단 시 클라이언트에 커스텀 차단 페이지(Block Page)를 전송합니다. 일반적으로 HTTP 403 응답과 차단 사유를 포함한 HTML 페이지를 반환합니다.

SNI 추출과 사전 판정

SSL Inspection에서 가장 먼저 수행하는 작업은 ClientHello 메시지에서 SNI(Server Name Indication)를 추출하는 것입니다. SNI는 TLS 핸드셰이크의 첫 메시지에 평문으로 포함되어 있으므로, TLS 복호화 없이도 접근할 수 있습니다. 이를 통해 바이패스 정책을 TLS 세션 수립 전에 판정할 수 있습니다.

/* ClientHello에서 SNI 추출 (TLS 레코드 파싱) */
static int
ssl_inspect_extract_sni (u8 *data, u32 len, u8 **sni_out)
{
    u8 *p = data;
    u16 tls_version, handshake_len, extensions_len;

    /* TLS Record Layer: ContentType(1) + Version(2) + Length(2) */
    if (len < 5 || p[0] != 0x16)    /* 0x16 = Handshake */
        return -1;

    tls_version = (p[1] << 8) | p[2];
    u16 record_len = (p[3] << 8) | p[4];
    p += 5;

    /* Handshake: MsgType(1) + Length(3) */
    if (p[0] != 0x01)                 /* 0x01 = ClientHello */
        return -1;

    u32 hs_len = (p[1] << 16) | (p[2] << 8) | p[3];
    p += 4;

    /* ClientHello: Version(2) + Random(32) + SessionID(var)
     *              + CipherSuites(var) + Compression(var)
     *              + Extensions(var) */
    p += 2 + 32;              /* Version + Random */
    p += 1 + p[0];             /* SessionID (길이 + 데이터) */
    p += 2 + ((p[0] << 8) | p[1]);  /* CipherSuites */
    p += 1 + p[0];             /* Compression */

    /* Extensions 순회하여 SNI(type=0x0000) 찾기 */
    extensions_len = (p[0] << 8) | p[1];
    p += 2;

    u8 *ext_end = p + extensions_len;
    while (p + 4 <= ext_end) {
        u16 ext_type = (p[0] << 8) | p[1];
        u16 ext_len = (p[2] << 8) | p[3];
        p += 4;

        if (ext_type == 0x0000) {    /* server_name 확장 */
            /* ServerNameList: Length(2) + Type(1) + NameLength(2) + Name */
            u16 sni_list_len = (p[0] << 8) | p[1];
            u8 name_type = p[2];
            u16 name_len = (p[3] << 8) | p[4];

            if (name_type == 0x00) {  /* host_name */
                *sni_out = vec_new (u8, name_len + 1);
                clib_memcpy (*sni_out, p + 5, name_len);
                (*sni_out)[name_len] = 0;
                return 0;
            }
        }
        p += ext_len;
    }
    return -1;  /* SNI 없음 (IP 직접 연결 등) */
}
코드 설명
  • 8~10행 TLS Record Layer의 ContentType을 확인합니다. 0x16은 Handshake 메시지를 의미하며, 첫 번째 패킷이 ClientHello임을 확인합니다.
  • 30~31행 ClientHello의 고정 필드(Version, Random)와 가변 필드(SessionID, CipherSuites, Compression)를 건너뛰어 Extensions 영역에 도달합니다.
  • 40행 Extension type 0x0000은 SNI(server_name) 확장입니다. TLS 1.3에서도 이 확장은 동일한 형식으로 유지됩니다.
  • 55행 SNI가 없는 경우는 IP 주소로 직접 연결하거나, ESNI(Encrypted SNI)/ECH(Encrypted Client Hello)를 사용하는 경우입니다. 이 경우 도메인 기반 정책 판정이 불가능합니다.

바이패스 정책 엔진(Policy Engine)

모든 TLS 트래픽을 검사하는 것은 법적·기술적으로 적절하지 않습니다. 바이패스 정책 엔진은 특정 조건에 해당하는 트래픽을 검사 없이 통과시킵니다.

바이패스 조건판정 시점이유
금융 기관 도메인SNI 확인 후온라인 뱅킹 인증 정보 보호, 규제 준수 (PCI-DSS)
의료 기관 도메인SNI 확인 후HIPAA 개인 건강 정보(PHI) 보호 의무
인증서 피닝(Certificate Pinning)SNI 확인 후앱이 특정 인증서/공개 키를 하드코딩하여 MITM 인증서 거부
mTLS (상호 인증)ClientHello 분석 후클라이언트 인증서가 필요한 연결은 프록시가 대행 불가
내부 서버 통신IP 기반내부 서비스 간 통신은 검사 불필요
VPN/터널 트래픽포트/프로토콜 기반이미 터널로 보호된 트래픽의 이중 검사 방지
QUIC/HTTP3UDP 443 포트QUIC은 핸드셰이크 자체가 암호화되어 투명 MITM 불가
/* 바이패스 정책 판정 */
static int
ssl_inspect_should_bypass (ssl_inspect_main_t *sim,
                           const u8 *sni,
                           ip4_address_t *dst_ip)
{
    /* 1. 도메인 화이트리스트 확인 (해시 테이블 O(1) 조회) */
    if (hash_get (sim->bypass_domain_hash, sni))
        return 1;

    /* 2. 와일드카드 매칭 (*.bank.com) */
    if (ssl_inspect_wildcard_match (sim->bypass_wildcards, sni))
        return 1;

    /* 3. IP 대역 확인 (내부 서버) */
    if (ip4_prefix_match (dst_ip, &sim->internal_prefix,
                          sim->internal_prefix_len))
        return 1;

    /* 4. 카테고리 기반 (금융, 의료 — 외부 URL DB 연동) */
    url_category_t cat = url_db_lookup (sni);
    if (cat == URL_CAT_FINANCIAL || cat == URL_CAT_HEALTHCARE)
        return 1;

    return 0;   /* 바이패스 조건 불일치 → 검사 진행 */
}

/* CLI: 바이패스 도메인 추가 */
/* vpp# ssl-inspect bypass add domain banking.example.com */
/* vpp# ssl-inspect bypass add domain *.medical-provider.org */
/* vpp# ssl-inspect bypass add prefix 10.0.0.0/8 */
/* vpp# ssl-inspect bypass add category financial */
/* vpp# show ssl-inspect bypass */

IDS/IPS 엔진 통합

SSL Inspection으로 확보한 평문 트래픽에 대해 침입 탐지/방지 시스템(IDS/IPS)을 적용할 수 있습니다. VPP는 두 가지 통합 방식을 지원합니다.

SSL Inspection + IDS/IPS 통합 방식 비교 방식 A: 인라인 통합 (VPP 플러그인) TLS 복호화 Hyperscan 패턴 매칭 VPP 노드 내 인라인 실행 TLS 재암호화 장점: 최저 지연, 최고 처리량 단점: Suricata/Snort 룰셋과 호환성 제한 방식 B: 외부 IDS/IPS 연동 (TAP/Mirror) TLS 복호화 평문 미러링 노드 memif/TAP으로 복제 전송 Suricata / Snort3 AF_PACKET 또는 memif 인터페이스 전체 Suricata 룰셋 적용 가능 verdict 판정 대기 IPS: block/allow TLS 재암호화 장점: 기존 Suricata/Snort 룰 100% 호환 단점: memif 복사 오버헤드, IPS 판정 대기 지연 IDS 모드(비차단)는 포워딩과 병렬 수행 가능 성능 비교 (100K 동시 연결, AES-256-GCM) 인라인 Hyperscan: ~30 Gbps, p99 지연 0.4ms 외부 Suricata (memif): ~15 Gbps, p99 지연 1.2ms

인라인 IDS 엔진은 Intel Hyperscan 라이브러리를 활용합니다. Hyperscan은 SIMD 명령어로 수천 개의 정규표현식 패턴을 동시에 매칭할 수 있으며, VPP의 벡터 처리 모델과 잘 어울립니다.

/* Hyperscan 기반 인라인 IDS 패턴 매칭 */
typedef struct {
    hs_database_t *db;        /* 컴파일된 패턴 데이터베이스 */
    hs_scratch_t **scratch;   /* 워커별 스크래치 공간 */
    u32 n_patterns;            /* 로드된 시그니처 수 */
} ssl_inspect_ids_t;

/* 패턴 매칭 콜백 */
static int
ids_match_handler (unsigned int id,
                   unsigned long long from,
                   unsigned long long to,
                   unsigned int flags,
                   void *ctx)
{
    ssl_inspect_match_ctx_t *mc = ctx;
    mc->matched_rule_id = id;
    mc->action = SSL_INSPECT_BLOCK;

    /* 첫 번째 매칭에서 중단 (IPS 모드) */
    return 1;
}

/* VPP 노드 내에서 Hyperscan 호출 */
ssl_inspect_action_t
ssl_inspect_ids_check (ssl_inspect_main_t *sim,
                       u8 *data, u32 len)
{
    ssl_inspect_match_ctx_t match = { .action = SSL_INSPECT_ALLOW };
    u32 thread_index = vlib_get_thread_index ();

    /* Hyperscan 스트리밍 스캔: 워커별 scratch 사용 */
    hs_scan (sim->ids.db, (const char *) data, len,
             0, sim->ids.scratch[thread_index],
             ids_match_handler, &match);

    return match.action;
}

QUIC/ECH 환경에서의 SSL Inspection 과제

QUIC과 ECH(Encrypted Client Hello)의 등장은 전통적인 SSL Inspection에 근본적인 도전을 제기합니다.

⚠️ VPP의 ECH/QUIC 지원 현실 (25.02 기준): VPP tlsopenssl·tlspicotls 엔진은 아직 ECH(draft-ietf-tls-esni) 네이티브 처리를 제공하지 않습니다. 즉 VPP 자체가 ECH를 복호화하거나 내부 SNI를 들여다보는 경로는 없으며, 본 절의 대응 전략은 모두 VPP 외부(DNS 정책·UDP 차단·TCP 폴백 유도·엔터프라이즈 MDM)에서 수행해야 합니다. 마찬가지로 quic 플러그인(quicly 기반)은 experimental 단계이며 프로덕션 SSL Inspection 파이프라인에서는 여전히 TCP+TLS 1.3 경로로 강제 폴백시키는 것이 현실적입니다.
기술SSL Inspection에 미치는 영향대응 전략
QUIC (UDP 443)핸드셰이크 자체가 암호화되어 투명 MITM이 불가능합니다. TCP처럼 SYN을 가로채는 방식을 사용할 수 없습니다UDP 443 차단 → TCP 폴백 유도, 또는 QUIC 인지 명시적 프록시 구현
ECH (Encrypted Client Hello)SNI가 암호화되어 도메인 기반 바이패스 정책 판정이 불가능합니다DNS-over-HTTPS 프록시에서 ECHConfig를 제거하여 ECH 비활성화 유도
Certificate Transparency동적 생성 인증서가 CT 로그에 없으므로 브라우저가 경고할 수 있습니다프록시 CA를 엔터프라이즈 정책으로 CT 예외 등록
TLS 1.3 0-RTTEarly Data가 핸드셰이크 완료 전에 전송되어 검사 시점 문제가 발생합니다Early Data 차단 후 1-RTT로 다운그레이드
인증서 피닝 앱모바일 앱이 특정 인증서를 하드코딩하여 프록시 CA를 거부합니다앱별 바이패스 정책 또는 MDM으로 피닝 해제
QUIC 환경 SSL Inspection 대응 전략 전략 A: QUIC 차단 → TCP 폴백 클라이언트 UDP 443 VPP: DROP UDP 443 차단 규칙 TCP 443 폴백 SSL Inspection 기존 TCP TLS 검사 장점 구현 간단 100% 검사 단점 QUIC 성능 불가 전략 B: QUIC 인지 프록시 클라이언트 QUIC VPP QUIC 프록시 QUIC 종단 검사+재개시 스트림별 독립 검사 가능 명시적 프록시 모드 필수 장점: QUIC 성능 유지, 스트림 다중화 활용, HTTP/3 지원 단점: 투명 모드 불가, 클라이언트 프록시 설정 필요, 복잡한 구현 VPP 구현 전략 권장 사항 1. 엔터프라이즈 환경: QUIC 차단(전략 A) + TCP SSL Inspection → 가장 실용적 2. 클라우드 SWG: QUIC 인지 프록시(전략 B) → WARP/PAC 파일로 프록시 자동 구성 3. ECH 대응: DNS 프록시에서 ECHConfig 제거 + QUIC 차단 → SNI 가시성 유지
# VPP에서 QUIC 차단 → TCP 폴백 유도 설정

# 1. ACL로 UDP 443 차단
vpp# create access-list ip permit+reflect src 0.0.0.0/0 \
     dst 0.0.0.0/0 proto udp dport 443 action deny

# 2. 인터페이스에 ACL 적용
vpp# set acl-plugin acl 0 interface host-eth0 input

# 3. 확인: 브라우저가 QUIC 시도 실패 후 TCP 443으로 폴백
# Chrome: chrome://flags/#enable-quic → Disabled로 확인 가능
# 브라우저 DevTools → Network → Protocol 컬럼에서 h2(TCP) 확인

SSL Inspection 성능 최적화

SSL Inspection은 이중 TLS 처리로 인해 단순 TLS 종단 대비 2배 이상의 암호화 연산 부하가 발생합니다. VPP 환경에서의 최적화 전략:

최적화 항목구현 방법성능 향상
인증서 캐싱도메인별 동적 인증서 + 키 쌍 캐시, LRU 교체 정책캐시 히트 95%+ 시 CPS 3x 향상
ECDSA 전환프록시 CA와 동적 인증서를 RSA → ECDSA P-256으로 전환서명 속도 5x 향상 (0.5ms → 0.1ms)
QAT 이중 오프로드양쪽 TLS 세션의 벌크 암호화 + 핸드셰이크를 QAT에 오프로드CPU 사용률 70% 감소
비동기 인증서 생성인증서 서명을 비동기 큐에 넣고 핸드셰이크를 파이프라이닝핸드셰이크 병목 해소
선택적 검사(Selective Inspection)SNI 기반 카테고리 분류 → 위험 카테고리만 심층 검사검사 대상 트래픽 50~70% 감소
TLS 1.3 전용TLS 1.2 이하 비활성화 → 1-RTT 핸드셰이크, 간소화된 cipher핸드셰이크 지연 30% 감소
벡터 배치 검사Hyperscan의 hs_scan_stream()으로 여러 세션 평문을 벡터 처리IDS 처리량 2x 향상
# startup.conf — SSL Inspection 고성능 설정 예시

session {
  evt_qs_memfd_seg
  event-queue-length 200000
  preallocated-sessions 256000    # 이중 세션이므로 2배 할당
  rx-fifo-size 32K
  tx-fifo-size 32K
}

tls {
  default-crypto-engine openssl
  async                           # 비동기 암호화 필수
}

tlsopenssl {
  engine cryptodev                # QAT 오프로드
  async
  max-async-frames 512
}

cpu {
  main-core 0
  corelist-workers 1-15           # SSL Inspection은 CPU 집약적
}

# SSL Inspection 전용 설정
ssl-inspect {
  proxy-ca-cert /etc/vpp/ssl-inspect/proxy-ca.pem
  proxy-ca-key /etc/vpp/ssl-inspect/proxy-ca.key
  cert-cache-size 100000          # 도메인 인증서 캐시 (10만 개)
  cert-cache-ttl 86400            # 캐시 TTL: 24시간
  cert-key-type ecdsa-p256        # 동적 인증서 키 유형
  bypass-list /etc/vpp/ssl-inspect/bypass-domains.txt
  ids-engine hyperscan            # 인라인 IDS 엔진
  ids-rules /etc/vpp/ssl-inspect/suricata.rules
}

SSL Inspection 환경의 대표적 성능 수치(Xeon Platinum 8380, QAT C62x, 16 워커):

항목단순 TLS 종단SSL Inspection (SW)SSL Inspection (QAT)
새 연결 (CPS)~150,000~40,000~100,000
처리량 (Gbps)~80~20~50
p99 지연 (ms)0.20.80.4
동시 연결500K250K250K
CPU 사용률25%90%35%
메모리 (연결당)~50KB~120KB~120KB
SSL Inspection 운영 시 주의사항: SSL Inspection은 사용자의 암호화 통신을 중간에서 해독하는 기술이므로 법적 고지와 동의가 필수적입니다. (1) 기업 내 사용 시 직원에게 사전 고지하고 동의를 받아야 합니다. (2) GDPR, 개인정보보호법 등 관련 규제를 준수해야 합니다. (3) 금융·의료 등 민감 데이터는 반드시 바이패스 정책을 적용해야 합니다. (4) 프록시 CA 개인 키의 유출은 전체 네트워크 보안에 치명적이므로 HSM(Hardware Security Module)에 저장하는 것을 권장합니다.

명시적 HTTP CONNECT 프록시 구현

명시적(Explicit) 프록시 모드에서는 클라이언트가 HTTP CONNECT 메서드로 프록시에 터널을 요청합니다. 프록시는 이 요청을 가로채어 SSL Inspection을 수행합니다.

/* HTTP CONNECT 프록시 핸들러 (개념적 구현) */
static int
ssl_inspect_handle_connect (session_t *client_session,
                            u8 *request, u32 len)
{
    u8 *host;
    u16 port;
    ssl_inspect_session_t *sis;

    /* "CONNECT api.example.com:443 HTTP/1.1" 파싱 */
    if (parse_connect_request (request, len, &host, &port))
        return -1;

    /* 바이패스 정책 확인 */
    if (ssl_inspect_should_bypass_domain (host)) {
        /* 바이패스: 원본 서버에 직접 TCP 터널 */
        send_connect_response (client_session,
                               "200 Connection Established");
        setup_tcp_tunnel (client_session, host, port);
        return 0;
    }

    /* SSL Inspection 세션 초기화 */
    sis = ssl_inspect_session_alloc ();
    sis->server_name = vec_dup (host);

    /* 1. 원본 서버에 TLS 연결 (프록시가 클라이언트 역할) */
    sis->server_tls_session_handle =
        ssl_inspect_connect_to_server (host, port);

    /* 2. 서버 인증서 수신 대기 → 동적 인증서 생성 */
    /*    → 비동기 콜백에서 처리 (server_connected_cb) */

    return 0;
}

/* 서버 연결 완료 콜백 */
static void
ssl_inspect_server_connected_cb (ssl_inspect_session_t *sis,
                                 session_t *server_session)
{
    X509 *server_cert, *proxy_cert;
    SSL *server_ssl;

    /* 서버 인증서 추출 */
    server_ssl = tls_ctx_get_ssl (sis->server_tls_ctx);
    server_cert = SSL_get_peer_certificate (server_ssl);

    /* 서버 인증서 검증 (CA 체인, CRL/OCSP) */
    if (ssl_inspect_verify_server_cert (server_cert) != 0) {
        /* 서버 인증서 검증 실패 → 클라이언트에 경고 */
        ssl_inspect_send_cert_error (sis);
        return;
    }

    /* 동적 인증서 생성 (캐시 확인 포함) */
    proxy_cert = ssl_inspect_get_or_generate_cert (
        sis, sis->server_name, server_cert);

    /* 클라이언트에 "200 Connection Established" 응답 */
    send_connect_response (sis->client_session,
                           "200 Connection Established");

    /* 클라이언트 측 TLS 세션 시작 (동적 인증서 사용) */
    sis->client_tls_ctx = ssl_inspect_start_client_tls (
        sis, proxy_cert);

    /* 이후 양쪽 TLS 세션이 모두 ESTABLISHED되면
     * ssl_inspect_engine 노드에서 평문 검사 시작 */
}
코드 설명
  • 10~11행 클라이언트가 보낸 CONNECT api.example.com:443 HTTP/1.1 요청에서 대상 호스트와 포트를 추출합니다. 이 시점에서는 아직 TLS 핸드셰이크가 시작되지 않았으므로 평문입니다.
  • 14~20행 바이패스 대상 도메인인 경우, SSL Inspection 없이 단순 TCP 터널을 설정합니다. 클라이언트와 서버가 직접 TLS 핸드셰이크를 수행하며 프록시는 바이트를 투명하게 중계합니다.
  • 27~28행 먼저 원본 서버에 TLS 연결을 수립합니다. 서버의 실제 인증서를 받아야 동적 인증서를 생성할 수 있기 때문입니다. 이 연결은 비동기로 진행됩니다.
  • 47~50행 서버 인증서의 유효성을 검증합니다. 프록시가 만료되거나 위조된 서버 인증서를 그대로 전달하면 안 되므로, CA 체인 검증 + CRL/OCSP 확인을 수행합니다. 검증 실패 시 클라이언트에 적절한 에러를 전달합니다.
  • 59~60행 200 Connection Established 응답을 보낸 후에 클라이언트 측 TLS 핸드셰이크를 시작합니다. 이 순서가 중요합니다. 클라이언트는 200 응답을 받은 후에 TLS ClientHello를 전송하기 때문입니다.

SSL Inspection 모니터링 및 디버깅

# SSL Inspection 상태 모니터링
vpp# show ssl-inspect summary
  Active inspection sessions:     12,345
  Bypassed sessions:               8,901
  Certificate cache entries:      45,678
  Certificate cache hit rate:     97.3%
  Threats blocked:                    42

# 인증서 캐시 상태
vpp# show ssl-inspect cert-cache [verbose]
  [0] *.google.com   hits: 15234  age: 3600s
  [1] api.github.com hits:  2341  age: 1800s
  [2] cdn.example.com hits:  892  age: 7200s

# 바이패스 통계
vpp# show ssl-inspect bypass stats
  Domain whitelist hits:          5,432
  Category bypass (financial):   2,100
  Category bypass (healthcare):    890
  Certificate pinning detected:    479
  Internal IP bypass:            1,000

# IDS 매칭 통계
vpp# show ssl-inspect ids stats
  Patterns loaded:               12,345
  Scans performed:            1,234,567
  Matches found:                    42
  Average scan time:             0.02ms

# 개별 세션 상세
vpp# show ssl-inspect session verbose index 42
  Client: 192.168.1.100:52341
  Server: 93.184.216.34:443 (example.com)
  Client TLS: TLSv1.3 TLS_AES_256_GCM_SHA384
  Server TLS: TLSv1.3 TLS_AES_256_GCM_SHA384
  Proxy cert: CN=example.com (ECDSA P-256)
  Bytes inspected: 1,048,576
  IDS matches: 0
  Action: ALLOW

# 실시간 검사 트레이싱
vpp# trace add ssl-inspect-classify 10
vpp# trace add ssl-inspect-engine 10
vpp# show trace
문제진단 명령해결책
인증서 오류 (브라우저 경고)show ssl-inspect cert-cache verbose프록시 CA가 클라이언트에 설치되었는지 확인, SAN 복사 로직 검증
핸드셰이크 지연show ssl-inspect summary 캐시 히트율 확인캐시 크기 증가, ECDSA 전환, QAT 오프로드 활성화
특정 앱 연결 실패show ssl-inspect bypass stats에서 피닝 감지 확인해당 앱 도메인을 바이패스 목록에 추가
IDS 오탐(False Positive)show ssl-inspect ids stats + 매칭 룰 ID 확인해당 룰을 suppress 또는 threshold 조정
처리량 부족show runtime에서 노드별 사이클 확인선택적 검사로 대상 트래픽 축소, 워커 수 증가
메모리 부족show memory verbose에서 세션 풀 확인preallocated-sessions을 이중 세션 고려하여 2배로 설정
실전 구현 가이드: 앞 섹션에서 SSL Inspection의 아키텍처 개요, 이중 TLS 세션, 동적 인증서 엔진, 투명·명시적 프록시, SNI 추출·바이패스, IDS/IPS 통합, QUIC/ECH 과제, 성능·모니터링을 다루었습니다. 이어지는 섹션은 같은 주제의 실전 구현 관점을 보충합니다. VCL/VLS/LD_PRELOAD/memif 중 선택 기준, CA 인프라 구성, SFC 분리형 구조, TLS 1.3 특이점, 양방향 데이터 릴레이 코드, 트러블슈팅 런북 등 운영 현장에서 반복되는 결정 지점을 정리했습니다.

SSL Inspection 실전 구현 가이드

현대 네트워크 트래픽의 90% 이상이 TLS로 암호화되어 있습니다. 방화벽이나 IDS/IPS가 암호화된 페이로드(Payload)를 검사하려면 SSL/TLS Inspection(SSL 가시성 확보)이 필수입니다. VPP는 유저스페이스에서 TLS 종단(Termination)과 재암호화(Re-encryption)를 수행할 수 있어, 커널 기반 프록시 대비 훨씬 높은 처리량으로 SSL Inspection을 구현할 수 있습니다.

이 절은 두 부분으로 나뉩니다. Part A: 기술 구현(원리·아키텍처·데이터 릴레이·성능)은 코드와 CLI 중심의 엔지니어링 참고 자료입니다. Part B: 운영과 거버넌스(배치 토폴로지·법적 컴플라이언스·감사 로그·트러블슈팅)는 보안 팀·운영 팀이 먼저 읽어야 할 정책과 런북입니다. 구현만 가지고 배포하면 법적·조직적 리스크가 커지므로, 두 파트를 모두 통과한 뒤에 실제 환경에 올리시기 바랍니다.

Part A — 기술 구현

SSL Inspection의 원리

SSL Inspection이 중간자 구조로서 "무엇을 하고 왜 필요한가"는 VPP on DPU 심화 문서 0번 소절에서 개념·Python 최소 구현·원리적 한계까지 이미 정리했고, 본 페이지의 SSL Inspection 아키텍처 개요에서도 VPP 관점에서 한 번 다루었습니다. 이 A 소절부터는 그 개념을 전제로, 실전 배포에서 실제로 부딪히는 구현 선택—VCL/VLS/LD_PRELOAD/memif 중 무엇을 고를지, CA 인프라를 어떻게 구성할지, 인라인/SFC/TLS 1.3 각 경로를 어떻게 분기할지—에 집중해서 설명합니다. 아래 아키텍처 그림은 그 선택들이 어디에 위치하는지를 한 장에 요약한 것입니다.

VPP 코드 관점에서 더 깊이 들어가려면: 이 Part A는 "선택과 구성"을 다루고, 실제로 VPP의 어떤 파일·어떤 콜백·어떤 그래프 노드가 관여하는가는 분량 관계로 별도 문서에 있습니다. 본 소절을 읽다가 "그래서 이게 VPP 소스에서 어디에 붙는가?"가 궁금해지면 다음을 참고해 주시기 바랍니다. 이 세 소절은 본 Part A 실전 구현 가이드의 하위 레이어에 해당합니다. 호스트 단독 배포라면 11·12번만으로 충분하고, DPU 오프로드까지 고려한다면 6.5번까지 함께 읽으시는 편이 좋습니다.
SSL Inspection: 클라이언트-검사기-서버 이중 TLS 세션 클라이언트 브라우저 / 앱 검사기의 CA 인증서를 신뢰 저장소에 설치 TLS 세션 ① 클라이언트 ↔ 검사기 VPP SSL 검사기 TLS 복호화 평문 검사 TLS 재암호화 인증서 생성 DPI · 악성코드 · DLP · URL 필터링 TLS 세션 ② 검사기 ↔ 원본 서버 원본 서버 example.com 실제 서버 인증서 (Let's Encrypt 등) 패킷 처리 단계별 흐름 ① ClientHello SNI 추출 ② 서버 연결 원본 인증서 수신 ③ 인증서 위조 CA로 동적 서명 ④ 클라이언트 응답 위조 인증서 전달 ⑤ 평문 검사 DPI / 정책 적용 ⑥ 재암호화·전달 양방향 릴레이 검사기는 자체 CA 인증서로 원본 서버의 인증서를 동적으로 위조(re-sign)합니다. 클라이언트가 이 CA를 신뢰하면 브라우저 경고 없이 투명하게 동작합니다. 바이패스(Bypass)가 필요한 경우 Certificate Pinning 모바일 앱, 금융 서비스 등 위조 인증서 거부 → 바이패스 필수 상호 TLS (mTLS) 클라이언트 인증서 필요 중간자가 클라이언트 키 없음 법적/개인정보 예외 의료·금융·법률 사이트 규정상 복호화 금지 대상 이런 트래픽은 SNI 기반으로 탐지하여 TLS 세션을 맺지 않고 그대로 통과시켜야 합니다.

VPP SSL Inspection 아키텍처

VPP에서 SSL Inspection을 구현하는 방식은 크게 두 가지입니다. 각각의 장단점을 이해해야 환경에 맞는 설계를 선택할 수 있습니다.

구현 방식구조장점단점적합 환경
인라인 프록시VPP 내 VCL + TLS 플러그인단일 프로세스, 낮은 지연VPP 플러그인 개발 필요고성능 NGFW
SFC 분리형VPP L4 분류 → memif → 외부 프록시기존 프록시 재활용(Recycling), 유연memif 오버헤드(Overhead), 복잡도기존 인프라 연동
SSL Inspection 구현 방식 비교 방식 A: 인라인 프록시 (VPP 내부에서 모두 처리) LAN NIC dpdk-input ip4-lookup TCP:443 감지 TLS 종단 VCL + tlsopenssl DPI 엔진 평문 HTTP 검사 TLS 재암호화 서버측 세션 WAN NIC dpdk-output 단일 VPP 프로세스 내부 — 모든 처리가 그래프 노드 체인에서 발생 방식 B: SFC 분리형 (VPP 분류 + 외부 SSL 프록시) LAN NIC dpdk-input L4 분류기 TLS(443) → memif memif 전달 제로카피 외부 SSL 프록시 Squid / mitmproxy / 커스텀 memif 복귀 검사 완료 WAN NIC dpdk-output 구현 방식별 비교 방식 A: 인라인 프록시 + 지연 최소 (memif 왕복 없음) + 단일 프로세스 관리 − VPP 플러그인으로 DPI 구현 필요 − 장애 시 전체 경로 영향 방식 B: SFC 분리형 + 기존 프록시(Squid 등) 재활용 + 프록시 장애 격리 가능 − memif 왕복 지연 추가 (~2μs) − 관리 복잡도 증가

VCL, VLS, LD_PRELOAD, memif 중 무엇을 선택할 것인가

SSL Inspection을 설계할 때 가장 먼저 정해야 하는 것은 "TLS를 어디서 종단할 것인가"가 아니라 "애플리케이션을 어떤 방식으로 VPP 호스트 스택에 붙일 것인가"입니다. 같은 TLS Inspection이라도 진입 방식에 따라 개발 난이도, 장애 격리(Isolation), 워커 배치, 멀티스레드 잠금(Lock) 경합(Contention) 양상이 크게 달라집니다.

방식장점위험적합한 팀/환경
직접 VCL지연이 가장 낮고 TLS 세션·FIFO·이벤트 큐를 세밀하게 제어할 수 있습니다코드 수정량이 많고, 자체 운영 런북을 새로 만들어야 합니다전용 보안 어플라이언스, 자체 프록시 개발팀
VLS 래핑기존 멀티스레드 TCP 서버 구조를 상당 부분 유지할 수 있습니다세션 공유가 많으면 락과 워커 간 clone/share RPC 비용이 커집니다사내 프록시, 자체 C/C++ 서버를 단계적으로 이전하는 팀
LD_PRELOAD가장 빠르게 파일럿을 만들 수 있습니다sendmsg(), fd 전달, 복잡한 소켓 옵션, 일부 라이브러리 훅 충돌이 문제될 수 있습니다nginx, curl, 간단한 TLS 리버스 프록시의 사전 검증
memif + 외부 프록시기존 Envoy, HAProxy, Squid를 재활용하기 쉽고 장애 격리가 좋습니다memif 왕복과 운영 복잡도가 추가됩니다이미 프록시 인프라가 있고 빠르게 붙여야 하는 조직
# 기존 HAProxy/Envoy를 VPP 호스트 스택 위에서 빠르게 검증하는 파일럿 예시
$ export VCL_CONFIG=/etc/vpp/vcl.conf
$ export LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libvcl_ldpreload.so

# 기존 설정 파일은 유지하고, 먼저 기능 호환성을 확인합니다
$ haproxy -f /etc/haproxy/haproxy.cfg

# 관찰 포인트
vpp# show session verbose
vpp# show runtime
vpp# show errors | grep -iE 'tls|session'
선택 기준: 파일럿 단계에서는 LD_PRELOAD가 가장 빠르지만, 운영 단계에서는 세션 소유권과 멀티스레드 모델이 명확한 직접 VCL 또는 장애 격리가 쉬운 memif 분리형이 보통 더 예측 가능합니다. 기존 프록시가 스레드 간에 소켓을 적극적으로 공유한다면 VLS 경로의 락 비용을 반드시 측정해야 합니다. "직접 VCL" 방향을 택했다면 VPP on DPU 6.5절의 VCL+OpenSSL 구현 골격(BIO 래퍼, SNI 콜백 내부 업스트림 핸드셰이크, 양방향 평문 펌프)을 레퍼런스로 재사용하실 수 있습니다. DPU 오프로드가 없는 순수 호스트 배포에서도 memif 부분만 내부 fifo로 대체하면 그대로 동작합니다.

인라인 SSL Inspection 구현

VPP의 VCL(VPP Communication Library)과 TLS 플러그인을 활용하여 단일 VPP 프로세스 내에서 SSL Inspection을 구현하는 방법입니다. 핵심은 두 개의 독립적인 TLS 컨텍스트를 관리하는 것입니다.

# /etc/vpp/startup.conf — SSL Inspection용 구성
unix {
    cli-listen /run/vpp/cli.sock
    log /var/log/vpp/vpp.log
    full-coredump
    gid vpp
}

dpdk {
    dev 0000:00:08.0 { name GigabitEthernet0/8/0 }  /* LAN */
    dev 0000:00:09.0 { name GigabitEthernet0/9/0 }  /* WAN */
    no-multi-seg
}

cpu {
    main-core 0
    corelist-workers 1-7          /* TLS 핸드셰이크가 CPU 집약적 */
}

buffers {
    buffers-per-numa 65536        /* TLS 레코드 버퍼링 위해 증가 */
    default data-size 2048
}

session {
    evt_qs_memfd_seg              /* 세션 이벤트 큐 공유 메모리 */
    event-queue-length 100000     /* 동시 세션 수에 비례 */
}

plugins {
    plugin default { disable }
    plugin dpdk_plugin.so { enable }
    plugin tlsopenssl_plugin.so { enable }
    plugin http_plugin.so { enable }
    plugin nat_plugin.so { enable }
    plugin acl_plugin.so { enable }
    plugin ping_plugin.so { enable }
}

CA 인증서 인프라 구성

SSL Inspection의 기반은 내부 CA(Certificate Authority)입니다. 검사기가 원본 서버의 인증서 정보를 복사하여 이 CA로 동적 서명한 인증서를 클라이언트에 제시합니다.

# 1. 내부 Root CA 키 쌍 생성
$ openssl ecparam -genkey -name prime256v1 -out inspection-ca.key
$ openssl req -new -x509 -key inspection-ca.key -sha256 -days 3650 \
    -out inspection-ca.crt \
    -subj "/CN=Internal SSL Inspection CA/O=MyOrg/C=KR"

# 2. VPP TLS에서 사용할 서버 키 (동적 인증서 서명용)
$ openssl ecparam -genkey -name prime256v1 -out inspection-server.key

# 3. 클라이언트(브라우저)에 CA 인증서 배포
# Linux: /usr/local/share/ca-certificates/ 에 복사 후 update-ca-certificates
# Windows: GPO로 Trusted Root CA에 배포
# macOS: Keychain Access에서 "항상 신뢰"로 추가

# 4. CKPAIR 등록은 CLI가 아니라 바이너리 API 경로입니다.
#    제어 평면(GoVPP/PAPI/자체 C 앱)에서 다음과 같이 주입합니다.
#   vl_api_app_add_cert_key_pair_t { certkey = cert||key }
#   -> 반환 index를 SNI별 라우팅 테이블에 저장
보안 경고: 내부 CA의 개인키(Private Key)가 유출되면 네트워크 내 모든 TLS 통신이 위험해집니다. 개인키는 반드시 HSM(Hardware Security Module) 또는 TPM에 보관하고, 파일 시스템 권한을 0600으로 제한하세요. CA 인증서의 유효기간도 운영 정책에 맞게 설정해야 합니다.

동적 인증서 생성 구현

SSL Inspection의 핵심 기술은 원본 서버 인증서의 Subject/SAN을 복제하여 내부 CA로 즉시 서명하는 것입니다. 이 과정이 TLS 핸드셰이크 지연의 가장 큰 비중을 차지합니다.

/* SSL Inspection 플러그인: 동적 인증서 생성 핵심 로직 */
/* OpenSSL API를 사용한 구현 예시 */

static X509 *
generate_inspection_cert (X509 *orig_cert,
                          EVP_PKEY *ca_key,
                          X509 *ca_cert)
{
  X509 *cert = X509_new ();
  X509_NAME *subj = X509_get_subject_name (orig_cert);

  /* 원본 인증서에서 Subject 복사 */
  X509_set_subject_name (cert, subj);

  /* Issuer를 내부 CA로 설정 */
  X509_set_issuer_name (cert, X509_get_subject_name (ca_cert));

  /* 원본의 SAN(Subject Alternative Name) 복사 — 필수! */
  int san_idx = X509_get_ext_by_NID (orig_cert, NID_subject_alt_name, -1);
  if (san_idx >= 0)
    {
      X509_EXTENSION *san_ext = X509_get_ext (orig_cert, san_idx);
      X509_add_ext (cert, san_ext, -1);
    }

  /* 일련번호, 유효기간 설정 */
  ASN1_INTEGER_set (X509_get_serialNumber (cert),
                    generate_unique_serial ());
  X509_gmtime_adj (X509_get_notBefore (cert), 0);
  X509_gmtime_adj (X509_get_notAfter (cert), 86400 * 365);

  /* 검사기 서버의 공개키 설정 */
  X509_set_pubkey (cert, inspection_server_key);

  /* 내부 CA 개인키로 서명 */
  X509_sign (cert, ca_key, EVP_sha256 ());

  return cert;
}

/* 캐시: 동일 서버에 대한 반복 생성 방지 */
typedef struct {
  u8 *server_name;        /* SNI 또는 Subject CN */
  X509 *cached_cert;      /* 생성된 위조 인증서 */
  f64 created_at;         /* 캐시 생성 시각 */
  f64 ttl;                /* 캐시 유효 시간 */
} ssl_cert_cache_entry_t;
인증서 캐싱: 동적 인증서 생성(특히 RSA 서명)은 비용이 큽니다. ECDSA P-256 서명은 RSA-2048 대비 약 10배 빠르므로, CA 키를 ECDSA로 생성하는 것이 성능에 유리합니다. 또한 SNI 기반 캐시를 두면 동일 서버에 대한 반복 핸드셰이크에서 인증서 생성을 건너뛸 수 있습니다. 일반적으로 캐시 TTL은 1~24시간이 적절합니다.

SNI 기반 바이패스와 정책 분류

모든 TLS 트래픽을 복호화하는 것은 성능 낭비이자 법적 리스크입니다. SNI(Server Name Indication)를 기반으로 검사 대상과 바이패스 대상을 분류해야 합니다. SNI는 TLS ClientHello의 확장 필드에 평문으로 포함되므로, TLS 세션을 맺기 전에 읽을 수 있습니다.

SNI 기반 트래픽 분류: 검사 vs 바이패스 TCP 연결 수립 dst port 443 ClientHello 파싱 SNI 확장 필드 추출 정책 테이블 조회 도메인 → 동작 매핑 SSL Inspection TLS 종단 → 검사 → 재암호화 검사 바이패스 TLS 세션 그대로 전달 통과 차단 (RST) 정책 위반 도메인 차단 WAN 전달 dpdk-output 정책 테이블 예시 도메인 패턴 동작 사유 *.bank.com, *.finance.kr 바이패스 금융 — 법적 복호화 금지, Certificate Pinning *.medical.org, health.* 바이패스 의료 — 개인건강정보(PHI) 보호 *.malware-domain.xyz 차단 위협 인텔리전스 차단 목록 * (기본) 검사 기본 정책 — 모든 나머지 TLS 트래픽
/* SNI 파싱: ClientHello에서 서버 이름 추출 */
static int
parse_sni_from_client_hello (u8 *data, u32 len,
                             u8 **sni_out, u16 *sni_len_out)
{
  /* TLS Record: ContentType(1) + Version(2) + Length(2) */
  if (len < 5 || data[0] != 0x16)  /* 0x16 = Handshake */
    return -1;

  /* Handshake: HandshakeType(1) + Length(3) */
  u8 *hs = data + 5;
  if (hs[0] != 0x01)  /* 0x01 = ClientHello */
    return -1;

  /* ClientHello: Version(2) + Random(32) + SessionID(var)
   * + CipherSuites(var) + Compression(var) + Extensions(var) */
  u32 offset = 5 + 4;  /* record header + handshake header */
  offset += 2 + 32;     /* version + random */
  offset += 1 + data[offset]; /* session ID length + data */

  u16 cs_len = (data[offset] << 8) | data[offset + 1];
  offset += 2 + cs_len;  /* cipher suites */
  offset += 1 + data[offset]; /* compression */

  /* Extensions 순회 → SNI(type=0x0000) 찾기 */
  u16 ext_total = (data[offset] << 8) | data[offset + 1];
  offset += 2;
  u32 ext_end = offset + ext_total;

  while (offset + 4 <= ext_end)
    {
      u16 ext_type = (data[offset] << 8) | data[offset + 1];
      u16 ext_len  = (data[offset + 2] << 8) | data[offset + 3];
      offset += 4;

      if (ext_type == 0x0000)  /* SNI extension */
        {
          /* SNI list: ListLen(2) + Type(1) + NameLen(2) + Name */
          u16 name_len = (data[offset + 3] << 8) | data[offset + 4];
          *sni_out = data + offset + 5;
          *sni_len_out = name_len;
          return 0;
        }
      offset += ext_len;
    }
  return -1;  /* SNI 없음 (IP 직접 연결 등) */
}
ECH(Encrypted Client Hello): TLS 1.3의 ECH 확장이 활성화되면 SNI가 암호화되어 위 파싱이 불가능합니다. ECH 대응으로는 DNS(DoH/DoT) 수준에서 도메인을 추적하거나, ECH 트래픽 자체를 차단하는 정책이 필요합니다. 현재 대부분의 브라우저에서 ECH는 실험적 기능이지만, 향후 보편화될 가능성이 높으므로 대비가 필요합니다.

SFC 분리형: VPP + 외부 SSL 프록시

기존 프록시(Squid SSL Bump, mitmproxy 등)를 재활용하면서 VPP의 고성능 패킷 분류만 활용하는 구조입니다. VPP가 TLS 트래픽을 식별하여 memif로 외부 프록시에 전달하고, 검사 완료 후 다시 VPP로 복귀시킵니다.

# VPP: TLS 트래픽 분류 및 memif 전달 구성

# 1. SSL 프록시용 memif 생성
vpp# create memif socket id 10 filename /run/vpp/memif-ssl-proxy.sock
vpp# create interface memif id 0 socket-id 10 master ring-size 2048
vpp# set interface state memif10/0 up
vpp# set interface ip address memif10/0 10.255.0.1/30

# 2. 프록시로부터 복귀용 memif
vpp# create memif socket id 11 filename /run/vpp/memif-ssl-return.sock
vpp# create interface memif id 0 socket-id 11 master ring-size 2048
vpp# set interface state memif11/0 up
vpp# set interface ip address memif11/0 10.255.1.1/30

# 3. ACL로 TCP 443 트래픽 분류 → 프록시로 라우팅
vpp# set acl-plugin acl permit+reflect proto 6 dport 443
vpp# set acl-plugin interface GigabitEthernet0/8/0 input acl 0

# 4. PBR(Policy-Based Routing): TCP 443 매칭 시 프록시로 전달
vpp# ip table add 100
vpp# ip route add 0.0.0.0/0 table 100 via 10.255.0.2 memif10/0

# classify 테이블로 TLS 트래픽을 FIB 100으로 전환
vpp# classify table mask l3 ip4 proto l4 dst_port
vpp# classify session table-index 0 match l3 ip4 proto 6 l4 dst_port 443 \
     action set-ip4-fib-id 100

# 5. 프록시에서 복귀한 트래픽은 기본 FIB로 WAN 전달
vpp# ip route add 0.0.0.0/0 via 203.0.113.254 GigabitEthernet0/9/0
# 외부 프록시 측: libmemif를 사용한 Squid SSL Bump 연동
# 또는 mitmproxy 기반 간이 구성

# mitmproxy 실행 (memif 대신 TAP으로 연결하는 간이 예시)
$ sudo ip link set vpp-proxy up
$ sudo ip addr add 10.255.0.2/30 dev vpp-proxy
$ sudo ip route add default via 10.255.0.1 dev vpp-proxy

# mitmproxy를 투명 프록시 모드로 실행
$ mitmproxy --mode transparent --listen-host 10.255.0.2 --listen-port 8443 \
    --set ssl_insecure=true \
    --set confdir=/etc/mitmproxy \
    -s inspection_policy.py

# iptables로 투명 리다이렉트 (프록시 호스트에서)
$ sudo iptables -t nat -A PREROUTING -i vpp-proxy -p tcp --dport 443 \
    -j REDIRECT --to-port 8443
libmemif 직접 연동: 최고 성능을 위해서는 외부 프록시가 libmemif를 직접 사용하여 VPP와 공유 메모리로 패킷을 교환해야 합니다. libmemif는 C/C++ 라이브러리로 제공되며, 기존 프록시에 패치(Patch)를 적용하거나 래퍼를 작성하여 통합할 수 있습니다. TAP 인터페이스를 경유하는 위 예시는 개발/테스트 용도에 적합합니다.

TLS 1.3 Inspection 고려사항

TLS 1.3은 핸드셰이크 구조가 크게 바뀌어 SSL Inspection 구현에 영향을 줍니다.

변경점TLS 1.2TLS 1.3Inspection 영향
핸드셰이크 왕복2-RTT1-RTT (0-RTT 가능)인증서 위조 시간 압박 증가
서버 인증서 위치평문 전송암호화 후 전송검사기가 먼저 서버와 핸드셰이크 완료해야 원본 인증서 확인 가능
세션 재개Session ID/TicketPSK (0-RTT)0-RTT 데이터는 재생 공격 위험 → 검사기에서 0-RTT 차단 또는 제한 필요
암호 스위트다양 (RC4, CBC 등)AEAD만 (GCM, ChaCha20)오래된 암호 스위트 다운그레이드 불가 → 검사기도 AEAD 지원 필수
SNI 암호화 (ECH)없음선택적 (초안)ECH 활성 시 SNI 파싱 불가 → DNS 기반 정책으로 전환 필요
/* TLS 1.3 핸드셰이크 시 Inspection 순서 */

/* 1. ClientHello 수신 → SNI 추출 + 바이패스 판단 */
/* 2. 검사 대상이면: 검사기가 원본 서버에 먼저 TLS 1.3 연결 */
/* 3. 서버의 EncryptedExtensions + Certificate 수신 (복호화 후) */
/* 4. 원본 인증서 정보로 위조 인증서 생성 */
/* 5. 클라이언트에게 위조 인증서로 ServerHello 응답 */

static int
tls13_inspection_handshake (ssl_inspect_ctx_t *ctx)
{
  /* 서버측 TLS 1.3 핸드셰이크 먼저 완료 */
  int rv = SSL_connect (ctx->server_ssl);
  if (rv != 1)
    return handle_ssl_error (ctx, rv);

  /* 서버 인증서 체인 가져오기 */
  X509 *server_cert = SSL_get_peer_certificate (ctx->server_ssl);
  if (!server_cert)
    return -1;

  /* 원본 인증서 검증 (OCSP, CRL 확인) */
  long verify_result = SSL_get_verify_result (ctx->server_ssl);
  if (verify_result != X509_V_OK)
    {
      /* 원본 서버 인증서 검증 실패 → 정책에 따라 차단 또는 경고 */
      log_cert_error (ctx, verify_result);
      if (ctx->policy & SSL_INSPECT_BLOCK_INVALID_CERT)
        return -1;
    }

  /* 동적 인증서 생성 (캐시 우선 확인) */
  X509 *forged = cert_cache_lookup (ctx->sni);
  if (!forged)
    {
      forged = generate_inspection_cert (server_cert,
                                          ctx->ca_key, ctx->ca_cert);
      cert_cache_insert (ctx->sni, forged, CERT_CACHE_TTL);
    }

  /* 클라이언트측 TLS 컨텍스트에 위조 인증서 설정 */
  SSL_use_certificate (ctx->client_ssl, forged);
  SSL_use_PrivateKey (ctx->client_ssl, ctx->server_key);

  /* 클라이언트 핸드셰이크 완료 */
  return SSL_accept (ctx->client_ssl);
}
0-RTT 주의: TLS 1.3의 0-RTT(Early Data)는 핸드셰이크 완료 전에 애플리케이션 데이터를 전송합니다. 이 데이터는 재생 공격(Replay Attack)에 취약하며, 검사기가 아직 서버 인증서를 확인하지 못한 상태에서 도착합니다. SSL Inspection 환경에서는 SSL_CTX_set_max_early_data(ctx, 0)으로 0-RTT를 비활성화하는 것이 안전합니다.

양방향 데이터 릴레이 구현

핸드셰이크가 완료되면 검사기는 양쪽 TLS 세션의 평문 데이터를 릴레이하면서, 중간에 DPI 엔진을 통과시킵니다. VPP의 세션 레이어를 활용하면 이 릴레이를 이벤트 기반으로 효율적으로 구현할 수 있습니다.

데이터 릴레이: 복호화 → 검사 → 재암호화 (요청 방향) 클라이언트 TLS 세션 TCP RX FIFO (암호문) SSL_read() → 평문 App RX FIFO (평문) DPI / 보안 검사 엔진 HTTP 파싱 (메서드, URI, 헤더) 악성코드 시그니처 매칭 DLP 정책 (개인정보 패턴) 허용 재암호화 진행 차단 RST 또는 블록 페이지 서버 TLS 세션 평문 → SSL_write() TCP TX FIFO (재암호문) 원본 서버로 전송 평문 평문 응답 방향: 서버 → DPI → 클라이언트 (역방향 동일 구조) 서버 응답 복호화 응답 DPI (악성코드, 데이터 유출) 클라이언트로 재암호화 성능 핵심: FIFO 간 평문 데이터 복사를 최소화해야 합니다. VPP 세션 레이어의 svm_fifo_peek()으로 zero-copy 읽기를 수행하고, DPI 판정 후 svm_fifo_dequeue()로 소비합니다.
/* 양방향 릴레이 핵심 로직 — VCL 세션 기반 */

static void
ssl_inspect_relay_data (ssl_inspect_ctx_t *ctx)
{
  u8 buf[16384];  /* TLS 레코드 최대 크기 */
  int n;

  /* 클라이언트 → 서버 방향 */
  while ((n = SSL_read (ctx->client_ssl, buf, sizeof (buf))) > 0)
    {
      /* DPI 엔진에 평문 전달 */
      dpi_verdict_t verdict = dpi_inspect (ctx->dpi_ctx,
                                           buf, n,
                                           DPI_DIR_CLIENT_TO_SERVER);

      if (verdict == DPI_BLOCK)
        {
          /* 차단: 클라이언트에 블록 페이지 전송 후 세션 종료 */
          send_block_page (ctx->client_ssl, ctx->block_reason);
          ssl_inspect_close (ctx);
          return;
        }

      /* 허용: 원본 서버로 재암호화하여 전달 */
      int written = SSL_write (ctx->server_ssl, buf, n);
      if (written <= 0)
        {
          handle_ssl_error (ctx, written);
          return;
        }
    }

  /* 서버 → 클라이언트 방향 (대칭) */
  while ((n = SSL_read (ctx->server_ssl, buf, sizeof (buf))) > 0)
    {
      dpi_verdict_t verdict = dpi_inspect (ctx->dpi_ctx,
                                           buf, n,
                                           DPI_DIR_SERVER_TO_CLIENT);

      if (verdict == DPI_BLOCK)
        {
          ssl_inspect_close (ctx);
          return;
        }

      SSL_write (ctx->client_ssl, buf, n);
    }
}

SSL Inspection 성능 고려사항

SSL Inspection은 TLS 핸드셰이크(특히 RSA/ECDSA 연산)와 대칭 암호화/복호화를 이중으로 수행하므로, 일반 패킷 포워딩 대비 CPU 부담이 매우 큽니다.

구성 요소CPU 비용 (상대)최적화 방법
RSA-2048 서명 (인증서 생성)매우 높음ECDSA P-256 전환 (10배 빠름), 인증서 캐싱
TLS 핸드셰이크높음세션 재개(PSK), QAT 비동기 오프로드
AES-GCM 대칭 암복호화중간AES-NI 활용 (기본 활성), QAT
DPI 시그니처 매칭가변 (룰 수 의존)Hyperscan/Vectorscan 가속, 룰 최적화
인증서 검증 (OCSP/CRL)네트워크 의존OCSP Stapling, 로컬 CRL 캐싱
# SSL Inspection 성능 모니터링

# TLS 핸드셰이크 횟수 및 소요 시간 확인
vpp# show tls
# 출력 예시:
# OpenSSL engine:
#   Ctx allocated: 45231
#   Handshakes:    12847 (active: 23)
#   Rx bytes:      4.2 GB
#   Tx bytes:      1.8 GB

# 세션 레이어 상태 (동시 세션 수)
vpp# show session verbose
# Transport   Active   Listening
# tcp         2341     4
# tls         1892     2

# 워커별 부하 분포 확인
vpp# show runtime
# session-queue 노드의 Vectors/Call이 워커 간 균등해야 합니다.
# 편향이 심하면 TCP 해시 분산(RSS/Flow Director) 조정 필요

# 인증서 캐시 적중률 (커스텀 플러그인 시)
# 적중률 90% 이상이면 양호, 50% 이하면 캐시 크기 확대 필요
구성동시 세션신규 핸드셰이크/초처리량 (Gbps)CPU 코어
VPP + OpenSSL (SW)50K~2,000 CPS~58
VPP + OpenSSL + QAT100K~8,000 CPS~124
커널 Squid SSL Bump10K~500 CPS~18
CPS(Connections Per Second)가 핵심: SSL Inspection에서 처리량(Gbps)보다 초당 새 연결 수(CPS)가 더 중요한 병목입니다. 핸드셰이크가 끝난 세션의 데이터 릴레이는 AES-NI 덕분에 빠르지만, 매 초 수천 개의 새 TLS 핸드셰이크를 처리하는 것이 CPU 집약적입니다. ECDSA CA + 인증서 캐싱 + QAT 조합이 CPS를 극대화합니다.

TCP 병목과 TLS 병목을 분리하는 운영 런북

SSL Inspection은 TCP와 TLS가 한꺼번에 보이기 때문에 문제를 잘못 분류하기 쉽습니다. 예를 들어 "HTTPS가 느리다"는 증상 하나만 보면 네트워크 경로, 핸드셰이크 CPU, 인증서 캐시 미스, 워커 편향, FIFO 고갈이 모두 같은 문제처럼 보입니다. 운영에서는 반드시 TCP 계층과 TLS 계층의 병목을 분리해야 합니다.

증상우선 의심할 계층확인 항목조치
새 연결 수만 급감합니다TLS 핸드셰이크show tls active handshake 수, 인증서 캐시 히트율ECDSA CA, 세션 재개, 인증서 캐시 확대, QAT 비동기 오프로드를 적용합니다
연결은 많지만 대역폭(Bandwidth)이 안 나옵니다TCP 전송재전송(Retransmission), RTT 상승, FIFO 사용률, 워커 편향RSS 재분배, rx/tx-fifo-size 조정, NIC 큐 수와 워커 핀닝을 재점검합니다
한쪽 방향만 유독 느립니다TCP 또는 재암호화 측 TLS클라이언트 측/서버 측 TLS 컨텍스트 처리 시간, ACK 편향양방향 워커 분산과 backend 방향 인증서 검증 지연을 따로 봅니다
CPU는 높지만 Gbps는 낮습니다TLS 제어 경로RSA/ECDSA 서명 비중, SNI 정책 엔진(Policy Engine), 동적 인증서 생성RSA에서 ECDSA 전환, 동적 인증서 캐시, 정책 매칭 단순화가 먼저입니다
메모리만 빠르게 줄어듭니다세션/FIFO/TLS 컨텍스트동시 세션 수, FIFO 크기, 인증서 캐시, 이중 TLS 세션 개수세션 산정식을 다시 계산하고, inspection 워크로드만 별도 프로세스로 분리합니다

운영 지표도 계층별로 나눠 보는 것이 좋습니다. TCP 관점에서는 RTT, 재전송, FIFO 포화, 워커별 session-queue 부하를 봐야 하고, TLS 관점에서는 핸드셰이크 CPS, 세션 재개율, 인증서 생성 지연, ALPN 선택, mTLS 실패 건수를 봐야 합니다. 둘을 한 그래프에 섞으면 병목 원인이 흐려집니다.

Part B — 운영과 거버넌스

아래부터는 기술 구현을 어떤 토폴로지·법적 근거·감사 체계 위에 올려 놓을지 다루는 절입니다. 배치 형태(배치 토폴로지), SNI/ALPN 바이패스 정책, 법적 근거 체크리스트, 트러블슈팅 런북이 여기 모여 있습니다. 기술 구현 파트(Part A)에서 결정한 구조가 여기의 운영 제약과 충돌하면 구현을 다시 조정해야 합니다.

SSL Inspection 트러블슈팅

증상진단원인해결
브라우저 인증서 경고브라우저에서 인증서 체인 확인내부 CA가 신뢰 저장소에 미설치GPO/MDM으로 CA 인증서 배포
특정 앱 연결 실패앱의 인증서 요구사항 확인Certificate Pinning해당 도메인을 바이패스 목록에 추가
핸드셰이크 타임아웃show tls active 핸드셰이크 수서버 연결 지연 또는 인증서 생성 병목ECDSA CA 전환, QAT 추가, 타임아웃 조정
CPS 저하show runtime session-queue 클럭RSA 서명 CPU 부하인증서 캐시 확대, CA 키를 ECDSA로 변경
QUIC/HTTP3 통과 안됨UDP 443 트래픽 확인QUIC은 UDP 기반 → TCP 분류기 미매칭UDP 443도 분류 대상에 추가, 또는 QUIC 차단 후 TCP 폴백 유도
mTLS 서비스 장애서버가 클라이언트 인증서 요구검사기에 원본 클라이언트 인증서 없음mTLS 대상을 바이패스, 또는 클라이언트 인증서 릴레이 구현
법적 고려사항: SSL Inspection은 사용자의 암호화 통신을 복호화하는 행위이므로, 반드시 조직의 보안 정책과 관련 법규(개인정보보호법, 전기통신사업법 등)를 준수해야 합니다. 사용자에게 SSL Inspection이 적용됨을 명확히 고지하고, 의료·금융·법률 등 민감한 카테고리는 반드시 바이패스하세요. 규정 위반 시 법적 책임이 발생할 수 있습니다.

SSL Inspection 배치 토폴로지 — 투명·명시·게이트웨이·DLP 연동

SSL Inspection은 어디에 놓이느냐에 따라 완전히 다른 설계가 됩니다. 네트워크 구조, 조직 보안 정책, 인증서 배포 가능 범위에 따라 아래 네 가지 토폴로지 중 하나 이상을 혼합 운영합니다. 각 패턴의 VPP 설정 포인트와 걸림돌을 함께 정리했습니다.

토폴로지위치인증서 배포 요건장점단점
투명(Transparent)인라인, 클라이언트 기본 게이트웨이MDM으로 전 기기 루트 CA 배포클라이언트 프록시 설정 불필요, 강제 적용 가능ECH·인증서 피닝 앱에서 실패, QUIC 우회 위험
명시적(Explicit HTTP CONNECT)클라이언트가 프록시 주소 지정루트 CA 배포 + PAC/WPAD트래픽 분류 명확, 실패 시 명시적 오류사용자가 프록시를 우회할 수 있음
게이트웨이(Enterprise SWG)데이터센터 egress관리 장비에만 CA 신뢰관리 트래픽만 선별 검사, 법적 리스크 낮음원격 근무자에 적용 어려움
DLP 사이드카VPP가 평문만 memif로 전달전통적 CA + DLP 벤더 통합DLP/IDS 기존 투자 재사용추가 홉, 평문이 사이드카까지 흐름

투명 토폴로지 VPP 설정

클라이언트 HTTPS를 강제로 가로채려면 투명 L2 브리지(Bridge) 또는 L3 라우팅으로 VPP가 해당 트래픽을 받게 한 뒤, 사용자 공간(User Space) Inspect 애플리케이션이 443번을 listen하면서 목적지 재작성(destination NAT 또는 세션 레이어 레벨 투명 리스너)으로 연결을 가로챕니다. 공식 VPP에 tcp-intercept addtls ca-signer add, tls dynamic-cert-cache size 같은 전용 CLI는 존재하지 않습니다 — 아래는 역할 수준의 개요이며, 실제 구현은 커스텀 플러그인/앱이 담당합니다.

# 기본 네트워크 구성 — LAN 인터페이스 준비
vpp# set interface l3 GigabitEthernet0/8/0
vpp# set interface state GigabitEthernet0/8/0 up
vpp# set interface ip address GigabitEthernet0/8/0 192.168.10.1/24

# 443 트래픽을 Inspect 애플리케이션으로 보내는 경로는 두 가지입니다:
#   (A) VPP NAT 또는 policy route로 목적지를 로컬 세션으로 재작성
#   (B) Inspect 앱이 transparent listener로 직접 수신 (세션 레이어 설정)
# 어느 쪽이든 'tcp-intercept add' 같은 일체형 CLI는 없습니다.
/* 동적 인증서 발급에 쓰이는 issuer CA는 Inspect 애플리케이션 코드가
   OpenSSL로 직접 로드하고, 즉석에서 leaf 인증서를 서명해 BAPI로
   app_add_cert_key_pair에 주입합니다. 캐시 크기·TTL은 애플리케이션 내부
   파라미터이며, VPP CLI로 노출되지 않습니다. */
EVP_PKEY *issuer_key = load_pem_privkey ("/etc/vpp/issuer.key");
X509     *issuer_crt = load_pem_cert    ("/etc/vpp/issuer.pem");

u32 ckpair_for_sni (const char *sni) {
  u32 *cached = lru_lookup (&cache, sni);
  if (cached) return *cached;
  X509 *leaf = sign_leaf_for (issuer_crt, issuer_key, sni);
  u32 idx = bapi_add_cert_key_pair (x509_to_pem (leaf), privkey_to_pem (leaf));
  lru_insert (&cache, sni, idx);
  return idx;
}

동적 인증서 캐시는 TTL을 짧게 잡지 마시기 바랍니다. 실사용 환경에서 하루 수천 도메인이 등장하므로 캐시 히트율이 50 % 이하면 발급 비용이 폭발합니다. 기본 1시간 TTL + 8K 엔트리로 시작해 히트율을 보면서 조정하시기 바랍니다. 이 파라미터는 애플리케이션 코드 안에서 결정되는 값입니다.

명시적 CONNECT 구성

HTTP CONNECT host:443으로 요청한 뒤 VPP가 터널을 수립하고 그 터널 안에서 TLS 핸드셰이크를 가로챕니다. 이미 본 문서의 명시적 HTTP CONNECT 프록시 구현 절에 기본 코드가 있으므로, 여기서는 운영 관점의 팁만 보충합니다.

인증서 피닝·공공 CT·ECH 우회 전략

모던 앱은 다양한 방식으로 MITM에 저항합니다. 모두가 완벽히 막히지는 않지만, 어느 정도 작동하는지 알고 있어야 설계가 가능합니다.

저항 기법동작탐지 방법대응
Public Key Pinning (HPKP, 모바일 앱)미리 알고 있는 공개 키 해시와 비교TLS 핸드셰이크는 성공하나 앱 내부에서 연결 거부해당 앱은 반드시 바이패스
Certificate Transparency (CT) 검증SCT가 없는 인증서 거부브라우저 콘솔에 NET::ERR_CERTIFICATE_TRANSPARENCY_REQUIRED내부 CA에 대해서는 CT 예외 정책 배포
Encrypted ClientHello (ECH)SNI를 암호화해 라우팅 불가ClientHello의 encrypted_client_hello 확장ECHConfig 배포해 선제적 복호, 불가 시 바이패스
DoH/DoTDNS 자체가 암호화되어 IP만 보임포트 853/443 outbound내부 DoH 리졸버 강제, 외부 직접 쿼리 차단
QUIC/HTTP3TCP 기반 가로채기 불가UDP 443 트래픽 급증UDP 443 차단 → HTTP/2 폴백 유도
금지해야 할 패턴: 피닝 앱을 "임시로" 거부 목록에 넣고 잊는 것은 장애로 이어집니다. 앱 업데이트 주기마다 피닝 세트가 바뀔 수 있으므로, 바이패스 도메인/SNI 리스트는 버전 관리하고 주기적으로 리뷰하시기 바랍니다.

DLP/ICAP/ClamAV 연동 — 복호화 후 파이프라인

평문을 얻은 뒤에는 이를 검사 엔진에 보내야 합니다. VPP에서 가장 흔한 3가지 연동 방식은 다음과 같습니다.

  1. ICAP over memif: VPP가 memif로 평문 스트림을 밖으로 빼고, ICAP 서버(예: c-icap + ClamAV)가 REQMOD/RESPMOD를 처리한 뒤 다시 VPP로 되돌립니다. 지연이 가장 낮지만 ICAP 구현 품질에 좌우됩니다.
  2. 소켓 미러링: 플러그인이 복호화된 바이트를 별도 VLS 세션으로 복제해 검사 엔진으로 보냅니다. 검사 엔진은 통과·차단만 결정하고 반환 데이터를 수정하지 못합니다. DLP에 적합합니다.
  3. 커널 사이드카: 평문을 AF_UNIX 또는 abstract namespace 소켓으로 Linux 프로세스에 전달. 전통적인 Suricata·Zeek 등을 재사용할 때 편합니다.
/* 미러링 샘플 — 수신 시 검사 엔진에도 동시에 write */
static void
forward_with_mirror (session_t *client, session_t *upstream, session_t *ids, u8 *buf, u32 n)
{
  (void) session_tx (upstream, buf, n);         /* 실서비스 */
  (void) session_tx (ids,      buf, n);         /* IDS 사이드카 — best effort */
}

미러 경로는 best effort로 취급하시기 바랍니다. IDS 사이드카가 느리다고 실서비스를 막으면 사용자 체감 품질이 떨어집니다. 실질 차단이 필요한 시나리오(DLP 차단)에서는 미러가 아니라 정방향 릴레이 구조로 바꿔, IDS 응답을 기다린 뒤 업스트림으로 흘려보내야 합니다.

감사 로그·법적 컴플라이언스

SSL Inspection은 기술 문제 이전에 거버넌스 문제입니다. 운영팀은 다음 로그 항목을 기본으로 남겨야 합니다.

# VPP elog + syslog 연동 예 — 한 줄로 감사 로그 생성
vpp# set logging class tls level info
vpp# set logging class session level info
vpp# set logging syslog-sender 10.0.0.5

# 운영 로그 조회
vpp# show log | grep -i inspection
syslog 명령 주의: VPP 로깅 프레임워크의 정확한 원격 syslog 설정 CLI는 릴리스마다 set logging syslog-sender·set logging unix-sink 등으로 달라져 왔습니다. 현재 사용 중인 VPP 버전에서 ? logging으로 사용 가능한 하위 명령을 먼저 확인하시기 바랍니다. 이 문서의 표기는 대표적인 형태이며 그대로 복사해 쓰실 수 있는 명령어가 아닙니다.
법적 컴플라이언스 체크리스트: (1) 개인정보보호법 — 복호화 범위와 보존 기간 명시. (2) 전기통신사업법 — 통신비밀 보호 관점에서 내부 전용 네트워크에 한정. (3) 직장 내 사생활 — 사원 동의 또는 취업 규칙 반영. (4) 의료·금융·종교 카테고리 자동 바이패스. (5) 감사 로그는 최소 수정 불가 저장소(WORM)에 보관. 이 다섯 가지를 먼저 확정하지 않은 상태에서는 기능 배포를 미루시기 바랍니다.

SSL Inspection 기능 검증 체크리스트

테스트명령 / 도구기대 결과
루트 CA 신뢰 확인openssl s_client -connect example.com:443 -servername example.com체인 최상위가 내부 CA, 오류 없음
바이패스 대상 확인뱅킹 앱 / 금융 사이트 실행앱 정상 동작, 로그에 bypass 표시
QUIC 폴백 강제curl --http3 https://www.example.com타임아웃 후 h2로 재접속
ECH 대응ECH 활성 브라우저로 접근폴백 또는 명시적 바이패스
성능 부하wrk -c 1000 -d 60s https://test.local/CPU·핸드셰이크/s·SSL fifo 카운터 안정
감사 로그 적재로그 수집 시스템에서 조회사용자 식별·SNI·결정 모두 기록
인증서 피닝 앱대상 앱 테스트자동으로 bypass 또는 명시적 차단 알림