TLS

FD.io VPP 호스트 스택의 TLS 구현을 심화 정리한 문서입니다. VPP 25.02 기준으로 TLS 아키텍처, 인증서·키 관리, 성능 최적화 기법, TLS/QUIC 보안 모범 사례까지를 한 곳에 모았습니다. 본 문서는 호스트 스택 개요에서 TLS 부분만을 별도로 분리한 심화 페이지입니다.

이 페이지에서 다루는 주요 h2 섹션은 다음과 같습니다. 첫째 VPP TLS 아키텍처는 세션 레이어 통합, TLS 엔진(picotls·openssl) 구조, 핸드셰이크 데이터 경로를 정리합니다. 둘째 VPP TLS 설정 및 인증서 관리는 startup.conf 설정, CKPAIR 등록, 인증서·키·CA 운영 절차를 다룹니다. 셋째 VPP TLS 성능 최적화는 비동기 암호화 프레임워크, QAT/cryptodev 오프로드, 세션 재개 튜닝을 설명합니다. 마지막 TLS/QUIC 보안 모범 사례는 cipher 선정, 0-RTT 재전송 방어, 인증서 자동 갱신 등 운영 관점의 보안 체크리스트를 정리합니다.

기초 개념: VCL/VLS 세션 레이어, TCP 호스트 스택 생명주기 같은 선행 개념은 호스트 스택 개요에서 먼저 확인해 주시기 바랍니다. 본 문서는 그 기반 위에서 TLS만을 깊이 다룹니다.

VPP TLS 아키텍처

VPP는 유저스페이스 TLS 종단을 자체 세션 레이어 위에 구현합니다. 커널의 kTLS가 소켓 계층에서 암호화(Encryption)를 처리하는 것과 달리, VPP는 세션 레이어의 전송 프로토콜 추상화(transport_register_protocol())를 활용하여 TLS를 TCP와 동일한 레벨의 전송 프로토콜로 등록합니다. 이를 통해 애플리케이션은 TLS 여부와 무관하게 동일한 세션 API를 사용할 수 있습니다.

TLS 세션 레이어 통합

VPP의 TLS는 세션 레이어(src/vnet/session/)와 TLS 플러그인(src/plugins/tlsopenssl/, src/plugins/tlsmbedtls/) 사이의 추상 계층인 src/vnet/tls/tls.c를 통해 동작합니다:

/* src/vnet/tls/tls.h — TLS 컨텍스트 구조체 */
typedef struct tls_ctx_
{
  union {
    transport_connection_t connection;  /* 세션 레이어 전송 연결 */
  };
  u32 tls_session_handle;              /* 하부 TCP 세션 */
  u32 app_session_handle;              /* 상위 애플리케이션 세션 */
  u32 listener_ctx_index;              /* 리스너 컨텍스트 인덱스 */
  u8  is_passive_close;                /* 수동 종료 플래그 */
  u8  resume;                          /* 비동기 핸드셰이크 재개 */
  u8  app_closed;                      /* 애플리케이션 종료 여부 */
  tls_ctx_type_t tls_type;             /* openssl / mbedtls / picotls */
  u32 ckpair_index;                    /* 인증서/키 쌍 인덱스 */
  /* ... 엔진별 opaque 데이터 ... */
} tls_ctx_t;

/* TLS를 전송 프로토콜로 등록 */
static const transport_proto_vft_t tls_proto = {
  .connect      = tls_connect,
  .close        = tls_disconnect,
  .send_params  = tls_send_params,
  .get_connection = tls_connection_get,
  .get_listener   = tls_listener_get,
  .custom_tx      = tls_custom_tx_callback,
  .format_connection = format_tls_connection,
};

/* 초기화 시 TRANSPORT_PROTO_TLS로 등록 */
transport_register_protocol (TRANSPORT_PROTO_TLS,
                             &tls_proto, FIB_PROTOCOL_IP4, ~0);
transport_register_protocol (TRANSPORT_PROTO_TLS,
                             &tls_proto, FIB_PROTOCOL_IP6, ~0);

이 구조 덕분에 세션 레이어는 TLS를 별도 처리 없이 일반 전송 프로토콜처럼 취급하며, session_open() 호출 시 TRANSPORT_PROTO_TLS를 지정하면 자동으로 TCP 연결 위에 TLS 핸드셰이크가 수행됩니다.

TLS 컨텍스트의 생명 주기는 다음과 같은 상태 전이를 따릅니다:

  1. TLS_CONN_STATE_NONE: tls_ctx_alloc()로 컨텍스트 할당, 엔진 유형 결정
  2. TLS_CONN_STATE_HANDSHAKE: TCP 연결 수립 후 엔진별 핸드셰이크 시작 (ctx_init_server() 또는 ctx_init_client())
  3. TLS_CONN_STATE_ESTABLISHED: 핸드셰이크 완료, 양방향 암호화 데이터 전송 가능
  4. TLS_CONN_STATE_PASSIVE_CLOSE: 원격 측이 close_notify 전송, 잔여 데이터 드레인
  5. TLS_CONN_STATE_CLOSED: 양방향 종료 완료, 컨텍스트 해제

각 상태 전이에서 TLS 추상 계층은 엔진 VFT(Virtual Function Table)를 통해 실제 암호 라이브러리 호출을 위임합니다:

/* src/vnet/tls/tls.h — TLS 엔진 플러그인 인터페이스 */
typedef struct tls_engine_vft_
{
  u32 (*ctx_alloc) (void);                    /* 컨텍스트 할당 */
  void (*ctx_free) (tls_ctx_t *ctx);          /* 컨텍스트 해제 */
  tls_ctx_t *(*ctx_get) (u32 ctx_index);      /* 인덱스로 컨텍스트 조회 */

  /* 핸드셰이크: 서버/클라이언트 초기화 */
  int (*ctx_init_server) (tls_ctx_t *ctx);
  int (*ctx_init_client) (tls_ctx_t *ctx);

  /* 데이터 전송: 애플리케이션 FIFO ↔ TLS 레코드 */
  int (*ctx_write) (tls_ctx_t *ctx,           /* 평문→암호화→TCP */
                    session_t *app_session,
                    transport_send_params_t *sp);
  int (*ctx_read) (tls_ctx_t *ctx,            /* TCP→복호화→평문 */
                   session_t *tls_session);

  /* 핸드셰이크 진행 (WANT_READ/WRITE 시 재호출) */
  int (*ctx_handshake_is_over) (tls_ctx_t *ctx);

  /* 전송 종료 및 리셋 */
  int (*ctx_transport_close) (tls_ctx_t *ctx);
  int (*ctx_app_close) (tls_ctx_t *ctx);
} tls_engine_vft_t;

/* 엔진 등록 (각 플러그인의 init 함수에서 호출) */
void tls_register_engine (const tls_engine_vft_t *vft,
                          tls_engine_type_t type);
플러그인 확장: 새로운 TLS 라이브러리(예: wolfSSL, AWS-LC)를 VPP에 통합하려면 tls_engine_vft_t의 콜백(Callback)들만 구현하면 됩니다. 세션 레이어와 TLS 추상 계층의 코드는 변경할 필요가 없으며, 이것이 VPP TLS의 핵심 설계 원칙입니다.
VPP TLS 세션 레이어 스택 VCL / 내장 앱 (echo_server 등) Session Layer (session_open / session_send) TLS 추상 계층 (tls.c) transport_register_protocol(TRANSPORT_PROTO_TLS) OpenSSL 엔진 mbedTLS 엔진 picotls 엔진 TCP 전송 계층 (TRANSPORT_PROTO_TCP) App Session TLS Transport

TLS 데이터 경로: FIFO 기반 암복호화

VPP TLS의 데이터 경로는 이중 세션(dual-session) 모델을 사용합니다. 하나의 TLS 연결은 내부적으로 두 개의 세션을 유지합니다: 하부 TCP 세션(암호문)과 상위 애플리케이션 세션(평문). 각 세션은 독립적인 FIFO(rx/tx) 쌍을 가지며, TLS 엔진이 양쪽 FIFO 사이에서 암복호화를 수행합니다:

VPP TLS 데이터 경로: 이중 세션 FIFO 모델 송신 경로 (App → Network) App 세션 session_send(평문) App TX FIFO 평문 데이터 TLS 엔진 SSL_write() TCP TX FIFO TLS 레코드 TCP 세션 tcp_output() 수신 경로 (Network → App) TCP 세션 tcp_input() TCP RX FIFO TLS 레코드 TLS 엔진 SSL_read() App RX FIFO 평문 데이터 App 세션 session_read(평문) custom_tx 콜백: TLS 송신 트리거 메커니즘 세션 레이어가 App TX FIFO에 데이터가 있음을 감지 → tls_custom_tx_callback() 호출 → ctx_write() VFT 콜백 → SSL_write()로 App TX FIFO 소비, TCP TX FIFO에 TLS 레코드 생산 → 메인 루프의 다음 벡터 처리에서 TCP가 TLS 레코드를 네트워크로 전송 OpenSSL 엔진 내부: BIO 메모리 체인 SSL* 객체 ↔ BIO_s_mem (읽기) ↔ TLS 레코드 레이어 ↔ BIO_s_mem (쓰기) VPP는 커널 소켓 BIO(BIO_s_socket) 대신 메모리 BIO를 사용하여 FIFO와 직접 연결 → syscall 제로

제로-카피(zero-copy)에 가까운 데이터 경로가 VPP TLS의 성능 핵심입니다. 커널 kTLS가 sendfile()splice()를 통해 커널-유저 경계를 최소화하려는 것과 달리, VPP는 애초에 전체 경로가 유저스페이스이므로 경계 자체가 존재하지 않습니다.

VPP TLS vs 커널 kTLS 아키텍처 비교

VPP TLS와 커널 kTLS는 근본적으로 다른 설계 철학을 따릅니다. 두 접근 방식의 구조적 차이를 이해하면 적합한 사용 시나리오를 판단할 수 있습니다:

VPP TLS vs 커널 kTLS 아키텍처 비교 VPP TLS (유저스페이스) 유저스페이스 (단일 프로세스) VCL / 애플리케이션 세션 레이어 + TLS 엔진 TCP 스택 (VPP 내장) DPDK / AF_XDP (NIC 직접) syscall: 0 컨텍스트 스위칭: 0 복사: FIFO 내 제로카피 핸드셰이크: 유저스페이스 내 인증서 교체: 무중단 kTLS (커널) 유저스페이스 nginx / 애플리케이션 OpenSSL (핸드셰이크만) syscall 경계 (send/recv/setsockopt) 커널 공간 kTLS 모듈 (데이터 암복호화) TCP 스택 (커널) NIC 드라이버 syscall: send/recv마다 컨텍스트 스위칭: 매 I/O 복사: sendfile() 제로카피 가능 핵심 차이: 핸드셰이크 위치 VPP: 핸드셰이크 + 데이터 모두 유저스페이스 — 높은 CPS (연결/초), 인증서 동적 관리 kTLS: 핸드셰이크는 유저스페이스, 데이터만 커널 — sendfile 최적화, 기존 앱 호환성
비교 항목VPP TLS커널 kTLS
핸드셰이크유저스페이스 (VPP 내)유저스페이스 (OpenSSL)
데이터 암복호화유저스페이스 (VPP 내)커널 (tls_sw/tls_device)
TCP 스택유저스페이스 (VPP 내장)커널 TCP
NIC 접근DPDK/AF_XDP (직접)커널 드라이버
syscall 횟수0 (완전 유저스페이스)send/recv 호출마다
sendfile 지원미지원 (FIFO 기반)지원 (제로카피 가능)
NIC HW TLSDPDK Cryptodev 경유tls_device 네이티브
기존 앱 호환LD_PRELOAD 또는 VCL 전환소켓(Socket) 옵션만 추가
인증서 교체무중단 (CLI/API)프로세스(Process) 재시작(Reboot) 필요
적합 워크로드고 CPS, 대규모 동시 연결고 처리량(Throughput), 파일 서빙

TLS 플러그인 비교: OpenSSL vs mbedTLS vs picotls

VPP는 세 가지 TLS 엔진 플러그인을 제공합니다. 각각 다른 유스케이스에 최적화되어 있으며, startup.conf에서 선택할 수 있습니다:

항목tlsopenssltlsmbedtlspicotls
라이브러리OpenSSL / BoringSSLMbed TLS (ARM)picotls (H2O)
TLS 1.2완전 지원완전 지원미지원
TLS 1.3완전 지원3.x에서 지원완전 지원 (전용)
비동기 암호화지원 (ENGINE API)미지원미지원
HW 오프로드QAT, Cryptodev미지원미지원
Cipher Suite전체제한적TLS 1.3 전용
메모리 사용높음 (~50KB/ctx)낮음 (~10KB/ctx)매우 낮음 (~5KB/ctx)
성능 (SW)높음중간매우 높음 (1.3 전용)
라이선스Apache 2.0Apache 2.0MIT
적합 시나리오범용, HW 가속임베디드, IoTTLS 1.3 전용 고성능
엔진 선택 가이드: 대부분의 프로덕션 환경에서는 tlsopenssl이 권장됩니다. TLS 1.3만 필요하고 최대 성능이 목표라면 picotls가 적합하며, 메모리가 극히 제한된 임베디드 환경에서는 tlsmbedtls를 고려합니다.
⚠️ picotls 선택 시 주의: tlspicotlsTLS 1.3 전용이며 TLS 1.2 폴백 경로가 없습니다. 레거시 클라이언트(구형 브라우저·IoT 펌웨어(Firmware)·임베디드 장치)와의 호환성이 조금이라도 요구되면 즉시 핸드셰이크가 실패하므로, 이 경우 tlsopenssl을 선택해야 합니다. 또한 VPP 릴리스 노트상 tlspicotls 플러그인 자체가 experimental로 분류되어 있어 프로덕션 전 단독 부하·회귀 테스트가 필수입니다.

각 엔진은 VPP 소스 트리의 독립적인 플러그인으로 존재합니다:

src/plugins/
├── tlsopenssl/          # OpenSSL/BoringSSL 엔진
│   ├── tls_openssl.c    # VFT 구현: ctx_init, ctx_write, ctx_read
│   ├── tls_async.c      # 비동기 ENGINE 연동 (QAT 등)
│   └── tls_bio.c        # BIO_s_mem 기반 FIFO 브릿지
├── tlsmbedtls/          # Mbed TLS 엔진
│   └── tls_mbedtls.c    # VFT 구현, mbedtls_ssl_* 래핑
├── tlspicotls/          # picotls 엔진 (TLS 1.3 전용)
│   └── tls_picotls.c    # VFT 구현, ptls_* 래핑
└── quic/                # QUIC 플러그인 (quicly 기반)
    └── quic.c           # TLS 1.3은 quicly 내부에서 처리
