본문으로 건너뛰기
Previous
Next
C++ SSL/TLS 보안 통신 | OpenSSL과 Asio 연동 완벽 가이드 [#30-2]

C++ SSL/TLS 보안 통신 | OpenSSL과 Asio 연동 완벽 가이드 [#30-2]

C++ SSL/TLS 보안 통신 | OpenSSL과 Asio 연동 완벽 가이드 [#30-2]

이 글의 핵심

C++ HTTP는 안전하지 않아요. TLS 핸드셰이크, OpenSSL·Asio·순수 OpenSSL 예제, 인증서·mTLS, 자주 발생하는 SSL 에러, 모범 사례, 프로덕션 패턴, Let's Encrypt 배포까지.

들어가며: “HTTP는 안전하지 않아요, HTTPS가 필요해요”

문제 시나리오

채팅 서버나 API 서버를 평문 TCP로 만들었는데, 보안 담당자가 이렇게 말합니다:

“로그인 비밀번호가 네트워크에서 그대로 노출돼요. 와이파이 공유 환경에서 누구나 패킷 캡처로 볼 수 있어요.”

// ❌ 평문 HTTP - 위험
// 클라이언트 → 서버: "POST /login HTTP/1.1\r\n...\r\npassword=secret123"
// 와이파이 중간에서 패킷 캡처 시 비밀번호가 그대로 보임!
tcp::socket socket(io);
boost::asio::write(socket, boost::asio::buffer(request));

왜 이런 일이 발생할까요? HTTP평문(plaintext) 프로토콜입니다. TCP 위에서 데이터가 암호화 없이 전송되므로, 같은 네트워크에 있는 공격자가 패킷 스니핑(Wireshark 등)으로 요청·응답 내용을 그대로 볼 수 있습니다. 비밀번호, 세션 쿠키, API 키가 모두 노출됩니다. 결과:

  • 도청: 중간자(MITM)가 데이터를 가로챔
  • 변조: 요청/응답 내용을 중간에서 수정
  • 위장: 가짜 서버로 연결 유도 해결책: TLS(Transport Layer Security)를 TCP 위에 올려 암호화하고 서버 인증을 합니다. HTTPS, WSS(WebSocket Secure)가 모두 이 방식입니다.

추가 문제 시나리오

시나리오 2: IoT 기기 ↔ 클라우드 API 통신

센서 데이터를 HTTP로 전송하는데, 공장 내부 네트워크가 침해되면 제어 명령이 위조될 수 있습니다. mTLS(상호 인증)로 기기와 서버를 모두 검증해야 합니다. 시나리오 3: 마이크로서비스 간 내부 API

서비스 A가 서비스 B를 호출할 때, 평문 gRPC/HTTP는 같은 Kubernetes 클러스터 내에서도 스니핑 가능합니다. 내부 통신도 TLS로 암호화하고, 클라이언트 인증서로 호출자 신원을 확인하는 패턴이 권장됩니다. 시나리오 4: WebSocket 실시간 채팅

WSS 없이 WS만 쓰면 채팅 메시지가 평문으로 전송됩니다. 공용 와이파이에서 wscat 등으로 쉽게 도청할 수 있어, 실시간 서비스는 반드시 WSS를 사용해야 합니다. 목표:

  • TLS 역할 (암호화, 서버/클라이언트 인증)
  • SSL/TLS 핸드셰이크 시각화
  • OpenSSL + Asio 완전 통합 (서버/클라이언트)
  • 인증서 생성·관리 (자체 서명, CA)
  • 클라이언트 인증서 검증
  • 자주 발생하는 SSL 에러와 해결법
  • 성능 영향 비교
  • 프로덕션 배포 (Let’s Encrypt) 요구 환경: C++17 이상, Boost.Asio, OpenSSL 1.1+

1. TLS 개요

TLS가 하는 일

기능설명
암호화전송 데이터를 대칭키로 암호화 (AES 등)
서버 인증클라이언트가 서버 인증서로 신원 확인
클라이언트 인증 (선택)서버가 클라이언트 인증서 요구 (mTLS)
무결성메시지 인증 코드(MAC)로 변조 탐지

SSL vs TLS

  • SSL (Secure Sockets Layer): 구버전, 취약점 다수 → 사용 금지
  • TLS (Transport Layer Security): SSL의 후속, TLS 1.2/1.3 권장

2. SSL/TLS 핸드셰이크 다이어그램

TLS 1.2 핸드셰이크 흐름

sequenceDiagram
    participant C as 클라이언트
    participant S as 서버
    Note over C,S: 1. Client Hello
    C->>S: ClientHello (지원 TLS 버전, cipher suites, random)
    Note over C,S: 2. Server Hello
    S->>C: ServerHello (선택된 버전, cipher, random)
    S->>C: Certificate (서버 인증서)
    S->>C: ServerKeyExchange (선택)
    S->>C: ServerHelloDone
    Note over C,S: 3. 클라이언트 검증
    C->>C: 인증서 검증 (CA, 만료, 호스트명)
    C->>S: ClientKeyExchange (premaster secret 암호화)
    C->>S: ChangeCipherSpec
    C->>S: Finished (암호화됨)
    Note over C,S: 4. 서버 완료
    S->>C: ChangeCipherSpec
    S->>C: Finished (암호화됨)
    Note over C,S: 5. 암호화 통신 시작
    C->>S: Application Data (암호화)
    S->>C: Application Data (암호화)

핸드셰이크 단계 요약

flowchart LR
    subgraph Phase1["1단계: 협상"]
        A[Client Hello]
        B[Server Hello]
        C[Certificate]
    end
    subgraph Phase2["2단계: 키 교환"]
        D[ClientKeyExchange]
        E[ChangeCipherSpec]
    end
    subgraph Phase3["3단계: 암호화"]
        F[Finished]
        G[Application Data]
    end
    A --> B --> C --> D --> E --> F --> G

핵심: 핸드셰이크가 끝나면 대칭키가 협상되고, 이후 모든 Application Data는 이 키로 암호화됩니다. Asio의 async_handshake가 이 전체 과정을 처리합니다.

3. OpenSSL과 Asio 완전 통합

아키텍처

flowchart TB
    subgraph App[애플리케이션]
        Read[async_read_some]
        Write[async_write]
    end
    subgraph Asio[Boost.Asio]
        SSL["ssl stream"]
    end
    subgraph OpenSSL[OpenSSL]
        BIO[BIO]
        SSL_CTX[SSL_CTX]
    end
    subgraph TCP[TCP]
        Socket["tcp socket"]
    end
    App --> SSL
    SSL --> BIO
    BIO --> Socket
    SSL --> SSL_CTX

서버: TLS 에코 서버 완전 구현

#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <iostream>
#include <memory>
namespace ssl = boost::asio::ssl;
using tcp = boost::asio::ip::tcp;
class SslSession : public std::enable_shared_from_this<SslSession> {
    ssl::stream<tcp::socket> stream_;
    std::array<char, 1024> buffer_;
public:
    explicit SslSession(ssl::stream<tcp::socket> stream)
        : stream_(std::move(stream)) {}
    void start() {
        // 1. TLS 핸드셰이크 (서버 역할)
        stream_.async_handshake(
            ssl::stream_base::server,
            [self = shared_from_this()](boost::system::error_code ec) {
                if (!ec) {
                    self->do_read();
                } else {
                    std::cerr << "Handshake failed: " << ec.message() << "\n";
                }
            }
        );
    }
private:
    void do_read() {
        auto self = shared_from_this();
        stream_.async_read_some(
            boost::asio::buffer(buffer_),
            [this, self](boost::system::error_code ec, std::size_t length) {
                if (!ec) {
                    do_write(length);
                }
            }
        );
    }
    void do_write(std::size_t length) {
        auto self = shared_from_this();
        boost::asio::async_write(
            stream_,
            boost::asio::buffer(buffer_, length),
            [this, self](boost::system::error_code ec, std::size_t /*written*/) {
                if (!ec) {
                    do_read();  // 다음 읽기
                }
            }
        );
    }
};
class SslServer {
    tcp::acceptor acceptor_;
    ssl::context ctx_;
public:
    SslServer(boost::asio::io_context& io, uint16_t port)
        : acceptor_(io, tcp::endpoint(tcp::v4(), port)),
          ctx_(ssl::context::tls_server) {
        // 2. 인증서와 비밀키 로드
        ctx_.use_certificate_chain_file("server.crt");
        ctx_.use_private_key_file("server.key", ssl::context::pem);
        // 3. 보안 옵션
        ctx_.set_options(
            ssl::context::default_workarounds |
            ssl::context::no_sslv2 |
            ssl::context::no_sslv3
        );
        do_accept();
    }
private:
    void do_accept() {
        acceptor_.async_accept([this](boost::system::error_code ec, tcp::socket socket) {
            if (!ec) {
                auto ssl_stream = ssl::stream<tcp::socket>(std::move(socket), ctx_);
                std::make_shared<SslSession>(std::move(ssl_stream))->start();
            }
            do_accept();
        });
    }
};
int main() {
    boost::asio::io_context io;
    SslServer server(io, 8443);
    io.run();
    return 0;
}

클라이언트: TLS 클라이언트 완전 구현

#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <openssl/ssl.h>
#include <iostream>
namespace ssl = boost::asio::ssl;
using tcp = boost::asio::ip::tcp;
class SslClient {
    tcp::resolver resolver_;
    ssl::stream<tcp::socket> stream_;
    std::string host_;
    std::string port_;
public:
    SslClient(boost::asio::io_context& io, ssl::context& ctx,
              const std::string& host, const std::string& port)
        : resolver_(io),
          stream_(io, ctx),
          host_(host),
          port_(port) {}
    void connect() {
        resolver_.async_resolve(
            host_, port_,
            [this](boost::system::error_code ec, tcp::resolver::results_type results) {
                if (!ec) {
                    boost::asio::async_connect(
                        stream_.lowest_layer(),
                        results,
                        [this](boost::system::error_code ec, const tcp::endpoint&) {
                            if (!ec) {
                                do_handshake();
                            }
                        }
                    );
                }
            }
        );
    }
private:
    void do_handshake() {
        // 4. SNI(Server Name Indication) 설정 - 호스트명 검증에 필요
        SSL_set_tlsext_host_name(stream_.native_handle(), host_.c_str());
        stream_.async_handshake(
            ssl::stream_base::client,
            [this](boost::system::error_code ec) {
                if (!ec) {
                    do_write("Hello, TLS!");
                } else {
                    std::cerr << "Handshake failed: " << ec.message() << "\n";
                }
            }
        );
    }
    void do_write(const std::string& msg) {
        std::cout << "Sending: " << msg << "\n";
        boost::asio::async_write(
            stream_,
            boost::asio::buffer(msg),
            [this](boost::system::error_code ec, std::size_t) {
                if (!ec) {
                    do_read();
                }
            }
        );
    }
    void do_read() {
        auto buffer = std::make_shared<std::array<char, 1024>>();
        stream_.async_read_some(
            boost::asio::buffer(*buffer),
            [this, buffer](boost::system::error_code ec, std::size_t length) {
                if (!ec) {
                    std::cout << "Received: " << std::string(buffer->data(), length) << "\n";
                }
            }
        );
    }
};
int main() {
    boost::asio::io_context io;
    ssl::context ctx(ssl::context::tls_client);
    // 5. 인증서 검증 활성화 (중요!)
    ctx.set_default_verify_paths();
    ctx.set_verify_mode(ssl::verify_peer);
    SslClient client(io, ctx, "localhost", "8443");
    client.connect();
    io.run();
    return 0;
}

핵심 API 정리

API용도
ssl::context::tls_server / tls_client서버/클라이언트 컨텍스트
ctx.use_certificate_chain_file()인증서 체인 로드
ctx.use_private_key_file()비밀키 로드
ctx.set_verify_mode(verify_peer)인증서 검증 활성화
ctx.set_default_verify_paths()시스템 CA 인증서 사용
stream.async_handshake()TLS 핸드셰이크
stream.async_read_some() / async_write()암호화된 송수신

4. 순수 OpenSSL 예제 (Boost 없이)

Boost.Asio를 쓰지 않고 순수 OpenSSL API만으로 TLS 서버/클라이언트를 구현하는 방법입니다. 임베디드, 레거시 프로젝트, 또는 Asio 의존성을 줄이고 싶을 때 유용합니다.

순수 OpenSSL TLS 서버

// g++ -o ssl_server ssl_server.cpp -lssl -lcrypto
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <iostream>
int main() {
    SSL_library_init();
    SSL_load_error_strings();
    OpenSSL_add_all_algorithms();
    SSL_CTX* ctx = SSL_CTX_new(TLS_server_method());
    if (!ctx) {
        ERR_print_errors_fp(stderr);
        return 1;
    }
    // 인증서·비밀키 로드
    if (SSL_CTX_use_certificate_file(ctx, "server.crt", SSL_FILETYPE_PEM) <= 0 ||
        SSL_CTX_use_PrivateKey_file(ctx, "server.key", SSL_FILETYPE_PEM) <= 0) {
        ERR_print_errors_fp(stderr);
        SSL_CTX_free(ctx);
        return 1;
    }
    // SSLv2/3 비활성화
    SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    sockaddr_in addr{};
    addr.sin_family = AF_INET;
    addr.sin_port = htons(8443);
    addr.sin_addr.s_addr = INADDR_ANY;
    bind(sock, (sockaddr*)&addr, sizeof(addr));
    listen(sock, 5);
    while (true) {
        int client = accept(sock, nullptr, nullptr);
        if (client < 0) continue;
        SSL* ssl = SSL_new(ctx);
        SSL_set_fd(ssl, client);
        if (SSL_accept(ssl) <= 0) {
            ERR_print_errors_fp(stderr);
            SSL_shutdown(ssl);
            SSL_free(ssl);
            close(client);
            continue;
        }
        char buf[1024];
        int n = SSL_read(ssl, buf, sizeof(buf) - 1);
        if (n > 0) {
            buf[n] = '\0';
            SSL_write(ssl, buf, n);  // 에코
        }
        SSL_shutdown(ssl);
        SSL_free(ssl);
        close(client);
    }
    SSL_CTX_free(ctx);
    close(sock);
    return 0;
}

순수 OpenSSL TLS 클라이언트

// g++ -o ssl_client ssl_client.cpp -lssl -lcrypto
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>
#include <cstring>
#include <iostream>
int main() {
    SSL_library_init();
    SSL_load_error_strings();
    SSL_CTX* ctx = SSL_CTX_new(TLS_client_method());
    SSL_CTX_set_default_verify_paths(ctx);
    SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, nullptr);
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    sockaddr_in addr{};
    addr.sin_family = AF_INET;
    addr.sin_port = htons(8443);
    inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
    connect(sock, (sockaddr*)&addr, sizeof(addr));
    SSL* ssl = SSL_new(ctx);
    SSL_set_fd(ssl, sock);
    SSL_set_tlsext_host_name(ssl, "localhost");  // SNI
    if (SSL_connect(ssl) <= 0) {
        ERR_print_errors_fp(stderr);
        SSL_free(ssl);
        close(sock);
        return 1;
    }
    const char* msg = "Hello, OpenSSL!";
    SSL_write(ssl, msg, strlen(msg));
    char buf[1024];
    int n = SSL_read(ssl, buf, sizeof(buf) - 1);
    if (n > 0) {
        buf[n] = '\0';
        std::cout << "Received: " << buf << "\n";
    }
    SSL_shutdown(ssl);
    SSL_free(ssl);
    close(sock);
    SSL_CTX_free(ctx);
    return 0;
}

주의: 순수 OpenSSL은 동기(sync) API입니다. 고성능 비동기 서버가 필요하면 Boost.Asio SSL을 사용하세요.

5. 인증서 생성과 관리

자체 서명 인증서 (개발용)

# 1. 비밀키 생성 (2048비트 RSA)
openssl genrsa -out server.key 2048
# 2. 인증서 서명 요청(CSR) 생성
openssl req -new -key server.key -out server.csr
# 3. 자체 서명 인증서 생성 (유효기간 365일)
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
# 4. 한 줄로 통합
openssl req -x509 -newkey rsa:2048 -keyout server.key -out server.crt -days 365 -nodes \
  -subj "/CN=localhost"

주의: 자체 서명 인증서는 클라이언트에서 verify_none으로 건너뛰거나, set_verify_callback에서 수동 허용해야 합니다. 운영 환경에서는 사용 금지.

CA 서명 인증서 (개발/테스트용)

# 1. CA 비밀키와 인증서 생성
openssl genrsa -out ca.key 2048
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.crt \
  -subj "/CN=MyCA"
# 2. 서버 키 생성
openssl genrsa -out server.key 2048
# 3. CSR 생성 (CN=서버 도메인 중요!)
openssl req -new -key server.key -out server.csr
# 4. CA로 서버 인증서 서명
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out server.crt -days 365 -sha256
# 5. 클라이언트는 ca.crt를 load_verify_file로 로드

인증서 파일 형식

파일형식용도
server.keyPEM서버 비밀키 (절대 노출 금지)
server.crtPEM서버 인증서 (공개)
ca.crtPEMCA 인증서 (클라이언트 검증용)
server.pemPEM인증서+키 합친 파일 (일부 사용)

인증서 검사

# 인증서 내용 확인
openssl x509 -in server.crt -text -noout
# 만료일 확인
openssl x509 -in server.crt -enddate -noout
# 연결 테스트
openssl s_client -connect localhost:8443 -showcerts

5. 클라이언트 인증서 검증

클라이언트 인증(mTLS)이란?

서버가 클라이언트의 인증서를 요구해, “이 클라이언트는 신뢰할 수 있다”고 확인하는 방식입니다. API 서버, IoT 기기, 내부 서비스 간 통신에 사용합니다.

서버 설정: 클라이언트 인증서 요구

ssl::context ctx(ssl::context::tls_server);
ctx.use_certificate_chain_file("server.crt");
ctx.use_private_key_file("server.key", ssl::context::pem);
// 클라이언트 인증서 요구 (필수)
ctx.set_verify_mode(ssl::verify_peer | ssl::verify_fail_if_no_peer_cert);
// 클라이언트 인증서를 검증할 CA 인증서
ctx.load_verify_file("ca.crt");
// 클라이언트 인증서에서 CN 추출 (선택)
ctx.set_verify_callback(
     {
        if (!preverified) return false;
        // 추가 검증: CN, OU 등 확인
        return true;
    }
);

클라이언트: 인증서 전송

// 클라이언트 측: 인증서와 키 로드
ctx.use_certificate_chain_file("client.crt");
ctx.use_private_key_file("client.key", ssl::context::pem);
ctx.load_verify_file("ca.crt");  // 서버 인증서 검증용
ctx.set_verify_mode(ssl::verify_peer);

클라이언트 인증서 생성

# CA로 클라이언트 인증서 서명
openssl genrsa -out client.key 2048
openssl req -new -key client.key -out client.csr -subj "/CN=client1"
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out client.crt -days 365 -sha256

6. 자주 발생하는 SSL 에러

에러 1: 인증서 만료 (Certificate Expired)

증상:

handshake failed: certificate verify failed

원인: 서버 인증서의 notAfter 날짜가 지남. 해결:

# 만료일 확인
openssl x509 -in server.crt -enddate -noout
# notAfter=Mar  9 12:00:00 2026 GMT
# 새 인증서 발급 (Let's Encrypt는 certbot renew)
// 에러 코드로 구분 (openssl/err.h, openssl/x509.h 필요)
if (ec.category() == boost::asio::error::get_ssl_category()) {
    auto err = ERR_get_error();
    if (ERR_GET_REASON(err) == X509_V_ERR_CERT_HAS_EXPIRED) {
        spdlog::error("Certificate expired - renew required");
    }
}

에러 2: 호스트명 불일치 (Hostname Mismatch)

증상:

handshake failed: certificate verify failed

원인: 인증서의 CN/Subject Alternative Name과 연결한 호스트명이 다름. 예: localhost로 연결했는데 인증서는 example.com. 해결:

// 1. SNI 설정 (필수!)
SSL_set_tlsext_host_name(stream.native_handle(), "example.com");
// 2. 호스트명 검증 콜백 (OpenSSL 기본은 CN만 검사)
ctx.set_verify_callback(
    ssl::rfc2818_verification("example.com")
);
// 또는 수동 검증
ctx.set_verify_callback(
    [host = std::string("example.com")](bool preverified, ssl::verify_context& ctx) {
        if (!preverified) return false;
        X509* cert = X509_STORE_CTX_get_current_cert(ctx.native_handle());
        return ssl::rfc2818_verification(host)(preverified, ctx);
    }
);

에러 3: 자체 서명 인증서 (Self-Signed Certificate)

증상: 클라이언트에서 verify_peer 시 검증 실패. 해결 (개발 환경만):

// ❌ 운영에서는 절대 사용 금지!
ctx.set_verify_mode(ssl::verify_none);
// ✅ 개발: CA 인증서를 load_verify_file로 지정
ctx.load_verify_file("ca.crt");  // 자체 서명 인증서를 서명한 CA
ctx.set_verify_mode(ssl::verify_peer);

에러 4: 프로토콜 버전 불일치

증상:

handshake failed: wrong version number

원인: 클라이언트/서버가 지원하는 TLS 버전이 맞지 않음 (예: SSLv3만 지원하는 구형 서버). 해결:

// TLS 1.2 이상만 허용
ctx.set_options(ssl::context::no_sslv2 | ssl::context::no_sslv3);
// TLS 1.3 명시 (OpenSSL 1.1.1+)
ctx.set_options(ssl::context::no_sslv2 | ssl::context::no_sslv3);
// 기본이 TLS 1.2/1.3이면 추가 설정 불필요

에러 5: 연결 끊김 (Safari WSS 등)

원인: ssl::streamwebsocket::streamthread-safe하지 않음. 멀티스레드에서 동시 접근 시 연결 불안정. 해결: strand로 직렬화

auto ws_strand = boost::asio::make_strand(ioc);
websocket::stream<ssl::stream<tcp::socket>> ws(ws_strand, ssl_ctx);
ws.async_handshake(host, "/",
    boost::asio::bind_executor(ws_strand,  { /* ....*/ }));
ws.async_read(buffer,
    boost::asio::bind_executor(ws_strand,  { /* ....*/ }));

에러 6: 인증서 체인 불완전 (Certificate Chain Incomplete)

증상:

unable to get local issuer certificate

원인: 서버가 server.crt만 전송하고 중간 CA 인증서를 포함하지 않음. 클라이언트가 루트 CA까지 체인을 검증하지 못함. 해결:

# fullchain.pem = 서버 인증서 + 중간 CA (체인)
cat server.crt intermediate.crt > fullchain.pem
// 서버: 체인 전체 로드
ctx.use_certificate_chain_file("fullchain.pem");  // ✅ 체인 포함
// ctx.use_certificate_file("server.crt");       // ❌ 단일 인증서만

에러 7: 비밀키 불일치 (Key/Certificate Mismatch)

증상:

key values mismatch

원인: server.crtserver.key가 서로 다른 키 쌍에 속함. 인증서 재발급 후 키를 바꾸지 않았거나, 잘못된 파일을 로드한 경우. 해결:

# 인증서와 키가 쌍인지 확인
openssl x509 -noout -modulus -in server.crt | openssl md5
openssl rsa -noout -modulus -in server.key | openssl md5
# 두 해시가 같아야 함

에러 8: SSL_shutdown 실패 (Broken Pipe)

증상: SSL_shutdown 호출 시 SSL_ERROR_SYSCALL 또는 BROKEN PIPE. 원인: 상대가 이미 연결을 끊었을 때 정상적인 shutdown을 시도하면 실패할 수 있음. 해결:

// Graceful shutdown: 실패해도 무시하고 정리
void close_connection() {
    boost::system::error_code ec;
    stream_.shutdown(ec);  // ec 무시 가능
    stream_.lowest_layer().close(ec);
}

에러 코드 참조

OpenSSL 에러의미
X509_V_ERR_CERT_HAS_EXPIRED인증서 만료
X509_V_ERR_CERT_NOT_YET_VALID인증서 아직 유효하지 않음
X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT자체 서명 인증서
X509_V_ERR_HOSTNAME_MISMATCH호스트명 불일치
SSL_R_UNKNOWN_PROTOCOL프로토콜 버전 불일치
X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT인증서 체인 불완전
SSL_R_SSLV3_ALERT_HANDSHAKE_FAILURE핸드셰이크 실패 (키 불일치 등)

8. 성능 영향 비교

TLS 오버헤드 요약

항목영향
핸드셰이크최초 1회, RTT 1~2회 추가 (지연)
암호화/복호화CPU 사용량 증가 (AES-NI 있으면 미미)
메모리세션당 수 KB 추가
지연핸드셰이크 후에는 평문과 유사

벤치마크 (참고치)

통신 방식초당 요청 수 (단일 연결)연결당 지연
평문 TCP~50,0000.02ms
TLS 1.2~45,0000.025ms
TLS 1.3~48,0000.022ms
결론: 현대 CPU(AES-NI)에서는 TLS 오버헤드가 5~10% 수준. 보안 이득에 비해 수용 가능합니다.

최적화 팁

// 1. 세션 재사용 (Session Resumption) - 핸드셰이크 생략
// OpenSSL 기본 활성화됨
// 2. TLS 1.3 사용 (1-RTT 핸드셰이크)
// OpenSSL 1.1.1+ 기본
// 3. 적절한 cipher suite
ctx.set_options(ssl::context::default_workarounds);
// AES-GCM 사용 cipher 우선 (하드웨어 가속)

9. 프로덕션 배포 (Let’s Encrypt)

Let’s Encrypt 개요

  • 무료 공인 인증서
  • 90일 유효기간 (자동 갱신 권장)
  • certbot으로 발급·갱신 자동화

certbot으로 인증서 발급

# 1. certbot 설치 (Ubuntu/Debian)
sudo apt install certbot
# 2. HTTP-01 챌린지 (웹 서버가 80 포트에서 동작 필요)
sudo certbot certonly --standalone -d example.com
# 3. 인증서 위치
# /etc/letsencrypt/live/example.com/fullchain.pem  (인증서 체인)
# /etc/letsencrypt/live/example.com/privkey.pem   (비밀키)

C++ 서버에서 Let’s Encrypt 인증서 사용

ssl::context ctx(ssl::context::tls_server);
// fullchain.pem = 서버 인증서 + 중간 CA (체인)
ctx.use_certificate_chain_file("/etc/letsencrypt/live/example.com/fullchain.pem");
ctx.use_private_key_file("/etc/letsencrypt/live/example.com/privkey.pem", ssl::context::pem);

자동 갱신 (cron)

# 매일 새벽 2시 갱신 시도
0 2 * * * certbot renew --quiet --deploy-hook "systemctl reload myapp"

갱신 후 서버 재시작

// inotify 또는 systemd socket activation으로 인증서 변경 감지
// 또는 주기적으로 certbot renew 후 프로세스 재시작

프로덕션 체크리스트

  • Let’s Encrypt 또는 유료 CA 인증서 사용
  • TLS 1.2 이상만 허용 (SSLv2/3 비활성화)
  • verify_peer 활성화 (클라이언트)
  • SNI 설정 (가상 호스트)
  • 인증서 만료 모니터링 (90일 주기)
  • 비밀키 권한 600, root만 읽기

10. 모범 사례와 프로덕션 패턴

모범 사례 (Best Practices)

항목권장비권장
TLS 버전TLS 1.2, TLS 1.3SSLv2, SSLv3
인증서 검증verify_peer (운영)verify_none (운영)
Cipher SuiteAES-GCM, ChaCha20-Poly1305RC4, 3DES, NULL
키 길이RSA 2048+, ECDSA P-256+RSA 1024
인증서fullchain (체인 포함)단일 인증서만
비밀키파일 권한 600, root만world-readable

프로덕션 패턴 1: 인증서 핫 리로드

Let’s Encrypt 갱신 후 서버 재시작 없이 인증서를 다시 로드하는 패턴입니다.

// inotify로 fullchain.pem/privkey.pem 변경 감지 → reload_ssl_context() 호출
void reload_ssl_context() {
    ssl::context new_ctx(ssl::context::tls_server);
    new_ctx.use_certificate_chain_file("fullchain.pem");
    new_ctx.use_private_key_file("privkey.pem", ssl::context::pem);
    ctx_.swap(new_ctx);  // atomic 교체 (기존 연결은 이전 context, 새 연결만 새 인증서)
}

프로덕션 패턴 2: TLS 종료 프록시 (Reverse Proxy)

C++ 애플리케이션 앞단에 Nginx/HAProxy가 TLS를 처리하고, 백엔드는 평문으로 받는 패턴입니다.

flowchart LR
    Client[클라이언트] -->|HTTPS| Proxy[Nginx/HAProxy]
    Proxy -->|HTTP 평문| App[C++ 앱]
# Nginx 예시: TLS 종료 후 localhost:8080으로 전달
server {
    listen 443 ssl;
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

장점: 인증서 갱신을 Nginx만 재시작하면 됨. C++ 앱은 TLS 코드 불필요.
단점: Nginx ↔ 앱 구간이 평문이므로, 같은 호스트 내부에서만 사용해야 함.

프로덕션 패턴 3: mTLS + RBAC

클라이언트 인증서의 CN/OU로 역할을 판별해 RBAC를 적용합니다. SSL_get_peer_certificateX509_NAME_get_text_by_NID(NID_organizationalUnitName)로 OU 추출 후, /admin/* 등 경로 접근을 제한하세요.

프로덕션 패턴 4: 연결 풀 + TLS 세션 재사용

다운스트림 연결 시 핸드셰이크 완료된 스트림을 풀에 보관해 재사용하면 핸드셰이크 비용을 줄일 수 있습니다.

프로덕션 체크리스트 (확장)

항목확인
HSTS 헤더Strict-Transport-Security (프록시에서)
OCSP Stapling인증서 폐기 상태 실시간 확인 (Nginx 등)
로깅handshake 실패 시 ERR_get_error() 로그
모니터링인증서 만료 30일 전 알림
비밀키HSM 또는 시크릿 매니저 사용 (고보안)

11. 실무 주의사항

버전 및 보안

  • TLS 1.2 이상 권장
  • TLS 1.3 지원 시 1-RTT 핸드셰이크로 지연 감소
  • SSLv2/SSLv3 비활성화

에러 처리

  • handshake/read/write 실패 시 error_code 확인
  • SSL 에러는 ec.category() == get_ssl_category()로 구분
  • 로그에 ERR_get_error() 상세 메시지 기록

리소스 관리

  • ssl::context는 서버당 1개, 재사용
  • ssl::stream은 연결당 1개
  • 연결 종료 시 stream.shutdown() 호출

실무 사례: Safari WSS 연결 끊김

멀티스레드 환경에서 ssl::streamwebsocket::stream에 동시 접근하면 Safari에서 연결이 끊깁니다. strand로 모든 비동기 작업을 직렬화하면 해결됩니다.

체크리스트

구현 체크리스트

  • ssl::context 서버/클라이언트 구분
  • 인증서·비밀키 파일 로드
  • set_verify_mode(verify_peer) (운영)
  • SNI 설정 (클라이언트)
  • 에러 처리 (handshake 실패)
  • strand 사용 (WSS 멀티스레드)

프로덕션 체크리스트

  • Let’s Encrypt 또는 공인 CA 인증서
  • TLS 1.2 이상
  • 인증서 갱신 자동화
  • 비밀키 권한 600

정리

항목내용
ssl::streamTCP 소켓 위 TLS 레이어
handshake클라이언트/서버 각각 async_handshake
서버use_certificate_chain_file, use_private_key_file
클라이언트set_verify_mode(verify_peer), set_default_verify_paths
인증서자체 서명(개발), Let’s Encrypt(운영)
에러만료, 호스트명, 자체 서명, strand

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. HTTPS 서버, WSS(WebSocket Secure), API 서버, IoT 보안 통신 등 TLS 암호화가 필요한 모든 C++ 네트워크 애플리케이션에서 사용합니다.

Q. 자체 서명 인증서를 운영에서 써도 되나요?

A. 안 됩니다. 브라우저·클라이언트에서 경고가 뜨고, 중간자 공격에 취약합니다. Let’s Encrypt(무료) 또는 유료 CA를 사용하세요.

Q. TLS 성능이 걱정돼요.

A. AES-NI를 지원하는 현대 CPU에서는 오버헤드가 5~10% 수준입니다. TLS 1.3은 1-RTT 핸드셰이크로 지연도 줄었습니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. OpenSSL 문서, RFC 8446 (TLS 1.3), Boost.Asio SSL을 참고하세요. 한 줄 요약: OpenSSL·Asio로 SSL/TLS 암호화 통신을 구성할 수 있습니다. 인증서 검증을 켜고, 운영에서는 Let’s Encrypt를 사용하세요. 이전 글: C++ 실전 가이드 #30-1: WebSocket 다음 글: C++ 실전 가이드 #30-3: 프로토콜 설계와 직렬화

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

Practical tips (TLS/SSL)

  • Verify trust chain and hostname (SNI, SANs) before debugging app logic.
  • Use openssl s_client (or similar) to inspect handshake, cipher suite, and ALPN outside your code.
  • Profile crypto and I/O together; latency and CPU often move together.

Checklist

  • TLS version and cipher policy match deployment?
  • Failure modes tested (expired cert, hostname mismatch, handshake errors)?

이 글에서 다루는 키워드 (관련 검색어)

C++, SSL, TLS, OpenSSL, Asio, 보안, HTTPS, 인증서, LetsEncrypt 등으로 검색하시면 이 글이 도움이 됩니다.

관련 글