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 재전송 방어, 인증서 자동 갱신 등 운영 관점의 보안 체크리스트를 정리합니다.
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 컨텍스트의 생명 주기는 다음과 같은 상태 전이를 따릅니다:
- TLS_CONN_STATE_NONE:
tls_ctx_alloc()로 컨텍스트 할당, 엔진 유형 결정 - TLS_CONN_STATE_HANDSHAKE: TCP 연결 수립 후 엔진별 핸드셰이크 시작 (
ctx_init_server()또는ctx_init_client()) - TLS_CONN_STATE_ESTABLISHED: 핸드셰이크 완료, 양방향 암호화 데이터 전송 가능
- TLS_CONN_STATE_PASSIVE_CLOSE: 원격 측이
close_notify전송, 잔여 데이터 드레인 - 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_engine_vft_t의 콜백(Callback)들만 구현하면 됩니다. 세션 레이어와 TLS 추상 계층의 코드는 변경할 필요가 없으며, 이것이 VPP TLS의 핵심 설계 원칙입니다.
TLS 데이터 경로: FIFO 기반 암복호화
VPP TLS의 데이터 경로는 이중 세션(dual-session) 모델을 사용합니다. 하나의 TLS 연결은 내부적으로 두 개의 세션을 유지합니다: 하부 TCP 세션(암호문)과 상위 애플리케이션 세션(평문). 각 세션은 독립적인 FIFO(rx/tx) 쌍을 가지며, TLS 엔진이 양쪽 FIFO 사이에서 암복호화를 수행합니다:
이 제로-카피(zero-copy)에 가까운 데이터 경로가 VPP TLS의 성능 핵심입니다. 커널 kTLS가 sendfile()과 splice()를 통해 커널-유저 경계를 최소화하려는 것과 달리, VPP는 애초에 전체 경로가 유저스페이스이므로 경계 자체가 존재하지 않습니다.
VPP TLS vs 커널 kTLS 아키텍처 비교
VPP TLS와 커널 kTLS는 근본적으로 다른 설계 철학을 따릅니다. 두 접근 방식의 구조적 차이를 이해하면 적합한 사용 시나리오를 판단할 수 있습니다:
| 비교 항목 | 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 TLS | DPDK Cryptodev 경유 | tls_device 네이티브 |
| 기존 앱 호환 | LD_PRELOAD 또는 VCL 전환 | 소켓(Socket) 옵션만 추가 |
| 인증서 교체 | 무중단 (CLI/API) | 프로세스(Process) 재시작(Reboot) 필요 |
| 적합 워크로드 | 고 CPS, 대규모 동시 연결 | 고 처리량(Throughput), 파일 서빙 |
TLS 플러그인 비교: OpenSSL vs mbedTLS vs picotls
VPP는 세 가지 TLS 엔진 플러그인을 제공합니다. 각각 다른 유스케이스에 최적화되어 있으며, startup.conf에서 선택할 수 있습니다:
| 항목 | tlsopenssl | tlsmbedtls | picotls |
|---|---|---|---|
| 라이브러리 | OpenSSL / BoringSSL | Mbed 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.0 | Apache 2.0 | MIT |
| 적합 시나리오 | 범용, HW 가속 | 임베디드, IoT | TLS 1.3 전용 고성능 |
tlsopenssl이 권장됩니다. TLS 1.3만 필요하고 최대 성능이 목표라면 picotls가 적합하며, 메모리가 극히 제한된 임베디드 환경에서는 tlsmbedtls를 고려합니다.
tlspicotls는 TLS 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 핸드셰이크 상세 타임라인
VPP TLS를 깊게 이해하려면, TCP 연결 수립과 TLS 핸드셰이크가 언제 겹치고 언제 분리되는지를 정확히 봐야 합니다. TCP가 먼저 바이트 스트림을 만들고, 그 위에서 TLS 엔진이 SSL_do_handshake() 또는 동등한 엔진 콜백을 반복 호출하며 상태를 전진시킵니다. 비동기 엔진을 쓰면 이 중간 단계가 메인 루프 밖으로 일부 밀려납니다. 운영 중 핸드셰이크 실패 진단은 프록시 — TLS 디버깅의 CLI 워크스루를, 호스트 스택 관점의 CPS 저하 분석은 호스트 스택 — TLS CPS가 낮음을 함께 참고합니다.
- TCP가 먼저 연결을 만듭니다 — TLS는 TCP 바이트 스트림이 준비되기 전에는 아무 것도 시작할 수 없습니다.
- 엔진 초기화가 이어집니다 — OpenSSL 기준으로는 BIO를 세션 핸들에 연결하고,
SSL_do_handshake()를 돌리기 시작합니다. - 핸드셰이크는 반복 호출 모델입니다 — 한 번의 함수 호출로 끝나는 것이 아니라, 새 TLS 레코드가 도착할 때마다 읽고 쓰기를 반복하며 상태를 전진시킵니다.
- 비동기 엔진은 WANT_ASYNC를 반환할 수 있습니다 — 이 경우 핸드셰이크 자체는 진행 중이지만, 암호 연산 완료 통지를 기다리며 메인 루프는 다른 작업을 계속할 수 있습니다.
- ESTABLISHED 이후에만 평문 API가 자연스러워집니다 — 그 전 단계에서 애플리케이션은 사실상 인증서 교환과 키 스케줄이 끝나기를 기다리는 상태입니다.
TLS 레코드와 TCP 세그먼트의 책임 분리
TCP와 TLS를 함께 다룰 때 가장 흔한 오해는 "TLS 레코드 하나가 TCP 패킷 하나"라고 생각하는 것입니다. 실제로는 전혀 그렇지 않습니다. TLS는 바이트 스트림 위에 논리적 레코드 경계를 만들고, TCP는 그 바이트들을 어떻게 쪼개고 언제 재전송할지 결정합니다.
| 현상 | 책임 계층 | VPP에서 실제로 일어나는 일 |
|---|---|---|
| 재전송 | TCP | 손실된 세그먼트는 TCP가 다시 보냅니다. TLS는 같은 레코드를 재암호화하지 않고, 이미 생성된 바이트 스트림이 재전송됩니다. |
| 순서 보장(Ordering) | TCP | out-of-order 세그먼트는 TCP 재조립 후에야 TLS 엔진으로 올라갑니다. TLS는 완성된 바이트 흐름만 읽습니다. |
| 레코드 경계 | TLS | 한 TLS 레코드는 여러 TCP 세그먼트에 걸칠 수 있고, 여러 TLS 레코드가 하나의 큰 세그먼트 묶음으로 나갈 수도 있습니다. |
| 혼잡 제어(Congestion Control)와 pacing | TCP | TLS가 평문을 빠르게 생산해도 실제 네트워크 전송 속도는 congestion window와 ACK 도착 속도가 정합니다. |
| close_notify | TLS | 애플리케이션 계층의 정상 종료 의사를 알리는 제어 메시지입니다. TCP FIN과는 별개의 의미를 가집니다. |
| FIN/RST | TCP | 전송 계층 종료입니다. TLS 관점에서는 close_notify 없이 FIN만 오면 truncation 가능성까지 의심해야 합니다. |
close_notify, 세션 재개 실패는 TLS 영역입니다.
TLS 엔진 플러그인 내부 구현 분석
VPP의 TLS 엔진 플러그인은 각각 고유한 I/O 패턴으로 암복호화를 수행합니다. 여기서는 OpenSSL, mbedTLS, picotls 세 엔진의 내부 데이터 경로와 구현 차이를 상세히 분석합니다.
openssl은 기능 완전성과 호환성이 가장 높아 기본 선택지이며, 비동기 ENGINE을 통해 QAT 연동이 표준화되어 있습니다. mbedtls는 메모리 사용이 적어 컨테이너(Container)·임베디드에 유리하지만 비동기 오프로드가 약합니다. picotls는 TLS 1.3 전용이면서 zero-copy 경로를 지향해 최대 처리량이 가장 높지만, 2026년 기준 experimental이므로 프로덕션 도입 전 호환성 검증이 필요합니다.
OpenSSL 엔진 ctx_write() 분석
tls_openssl_ctx_write() 함수는 애플리케이션이 보낸 평문 데이터를 TLS 레코드로 암호화하여 TCP 스택에 전달하는 핵심 경로입니다. OpenSSL의 BIO(Basic I/O) 추상화 계층을 활용하여 메모리 기반 I/O를 수행합니다:
- App TX FIFO에서 평문 dequeue:
svm_fifo_peek()로 애플리케이션이 기록한 평문 데이터를 읽어옵니다 - BIO_write() → SSL_write(): 평문 데이터를 OpenSSL의 내부 BIO 버퍼에 기록하면,
SSL_write()가 TLS 레코드를 생성하고 암호화를 수행합니다 - BIO_read()로 암호문 추출: 암호화된 TLS 레코드를 출력 BIO에서 읽어옵니다
- 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를 활용합니다:
- TCP RX FIFO에서 암호문 dequeue: TCP 스택이 수신한 TLS 레코드를 읽어옵니다
- BIO_write()로 입력 BIO에 주입: 암호문을 OpenSSL의 입력 BIO 버퍼에 기록합니다
- SSL_read()로 복호화: OpenSSL이 TLS 레코드를 파싱하고 복호화하여 평문을 반환합니다
- 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) 방식으로 처리됩니다:
- session_open(TRANSPORT_PROTO_TLS): 세션 레이어가 TLS 연결 요청
- tls_connect(): TLS 추상 계층이 먼저 TCP 연결 수립
- TCP 3-way 핸드셰이크: VPP TCP 스택에서 수행
- tls_session_connected_cb(): TCP 연결 완료 콜백
- tls_ctx_handshake_*(): 엔진별 TLS 핸드셰이크 시작
- 비동기 핸드셰이크:
SSL_do_handshake()가SSL_ERROR_WANT_READ/WRITE반환 시 이벤트 대기 - 핸드셰이크 완료: 애플리케이션에 연결 완료 통지
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_CIPHER | SSL_do_handshake() 반환값 < 0 → alert 전송 → 세션 정리 | trace add tls-input 100으로 핸드셰이크 메시지 확인 |
| 인증서 만료 | X509_V_ERR_CERT_HAS_EXPIRED | verify callback에서 거부 → connected_cb에 오류 전파 | openssl x509 -noout -enddate로 PEM 파일 유효기간 직접 확인 |
| 메모리 부족 | SSL_R_MALLOC_FAILURE | SSL_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_CFG의 crypto_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
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) 모델 자체가 달라지는 구조적 변경입니다.
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 |
| 등록 API | ENGINE_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;
}
마이그레이션 체크리스트:
- API 호출 치환 —
ENGINE_*→OSSL_PROVIDER_*,SSL_CTX_new→SSL_CTX_new_ex(libctx 인자 추가). - property query 문법 학습 —
"provider=qat"(필수),"?provider=qat"(선택),"fips=yes"(FIPS 모드). 이 문법으로 알고리즘별 디스패치가 결정됩니다. - thread-local libctx — VPP 워커별로 독립
OSSL_LIB_CTX를 사용하면 전역 락이 사라져 멀티워커 확장성이 크게 개선됩니다. - legacy provider 필요 여부 — DES, MD5, RC4, Blowfish 등의 구식 알고리즘을 쓰면
OSSL_PROVIDER_load (libctx, "legacy")를 별도로 호출해야 합니다. VPP TLS는 기본적으로 필요 없습니다. - 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 레코드 레이어와 프래그먼테이션
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 TLS 설정 및 인증서 관리
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
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_index의 SSL_CTX로 전환합니다.
매칭 우선순위(Priority):
- 정확한 도메인 매칭 (api.example.com)
- 와일드카드 매칭 (*.example.com)
- 기본 인증서 (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.3 | TLS_AES_256_GCM_SHA384 | 256-bit, AEAD |
| TLS 1.3 | TLS_CHACHA20_POLY1305_SHA256 | 256-bit, 소프트웨어 최적 |
| TLS 1.2 | ECDHE-RSA-AES256-GCM-SHA384 | PFS + 256-bit AEAD |
| TLS 1.2 | ECDHE-ECDSA-AES128-GCM-SHA256 | PFS + 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);
- 핸드셰이크 후 세션 API로 peer certificate를 조회해, 애플리케이션 단에서 SPIFFE ID·조직 DN 기반 권한 판정을 직접 수행할 수 있습니다. 사이드카 프록시에 의존하던 레이어가 사라질 수 있습니다.
- TLS 프레임워크는 production이지만 TLS OpenSSL 엔진 자체는 여전히 experimental 분류입니다. 신규 기능 사용 시 엔진 버전 고정(pin)과 장애 시 폴백 경로를 먼저 설계하시기 바랍니다.
- IPsec ESP도 같은 26.02에서 crypto+HMAC을 단일 op로 통합했으므로, TLS 처리 코어와 IPsec 코어가 같은 워커에 배치될 경우 크립토 코어 사용률 프로파일이 달라질 수 있습니다. IPsec 섹션을 병행 확인하시기 바랍니다.
TLS 세션 재사용과 0-RTT
VPP는 TLS 1.3의 세션 티켓(Session Ticket)과 PSK(Pre-Shared Key)를 지원하여 재연결 시 핸드셰이크를 생략하거나 0-RTT로 단축할 수 있습니다:
TLS 1.3 세션 티켓 흐름
최초 연결:
- 전체 핸드셰이크를 수행합니다 (1-RTT).
- 서버가 NewSessionTicket 메시지를 전송합니다.
- 클라이언트가 티켓 +
resumption_master_secret을 저장합니다.
재연결:
- ClientHello에
pre_shared_key확장을 포함합니다. - 서버가 PSK를 검증하여 0-RTT 또는 1-RTT로 재개합니다.
- 0-RTT 시 ClientHello와 함께 Early Data를 전송할 수 있습니다.
VPP에서 세션 티켓 설정 (startup.conf 또는 CLI):
tls {
session-ticket-lifetime 3600 # 티켓 유효기간 (초)
session-ticket-key-rotation 1800 # 키 교체 주기
}
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 | 메모리 사용량 상대적으로 큼, 코드베이스 복잡 | 범용 웹 프록시, 엔터프라이즈 규정 준수 환경 |
| mbedTLS | 1.2 / 1.3 | 작은 바이너리, 내장·에지 장비에 적합, 타이밍 공격 대응 설계 | 하드웨어 오프로드 경로 부족, TLS 1.3 미들웨어 기능 일부 미지원 | ARM 에지 게이트웨이, 임베디드 VPP 빌드 |
| picotls | 1.3 전용 | 최소 오버헤드, QUIC과 공용, 0-RTT 빠름 | TLS 1.2 지원 없음, cipher 범위가 좁음 | QUIC/HTTP3 서버, 모던 1.3 전용 서비스 |
| openssl-async | 1.2 / 1.3 | QAT 등 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
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, ALPN | TCP RST 직후 세션 소멸 | SNI 미지정, 지원 버전 불일치 |
| 2. ServerHello + EncryptedExtensions | 서버(VPP) | selected cipher, selected SNI, ALPN response | handshake failure alert | 매칭되는 cipher 없음, CKPAIR 누락 |
| 3. Certificate | 서버(VPP) | leaf + intermediates | unknown_ca/bad_cert | 체인 누락, 루트 신뢰 설정 오류 |
| 4. CertificateVerify + Finished | 서버(VPP) | 전체 transcript 서명 | 시간 초과 | 개인 키 미스매치, 서명 알고리즘 불일치 |
| 5. (mTLS) Certificate + CertVerify | 클라이언트 | 클라이언트 인증서 | alert certificate_required | CertificateRequest에 반응하지 못함 |
| 6. Finished | 클라이언트 | transcript MAC | alert 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에 주입하는 방식을 고려하시기 바랍니다.
성능 관찰 지표 — 무엇을 보고 튜닝을 결정하는가
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 errors | tlsopenssl 플러그인 로드 여부 확인, 애플리케이션 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_mac | MTU, 미들박스, 시간 동기화(NTP) | PMTUD 확인, 시간 교정, 특정 중간자 배제 |
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]);
비동기와 동기 모드의 핵심 차이를 이해하는 것이 중요합니다:
| 항목 | 동기 모드 (기본) | 비동기 모드 |
|---|---|---|
| 암호화 실행 | 메인 루프에서 즉시 수행 | 큐에 넣고 나중에 결과 수거 |
| 메인 루프 블로킹 | 암호화 완료까지 대기 | 다른 패킷 계속 처리 |
| 처리량 | 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 QAT | Cryptodev PMD | AES-GCM, ChaCha20, RSA, ECDH | ~100 Gbps (bulk), ~50K CPS |
| NVIDIA ConnectX-6+ | inline TLS | AES-128/256-GCM | ~200 Gbps (NIC inline) |
| ARM CryptoCell | Cryptodev PMD | AES-GCM, SHA-256 | ~10 Gbps |
| SW fallback | OpenSSL 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
}
멀티 워커 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
}
cpu { ... } 섹션에서 워커-NUMA 매핑(Mapping)을 정확히 설정하면, 원격 NUMA 접근에 의한 ~30% 지연 증가를 방지할 수 있습니다.
멀티 워커 환경에서 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) | kTLS | nginx (userspace) |
|---|---|---|---|---|
| 새 연결 (CPS) | ~80,000 | ~150,000 | ~50,000 | ~30,000 |
| 처리량 (Gbps) | ~40 | ~80 | ~30 | ~15 |
| p99 지연 (ms) | 0.3 | 0.2 | 0.5 | 1.2 |
| 동시 연결 | 500K+ | 500K+ | 100K | 50K |
| CPU 사용률 | 높음 (전용 코어) | 낮음 (오프로드) | 중간 (커널) | 높음 (프로세스) |
| 컨텍스트 스위칭(Context Switching) | 없음 | 없음 | 있음 | 많음 |
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 유리 |
| Hugepages | 1GB 또는 2MB 페이지 | TLB 미스 감소, ~10% 처리량 향상 |
| 워커 수 | NIC 큐 수와 동일 | RSS 기반 워커 분산 최적화 |
| NUMA 배치 | NIC과 같은 NUMA 노드 | 원격 NUMA 접근 ~30% 지연 방지 |
| 세션 프리얼로케이션 | preallocated-sessions 128000 | 런타임 할당 지연 제거 |
| QAT 오프로드 | engine cryptodev | CPU 사용률 ~70% 감소 |
TLS/QUIC 보안 모범 사례
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 취약점으로 비권장
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 전용 |
| Cipher | AES-128-GCM | AES-256-GCM + ChaCha20 | AES-256-GCM 단독 |
| 키 교환 | ECDHE P-256 | ECDHE P-256/P-384 | X25519 + P-384 |
| 인증서 | RSA 2048 | ECDSA P-256 | ECDSA P-384 |
| OCSP | 비활성 | 스테이플링 활성 | 필수(Must-Staple) |
| 0-RTT | 비활성 | 멱등 요청만 허용 | 비활성 |
| 세션 티켓 순환 | 수동 | 24시간 자동 | 6시간 자동 |
| mTLS | 비활성 | 선택적 | 필수 |
| 주소 검증 | 비활성 | 활성 | 필수 + IP 제한 |
ssllabs.com 또는 testssl.sh 도구로 설정 검증을 권장합니다.
관련 문서
이 주제와 관련된 다른 문서를 더 깊이 이해하고 싶다면 다음을 참고하시기 바랍니다.
오픈소스 코드 인용 고지
| 프로젝트 | 저작권자 | 라이선스 | 공식 저장소 |
|---|---|---|---|
| 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 |
코드 블록 내 주석에 원본 파일 경로가 표기된 부분은 해당 프로젝트 소스 코드에서 발췌·간략화한 것입니다. 전체 소스 코드는 위 공식 저장소에서 확인하시기 바랍니다.