TLS 엔진 플러그인 내부 구조 (OpenSSL 기준) tls.c — tls_engine_vft_t 디스패치 tlsopenssl 플러그인 SSL_CTX (리스너당 1개) SSL* (연결당 1개) BIO_read BIO_write TCP FIFO (rx/tx) 비동기: ENGINE API (QAT) ~50KB/컨텍스트 tlsmbedtls 플러그인 mbedtls_ssl_config mbedtls_ssl_context mbedtls_ssl_send/recv 콜백 TCP FIFO (rx/tx) 동기 전용 ~10KB/컨텍스트 tlspicotls 플러그인 ptls_context_t (TLS 1.3 only) ptls_t* (연결당) ptls_send/receive 직접 호출 TCP FIFO (rx/tx) 동기 전용, 최소 오버헤드 ~5KB/컨텍스트

TLS 핸드셰이크 상세 타임라인

VPP TLS를 깊게 이해하려면, TCP 연결 수립과 TLS 핸드셰이크가 언제 겹치고 언제 분리되는지를 정확히 봐야 합니다. TCP가 먼저 바이트 스트림을 만들고, 그 위에서 TLS 엔진이 SSL_do_handshake() 또는 동등한 엔진 콜백을 반복 호출하며 상태를 전진시킵니다. 비동기 엔진을 쓰면 이 중간 단계가 메인 루프 밖으로 일부 밀려납니다. 운영 중 핸드셰이크 실패 진단은 프록시 — TLS 디버깅의 CLI 워크스루를, 호스트 스택 관점의 CPS 저하 분석은 호스트 스택 — TLS CPS가 낮음을 함께 참고합니다.

VPP TLS 핸드셰이크 타임라인 VCL / App TLS 추상 계층 TCP + 네트워크 VPPCOM_PROTO_TLS 생성 tls_connect() TLS 컨텍스트 할당 TCP connect 시작 SYN, SYN/ACK, ACK ESTABLISHED 이제 TLS 바이트 교환 가능 ctx_init_client/server SSL 객체와 BIO 연결 CONNECTED 대기 아직 평문 송수신 금지 핸드셰이크 왕복 ClientHello → ServerHello / EncryptedExtensions / Certificate / Finished → Client Finished 각 왕복마다 TLS 엔진은 SSL_do_handshake()를 호출하고, 필요한 암호문을 TCP FIFO에 밀어 넣습니다 vppcom_session_read/write() TLS_CONN_STATE_ESTABLISHED 평문 FIFO 사용 시작 암호문 송수신 계속
  1. TCP가 먼저 연결을 만듭니다 — TLS는 TCP 바이트 스트림이 준비되기 전에는 아무 것도 시작할 수 없습니다.
  2. 엔진 초기화가 이어집니다 — OpenSSL 기준으로는 BIO를 세션 핸들에 연결하고, SSL_do_handshake()를 돌리기 시작합니다.
  3. 핸드셰이크는 반복 호출 모델입니다 — 한 번의 함수 호출로 끝나는 것이 아니라, 새 TLS 레코드가 도착할 때마다 읽고 쓰기를 반복하며 상태를 전진시킵니다.
  4. 비동기 엔진은 WANT_ASYNC를 반환할 수 있습니다 — 이 경우 핸드셰이크 자체는 진행 중이지만, 암호 연산 완료 통지를 기다리며 메인 루프는 다른 작업을 계속할 수 있습니다.
  5. ESTABLISHED 이후에만 평문 API가 자연스러워집니다 — 그 전 단계에서 애플리케이션은 사실상 인증서 교환과 키 스케줄이 끝나기를 기다리는 상태입니다.

TLS 레코드와 TCP 세그먼트의 책임 분리

TCP와 TLS를 함께 다룰 때 가장 흔한 오해는 "TLS 레코드 하나가 TCP 패킷 하나"라고 생각하는 것입니다. 실제로는 전혀 그렇지 않습니다. TLS는 바이트 스트림 위에 논리적 레코드 경계를 만들고, TCP는 그 바이트들을 어떻게 쪼개고 언제 재전송할지 결정합니다.

현상책임 계층VPP에서 실제로 일어나는 일
재전송TCP손실된 세그먼트는 TCP가 다시 보냅니다. TLS는 같은 레코드를 재암호화하지 않고, 이미 생성된 바이트 스트림이 재전송됩니다.
순서 보장(Ordering)TCPout-of-order 세그먼트는 TCP 재조립 후에야 TLS 엔진으로 올라갑니다. TLS는 완성된 바이트 흐름만 읽습니다.
레코드 경계TLS한 TLS 레코드는 여러 TCP 세그먼트에 걸칠 수 있고, 여러 TLS 레코드가 하나의 큰 세그먼트 묶음으로 나갈 수도 있습니다.
혼잡 제어(Congestion Control)와 pacingTCPTLS가 평문을 빠르게 생산해도 실제 네트워크 전송 속도는 congestion window와 ACK 도착 속도가 정합니다.
close_notifyTLS애플리케이션 계층의 정상 종료 의사를 알리는 제어 메시지입니다. TCP FIN과는 별개의 의미를 가집니다.
FIN/RSTTCP전송 계층 종료입니다. TLS 관점에서는 close_notify 없이 FIN만 오면 truncation 가능성까지 의심해야 합니다.
핵심 정리: TCP 튜닝 문제인지 TLS 문제인지 헷갈릴 때는 "이 증상이 바이트 전달의 신뢰성 문제인가, 아니면 인증·암복호화·레코드 문제인가"를 먼저 분리하세요. 재전송, RTT, cwnd, 윈도우 축소는 TCP 영역이고, 인증서 오류, ALPN 불일치, close_notify, 세션 재개 실패는 TLS 영역입니다.

TLS 엔진 플러그인 내부 구현 분석

VPP의 TLS 엔진 플러그인은 각각 고유한 I/O 패턴으로 암복호화를 수행합니다. 여기서는 OpenSSL, mbedTLS, picotls 세 엔진의 내부 데이터 경로와 구현 차이를 상세히 분석합니다.

TLS 엔진 3종 — 내부 I/O 경로와 ctx_write() 구조 비교 공통 입력 App TX FIFO (평문) svm_fifo_t ↓ ctx_write() 호출 app_tx_data 평문 조각 ↓ 암호화 TCP TX FIFO TLS 레코드 OpenSSL 엔진 BIO 추상화 + SSL_write svm_fifo_dequeue → buf SSL_write(ssl, buf, n) BIO_write 체인 (memory BIO) → 암호화된 레코드를 내부 membuf에 축적 BIO_read(wbio) → tls_buf svm_fifo_enqueue(tcp TX) 복사: 평문 1회, 암호문 1회 청크 단위 루프 처리 AES-NI · 비동기 ENGINE 지원 성숙도 ◎ · 최대 호환성 mbedTLS 엔진 콜백 기반 send/recv svm_fifo_dequeue → buf mbedtls_ssl_write f_send 콜백 직접 호출 → 내부 write 버퍼 생략 → tcp TX FIFO에 즉시 enqueue mbedtls_aes_crypt_* 실행 복사: 평문 1회만 중간 BIO 버퍼 없음 경량 · IoT/임베디드 친화 AES-NI 지원 (하드웨어 감지) 성숙도 ○ picotls 엔진 TLS 1.3 only · 평면 버퍼 svm_fifo_peek → ptr ptls_buffer_t 연결 ptls_send(ptls, buf, in, n) → 단일 평면 버퍼에 직접 레코드 기록 svm_fifo_enqueue_nocopy 복사: 이상적으로 0회 (zero-copy 지향) TLS 1.3 전용 · 최신 기능 QUIC와 코드 공유 (picoquic) 성숙도 △ · experimental 공통 입력(App TX FIFO) → 각 엔진이 자기 방식으로 암호화 → 공통 출력(TCP TX FIFO). 복사 횟수가 엔진 선택의 핵심 지표입니다.
엔진 선택 가이드: openssl은 기능 완전성과 호환성이 가장 높아 기본 선택지이며, 비동기 ENGINE을 통해 QAT 연동이 표준화되어 있습니다. mbedtls는 메모리 사용이 적어 컨테이너(Container)·임베디드에 유리하지만 비동기 오프로드가 약합니다. picotls는 TLS 1.3 전용이면서 zero-copy 경로를 지향해 최대 처리량이 가장 높지만, 2026년 기준 experimental이므로 프로덕션 도입 전 호환성 검증이 필요합니다.
읽기 팁: 아래 세 소절의 코드를 비교할 때 svm_fifo에서 데이터를 꺼내는 방식(dequeue vs peek), 엔진이 중간 버퍼(Buffer)를 두는지 여부(BIO vs 콜백 vs 평면 버퍼), TCP TX FIFO에 넣는 방식(enqueue vs enqueue_nocopy) 세 축만 머릿속에 두면 구현 차이가 명확하게 정리됩니다.

OpenSSL 엔진 ctx_write() 분석

tls_openssl_ctx_write() 함수는 애플리케이션이 보낸 평문 데이터를 TLS 레코드로 암호화하여 TCP 스택에 전달하는 핵심 경로입니다. OpenSSL의 BIO(Basic I/O) 추상화 계층을 활용하여 메모리 기반 I/O를 수행합니다:

  1. App TX FIFO에서 평문 dequeue: svm_fifo_peek()로 애플리케이션이 기록한 평문 데이터를 읽어옵니다
  2. BIO_write() → SSL_write(): 평문 데이터를 OpenSSL의 내부 BIO 버퍼에 기록하면, SSL_write()가 TLS 레코드를 생성하고 암호화를 수행합니다
  3. BIO_read()로 암호문 추출: 암호화된 TLS 레코드를 출력 BIO에서 읽어옵니다
  4. TCP TX FIFO에 암호문 enqueue: svm_fifo_enqueue()로 암호문을 TCP 전송 큐에 삽입합니다
/* tls_openssl_ctx_write() — 평문 → TLS 레코드 암호화 경로 */
static int
tls_openssl_ctx_write (tls_ctx_t *ctx, session_t *app_session,
                       transport_send_params_t *sp)
{
  openssl_ctx_t *oc = (openssl_ctx_t *) ctx;
  svm_fifo_t *f = app_session->tx_fifo;
  u32 deq_max, wrote = 0;
  int rv;

  /* 1단계: App TX FIFO에서 전송 가능한 최대 바이트 확인 */
  deq_max = svm_fifo_max_dequeue_cons (f);
  if (!deq_max)
    return 0;

  /* 2단계: FIFO에서 평문 데이터를 읽어 SSL_write()로 암호화 */
  while (deq_max > 0)
    {
      u32 len = clib_min (deq_max, TLS_CHUNK_SIZE);
      svm_fifo_peek (f, wrote, len, oc->write_buf);

      /* SSL_write()가 내부적으로 BIO를 통해 TLS 레코드 생성 */
      rv = SSL_write (oc->ssl, oc->write_buf, len);
      if (rv <= 0)
        {
          int err = SSL_get_error (oc->ssl, rv);
          if (err == SSL_ERROR_WANT_WRITE)
            break;
          return -1;
        }

      wrote += rv;
      deq_max -= rv;
    }

  /* 3단계: 출력 BIO에서 암호문을 읽어 TCP TX FIFO에 enqueue */
  openssl_write_from_bio_to_fifo (oc->ssl, app_session);

  /* 4단계: 소비된 바이트만큼 App TX FIFO에서 제거 */
  if (wrote)
    svm_fifo_dequeue_drop (f, wrote);

  return wrote;
}

openssl_write_from_bio_to_fifo() 내부에서는 BIO_ctrl_pending()으로 출력 BIO에 대기 중인 암호문 크기를 확인한 후, BIO_read()로 추출하여 TCP 세션의 TX FIFO에 기록합니다. 이 BIO 기반 간접 경로가 OpenSSL 엔진의 특징이며, 동시에 성능 오버헤드(Overhead)의 원인이기도 합니다.

OpenSSL 엔진 ctx_read() 분석

tls_openssl_ctx_read()는 TCP에서 수신한 암호문을 복호화(Decryption)하여 애플리케이션에 전달하는 수신 경로입니다. 쓰기 경로의 역방향으로 BIO를 활용합니다:

  1. TCP RX FIFO에서 암호문 dequeue: TCP 스택이 수신한 TLS 레코드를 읽어옵니다
  2. BIO_write()로 입력 BIO에 주입: 암호문을 OpenSSL의 입력 BIO 버퍼에 기록합니다
  3. SSL_read()로 복호화: OpenSSL이 TLS 레코드를 파싱하고 복호화하여 평문을 반환합니다
  4. App RX FIFO에 평문 enqueue: 복호화된 평문을 애플리케이션 수신 큐에 삽입합니다
/* tls_openssl_ctx_read() — TLS 레코드 복호화 → 평문 경로 */
static int
tls_openssl_ctx_read (tls_ctx_t *ctx, session_t *tls_session)
{
  openssl_ctx_t *oc = (openssl_ctx_t *) ctx;
  session_t *app_session;
  svm_fifo_t *app_rx_fifo;
  int read = 0, rv;

  /* 1단계: TCP RX FIFO → 입력 BIO로 암호문 전달 */
  openssl_read_from_fifo_to_bio (oc->ssl, tls_session);

  app_session = session_get_from_handle (ctx->app_session_handle);
  app_rx_fifo = app_session->rx_fifo;

  /* 2단계: SSL_read()로 복호화된 평문 추출 */
  while (1)
    {
      rv = SSL_read (oc->ssl, oc->read_buf, TLS_CHUNK_SIZE);
      if (rv <= 0)
        {
          int err = SSL_get_error (oc->ssl, rv);
          if (err == SSL_ERROR_WANT_READ)
            break;  /* 추가 데이터 대기 필요 */
          if (err == SSL_ERROR_ZERO_RETURN)
            break;  /* TLS close_notify 수신 */
          return -1;
        }

      /* 3단계: 복호화된 평문을 App RX FIFO에 enqueue */
      rv = svm_fifo_enqueue (app_rx_fifo, rv, oc->read_buf);
      if (rv < 0)
        break;

      read += rv;
    }

  return read;
}

SSL_ERROR_WANT_READ는 OpenSSL이 완전한 TLS 레코드를 구성하기에 데이터가 부족할 때 반환됩니다. 이 경우 VPP는 이벤트 루프(Event Loop)로 제어를 반환하고, TCP로부터 추가 데이터가 도착하면 다시 ctx_read()를 호출합니다. SSL_ERROR_ZERO_RETURN은 상대방이 TLS close_notify를 전송했음을 의미하며, 정상적인 연결 종료 절차를 시작합니다.

mbedTLS 엔진 차이점

mbedTLS 엔진은 OpenSSL의 BIO 추상화 대신 콜백 기반 I/O 모델을 사용합니다. mbedtls_ssl_set_bio()로 등록한 커스텀 send/recv 콜백이 FIFO에 직접 접근하므로, 별도의 BIO 계층 오버헤드가 발생하지 않습니다:

/* mbedTLS 엔진 — 콜백 기반 I/O 설정 */
static void
mbedtls_ctx_init (tls_ctx_t *ctx)
{
  mbedtls_ctx_t *mc = (mbedtls_ctx_t *) ctx;

  mbedtls_ssl_init (&mc->ssl);
  mbedtls_ssl_setup (&mc->ssl, &mc->conf);

  /* BIO 대신 커스텀 콜백으로 FIFO 직접 연결 */
  mbedtls_ssl_set_bio (&mc->ssl, ctx,
                       tls_mbedtls_send_cb,   /* 송신 콜백 */
                       tls_mbedtls_recv_cb,   /* 수신 콜백 */
                       NULL);                 /* 타임아웃 콜백 없음 */
}

/* 송신 콜백: 암호문을 TCP TX FIFO에 직접 기록 */
static int
tls_mbedtls_send_cb (void *ctx_ptr, const unsigned char *buf,
                     size_t len)
{
  tls_ctx_t *ctx = (tls_ctx_t *) ctx_ptr;
  session_t *tls_session;
  int rv;

  tls_session = session_get_from_handle (ctx->tls_session_handle);
  rv = svm_fifo_enqueue (tls_session->tx_fifo, len, buf);
  if (rv < 0)
    return MBEDTLS_ERR_SSL_WANT_WRITE;

  return rv;
}

/* 수신 콜백: TCP RX FIFO에서 암호문을 직접 읽기 */
static int
tls_mbedtls_recv_cb (void *ctx_ptr, unsigned char *buf,
                     size_t len)
{
  tls_ctx_t *ctx = (tls_ctx_t *) ctx_ptr;
  session_t *tls_session;
  int rv;

  tls_session = session_get_from_handle (ctx->tls_session_handle);
  rv = svm_fifo_dequeue (tls_session->rx_fifo, len, buf);
  if (rv < 0)
    return MBEDTLS_ERR_SSL_WANT_READ;

  return rv;
}

콜백 내부에서 svm_fifo_enqueue() / svm_fifo_dequeue()를 직접 호출하기 때문에, OpenSSL처럼 BIO 버퍼를 거치는 중간 복사가 제거됩니다. 다만 mbedTLS 자체의 암호화 연산 속도가 OpenSSL 대비 느리므로, 전체 처리량에서는 큰 이점을 얻기 어렵습니다. mbedTLS 엔진은 주로 메모리 제약이 있는 임베디드 환경이나 라이선스 요건(Apache 2.0)이 중요한 경우에 선택됩니다.

picotls 엔진 최적화

picotls 엔진은 TLS 1.3 전용 구현체로, TLS 1.0~1.2 레거시 코드가 전혀 없어 코드베이스가 극히 작고 성능이 우수합니다. VPP의 picotls 엔진이 최고 성능을 달성하는 핵심 요인은 다음과 같습니다:

  • TLS 1.3 전용: 이전 버전 호환 코드가 없으므로 분기 예측(Branch Prediction) 실패가 감소하고, 핸드셰이크 경로가 단순합니다
  • 직접 버퍼 관리: ptls_buffer_t 구조체(Struct)를 통해 VPP FIFO와 직접 연동하며, BIO나 콜백 계층 없이 버퍼 포인터를 직접 전달합니다
  • Zero-copy 최적화: ptls_send()ptls_receive()가 입출력(I/O) 버퍼를 직접 참조하므로 불필요한 메모리 복사가 최소화됩니다
  • 경량 컨텍스트: 세션당 메모리 사용량이 약 5KB 수준으로, OpenSSL의 ~34KB 대비 매우 작습니다
/* picotls 엔진 — 직접 버퍼 기반 암호화 경로 */
static int
picotls_ctx_write (tls_ctx_t *ctx, session_t *app_session,
                    transport_send_params_t *sp)
{
  picotls_ctx_t *ptc = (picotls_ctx_t *) ctx;
  svm_fifo_t *f = app_session->tx_fifo;
  ptls_buffer_t sendbuf;
  u32 deq_max, wrote = 0;
  int rv;

  deq_max = svm_fifo_max_dequeue_cons (f);
  if (!deq_max)
    return 0;

  /* ptls_buffer를 스택에 초기화 — 힙 할당 회피 */
  ptls_buffer_init (&sendbuf, "", 0);

  while (deq_max > 0)
    {
      u32 len = clib_min (deq_max, TLS_CHUNK_SIZE);
      svm_fifo_peek (f, wrote, len, ptc->write_buf);

      /* ptls_send()가 TLS 1.3 레코드를 직접 생성 */
      rv = ptls_send (ptc->tls, &sendbuf,
                     ptc->write_buf, len);
      if (rv != 0)
        break;

      wrote += len;
      deq_max -= len;
    }

  /* sendbuf에 축적된 암호문을 TCP TX FIFO에 일괄 전송 */
  if (sendbuf.off > 0)
    {
      session_t *tls_session;
      tls_session = session_get_from_handle (ctx->tls_session_handle);
      svm_fifo_enqueue (tls_session->tx_fifo,
                       sendbuf.off, sendbuf.base);
    }

  if (wrote)
    svm_fifo_dequeue_drop (f, wrote);

  ptls_buffer_dispose (&sendbuf);
  return wrote;
}

/* picotls 엔진 — 직접 버퍼 기반 복호화 경로 */
static int
picotls_ctx_read (tls_ctx_t *ctx, session_t *tls_session)
{
  picotls_ctx_t *ptc = (picotls_ctx_t *) ctx;
  ptls_buffer_t decryptbuf;
  session_t *app_session;
  u32 deq_max;
  int rv;

  deq_max = svm_fifo_max_dequeue_cons (tls_session->rx_fifo);
  if (!deq_max)
    return 0;

  ptls_buffer_init (&decryptbuf, "", 0);

  /* TCP RX FIFO에서 암호문을 읽어 직접 복호화 */
  svm_fifo_dequeue (tls_session->rx_fifo, deq_max,
                    ptc->read_buf);

  size_t consumed = deq_max;
  rv = ptls_receive (ptc->tls, &decryptbuf,
                    ptc->read_buf, &consumed);

  if (rv == 0 && decryptbuf.off > 0)
    {
      app_session = session_get_from_handle (
        ctx->app_session_handle);
      svm_fifo_enqueue (app_session->rx_fifo,
                       decryptbuf.off, decryptbuf.base);
    }

  ptls_buffer_dispose (&decryptbuf);
  return (rv == 0) ? decryptbuf.off : -1;
}

ptls_send()는 입력 평문에서 TLS 1.3 레코드를 직접 생성하여 ptls_buffer_t에 축적합니다. ptls_receive()consumed 포인터를 통해 실제 처리된 바이트 수를 반환하므로, 부분 레코드 도착 시에도 정확한 FIFO 관리가 가능합니다.

TLS 엔진 내부 API 비교

항목 OpenSSL mbedTLS picotls
I/O 모델 BIO (메모리 BIO 쌍) 콜백 (send/recv) 직접 버퍼 (ptls_buffer_t)
암호화 호출 SSL_write() mbedtls_ssl_write() ptls_send()
복호화 호출 SSL_read() mbedtls_ssl_read() ptls_receive()
FIFO 연동 방식 BIO_write → BIO_read 간접 콜백 내 FIFO 직접 접근 버퍼 포인터 직접 전달
중간 복사 횟수 2회 (BIO 입출력) 1회 (콜백 버퍼) 0~1회 (zero-copy 가능)
TLS 버전 지원 1.0 / 1.1 / 1.2 / 1.3 1.2 / 1.3 1.3 전용
세션당 메모리 ~34KB ~10KB ~5KB
핸드셰이크 모드 비동기 (WANT_READ/WRITE) 비동기 (WANT_READ/WRITE) 동기 (단일 RTT)
라이선스 Apache 2.0 Apache 2.0 MIT
비동기 HW 가속 ENGINE API 지원 ALT 함수 교체 미지원

TLS 핸드셰이크 유저스페이스 처리 흐름

VPP의 TLS 핸드셰이크는 전적으로 유저스페이스에서 수행됩니다. 커널 소켓을 거치지 않으므로 syscall 오버헤드가 없으며, VPP의 이벤트 루프(Event Loop)와 통합되어 비차단(Non-blocking) 방식으로 처리됩니다:

  1. session_open(TRANSPORT_PROTO_TLS): 세션 레이어가 TLS 연결 요청
  2. tls_connect(): TLS 추상 계층이 먼저 TCP 연결 수립
  3. TCP 3-way 핸드셰이크: VPP TCP 스택에서 수행
  4. tls_session_connected_cb(): TCP 연결 완료 콜백
  5. tls_ctx_handshake_*(): 엔진별 TLS 핸드셰이크 시작
  6. 비동기 핸드셰이크: SSL_do_handshake()SSL_ERROR_WANT_READ/WRITE 반환 시 이벤트 대기
  7. 핸드셰이크 완료: 애플리케이션에 연결 완료 통지
VPP TLS 1.3 핸드셰이크 시퀀스 VCL App Session Layer TLS Engine TCP / 원격 session_open(TLS) TCP SYN → ← TCP SYN+ACK → ACK tls_ctx_handshake() ClientHello + key_share ServerHello + EE + Cert + Finished Client Finished 1-RTT 핸드셰이크 완료 session_connected_cb() 암호화된 애플리케이션 데이터 송수신 TLS 1.3 0-RTT (Early Data) PSK 기반 재연결 시 ClientHello와 함께 애플리케이션 데이터 전송 가능 (replay 위험 주의)

TLS 연결 수립 소스 코드 분석

VPP TLS 연결 수립은 클라이언트 측과 서버 측에서 서로 다른 경로를 거칩니다. 전체 흐름은 세션 레이어 → 전송(TCP) 연결 → TLS 핸드셰이크 → 암호화 엔진 초기화 순서로 진행됩니다.

클라이언트 측 연결 수립 흐름

클라이언트의 TLS 연결은 tls_connect()에서 시작하여, TCP 연결이 완료된 후 비로소 TLS 핸드셰이크가 시작됩니다. 이 2단계 구조가 VPP TLS의 핵심 설계입니다:

/* 클라이언트 TLS 연결 수립 전체 흐름 */
/* 1단계: tls_connect() — TLS 컨텍스트 생성 + TCP 연결 시작 */
static int
tls_connect (transport_endpoint_cfg_t *tep)
{
  tls_ctx_t *ctx;
  u32 ctx_handle;

  /* TLS 컨텍스트 할당 및 초기화 */
  ctx = tls_ctx_alloc (tep->transport_proto);
  ctx_handle = tls_ctx_handle (ctx);

  /* 인증서, SNI(Server Name), ALPN 등 설정 복사 */
  ctx->tls_type = TLS_CLIENT;
  clib_memcpy (&ctx->srv_hostname, tep->hostname, hostname_len);

  /* 하부 TCP 전송 연결 시작 — 아직 TLS 아님 */
  session_open (TRANSPORT_PROTO_TCP, tep, ctx->app_session_handle);
  /* → TCP SYN 전송, 응답 대기 */
  return 0;
}

/* 2단계: TCP 연결 완료 콜백 → TLS 핸드셰이크 시작 */
static int
tls_session_connected_cb (u32 tls_app_index,
                           u32 ctx_handle,
                           session_t *tcp_session,
                           session_error_t err)
{
  tls_ctx_t *ctx = tls_ctx_get (ctx_handle);

  if (err)
    {
      /* TCP 연결 실패 → 상위 레이어에 오류 전파 */
      tls_notify_app_connected (ctx, SESSION_E_REFUSED);
      return -1;
    }

  /* TCP 세션 핸들 저장 */
  ctx->tls_session_handle = session_handle (tcp_session);

  /* 암호화 엔진의 클라이언트 초기화 호출 */
  return tls_ctx_init_client (ctx);
  /* → openssl: SSL_new() + SSL_set_connect_state() + SSL_do_handshake() */
  /* → picotls: ptls_new() + ptls_handshake() */
}
코드 분석: 2단계 연결 수립

tls_connect()는 즉시 TLS 핸드셰이크를 시작하지 않습니다. 먼저 session_open(TRANSPORT_PROTO_TCP, ...)으로 TCP 3-way 핸드셰이크를 시작하고, TCP 연결이 완료되면 VPP 세션 레이어가 tls_session_connected_cb() 콜백을 호출합니다. 이 시점에서 비로소 tls_ctx_init_client()를 통해 SSL 객체가 생성되고 SSL_do_handshake()가 호출됩니다. 이러한 비동기 2단계 설계 덕분에 TCP 연결 대기 중에도 VPP 메인 루프가 다른 패킷(Packet)을 계속 처리할 수 있습니다.

서버 측 연결 수락 흐름

서버 측에서는 tls_start_listen()으로 TLS 리스너를 등록한 후, 클라이언트로부터 TCP 연결이 들어오면 자동으로 TLS 핸드셰이크가 시작됩니다:

/* 서버 TLS 리스너 등록 */
static u32
tls_start_listen (u32 tls_listener_index,
                   transport_endpoint_cfg_t *tep)
{
  tls_ctx_t *lctx;
  u32 tcp_listener;

  /* TLS 리스너 컨텍스트 생성 */
  lctx = tls_listener_ctx_alloc ();
  lctx->tls_type = TLS_SERVER;

  /* 인증서/키 파일 경로 설정 */
  lctx->tls_cert_file = tep->cert_file;
  lctx->tls_key_file = tep->key_file;

  /* 하부 TCP 리스너 등록 */
  tcp_listener = session_start_listen (TRANSPORT_PROTO_TCP, tep);
  return lctx->listener_index;
}

/* TCP 연결 수락 → TLS 핸드셰이크 시작 */
static int
tls_session_accept_callback (session_t *tcp_session)
{
  tls_ctx_t *lctx, *ctx;

  /* 리스너 컨텍스트에서 인증서 정보 복사 */
  lctx = tls_listener_ctx_get (tcp_session->listener_handle);
  ctx = tls_ctx_alloc (lctx->tls_type);

  ctx->tls_session_handle = session_handle (tcp_session);
  ctx->listener_ctx_index = lctx->listener_index;

  /* 서버 암호화 엔진 초기화 → SSL_set_accept_state() */
  return tls_ctx_init_server (ctx);
  /* → ClientHello 수신 대기 → SSL_do_handshake() 진행 */
}
코드 분석: 서버 측 연결 수락

tls_start_listen()은 TCP 리스너를 내부적으로 등록하고, 새로운 TCP 연결이 들어올 때마다 tls_session_accept_callback()이 호출됩니다. 이 콜백에서 tls_ctx_init_server()를 호출하여 SSL 컨텍스트를 생성하고, SSL_set_accept_state()로 서버 모드를 설정합니다. 이후 클라이언트의 ClientHello가 TCP RX FIFO에 도착하면 tls_handshake_rx()에서 이를 읽어 OpenSSL에 전달하여 핸드셰이크를 진행합니다.

핸드셰이크 오류 경로 분석

TLS 핸드셰이크가 실패하는 주요 원인과 VPP의 내부 처리 경로입니다:

실패 원인OpenSSL 오류 코드VPP 내부 처리디버깅(Debugging) 방법
인증서 검증 실패X509_V_ERR_*tls_ctx_init_client()에서 verify callback 실패 → 세션 종료show errors에 tls-handshake-fail 카운터 확인
cipher 불일치SSL_R_NO_SHARED_CIPHERSSL_do_handshake() 반환값 < 0 → alert 전송 → 세션 정리trace add tls-input 100으로 핸드셰이크 메시지 확인
인증서 만료X509_V_ERR_CERT_HAS_EXPIREDverify callback에서 거부 → connected_cb에 오류 전파openssl x509 -noout -enddate로 PEM 파일 유효기간 직접 확인
메모리 부족SSL_R_MALLOC_FAILURESSL_new() NULL 반환 → ctx_init 실패 → 연결 거부show memory verbose로 세그먼트 사용량 확인
핸드셰이크 타임아웃N/A세션 idle-timeout 만료 → session_cleanup() 호출show session verbose에서 tls 세션의 HANDSHAKE 상태 체류 시간
/* 핸드셰이크 진행 및 오류 처리 (openssl_ctx_handshake_rx) */
static int
openssl_ctx_handshake_rx (tls_ctx_t *ctx, session_t *tcp_session)
{
  openssl_ctx_t *oc = (openssl_ctx_t *) ctx;
  int rv;

  /* TCP RX FIFO에서 암호화된 핸드셰이크 데이터 읽기 */
  openssl_try_handshake_read (oc, tcp_session);

  /* OpenSSL 핸드셰이크 계속 진행 */
  rv = SSL_do_handshake (oc->ssl);

  if (rv == 1)
    {
      /* 핸드셰이크 성공 → 애플리케이션에 알림 */
      ctx->state = TLS_STATE_ESTABLISHED;
      if (ctx->tls_type == TLS_CLIENT)
        tls_notify_app_connected (ctx, SESSION_E_NONE);
      else
        tls_notify_app_accept (ctx);
      return 0;
    }

  int ssl_err = SSL_get_error (oc->ssl, rv);
  if (ssl_err == SSL_ERROR_WANT_READ || ssl_err == SSL_ERROR_WANT_WRITE)
    {
      /* 핸드셰이크 진행 중 — 추가 데이터 필요 */
      openssl_try_handshake_write (oc, tcp_session);
      return 0;
    }

  /* 핸드셰이크 실패 — 오류 처리 */
  tls_handshake_failed (ctx);
  return -1;
}
코드 분석: 핸드셰이크 상태 머신

openssl_ctx_handshake_rx()는 TLS 핸드셰이크의 핵심 반복 루프입니다. TCP FIFO에서 데이터를 읽어 SSL_do_handshake()에 전달하고, 반환값에 따라 세 가지 경로로 분기합니다. 반환값 1은 핸드셰이크 완료를 의미하며, SSL_ERROR_WANT_READ/WRITE는 추가 데이터가 필요하다는 의미입니다. 그 외의 오류는 tls_handshake_failed()로 처리되어 세션이 정리됩니다. VPP의 비동기 모델에서 이 함수는 TCP RX 이벤트마다 반복 호출되므로, 전체 핸드셰이크가 여러 벡터 처리 사이클에 걸쳐 비차단(Non-blocking)으로 완료됩니다.

암호화 엔진 선택

TLS 암호화 엔진은 플러그인 단위로 고릅니다. 사용하려는 엔진의 플러그인(tlsopenssl, tlsmbedtls, tlspicotls)을 로드한 뒤, 애플리케이션이 세션을 만들 때 VPPCOM_ATTR_SET_ENDPT_EXT_CFGcrypto_engine 필드로 어느 엔진을 쓸지 지정합니다. tlsopenssl에만 고유한 런타임 튜닝 CLI가 세 개 존재합니다.

# 플러그인 로드 현황 확인
vpp# show plugins | grep -i tls

# OpenSSL 플러그인의 엔진·알고리즘 설정 (실제 CLI)
vpp# tls openssl set engine qat async
vpp# tls openssl set alg aes-128-gcm async

# OpenSSL 3.x provider 로드 (QAT, FIPS 등)
vpp# tls openssl load-provider qatprovider

# 레코드 크기·파이프라인 튜닝
vpp# tls openssl set-tls record-size 16384 record-split-size 4096 max-pipelines 8
엔진 전환 주의: 런타임 엔진 변경은 새로운 연결에만 적용됩니다. 기존 TLS 세션은 원래 엔진으로 계속 동작합니다. 또한 picotls는 TLS 1.3만 지원하므로, TLS 1.2 연결이 필요한 클라이언트가 있다면 openssl을 유지해야 합니다. VPP 공식 tlsopenssl 소스에는 show tls engines, set tls crypto handler 같은 명령이 존재하지 않습니다 — 엔진 선택은 위 세 개의 tls openssl CLI 또는 애플리케이션 측 ENDPT_EXT_CFG로 이루어집니다.

OpenSSL 3.x Provider API 마이그레이션

OpenSSL 1.1.x의 ENGINE API는 3.0부터 deprecated로 표시되었습니다(완전 제거 시점은 향후 릴리스에서 확정됩니다). 하드웨어 가속기(QAT, Mellanox BlueField, AWS Nitro)를 VPP TLS와 연동하던 기존 ENGINE_set_default_* 호출은 새로운 Provider API로 교체되어야 합니다. 이 마이그레이션은 단순한 이름 바꾸기가 아니라 알고리즘 디스패치(Dispatch) 모델 자체가 달라지는 구조적 변경입니다.

VPP 본체와의 실제 결합 상태: 이 절의 Provider 코드는 OpenSSL 3.x 공식 문서를 따른 연동 레퍼런스 패턴입니다. tlsopenssl 플러그인이 Provider API로 완전히 전환된 릴리스가 언제인지는 VPP 버전별로 차이가 있으므로, 프로덕션 적용 전에 사용 중인 VPP 릴리스 노트와 src/plugins/tlsopenssl 소스를 교차 확인하시기 바랍니다. 구 릴리스에서는 여전히 ENGINE API 경로가 기본일 수 있습니다.
항목ENGINE (1.1.x)Provider (3.x)
로딩 단위엔진 동적 모듈 1개다수의 provider (default, legacy, fips, qat 등)
알고리즘 디스패치전역 기본 엔진 교체property query 기반 fetch
등록 APIENGINE_set_default_RSA()OSSL_PROVIDER_load() + property
FIPS 모드별도 FIPS 빌드 필요fips provider 로드만으로 전환
thread safety전역 락 기반provider context 독립
/* Before: OpenSSL 1.1.x ENGINE 방식 (deprecated) */
static int
tls_openssl_enable_qat_engine_legacy (void)
{
  ENGINE_load_dynamic ();
  ENGINE *e = ENGINE_by_id ("qat");
  if (!e) return -1;

  if (!ENGINE_init (e)) { ENGINE_free (e); return -1; }

  /* 전역 기본 엔진 교체 — 모든 RSA/ECDH가 QAT로 */
  ENGINE_set_default_RSA (e);
  ENGINE_set_default_EC (e);
  ENGINE_set_default_ciphers (e);
  ENGINE_free (e);
  return 0;
}

/* After: OpenSSL 3.x Provider 방식 */
static int
tls_openssl_enable_qat_provider (OSSL_LIB_CTX **libctx_out)
{
  OSSL_LIB_CTX *libctx = OSSL_LIB_CTX_new ();
  if (!libctx) return -1;

  /* 1. QAT provider 로드 */
  OSSL_PROVIDER *qat = OSSL_PROVIDER_load (libctx, "qatprovider");
  if (!qat) {
    OSSL_LIB_CTX_free (libctx);
    return -1;
  }

  /* 2. default provider도 함께 로드 (폴백용) */
  OSSL_PROVIDER_load (libctx, "default");

  /* 3. Property query — "QAT 있으면 QAT, 없으면 default" */
  if (!EVP_set_default_properties (libctx,
          "?provider=qatprovider")) {
    /* ? 접두사 = optional, 실패 시 default로 폴백 */
    OSSL_PROVIDER_unload (qat);
    OSSL_LIB_CTX_free (libctx);
    return -1;
  }

  *libctx_out = libctx;
  return 0;
}

/* VPP TLS 엔진에서 사용 — SSL_CTX에 libctx 전달 */
static int
tls_openssl_ctx_new_with_provider (openssl_main_t *om)
{
  OSSL_LIB_CTX *libctx;
  if (tls_openssl_enable_qat_provider (&libctx) < 0)
    libctx = NULL;  /* 기본 컨텍스트 사용 */

  om->ssl_ctx = SSL_CTX_new_ex (libctx, NULL, TLS_method ());
  /* 이후 SSL_CTX 옵션 설정은 동일 */
  return 0;
}

마이그레이션 체크리스트:

  1. API 호출 치환ENGINE_*OSSL_PROVIDER_*, SSL_CTX_newSSL_CTX_new_ex (libctx 인자 추가).
  2. property query 문법 학습"provider=qat"(필수), "?provider=qat"(선택), "fips=yes"(FIPS 모드). 이 문법으로 알고리즘별 디스패치가 결정됩니다.
  3. thread-local libctx — VPP 워커별로 독립 OSSL_LIB_CTX를 사용하면 전역 락이 사라져 멀티워커 확장성이 크게 개선됩니다.
  4. legacy provider 필요 여부 — DES, MD5, RC4, Blowfish 등의 구식 알고리즘을 쓰면 OSSL_PROVIDER_load (libctx, "legacy")를 별도로 호출해야 합니다. VPP TLS는 기본적으로 필요 없습니다.
  5. ABI 호환성 — 빌드 시 OPENSSL_SUPPRESS_DEPRECATED를 설정해 경고를 차단할 수 있지만, 장기적으로는 Provider API로 완전 전환해야 합니다. VPP 25.02부터 tls_openssl 플러그인의 기본 코드 경로는 Provider API입니다.

QAT 특수 사항: Intel QAT의 경우 qatengine(1.1.x 호환 래퍼)과 qatprovider(3.x 네이티브)가 공존합니다. 새 배포는 qatprovider를 사용하되, 기존 배포를 즉시 전환할 수 없다면 OpenSSL 3.x + qatengine 조합도 한시적으로 허용됩니다(deprecated 경고 포함). openssl list -providers로 로드된 provider를 확인하고, openssl speed -provider qatprovider -evp rsa2048로 오프로드가 실제 작동하는지 검증하는 것이 필수입니다.

OpenSSL BIO 메모리 체인 구현 분석

VPP의 tls_openssl 엔진은 OpenSSL의 BIO(Basic I/O) 추상화 계층을 활용하여 TLS 프로토콜 처리와 세션 FIFO 간의 데이터 흐름을 연결합니다. 핵심은 두 개의 BIO_s_mem 객체를 생성하여 각각 수신(RX)과 송신(TX) 방향의 TLS 레코드 버퍼로 사용하는 것입니다.

BIO_s_mem → FIFO 브릿지 구조: VPP는 OpenSSL의 표준 소켓 BIO 대신 메모리 BIO를 사용합니다. tls_openssl_bio_read() 콜백은 TCP RX FIFO에서 암호화된 TLS 레코드를 디큐(Dequeue)하여 OpenSSL에 전달하고, tls_openssl_bio_write() 콜백은 OpenSSL이 생성한 암호화된 레코드를 TCP TX FIFO에 인큐(Enqueue)합니다. 이 구조 덕분에 OpenSSL은 실제 소켓 없이도 TLS 처리를 수행할 수 있습니다.

/* OpenSSL 서버 컨텍스트 초기화 의사코드 */
static int
openssl_ctx_init_server (tls_ctx_t *ctx)
{
  openssl_ctx_t *oc = (openssl_ctx_t *) ctx;
  openssl_main_t *om = &openssl_main;

  /* 1. SSL_CTX 생성 및 인증서 설정 */
  SSL_CTX *ssl_ctx = SSL_CTX_new (TLS_server_method ());
  SSL_CTX_use_certificate_chain_file (ssl_ctx, ctx->tls_cert_file);
  SSL_CTX_use_PrivateKey_file (ssl_ctx, ctx->tls_key_file, SSL_FILETYPE_PEM);
  SSL_CTX_set_verify (ssl_ctx, SSL_VERIFY_NONE, NULL);

  /* 2. SSL 세션 객체 생성 */
  oc->ssl = SSL_new (ssl_ctx);

  /* 3. 메모리 BIO 쌍 생성 및 연결 */
  BIO *rbio = BIO_new (BIO_s_mem ());  /* TCP RX → OpenSSL 입력 */
  BIO *wbio = BIO_new (BIO_s_mem ());  /* OpenSSL 출력 → TCP TX */
  BIO_set_mem_eof_return (rbio, -1);
  BIO_set_mem_eof_return (wbio, -1);

  /* SSL 객체에 BIO 연결 (SSL이 소유권 획득) */
  SSL_set_bio (oc->ssl, rbio, wbio);

  /* 4. 서버 모드(accept)로 설정 */
  SSL_set_accept_state (oc->ssl);

  return 0;
}

데이터 송신 경로에서는 애플리케이션 FIFO의 평문 데이터가 OpenSSL을 거쳐 암호화된 후 TCP FIFO로 전달됩니다. 이 과정은 openssl_ctx_write() 함수에서 처리됩니다:

/* 데이터 송신: 앱 FIFO → OpenSSL 암호화 → TCP TX FIFO */
static int
openssl_ctx_write (tls_ctx_t *ctx, session_t *app_session,
                   transport_send_params_t *sp)
{
  openssl_ctx_t *oc = (openssl_ctx_t *) ctx;
  svm_fifo_t *app_rx_fifo = app_session->rx_fifo;  /* 앱→VPP 방향 */
  svm_fifo_t *tls_tx_fifo = ctx->tls_session->tx_fifo;
  int wrote = 0, rv;
  u32 deq_max, deq_now;

  deq_max = svm_fifo_max_dequeue_cons (app_rx_fifo);
  if (!deq_max)
    return 0;

  /* 1단계: 앱 FIFO에서 평문 데이터를 peek */
  deq_now = clib_min (deq_max, sp->max_burst_size);
  u8 *buf = vec_new (u8, deq_now);
  svm_fifo_peek (app_rx_fifo, 0, deq_now, buf);

  /* 2단계: SSL_write()로 OpenSSL에 평문 전달 → 내부 암호화 */
  rv = SSL_write (oc->ssl, buf, deq_now);
  if (rv > 0)
    {
      wrote = rv;
      svm_fifo_dequeue_drop (app_rx_fifo, rv);

      /* 3단계: wbio에서 암호화된 데이터를 읽어 TCP TX FIFO에 인큐 */
      BIO *wbio = SSL_get_wbio (oc->ssl);
      int pending = BIO_ctrl_pending (wbio);
      if (pending > 0)
        {
          u8 *tls_buf = vec_new (u8, pending);
          BIO_read (wbio, tls_buf, pending);
          svm_fifo_enqueue (tls_tx_fifo, pending, tls_buf);
          vec_free (tls_buf);
        }
    }

  vec_free (buf);
  return wrote;
}

수신 경로는 반대 방향으로 동작합니다. TCP RX FIFO의 암호화된 데이터가 OpenSSL을 거쳐 복호화된 후 애플리케이션 FIFO로 전달됩니다:

/* 데이터 수신: TCP RX FIFO → OpenSSL 복호화 → 앱 FIFO */
static int
openssl_ctx_read (tls_ctx_t *ctx, session_t *tls_session)
{
  openssl_ctx_t *oc = (openssl_ctx_t *) ctx;
  svm_fifo_t *tls_rx_fifo = tls_session->rx_fifo;
  svm_fifo_t *app_tx_fifo = ctx->app_session->tx_fifo;
  int read = 0, rv;
  u32 deq_max;

  deq_max = svm_fifo_max_dequeue_cons (tls_rx_fifo);
  if (!deq_max)
    return 0;

  /* 1단계: TCP RX FIFO에서 암호화된 TLS 레코드를 peek */
  u8 *buf = vec_new (u8, deq_max);
  svm_fifo_peek (tls_rx_fifo, 0, deq_max, buf);

  /* 2단계: rbio에 암호화된 데이터 기록 → OpenSSL 입력 */
  BIO *rbio = SSL_get_rbio (oc->ssl);
  BIO_write (rbio, buf, deq_max);
  svm_fifo_dequeue_drop (tls_rx_fifo, deq_max);

  /* 3단계: SSL_read()로 복호화된 평문 추출 */
  u8 plain_buf[16384];  /* TLS 레코드 최대 크기 */
  rv = SSL_read (oc->ssl, plain_buf, sizeof (plain_buf));
  if (rv > 0)
    {
      read = rv;
      /* 4단계: 복호화된 데이터를 앱 FIFO에 인큐 */
      svm_fifo_enqueue (app_tx_fifo, rv, plain_buf);
    }

  vec_free (buf);
  return read;
}

FIFO 제로카피 최적화: 위의 의사코드는 이해를 위해 버퍼 복사 방식으로 작성되었지만, 실제 VPP 구현에서는 svm_fifo_segments()를 사용하여 FIFO 내부 메모리에 대한 직접 포인터를 얻습니다. 이를 통해 중간 버퍼 할당과 복사를 제거하여 성능을 극대화합니다:

/* 제로카피 FIFO 세그먼트 접근 */
svm_fifo_seg_t segs[2];  /* 링버퍼이므로 최대 2개 세그먼트 */
u32 n_segs = 2;

/* FIFO 내부 메모리에 대한 직접 포인터 획득 (복사 없음) */
svm_fifo_segments (tls_rx_fifo, 0, segs, &n_segs, deq_max);

/* 세그먼트 데이터를 직접 BIO에 기록 */
for (int i = 0; i < n_segs; i++)
  BIO_write (rbio, segs[i].data, segs[i].len);

/* 처리 완료 후 FIFO에서 제거 */
svm_fifo_dequeue_drop (tls_rx_fifo, deq_max);

이 최적화는 특히 대용량 트래픽 처리 시 효과적입니다. SVM FIFO가 공유 메모리 위의 링버퍼로 구현되어 있기 때문에, 데이터가 버퍼 경계를 넘는 경우 최대 2개의 세그먼트가 반환됩니다. 각 세그먼트는 연속 메모리 영역을 가리키므로, BIO_write()에 직접 전달할 수 있습니다.

TLS 세션 재사용 내부 구현

TLS 핸드셰이크는 CPU 집약적인 비대칭 암호 연산을 포함하므로, 세션 재사용은 대규모 연결 환경에서 필수적인 최적화입니다. VPP는 TLS 1.2의 Session Ticket과 TLS 1.3의 PSK(Pre-Shared Key) 메커니즘을 모두 지원합니다.

Session Ticket 메커니즘

VPP는 SSL_CTX_set_tlsext_ticket_key_cb()를 통해 세션 티켓의 암호화/복호화 키를 직접 관리합니다. 이 방식은 다중 워커 스레드(Thread) 환경에서 모든 워커가 동일한 티켓 키를 공유할 수 있게 합니다:

  • 티켓 키 구조: 각 리스너(listener)별로 고유한 티켓 키 세트를 관리합니다. 키는 128비트 키 이름(key name), 256비트 AES 키, 256비트 HMAC 키로 구성됩니다.
  • 키 로테이션: 현재 키(current key)와 이전 키(previous key)를 동시에 유지하여 키 전환 시 기존 티켓이 즉시 무효화(Invalidation)되지 않도록 합니다.
  • 암호화 알고리즘: 티켓 암호화에는 AES-256-CBC를, 무결성(Integrity) 검증에는 HMAC-SHA-256을 사용합니다.
/* Session Ticket 키 콜백 의사코드 */
typedef struct
{
  u8 key_name[16];     /* 티켓 식별용 키 이름 */
  u8 aes_key[32];       /* AES-256-CBC 암호화 키 */
  u8 hmac_key[32];      /* HMAC-SHA-256 무결성 키 */
  f64 created_at;       /* 키 생성 시각 */
} tls_ticket_key_t;

typedef struct
{
  tls_ticket_key_t current;   /* 새 티켓 암호화에 사용 */
  tls_ticket_key_t previous;  /* 기존 티켓 복호화용 */
  f64 rotation_interval;       /* 키 로테이션 주기 (초) */
} tls_ticket_key_ctx_t;

static int
tls_ticket_key_cb (SSL *ssl, u8 *key_name,
                   u8 *iv, EVP_CIPHER_CTX *ectx,
                   HMAC_CTX *hctx, int enc)
{
  tls_ticket_key_ctx_t *tkc = get_ticket_key_ctx (ssl);

  if (enc)  /* 암호화: 새 티켓 생성 */
    {
      /* 현재 키의 이름을 티켓에 기록 */
      clib_memcpy (key_name, tkc->current.key_name, 16);

      /* 랜덤 IV 생성 */
      RAND_bytes (iv, EVP_MAX_IV_LENGTH);

      /* AES-256-CBC로 세션 상태 암호화 */
      EVP_EncryptInit_ex (ectx, EVP_aes_256_cbc (),
                          NULL, tkc->current.aes_key, iv);

      /* HMAC-SHA-256으로 무결성 서명 */
      HMAC_Init_ex (hctx, tkc->current.hmac_key, 32,
                    EVP_sha256 (), NULL);
      return 1;  /* 성공 */
    }
  else  /* 복호화: 기존 티켓 검증 */
    {
      tls_ticket_key_t *key = NULL;

      /* 키 이름으로 현재/이전 키 매칭 */
      if (!memcmp (key_name, tkc->current.key_name, 16))
        key = &tkc->current;
      else if (!memcmp (key_name, tkc->previous.key_name, 16))
        key = &tkc->previous;
      else
        return 0;  /* 알 수 없는 키 → 전체 핸드셰이크 수행 */

      HMAC_Init_ex (hctx, key->hmac_key, 32,
                    EVP_sha256 (), NULL);
      EVP_DecryptInit_ex (ectx, EVP_aes_256_cbc (),
                          NULL, key->aes_key, iv);

      /* 이전 키로 복호화된 경우 새 티켓 발급 권고 */
      return (key == &tkc->previous) ? 2 : 1;
    }
}

PSK (Pre-Shared Key) 재연결

TLS 1.3에서는 Session Ticket 대신 PSK 기반의 세션 재사용을 사용합니다. 서버는 핸드셰이크 완료 후 New Session Ticket(NST) 메시지를 전송하여 클라이언트에게 PSK를 제공합니다:

  • NST 메시지 처리: SSL_CTX_sess_set_new_cb()를 통해 새 세션이 생성될 때마다 콜백이 호출됩니다. VPP는 이 콜백에서 세션 정보를 캐시(Cache)하고 티켓 수명을 설정합니다.
  • 0-RTT Early Data: TLS 1.3 PSK를 사용하면 핸드셰이크 완료 전에 애플리케이션 데이터를 전송할 수 있습니다. VPP는 SSL_CTX_set_max_early_data()로 0-RTT 데이터의 최대 크기를 설정합니다.
  • 재전송(Retransmission) 방어: 0-RTT 데이터는 재전송 공격(replay attack)에 취약하므로, VPP는 SSL_CTX_set_options(SSL_OP_NO_ANTI_REPLAY)를 명시적으로 해제하지 않는 한 기본 재전송 방어를 활성화합니다.
/* TLS 1.3 PSK / 0-RTT 설정 의사코드 */
static void
openssl_configure_tls13_resumption (SSL_CTX *ssl_ctx,
                                     tls_cfg_t *cfg)
{
  /* 세션 캐시 모드: 서버측 캐시 + 자동 NST 발행 */
  SSL_CTX_set_session_cache_mode (ssl_ctx,
    SSL_SESS_CACHE_SERVER | SSL_SESS_CACHE_NO_INTERNAL);

  /* 새 세션 생성 시 콜백 등록 */
  SSL_CTX_sess_set_new_cb (ssl_ctx, tls_session_new_cb);

  /* 세션 티켓 수명 설정 (기본 7200초) */
  SSL_CTX_set_timeout (ssl_ctx, cfg->session_timeout);

  /* 0-RTT Early Data 설정 */
  if (cfg->enable_early_data)
    {
      /* 최대 early data 크기 (16KB 권장) */
      SSL_CTX_set_max_early_data (ssl_ctx, 16384);

      /* 재전송 방어 활성화 (기본값) */
      SSL_CTX_set_options (ssl_ctx, SSL_OP_ANTI_REPLAY);
    }
}

/* 새 세션 콜백: NST 수신 시 호출 */
static int
tls_session_new_cb (SSL *ssl, SSL_SESSION *session)
{
  tls_ctx_t *ctx = SSL_get_app_data (ssl);

  /* 세션 티켓을 직렬화하여 캐시에 저장 */
  u32 len;
  u8 *data = NULL;
  len = i2d_SSL_SESSION (session, &data);
  if (len > 0)
    tls_session_cache_add (ctx->listener_index, data, len);
  OPENSSL_free (data);

  return 0;  /* OpenSSL이 세션 참조 해제 */
}

/* 서버측 0-RTT early data 수신 처리 */
static int
openssl_handle_early_data (tls_ctx_t *ctx)
{
  openssl_ctx_t *oc = (openssl_ctx_t *) ctx;
  u8 buf[16384];
  size_t read_bytes;
  int status;

  status = SSL_read_early_data (oc->ssl, buf,
                                 sizeof (buf), &read_bytes);
  switch (status)
    {
    case SSL_READ_EARLY_DATA_SUCCESS:
      /* 복호화된 0-RTT 데이터를 앱 FIFO에 전달 */
      svm_fifo_enqueue (ctx->app_session->tx_fifo,
                        read_bytes, buf);
      return read_bytes;

    case SSL_READ_EARLY_DATA_FINISH:
      /* Early data 완료, 일반 핸드셰이크로 전환 */
      return 0;

    case SSL_READ_EARLY_DATA_ERROR:
      /* 재전송 감지 또는 PSK 불일치 → 전체 핸드셰이크 */
      return -1;
    }

  return -1;
}
성능 효과: TLS 세션 재사용을 활성화하면 재연결 시 전체 핸드셰이크(2-RTT)를 생략하고 약식 핸드셰이크(1-RTT) 또는 0-RTT로 연결을 재개할 수 있습니다. 이는 연결 설정 지연(Latency)을 50~100% 감소시키며, 서버의 CPU 부하도 비대칭 암호 연산 생략으로 크게 줄어듭니다.

TLS 레코드 레이어와 프래그먼테이션

VPP의 TLS 엔진은 애플리케이션 데이터를 TLS 레코드(최대 16KB)로 분할하여 TCP FIFO에 기록합니다. 이 과정에서 VPP 특유의 최적화가 적용됩니다:

/* TLS 레코드 구조 (RFC 8446 Section 5.1) */
struct {
  ContentType type;         /* 0x17=application_data, 0x16=handshake */
  ProtocolVersion version;  /* TLS 1.2: 0x0303, TLS 1.3: 0x0303 (호환) */
  uint16 length;            /* 최대 16384 + 256 (패딩) */
  opaque fragment[length];  /* 암호화된 페이로드 */
} TLSPlaintext;

/* VPP 최적화: 레코드 크기 조절 */
/*
 * - 핸드셰이크 시: 작은 레코드 (빠른 첫 바이트)
 * - 벌크 전송 시: 최대 레코드 (오버헤드 최소화)
 * - FIFO 잔량 기반 동적 크기 결정
 * - AES-GCM: 레코드당 16바이트 인증 태그 + 8바이트 nonce
 */
레코드 크기오버헤드적합 시나리오
1KB~2.3% (24B/1024B)인터랙티브, 낮은 지연
4KB~0.6%웹 페이지(Page) 전송
16KB (최대)~0.15%대용량 파일, 스트리밍
레코드 크기와 성능: VPP는 FIFO에 축적된 데이터량에 따라 레코드 크기를 자동 조절합니다. 소량 데이터는 작은 레코드로 즉시 전송하고(지연 최소화), 대량 데이터는 최대 레코드로 묶어 전송합니다(처리량 최대화). 이는 커널 kTLS의 정적 레코드 크기 대비 유연한 접근입니다.

VPP TLS 설정 및 인증서 관리

⚠ 이 절의 CLI 예시는 대부분 개념적 표기입니다. 아래 블록에 tls cert add, tls ca-cert add, show tls engines, show tls certs, show tls ctx verbose, set tls cipher, tls key add 같은 명령이 등장하지만, 공식 tlsopenssl 플러그인 소스(src/plugins/tlsopenssl/tls_openssl.c)에는 이 명령들이 존재하지 않습니다. 실제 존재하는 TLS 전용 CLI는 tls openssl set, tls openssl set-tls, tls openssl load-provider 세 개뿐이며, 인증서·CA·cipher·세션 티켓 관리는 모두 바이너리 API(app_add_cert_key_pair) 또는 OpenSSL SSL_CTX 수준의 설정 경로를 통해 이루어집니다. 아래 블록은 "무엇을 해야 하는가"를 표현하는 의사 CLI로 읽어 주시기 바랍니다. 실제 배포 시에는 VCL 레시피의 BAPI 예시TLS 운영 심화의 정정된 절차를 참고하시면 됩니다.

startup.conf TLS 설정

VPP의 TLS 관련 설정은 startup.conf의 여러 섹션에 분산되어 있습니다:

# /etc/vpp/startup.conf — TLS 관련 설정

tls {
  # 기본 암호화 엔진
  default-crypto-engine openssl

  # 최소 TLS 버전 (1.2 또는 1.3)
  tls-min-version 1.2

  # TLS 세션 캐시 크기 (세션 재사용)
  ca-cert-path /etc/vpp/certs/ca.pem

  # 비동기 암호화 활성화 (openssl 엔진만)
  async
}

# OpenSSL 엔진 특정 설정
tlsopenssl {
  # OpenSSL ENGINE 로드 (QAT 등)
  engine qat

  # 비동기 모드 (연산 큐잉)
  async

  # 최대 비동기 대기 프레임 수
  max-async-frames 256
}

인증서 및 개인 키 관리

VPP는 CLI와 API를 통해 TLS 인증서를 동적으로 관리합니다. 인증서는 인증서-키 쌍(ckpair) 단위로 등록되며, 애플리케이션이 리스너를 생성할 때 ckpair 인덱스를 참조합니다:

# PEM 형식 인증서/키 추가
vpp# tls cert add cert /etc/vpp/certs/server.pem \
                  key /etc/vpp/certs/server.key

# 인증서 목록 확인
vpp# show tls certs

# DER 형식도 지원
vpp# tls cert add cert /etc/vpp/certs/server.der \
                  key /etc/vpp/certs/server.key.der \
                  format der

# CA 인증서 (mTLS 클라이언트 검증용)
vpp# tls ca-cert add /etc/vpp/certs/ca-chain.pem
VPP TLS 인증서 로딩 흐름 CLI / Binary API ckpair 관리자 app_cert_key_pair_add() 인증서 저장소 ckpair_index → {cert, key} app_listener_alloc() TLS 리스너 생성 ckpair_index 참조 SSL_CTX_use_certificate() 엔진별 인증서/키 로드 연결별 SSL* 객체 생성 → 핸드셰이크 지원 형식 • PEM (Base64) • DER (Binary) • PKCS#12 (API)

SNI 기반 멀티 도메인 인증서 관리

VPP는 SNI(Server Name Indication)를 기반으로 하나의 리스너에서 여러 도메인의 인증서를 제공할 수 있습니다. 클라이언트가 ClientHello에 포함한 서버 이름에 따라 적절한 인증서가 자동으로 선택됩니다:

# 여러 도메인 인증서 등록
vpp# tls cert add cert /etc/vpp/certs/example-com.pem \
                  key /etc/vpp/certs/example-com.key
# → ckpair_index = 0

vpp# tls cert add cert /etc/vpp/certs/api-example-com.pem \
                  key /etc/vpp/certs/api-example-com.key
# → ckpair_index = 1

# 와일드카드 인증서도 지원
vpp# tls cert add cert /etc/vpp/certs/wildcard-example-com.pem \
                  key /etc/vpp/certs/wildcard-example-com.key
# → ckpair_index = 2  (*.example.com 매칭)

SNI 콜백을 통한 인증서 선택 (OpenSSL 엔진)

tls_openssl.c에서 SSL_CTX_set_tlsext_servername_callback()으로 SNI 콜백을 등록합니다. ClientHello의 server_name 확장을 파싱하여 매칭되는 ckpair_indexSSL_CTX로 전환합니다.

매칭 우선순위(Priority):

  1. 정확한 도메인 매칭 (api.example.com)
  2. 와일드카드 매칭 (*.example.com)
  3. 기본 인증서 (fallback)

인증서 무중단 교체

VPP의 유저스페이스 TLS는 인증서를 프로세스 재시작 없이 동적으로 교체할 수 있습니다. 이는 커널 kTLS나 nginx에서 인증서 교체 시 reload/restart가 필요한 것과 대비되는 핵심 장점입니다:

# 1. 현재 인증서 상태 확인
vpp# show tls certs
[0] CN=example.com  expires=2026-12-31
    active connections: 15234

# 2. 새 인증서 추가 (기존 인덱스 덮어쓰기)
vpp# tls cert update index 0 \
     cert /etc/vpp/certs/new-server.pem \
     key /etc/vpp/certs/new-server.key

# 3. 기존 연결: 원래 인증서로 계속 동작 (중단 없음)
# 4. 새 연결: 새 인증서 사용

# Let's Encrypt ACME 자동화 스크립트 예시:
# certbot renew --deploy-hook \
#   "vppctl tls cert update index 0 \
#    cert /etc/letsencrypt/live/example.com/fullchain.pem \
#    key /etc/letsencrypt/live/example.com/privkey.pem"

Cipher Suite 설정

TLS 1.2와 1.3은 Cipher Suite 협상 방식이 근본적으로 다릅니다. VPP에서의 설정 방법:

# TLS 1.2 cipher suite 설정 (OpenSSL 형식)
vpp# set tls cipher ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256

# TLS 1.3 ciphersuite 설정 (별도 설정)
vpp# set tls ciphersuites TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256

# 현재 설정 확인
vpp# show tls config
TLS 버전권장 Cipher Suite보안 수준
TLS 1.3TLS_AES_256_GCM_SHA384256-bit, AEAD
TLS 1.3TLS_CHACHA20_POLY1305_SHA256256-bit, 소프트웨어 최적
TLS 1.2ECDHE-RSA-AES256-GCM-SHA384PFS + 256-bit AEAD
TLS 1.2ECDHE-ECDSA-AES128-GCM-SHA256PFS + ECDSA + AEAD

상호 인증(mTLS) 구성

mTLS(Mutual TLS)는 서버뿐 아니라 클라이언트도 인증서를 제시하여 양방향 인증을 수행합니다. VPP에서의 mTLS 구성:

# 1. 서버 인증서/키 등록
vpp# tls cert add cert /etc/vpp/certs/server.pem \
                  key /etc/vpp/certs/server.key

# 2. CA 인증서 등록 (클라이언트 인증서 검증용)
vpp# tls ca-cert add /etc/vpp/certs/client-ca.pem

# 3. 클라이언트 인증서 검증 활성화
# VCL 애플리케이션에서 session_attr에 설정:
#   transport_cfg.is_mtls = 1;
/* mTLS 서버 VCL 설정 예시 */
vppcom_session_tls_set_verify (session_handle,
    VPPCOM_TLS_VERIFY_PEER |
    VPPCOM_TLS_VERIFY_FAIL_IF_NO_PEER_CERT);

/* 클라이언트 인증서 DN 확인 콜백 */
vppcom_session_tls_set_verify_cb (session_handle,
    my_verify_callback, my_ctx);
mTLS 유스케이스: 서비스 메시(Istio, Envoy 연동), 제로 트러스트 네트워크, API 게이트웨이 인증에서 VPP mTLS가 활용됩니다. 커널 kTLS 대비 인증서 교체가 재시작 없이 가능하다는 장점이 있습니다.
v26.02 변경 — 서버측 mTLS와 peer cert 조회: VPP 26.02(2026-02-25)는 TLS/TLS engine 플러그인에 server side mtls supportretrieving peer certificate 경로를 공식 추가했습니다(릴리스 노트). 25.02 기준에서 "VCL에서 클라이언트 인증서 DN을 가져올 수 없어 사이드카를 통해야 한다"던 제약이 해소됩니다. 26.02로 올리실 때 체크할 점:
  • 핸드셰이크 후 세션 API로 peer certificate를 조회해, 애플리케이션 단에서 SPIFFE ID·조직 DN 기반 권한 판정을 직접 수행할 수 있습니다. 사이드카 프록시에 의존하던 레이어가 사라질 수 있습니다.
  • TLS 프레임워크는 production이지만 TLS OpenSSL 엔진 자체는 여전히 experimental 분류입니다. 신규 기능 사용 시 엔진 버전 고정(pin)과 장애 시 폴백 경로를 먼저 설계하시기 바랍니다.
  • IPsec ESP도 같은 26.02에서 crypto+HMAC을 단일 op로 통합했으므로, TLS 처리 코어와 IPsec 코어가 같은 워커에 배치될 경우 크립토 코어 사용률 프로파일이 달라질 수 있습니다. IPsec 섹션을 병행 확인하시기 바랍니다.
VPP mTLS (상호 인증) 핸드셰이크 흐름 클라이언트 VPP 서버 (mTLS) ClientHello + supported_versions + key_share ServerHello + EncryptedExtensions CertificateRequest (CA 목록 포함) Certificate (서버 인증서) CertificateVerify + Finished 클라이언트: 서버 인증서 검증 (CA 체인) Certificate (클라이언트 인증서) CertificateVerify (개인 키로 서명) Finished VPP: 클라이언트 인증서 검증 CA 체인 확인 + CRL/OCSP + DN 콜백 양방향 인증 완료 — 암호화 데이터 전송 mTLS 실패 시나리오 클라이언트 인증서 미제출 → handshake_failure | 만료/CA 불일치 → bad_certificate | CRL 등재 → certificate_revoked

TLS 세션 재사용과 0-RTT

VPP는 TLS 1.3의 세션 티켓(Session Ticket)PSK(Pre-Shared Key)를 지원하여 재연결 시 핸드셰이크를 생략하거나 0-RTT로 단축할 수 있습니다:

TLS 1.3 세션 티켓 흐름

최초 연결:

  1. 전체 핸드셰이크를 수행합니다 (1-RTT).
  2. 서버가 NewSessionTicket 메시지를 전송합니다.
  3. 클라이언트가 티켓 + resumption_master_secret을 저장합니다.

재연결:

  1. ClientHello에 pre_shared_key 확장을 포함합니다.
  2. 서버가 PSK를 검증하여 0-RTT 또는 1-RTT로 재개합니다.
  3. 0-RTT 시 ClientHello와 함께 Early Data를 전송할 수 있습니다.

VPP에서 세션 티켓 설정 (startup.conf 또는 CLI):

tls {
    session-ticket-lifetime 3600     # 티켓 유효기간 (초)
    session-ticket-key-rotation 1800 # 키 교체 주기
}
0-RTT 보안 주의: 0-RTT Early Data는 재전송(Retransmission) 공격(replay attack)에 취약합니다. 공격자가 0-RTT 패킷을 캡처하여 재전송하면 서버가 중복 처리할 수 있습니다. 따라서 0-RTT는 멱등(idempotent) 요청(GET 등)에만 허용하고, 상태 변경 요청(POST, PUT)에는 사용하지 않아야 합니다. VPP의 애플리케이션 레벨에서 SSL_get_early_data_status()로 Early Data 여부를 확인하여 적절히 처리하세요.

TLS 운영·디버깅 심화 — 엔진 선택·핸드셰이크·세션·인증서 체인·장애 추적

지금까지 TLS 아키텍처·레코드 레이어·세션 재사용·0-RTT를 분리해서 다뤘습니다. 이 절은 현장에서 가장 자주 묻는 질문들 — "엔진은 어떻게 선택하나", "핸드셰이크가 왜 실패하나", "인증서 체인은 어떻게 검증되나", "무엇을 보고 성능을 판단하나" — 에 답하는 실전 가이드입니다. CLI 결과, packet trace 샘플, 자주 마주치는 오류 메시지와 원인을 모두 포함했습니다.

엔진 선택 매트릭스 — OpenSSL vs mbedTLS vs picotls vs async

VPP 25.x 기준으로 사용할 수 있는 TLS 엔진은 4종류이며, 하나의 VPP 인스턴스 안에서 동시에 로드해 tls-engine 속성으로 워크로드별로 분리할 수 있습니다.

엔진지원 TLS 버전강점약점권장 시나리오
OpenSSL (기본)1.2 / 1.3가장 넓은 cipher 지원, QAT·ENGINE 연동, FIPS 140-2메모리 사용량 상대적으로 큼, 코드베이스 복잡범용 웹 프록시, 엔터프라이즈 규정 준수 환경
mbedTLS1.2 / 1.3작은 바이너리, 내장·에지 장비에 적합, 타이밍 공격 대응 설계하드웨어 오프로드 경로 부족, TLS 1.3 미들웨어 기능 일부 미지원ARM 에지 게이트웨이, 임베디드 VPP 빌드
picotls1.3 전용최소 오버헤드, QUIC과 공용, 0-RTT 빠름TLS 1.2 지원 없음, cipher 범위가 좁음QUIC/HTTP3 서버, 모던 1.3 전용 서비스
openssl-async1.2 / 1.3QAT 등 asynchronous 엔진과 결합 시 처리량 수 배 증가하드웨어 가속기가 없으면 오히려 오버헤드QAT·Nitrox 카드가 장착된 고부하 L7 프록시
# 플러그인 로드는 plugin { path ... } / enable 규칙을 따릅니다.
# tlsopenssl, tlsmbedtls, tlspicotls가 각자 독립 플러그인으로 설치됩니다.
vpp# show plugins | grep -i tls

# OpenSSL 플러그인의 엔진/알고리즘 설정 (실제 VLIB_CLI 명령)
vpp# tls openssl set engine qat async
vpp# tls openssl set alg aes-128-gcm async

# OpenSSL 3.x에서 provider 기반 엔진 로드
vpp# tls openssl load-provider qatprovider

# 레코드 크기·파이프라인 튜닝
vpp# tls openssl set-tls record-size 16384 record-split-size 4096 max-pipelines 8

# 세션이 실제로 어떤 프로토콜로 협상됐는지 확인
vpp# show session verbose 2
결정 트리: (1) QUIC도 같이 쓴다 → picotls. (2) FIPS 인증 또는 QAT 오프로드가 필요하다 → openssl 또는 openssl-async. (3) ARM 에지에서 바이너리를 줄여야 한다 → mbedtls. (4) 애매하면 기본 openssl을 유지하시기 바랍니다. 전환 비용보다 검증 부담이 큽니다.
실제 존재하는 TLS CLI는 3개뿐입니다. tlsopenssl 플러그인이 등록하는 CLI는 (1) tls openssl set [engine <name>] [alg <alg> [async]], (2) tls openssl set-tls [record-size ...] [record-split-size ...] [max-pipelines ...], (3) tls openssl load-provider <name> 세 가지입니다. show tls engines, set session tls engine, tls ckpair add, tls ca-cert add, tls client-cert required, tls session-ticket-key, tls ocsp stapling, tls cipher-list, tls min-version/max-version 같은 명령은 공식 tlsopenssl 소스에 존재하지 않습니다. 인증서·CA·세션 티켓·cipher 정책 등은 모두 바이너리 API(app_add_cert_key_pair 등) 또는 플러그인 빌드 옵션으로 관리합니다. 아래 표에 CLI처럼 적어 둔 항목은 모두 "동일 의미의 관리 행위를 수행해야 한다"는 개념적 지시로 읽어 주시기 바랍니다.

핸드셰이크 해부 — 바이트 단위로 본 ClientHello부터 Finished까지

핸드셰이크 실패의 90 %는 ClientHello 내용과 서버 설정의 불일치에서 발생합니다. VPP에서 이를 빠르게 추적하려면 packet-trace와 엔진 레벨 디버그 로그 두 가지를 조합합니다.

# VPP 전체 trace — TLS 레코드 경계까지 드러납니다
vpp# trace add dpdk-input 100
vpp# clear trace
# 클라이언트에서 재현 후
vpp# show trace

# 세션 레이어 로그 클래스를 debug 수준으로 올려 핸드셰이크 단계를 관찰
vpp# set logging class session level debug
vpp# show log | grep -iE 'tls|session'

정상 흐름을 단계별로 기록한 예시입니다. 각 단계에서 멈춘다면 그 위치에 대응하는 원인을 바로 확인할 수 있습니다.

단계누가 보냄핵심 필드실패 시 증상가능한 원인
1. ClientHello클라이언트supported_versions, cipher_suites, SNI, ALPNTCP RST 직후 세션 소멸SNI 미지정, 지원 버전 불일치
2. ServerHello + EncryptedExtensions서버(VPP)selected cipher, selected SNI, ALPN responsehandshake failure alert매칭되는 cipher 없음, CKPAIR 누락
3. Certificate서버(VPP)leaf + intermediatesunknown_ca/bad_cert체인 누락, 루트 신뢰 설정 오류
4. CertificateVerify + Finished서버(VPP)전체 transcript 서명시간 초과개인 키 미스매치, 서명 알고리즘 불일치
5. (mTLS) Certificate + CertVerify클라이언트클라이언트 인증서alert certificate_requiredCertificateRequest에 반응하지 못함
6. Finished클라이언트transcript MACalert decrypt_error키 교환 실패, 시각 동기화 오류

자주 보는 TLS 경고 코드와 해석

Alert원인VPP 측 조치
unrecognized_name (112)클라이언트가 보낸 SNI에 매칭되는 CKPAIR 없음바이너리 API로 도메인별 CKPAIR 등록, 애플리케이션 attach 시 기본 CKPAIR 지정
handshake_failure (40)공통 cipher 없음플러그인 빌드 옵션 또는 OpenSSL 설정 파일에서 cipher 정책 확장. 클라이언트가 TLS 1.3이면 TLS_AES_128_GCM_SHA256가 포함되어 있는지 확인
bad_certificate (42) / unknown_ca (48)체인 검증 실패전체 체인(leaf → ICA → (선택) root)을 하나의 PEM으로 병합해 CKPAIR에 주입
unsupported_extension (110)엔진이 확장을 모름picotls → openssl 플러그인으로 교체하거나 엔진 버전 업그레이드
protocol_version (70)허용 버전 불일치플러그인 레벨에서 TLS 1.2/1.3 최소·최대 버전 조정(런타임 CLI 아님, 설정·빌드 옵션)
decode_error (50)변조된 레코드 또는 MTU 이슈로 잘린 레코드중간 MTU/PMTUD 확인, show errors에서 fragmentation 카운터 조사

인증서 체인·검증·CRL/OCSP

VPP는 PEM 포맷 인증서를 바이너리 API app_add_cert_key_pair로 등록합니다(CLI 경로는 없습니다). 클라이언트가 체인을 따라가려면 leaf 인증서 다음에 모든 중간 CA를 같은 버퍼에 이어 붙여 메시지에 담습니다. 루트는 포함하지 마시기 바랍니다 — 클라이언트가 이미 가지고 있어야 하며, 포함해도 손해는 없지만 핸드셰이크 페이로드(Payload)가 커집니다.

# 체인 준비 (leaf → ICA 순서)
cat leaf.pem ica1.pem ica2.pem > fullchain.pem

# VPP 외부 도구(vpp_echo, GoVPP 애플리케이션 등)가 BAPI로 등록
#   vl_api_app_add_cert_key_pair_t { cert + key payload }
# 반환되는 index를 VCL에 CKPAIR 인덱스로 전달

여러 도메인을 하나의 VPP 인스턴스로 처리하려면 SNI별로 CKPAIR를 여러 개 만들고, 애플리케이션 레벨에서 SNI → ckpair_index 매핑을 유지한 뒤 연결마다 VPPCOM_ATTR_SET_ENDPT_EXT_CFG로 선택하시기 바랍니다. 공식 CLI에 SNI 자동 라우팅(Routing)은 없습니다.

OCSP stapling, CRL, session ticket key 관리는 모두 OpenSSL 수준의 기능입니다. VPP tlsopenssl 플러그인은 OpenSSL SSL_CTX를 그대로 사용하므로, 이들 기능을 쓰려면 (1) OpenSSL 설정 파일(openssl.cnf), (2) 환경변수, (3) 플러그인 코드 수정으로 접근하는 것이 정공법입니다. 운영 환경에서 자주 요구되는 OCSP must-staple·짧은 수명 인증서(SPIFFE X.509-SVID)는 OpenSSL 쪽에서 충분히 지원되므로 그 경로를 따르시면 됩니다. CRL 파일 직접 검증은 데이터 평면에서 비용이 크니 피하시기 바랍니다.

세션 티켓·재사용 캐시 운영

TLS 1.3 세션 티켓은 NewSessionTicket 메시지로 발급되며, 서버가 대칭 키(STEK)로 암호화해 클라이언트에게 넘깁니다. 여러 VPP 인스턴스가 같은 티켓을 공유하려면 OpenSSL 레벨에서 STEK 콜백(SSL_CTX_set_tlsext_ticket_key_evp_cb)을 직접 구현해 모든 인스턴스에 동일한 키 자료를 주입해야 합니다. VPP의 tlsopenssl 플러그인 자체에는 STEK를 조작하는 공식 CLI가 없으므로, 이 요구 사항이 있다면 플러그인 코드를 수정하거나 별도 프로세스에서 SSL_CTX를 래핑해 VPP에 주입하는 방식을 고려하시기 바랍니다.

STEK 유출 영향: 티켓 암호화 키가 유출되면 과거 녹음된 세션의 resumption 직후 데이터를 복호화할 수 있습니다. (TLS 1.3 full handshake는 PFS 덕분에 안전하지만 0-RTT 페이로드는 취약합니다.) 키 자료는 HSM 또는 systemd-creds 수준 이상의 비밀 저장소에 두고, 정기 rotate 계획을 운영 체크리스트에 넣으시기 바랍니다.

성능 관찰 지표 — 무엇을 보고 튜닝을 결정하는가

TLS 병목(Bottleneck)은 대개 세 가지 중 하나입니다. (1) 핸드셰이크 CPU, (2) record 암복호 throughput, (3) FIFO backpressure. 각각을 분리해 관찰하는 CLI가 다릅니다.

① 핸드셰이크 초당 완료 수 — ECDHE 서명 비용을 짐작
vpp# show session stats | grep -i handshake
handshake-complete:  12845/s
handshake-failure:      23/s

② 레코드 처리량 — crypto 워커당 평균 batch 크기
vpp# show crypto engines
  openssl:  batch=32  pending=3  out-of-order=0

③ FIFO backpressure — 송신 큐가 차서 write가 막히는 비율
vpp# show session verbose 3 | grep -E 'tx-fifo.*full|rx-fifo.*full'

④ 워커 CPU 분포 — 한 워커가 80% 넘으면 분산 부족
vpp# show runtime | awk '/tls-|crypto-|session-/ {print}'

핸드셰이크가 병목이면 (1) cipher를 ECDHE+AES-GCM으로 고정, (2) picotls 또는 openssl-async + QAT로 전환, (3) 세션 재사용과 0-RTT를 활성화해 full handshake 비율을 낮추시기 바랍니다. 레코드 처리가 병목이면 cryptodev 오프로드 또는 워커 수 증설, FIFO 병목이면 FIFO 크기 증설과 애플리케이션 읽기 속도 점검이 정답입니다.

장애 플레이북 — 증상별 최소 복구 시나리오

증상먼저 확인할 것조치
특정 도메인만 handshake_failure애플리케이션 측 CKPAIR 테이블(직접 관리)에서 SNI 매핑, 만료일체인 재등록(BAPI 재호출), 인증서 교체
모든 도메인에서 TLS 드롭show plugins, show crypto engines, show errorstlsopenssl 플러그인 로드 여부 확인, 애플리케이션 attach에서 기본 CKPAIR 지정
Resumption이 전혀 안 됨플러그인 코드의 STEK 콜백 설정 여부, LB 후단 STEK 일관성STEK 콜백 구현, 모든 인스턴스에 같은 키 주입
mTLS 클라이언트만 실패클라이언트 CA가 CKPAIR 체인 또는 플러그인 trust store에 들어있는지CA 번들을 포함한 SSL_CTX 구성 경로 점검
0-RTT가 동작하지 않음picotls/openssl 엔진 버전, max_early_data 설정엔진 업데이트, TLS 1.3 고정
CPU가 특정 워커에 몰림show runtime의 vectors/call 분포RSS 재조정, session-affinity 힌트 비활성화
간헐 bad_record_macMTU, 미들박스, 시간 동기화(NTP)PMTUD 확인, 시간 교정, 특정 중간자 배제
사전 예방: 운영 환경에서는 모든 CKPAIR의 만료일을 Prometheus로 뽑아 경보를 걸어두시기 바랍니다. VPP는 만료 인증서도 그대로 핸드셰이크를 시도하므로, 자체 모니터링이 없으면 배포 직후가 아니라 만료 당일에 장애가 터집니다. openssl x509 -noout -enddate -in fullchain.pem 한 줄로도 충분한 스크립트를 만들 수 있습니다.

VPP TLS 성능 최적화

비동기 암호화 프레임워크

VPP의 비동기 암호화(src/vnet/tls/tls_async.c)는 TLS 핸드셰이크와 데이터 암복호화를 VPP 메인 루프에서 분리하여, 암호 연산이 완료될 때까지 다른 패킷 처리를 계속할 수 있게 합니다:

/* 비동기 암호화 흐름 */
/* 1. 암호 연산 큐잉 */
tls_async_enqueue_op (vm, ctx, op_type, data, len);

/* 2. 메인 루프가 다른 노드 처리 계속 */
/* ... 벡터 패킷 처리 ... */

/* 3. 완료된 연산 결과 수거 */
n_ops = tls_async_dequeue (vm, &completed_ops);

/* 4. 결과 처리 (암호문 전송 또는 평문 전달) */
for (i = 0; i < n_ops; i++)
  tls_async_process_completed (completed_ops[i]);
VPP 비동기 TLS 암호화 파이프라인 VPP 메인 루프 (벡터 처리) input → classify → ip4-lookup → tls-encrypt → output (계속 처리) enqueue Crypto Op 큐 encrypt / decrypt / handshake SW 엔진 (OpenSSL) HW 엔진 (QAT/Cryptodev) 완료 큐 dequeue 비동기 암호화 이점 • 메인 루프 블로킹 방지 • HW 가속기 파이프라이닝 • 배치 처리로 처리량 극대화 • 동기 대비 2~5x 핸드셰이크 처리량

비동기와 동기 모드의 핵심 차이를 이해하는 것이 중요합니다:

항목동기 모드 (기본)비동기 모드
암호화 실행메인 루프에서 즉시 수행큐에 넣고 나중에 결과 수거
메인 루프 블로킹암호화 완료까지 대기다른 패킷 계속 처리
처리량CPU 바운드HW 가속기 파이프라이닝으로 향상
핸드셰이크 CPS~30K (RSA-2048)~80K (SW), ~150K (QAT)
지연시간안정적 (즉시 완료)약간 증가 (큐잉 오버헤드)
적합 시나리오적은 연결, 낮은 지연 필수대량 연결, 높은 CPS 필요
/* 비동기 암호화 상세 흐름 (tls_async.c) */

/* 핸드셰이크 비동기 처리 */
int tls_async_openssl_ctx_init (tls_ctx_t *ctx)
{
  openssl_ctx_t *oc = (openssl_ctx_t *) ctx;

  /* SSL_MODE_ASYNC 활성화 → SSL_do_handshake()가
   * ASYNC_pause_job()으로 제어 반환 가능 */
  SSL_set_mode (oc->ssl, SSL_MODE_ASYNC);

  /* ENGINE 설정 (QAT 등): RSA/ECDH 연산을
   * 하드웨어에 오프로드 */
  SSL_set_engine (oc->ssl, async_engine);

  return 0;
}

/* 메인 루프 통합: tls-input 노드에서 호출 */
static uword
tls_async_process_node_fn (vlib_main_t *vm, ...)
{
  /* 1단계: 완료된 비동기 연산 수거 */
  n = openssl_async_poll_events (&events);

  /* 2단계: 각 완료 이벤트 처리 */
  for (i = 0; i < n; i++) {
    ctx = tls_ctx_get (events[i].ctx_index);
    if (ctx->resume)
      /* 핸드셰이크 재개 또는 데이터 전달 */
      tls_ctx_resume_handshake (ctx);
  }
}

DPDK Cryptodev TLS 오프로드

VPP는 DPDK의 Cryptodev 프레임워크를 통해 하드웨어 암호화 가속기에 TLS 연산을 오프로드할 수 있습니다. 대표적인 하드웨어:

디바이스인터페이스지원 알고리즘성능
Intel QATCryptodev PMDAES-GCM, ChaCha20, RSA, ECDH~100 Gbps (bulk), ~50K CPS
NVIDIA ConnectX-6+inline TLSAES-128/256-GCM~200 Gbps (NIC inline)
ARM CryptoCellCryptodev PMDAES-GCM, SHA-256~10 Gbps
SW fallbackOpenSSL PMD전체CPU 의존
# startup.conf — QAT Cryptodev 설정
dpdk {
  dev 0000:3d:01.0 {
    name crypto0
  }
}

tlsopenssl {
  # DPDK Cryptodev 엔진 사용
  engine cryptodev

  # 비동기 모드 (QAT 파이프라이닝)
  async

  # Cryptodev 큐 쌍 수
  cryptodev-queue-pairs 4
}
Intel QAT 하드웨어 TLS 오프로드 아키텍처 VPP 프로세스 (유저스페이스) TLS 세션 레이어 OpenSSL 엔진 SSL_MODE_ASYNC ENGINE API qat_engine 핸드셰이크 연산 (비대칭) • RSA-2048/4096 서명/검증 • ECDHE P-256/P-384 키교환 • X25519 키교환 벌크 암호화 (대칭) • AES-128/256-GCM • ChaCha20-Poly1305 • AES-CBC + HMAC-SHA PCIe 3.0 x16 Intel QAT 하드웨어 (C62x / 4xxx) PKE 엔진 RSA/ECDH Cipher 엔진 AES/ChaCha Hash 엔진 SHA/GCM-GHASH 압축 엔진 Deflate/LZ4 링 기반 큐 쌍 (Queue Pair) × N — DPDK Cryptodev PMD로 접근 QAT C62x: 최대 128 큐 쌍, ~100 Gbps bulk, ~50K RSA-2048 ops/sec 오프로드 효과 비교 RSA-2048 서명 (핸드셰이크) CPU: ~30K/s QAT: ~50K/s AES-256-GCM (벌크) CPU: 40G QAT: 100G CPU 사용률 (100K conn) SW: ~85% QAT: ~25% 절약된 CPU 코어를 패킷 처리에 활용 가능 TCO(총 소유 비용) 절감 전력 소비 ~40% 감소

멀티 워커 TLS 분산

VPP의 멀티 워커 환경에서 TLS 세션은 워커 간에 분산됩니다. 각 워커는 독립적인 TLS 컨텍스트 풀을 유지하며, NUMA 인지 메모리 할당으로 원격 메모리 접근을 최소화합니다:

# startup.conf — 멀티 워커 TLS 최적화
cpu {
  main-core 0
  corelist-workers 1-7
  # TLS 워커를 NUMA 0/1에 분산
}

session {
  # 워커별 TLS 세션 풀 크기
  preallocated-sessions 128000
  v4-session-table-buckets 64000
  v4-session-table-memory 512m

  # 이벤트 큐 크기 (TLS 비동기에 충분히)
  event-queue-length 100000
}
NUMA 인지 배치: TLS 컨텍스트(~50KB/연결)는 워커가 실행되는 NUMA 노드의 로컬 메모리에 할당됩니다. cpu { ... } 섹션에서 워커-NUMA 매핑(Mapping)을 정확히 설정하면, 원격 NUMA 접근에 의한 ~30% 지연 증가를 방지할 수 있습니다.
NUMA 인지 멀티 워커 TLS 토폴로지 NUMA 노드 0 NIC (PCIe NUMA 0) RSS Worker 1 Core 1 TLS ctx 풀 Worker 2 Core 2 TLS ctx 풀 Worker 3 Core 3 TLS ctx 풀 W 4 Core 4 로컬 메모리 (NUMA 0) TLS 컨텍스트 풀 + 세션 테이블 + FIFO 세그먼트 QAT 0 (PCIe NUMA 0) 모든 접근이 로컬 NUMA → 메모리 지연: ~80ns NUMA 노드 1 NIC (PCIe NUMA 1) Worker 5 Core 8 TLS ctx 풀 Worker 6 Core 9 TLS ctx 풀 Worker 7 Core 10 TLS ctx 풀 W 8 Core 11 로컬 메모리 (NUMA 1) TLS 컨텍스트 풀 + 세션 테이블 + FIFO 세그먼트 QAT 1 (PCIe NUMA 1) 모든 접근이 로컬 NUMA → 메모리 지연: ~80ns NUMA 교차 접근 시 성능 저하 원격 NUMA 메모리 접근: ~130ns (+60%) | NIC-워커 NUMA 불일치: PCIe 교차 트래픽 + 캐시 바운싱

멀티 워커 환경에서 TLS 세션의 워커 할당은 RSS(Receive Side Scaling) 해시(Hash)에 의해 결정됩니다. NIC의 RSS가 5-tuple 해시로 패킷을 특정 큐에 분배하면, 해당 큐를 담당하는 VPP 워커가 TCP 연결과 TLS 세션을 모두 소유합니다. 이 구조에서 중요한 최적화 포인트:

설정 항목올바른 설정잘못된 설정 시 영향
NIC 큐 수= 워커 수일부 워커 유휴 또는 과부하
NIC-워커 NUMA 매핑NIC과 같은 NUMA의 코어PCIe 교차 트래픽, +30~60% 지연
QAT-워커 NUMA 매핑QAT와 같은 NUMA의 워커암호화 연산 원격 메모리 접근
Hugepage NUMA 분배각 NUMA 노드에 균등 할당한쪽 NUMA에서 메모리 부족
TLS 세션 풀워커별 독립 풀풀 공유 시 잠금 경합(Lock Contention)(Contention)

성능 비교: VPP TLS vs kTLS vs nginx

동일 하드웨어(Xeon Platinum 8380, 2.3 GHz, 128GB)에서의 대표적 TLS 성능 비교:

항목VPP TLS (SW)VPP TLS (QAT)kTLSnginx (userspace)
새 연결 (CPS)~80,000~150,000~50,000~30,000
처리량 (Gbps)~40~80~30~15
p99 지연 (ms)0.30.20.51.2
동시 연결500K+500K+100K50K
CPU 사용률높음 (전용 코어)낮음 (오프로드)중간 (커널)높음 (프로세스)
컨텍스트 스위칭(Context Switching)없음없음있음많음
TLS 성능 비교 (새 연결/초, 처리량 Gbps) 상대 성능 100% 66% 33% 0 150K 80G VPP+QAT 80K 40G VPP SW 50K 30G kTLS 30K 15G nginx CPS (진한색) Gbps (연한색)

TLS 성능 튜닝 체크리스트

항목권장 설정효과
비동기 암호화tlsopenssl { async }핸드셰이크 처리량 2~5x 향상
세션 캐시TLS 1.3 PSK 활성화재연결 0-RTT, CPU 절약
Cipher 선택AES-128-GCM (AES-NI 있을 때)AES-256 대비 ~15% 빠름
Cipher 선택ChaCha20 (AES-NI 없을 때)소프트웨어 최적, ARM 유리
Hugepages1GB 또는 2MB 페이지TLB 미스 감소, ~10% 처리량 향상
워커 수NIC 큐 수와 동일RSS 기반 워커 분산 최적화
NUMA 배치NIC과 같은 NUMA 노드원격 NUMA 접근 ~30% 지연 방지
세션 프리얼로케이션preallocated-sessions 128000런타임 할당 지연 제거
QAT 오프로드engine cryptodevCPU 사용률 ~70% 감소

TLS/QUIC 보안 모범 사례

이 절의 CLI 예시(set tls cipher, tls cert add, tls key add, vppctl tls cert reload 등)는 개념적 표기입니다. 공식 tlsopenssl 플러그인에는 cipher·인증서 관리 전용 CLI가 없고, 실제 작업은 OpenSSL 설정(SSL_CTX_set_ciphersuites, SSL_CTX_set_cipher_list)과 바이너리 API app_add_cert_key_pair로 수행합니다. 읽으실 때는 "어떤 정책을 가져야 하는가"를 취하시고, 실제 적용 경로는 TLS 설정 절의 경고 박스를 따라 주시기 바랍니다.

VPP 환경에서 TLS/QUIC을 운영할 때 보안을 강화하기 위한 설정, 인증서 관리, 공격 방어 전략을 다룹니다.

Cipher Suite 선택 가이드

TLS 1.3에서 사용 가능한 cipher suite는 5개로 제한되며, 보안성과 성능 간 균형을 고려하여 선택해야 합니다:

Cipher Suite보안 등급성능권장 여부비고
TLS_AES_256_GCM_SHA384최고높음권장AES-NI 하드웨어 가속 지원
TLS_AES_128_GCM_SHA256높음매우 높음권장기본값, 대부분 환경에 적합
TLS_CHACHA20_POLY1305_SHA256높음높음권장AES-NI 없는 환경(ARM 등)에 최적
TLS_AES_128_CCM_SHA256높음중간조건부IoT/임베디드 환경 전용
TLS_AES_128_CCM_8_SHA256중간중간비권장태그 크기 축소로 보안성 저하
# VPP TLS cipher suite 설정
vpp# set tls cipher TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256

# TLS 1.2 하위 호환이 필요한 경우 (비권장)
vpp# set tls cipher ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384

# 비권장/폐기 cipher 명시적 비활성화
# RC4, DES, 3DES, MD5, SHA1 기반은 절대 사용 금지
# CBC 모드는 BEAST/POODLE 취약점으로 비권장
QUIC과 cipher suite: QUIC은 TLS 1.3만 지원하므로 위 5개 cipher suite만 사용 가능합니다. QUIC의 기본 cipher는 TLS_AES_128_GCM_SHA256이며, quicly의 picotls 엔진에서 처리됩니다. 별도 설정 없이도 안전한 기본값이 적용됩니다.

인증서 관리 자동화

운영 환경에서 인증서 만료는 서비스 장애의 주요 원인입니다. 자동 갱신 체계를 구축하여 이를 방지해야 합니다:

# Let's Encrypt ACME 클라이언트로 인증서 자동 발급
certbot certonly --standalone -d vpp.example.com \
  --preferred-challenges http \
  --non-interactive --agree-tos -m admin@example.com

# 발급된 인증서를 VPP에 등록
vpp# tls cert add /etc/letsencrypt/live/vpp.example.com/fullchain.pem
vpp# tls key add /etc/letsencrypt/live/vpp.example.com/privkey.pem

# 갱신 자동화 (crontab)
# 0 3 * * * certbot renew --post-hook "vppctl tls cert reload"

# 인증서 만료일 모니터링
openssl x509 -enddate -noout -in /etc/letsencrypt/live/vpp.example.com/cert.pem
# notAfter=Mar 15 12:00:00 2027 GMT

키 순환(Key Rotation) 전략

TLS 세션 티켓 키 순환
세션 티켓(Session Ticket) 암호화 키는 주기적으로 교체해야 합니다. 오래된 티켓 키가 유출되면 과거 세션의 PFS(Perfect Forward Secrecy)가 침해될 수 있습니다. 권장 순환 주기는 24시간입니다.
QUIC 0-RTT 키 관리
0-RTT에 사용되는 PSK(Pre-Shared Key)는 재전송 공격(Replay Attack)에 취약합니다. 서버 측에서 0-RTT 티켓의 유효 기간을 제한하고, 사용된 티켓을 추적하여 중복 사용을 방지해야 합니다.
인증서 키 순환
RSA/ECDSA 개인 키는 연 1회 이상 교체를 권장합니다. ECDSA P-256 키는 RSA 2048 대비 키 크기가 작아 핸드셰이크 성능이 우수하며 동등한 보안 수준을 제공합니다.

보안 강화 설정

OCSP 스테이플링(Stapling)

OCSP 스테이플링은 서버가 인증서 유효성 증명을 미리 확보하여 클라이언트의 OCSP 조회 지연을 제거합니다:

# OpenSSL 엔진에서 OCSP 스테이플링 활성화
# startup.conf
tls {
  # OCSP 응답 파일 경로 (주기적으로 갱신 필요)
  ocsp-response /etc/vpp/ocsp-response.der
}

# OCSP 응답 수동 갱신
openssl ocsp -issuer ca.pem -cert server.pem \
  -url http://ocsp.ca-provider.com \
  -respout /etc/vpp/ocsp-response.der

# 자동 갱신 스크립트 (12시간마다)
# 0 */12 * * * /usr/local/bin/update-ocsp.sh

0-RTT 재전송 공격 방어

QUIC과 TLS 1.3의 0-RTT는 지연시간을 줄이지만, 재전송 공격에 취약합니다. 방어 전략은 다음과 같습니다:

방어 수단적용 대상설명
멱등성(Idempotent) 요청만 허용애플리케이션0-RTT에서 GET 요청만 처리, POST/PUT 등 상태 변경 요청은 1-RTT 이후 처리
single-use 티켓서버0-RTT 티켓을 1회 사용 후 폐기하여 재전송 차단
타임스탬프 검증서버0-RTT 데이터의 시간 범위를 제한하여 오래된 재전송 거부
0-RTT 비활성화서버보안이 최우선인 환경에서 enable-0rtt 설정 제거

속도 제한 및 DDoS 완화

QUIC은 UDP 기반이므로 IP 스푸핑 기반 반사 공격에 취약할 수 있습니다. VPP에서의 방어 설정입니다:

# VPP ACL 기반 속도 제한
vpp# set acl-plugin acl permit+reflect src 0.0.0.0/0 dst 0.0.0.0/0 \
     proto udp dport 443 rate-limit 10000/s

# QUIC Initial 패킷 크기 검증 (RFC 9000: 최소 1200 바이트)
# quicly가 자동으로 크기 미달 Initial 패킷 드롭

# Retry 토큰 기반 주소 검증
quic {
  # 클라이언트 주소 검증 활성화
  require-address-validation
  # Retry 토큰 유효 기간 (초)
  retry-token-lifetime 120
}

# 연결 수 제한 (IP당)
quic {
  max-connections-per-ip 100
}

보안 설정 비교 요약

보안 항목기본값권장 설정최고 보안
TLS 버전1.2+1.3 전용1.3 전용
CipherAES-128-GCMAES-256-GCM + ChaCha20AES-256-GCM 단독
키 교환ECDHE P-256ECDHE P-256/P-384X25519 + P-384
인증서RSA 2048ECDSA P-256ECDSA P-384
OCSP비활성스테이플링 활성필수(Must-Staple)
0-RTT비활성멱등 요청만 허용비활성
세션 티켓 순환수동24시간 자동6시간 자동
mTLS비활성선택적필수
주소 검증비활성활성필수 + IP 제한
보안 점검 체크리스트: 운영 환경 배포 전 다음 사항을 반드시 확인하세요. (1) TLS 1.2 이하 비활성화, (2) 안전하지 않은 cipher suite 제거, (3) 인증서 자동 갱신 설정, (4) OCSP 스테이플링 활성화, (5) 0-RTT 재전송 방어 설정, (6) 연결 수 제한 및 속도 제한 적용. ssllabs.com 또는 testssl.sh 도구로 설정 검증을 권장합니다.

이 주제와 관련된 다른 문서를 더 깊이 이해하고 싶다면 다음을 참고하시기 바랍니다.

오픈소스 코드 인용 고지

라이선스 고지: 이 문서의 코드 예제에는 아래 오픈소스 프로젝트의 소스 코드에서 발췌·간략화한 내용이 포함되어 있습니다. 해당 코드 블록에는 원본 프로젝트의 라이선스가 그대로 적용되며, 본 사이트의 CC BY-NC-SA 4.0 라이선스 대상에서 제외됩니다. 이들 코드의 포함은 한국 저작권법 제28조 및 제35조의5에 근거한 교육 목적의 공정 이용에 해당합니다.
프로젝트저작권자라이선스공식 저장소
VPP (Vector Packet Processing) FD.io contributors Apache License 2.0 github.com/FDio/vpp
DPDK (Data Plane Development Kit) DPDK contributors BSD 3-Clause License github.com/DPDK/dpdk

코드 블록 내 주석에 원본 파일 경로가 표기된 부분은 해당 프로젝트 소스 코드에서 발췌·간략화한 것입니다. 전체 소스 코드는 위 공식 저장소에서 확인하시기 바랍니다.