본문 바로가기
TYLER SONGBlog
← Blog index
CS13 min read

TLS: 인증된 암호화 채널을 만드는 전송 계층 보안

전송 계층 보안(TLS, Transport Layer Security)은 서버 신원을 인증하고, 통신 내용을 암호화하며, 데이터 변조를 탐지하는 프로토콜이다. 공개 키 암호는 주로 인증과 키 합의에 사용하고, 실제 데이터는 성능이 좋은 대칭 키로 암호화한다. HTTPS는 HTTP를 TLS 채널 위에서 전송한다.

By 송민성

1. 개념

전송 계층 보안(TLS, Transport Layer Security)은 클라이언트와 서버 사이에 인증된 암호화 채널을 만드는 프로토콜이다. 주로 전송 제어 프로토콜(TCP) 위에서 동작하며, HTTPS에서는 기본적으로 TCP 443번 포트를 사용한다.

TLS가 제공하는 보안 속성은 세 가지다.

  • 기밀성(Confidentiality): 데이터를 암호화해 제삼자가 내용을 읽지 못하게 한다.
  • 무결성(Integrity): 전송 중 데이터가 변경됐는지 검증한다.
  • 인증(Authentication): 인증서와 전자서명(Digital Signature)으로 통신 상대의 신원을 확인한다.

TLS에는 역할이 다른 여러 키가 등장한다.

키역할일반적인 수명
서버 인증서의 공개 키서버 서명 검증인증서 유효 기간
서버 개인 키서버 신원 증명용 서명장기간 보관
키 교환용 임시 키공유 비밀 계산연결 또는 핸드셰이크 단위
트래픽 키(Traffic Key)실제 데이터 암호화세션 단위

서버 인증서의 공개 키로 모든 데이터를 직접 암호화하지 않는다. 공개 키 연산은 상대적으로 비용이 크고 큰 데이터를 처리하기에 적합하지 않다. TLS는 핸드셰이크(Handshake)에서 공유 비밀을 만들고, 여기서 대칭 키를 유도해 실제 데이터를 암호화한다.

2. 왜 사용하는가

암호화되지 않은 HTTP를 사용하면 네트워크 경로에 있는 공격자가 요청과 응답을 읽거나 변경할 수 있다.

예를 들어 다음 정보가 노출될 수 있다.

  • 로그인 비밀번호와 세션 쿠키
  • 접근 토큰과 인증 헤더
  • 요청 주소와 폼 데이터
  • 서버가 반환한 개인정보
  • 내려받는 자바스크립트 코드

암호화만으로는 충분하지 않다. 공격자가 클라이언트와 서버 사이에서 각각 별도 연결을 만들면 중간자 공격(MITM, Man-in-the-Middle Attack)이 가능하다. TLS는 인증 기관(CA, Certificate Authority)이 서명한 인증서를 검증해 접속 대상의 신원도 확인한다.

TLS는 다음과 같은 프로토콜에서 사용된다.

  • HTTPS
  • 보안 웹소켓(WSS, WebSocket Secure)
  • 보안 전자우편 전송
  • 데이터베이스 암호화 연결
  • 응용 프로그램 간 API 통신
  • 상호 TLS(mTLS, Mutual TLS)를 사용하는 내부 서비스 통신

3. 동작 원리

3.1 TCP 연결

일반적인 HTTPS 연결에서는 먼저 TCP 연결을 수립한다.

text
클라이언트                             서버
    │                                   │
    │──────── TCP 연결 요청 ───────────>│
    │<─────── TCP 연결 응답 ────────────│
    │──────── TCP 연결 확인 ───────────>│

TCP 연결만으로는 데이터가 암호화되지 않는다. TLS 핸드셰이크가 완료된 뒤에야 보호된 응용 데이터(Application Data)를 주고받을 수 있다.

3.2 클라이언트 헬로

클라이언트는 클라이언트 헬로(ClientHello)를 보낸다. TLS 1.3에서는 다음 정보가 포함될 수 있다.

  • 지원하는 TLS 버전
  • 지원하는 암호 스위트(Cipher Suite)
  • 클라이언트 난수
  • 키 합의용 공개값
  • 서버 이름 표시(SNI, Server Name Indication)
  • 응용 계층 프로토콜 협상(ALPN, Application-Layer Protocol Negotiation) 목록

서버 이름 표시(SNI)는 하나의 IP 주소에서 여러 도메인을 운영할 때 서버가 적절한 인증서를 선택하도록 돕는다.

응용 계층 프로토콜 협상(ALPN)은 TLS 연결 위에서 HTTP/1.1과 HTTP/2 중 어떤 프로토콜을 사용할지 협상하는 데 사용된다.

3.3 서버 헬로와 인증서

서버는 서버 헬로(ServerHello)로 선택한 TLS 버전, 암호 스위트, 키 합의용 공개값을 보낸다. 이어서 인증서 체인(Certificate Chain)과 핸드셰이크 데이터에 대한 전자서명을 전달한다.

text
클라이언트                             서버
    │──────── ClientHello ─────────────>│
    │<─────── ServerHello ──────────────│
    │<─────── 인증서 체인 ──────────────│
    │<─────── 전자서명 ─────────────────│

클라이언트는 서버 인증서를 다음 순서로 검증한다.

  1. 접속한 호스트 이름이 인증서의 주체 대체 이름(SAN, Subject Alternative Name)에 포함되는지 확인한다.
  2. 현재 시간이 인증서 유효 기간 안에 있는지 확인한다.
  3. 인증서 체인의 서명을 차례대로 검증한다.
  4. 체인의 최상위 인증 기관이 운영체제나 응용 프로그램의 신뢰 저장소(Trust Store)에 있는지 확인한다.
  5. 서버가 인증서 공개 키에 대응하는 개인 키를 소유하는지 전자서명으로 검증한다.

인증 기관의 서명은 해당 서버를 무조건 안전한 서비스로 보증하지 않는다. 인증서에 적힌 공개 키와 도메인의 연결을 보증한다.

3.4 키 합의와 키 유도

TLS 1.3은 일반적으로 타원 곡선 디피-헬먼 임시 키 합의(ECDHE, Elliptic Curve Diffie–Hellman Ephemeral)를 사용한다.

클라이언트와 서버는 각자의 개인값을 전송하지 않고 공개값만 교환한다. 양쪽은 공개값과 자신의 개인값을 조합해 동일한 공유 비밀(Shared Secret)을 계산한다.

text
클라이언트                            서버
개인값 a                              개인값 b
공개값 A                              공개값 B

A 전송 ──────────────────────────────>
<────────────────────────────── B 전송

공유 비밀 계산                        공유 비밀 계산
f(a, B)                               f(b, A)
       두 계산 결과는 같음

공유 비밀 자체를 트래픽 키로 바로 사용하지 않는다. 키 유도 함수(HKDF, HMAC-based Key Derivation Function)가 공유 비밀과 핸드셰이크 정보를 조합해 용도별 키를 만든다.

  • 클라이언트 송신용 트래픽 키
  • 서버 송신용 트래픽 키
  • 초기화 벡터(IV, Initialization Vector)
  • 핸드셰이크 검증용 키

송신 방향마다 다른 키를 사용하므로 클라이언트와 서버의 암호화 상태가 분리된다.

3.5 핸드셰이크 검증

양쪽은 지금까지 교환한 핸드셰이크 메시지 전체의 해시를 이용해 완료 메시지(Finished Message)를 검증한다.

공격자가 암호 스위트나 키 교환 값을 바꾸면 핸드셰이크 해시가 달라진다. 완료 메시지 검증이 실패하므로 연결이 종료된다.

text
클라이언트                             서버
    │<────── 암호화된 Finished ─────────│
    │──────── 암호화된 Finished ───────>│
    │                                   │
    │<════ 인증·암호화된 통신 ═════════>│

3.6 응용 데이터 암호화

핸드셰이크가 끝나면 HTTP 요청과 응답은 TLS 레코드(Record)에 담긴다.

현대 TLS는 인증된 암호화(AEAD, Authenticated Encryption with Associated Data)를 사용한다. 대표적인 알고리즘은 다음과 같다.

  • AES-GCM
  • ChaCha20-Poly1305

인증된 암호화는 기밀성과 무결성을 함께 제공한다. 암호문이나 관련 메타데이터가 변경되면 인증 태그 검증에 실패하고 해당 레코드는 거부된다.

3.7 세션 재개

이전에 연결한 클라이언트는 세션 재개(Session Resumption)를 사용할 수 있다. TLS 1.3에서는 서버가 발급한 사전 공유 키(PSK, Pre-Shared Key) 기반 세션 티켓을 이용할 수 있다.

세션 재개는 이전 세션의 정보를 활용해 새 핸드셰이크 비용을 줄인다. 새 연결이므로 트래픽 키는 다시 유도한다.

TLS 1.3의 조기 데이터(0-RTT Early Data)는 핸드셰이크 완료 전에 응용 데이터를 보낼 수 있지만 재전송 공격(Replay Attack)에 노출될 수 있다. 결제, 상태 변경, 일회성 토큰 소비 같은 요청에는 사용하지 않아야 한다.

4. 코드 예제

다음 예제는 자체 서명 인증서를 만들고, 타입스크립트(TypeScript) HTTPS 서버와 클라이언트를 실행한다. 클라이언트는 생성한 인증서를 신뢰 기관으로 직접 지정한다.

4.1 인증서 생성

shell
openssl req \ -x509 \ -newkey rsa:2048 \ -sha256 \ -nodes \ -days 30 \ -keyout server-key.pem \ -out server-cert.pem \ -subj "/CN=localhost" \ -addext "subjectAltName=DNS:localhost"

server-key.pem은 서버 개인 키다. 외부에 공개하거나 저장소에 커밋하면 안 된다.

4.2 실행 환경 준비

shell
npm init -y npm install --save-dev typescript tsx @types/node

4.3 HTTPS 서버

server.ts 파일을 만든다.

typescript
import { readFileSync } from "node:fs"; import { createServer } from "node:https"; const server = createServer( { key: readFileSync("server-key.pem"), cert: readFileSync("server-cert.pem"), minVersion: "TLSv1.2", }, (request, response) => { const tlsSocket = request.socket; response.writeHead(200, { "content-type": "application/json; charset=utf-8", }); response.end( JSON.stringify({ message: "TLS 연결 성공", protocol: tlsSocket.getProtocol(), cipher: tlsSocket.getCipher(), }), ); }, ); server.listen(8443, "localhost", () => { console.log("https://localhost:8443 에서 실행 중"); });

서버를 실행한다.

shell
npx tsx server.ts

4.4 인증서를 검증하는 HTTPS 클라이언트

client.ts 파일을 만든다.

typescript
import { readFileSync } from "node:fs"; import { request } from "node:https"; const req = request( { hostname: "localhost", port: 8443, path: "/", method: "GET", ca: readFileSync("server-cert.pem"), rejectUnauthorized: true, }, (response) => { response.setEncoding("utf8"); let body = ""; response.on("data", (chunk: string) => { body += chunk; }); response.on("end", () => { console.log(`상태 코드: ${response.statusCode}`); console.log(JSON.parse(body)); }); }, ); req.on("socket", (socket) => { socket.on("secureConnect", () => { console.log(`협상된 TLS 버전: ${socket.getProtocol()}`); console.log("인증서 검증 완료"); }); }); req.on("error", (error: Error) => { console.error(`연결 실패: ${error.message}`); process.exitCode = 1; }); req.end();

다른 터미널에서 클라이언트를 실행한다.

shell
npx tsx client.ts

예상 출력은 다음과 같은 형태다. 실제 암호 스위트는 실행 환경에 따라 달라질 수 있다.

text
협상된 TLS 버전: TLSv1.3
인증서 검증 완료
상태 코드: 200
{
  message: 'TLS 연결 성공',
  protocol: 'TLSv1.3',
  cipher: {
    name: 'TLS_AES_256_GCM_SHA384',
    standardName: 'TLS_AES_256_GCM_SHA384',
    version: 'TLSv1.3'
  }
}

자체 서명 인증서를 시스템이 자동으로 신뢰하지 않는 것은 정상이다. 예제에서는 ca 옵션으로 신뢰할 인증서를 명시했다. rejectUnauthorized: false로 검증을 끄면 암호화는 유지되지만 서버 신원을 확인할 수 없어 중간자 공격을 막지 못한다.

4.5 명령줄에서 TLS 연결 확인

shell
openssl s_client \ -connect localhost:8443 \ -servername localhost \ -CAfile server-cert.pem

출력에서 다음 항목을 확인할 수 있다.

  • 협상된 TLS 버전
  • 선택된 암호 스위트
  • 서버 인증서 체인
  • 인증서 검증 결과
  • 응용 계층 프로토콜 협상 결과

5. 시간 복잡도 또는 성능 특성

TLS의 비용은 핸드셰이크와 데이터 전송으로 나눌 수 있다.

핸드셰이크 지연

TCP 기반 TLS 1.3의 일반적인 새 연결은 다음 네트워크 왕복을 거친다.

  • TCP 연결 수립: 1회 왕복 시간(RTT)
  • TLS 1.3 핸드셰이크: 응용 데이터를 보내기 전 1회 왕복 시간
  • TLS 1.2 전체 핸드셰이크: 일반적으로 응용 데이터를 보내기 전 2회 왕복 시간

따라서 연결을 매 요청마다 새로 만들면 네트워크 왕복 지연이 반복된다. HTTP 연결 재사용, HTTP/2 다중화(Multiplexing), 세션 재개가 이 비용을 줄인다.

암호화 비용

핸드셰이크에서는 키 교환과 전자서명 검증 같은 공개 키 연산이 발생한다. 연결이 만들어진 뒤의 데이터는 대칭 키 기반 인증된 암호화로 처리한다.

길이가 n바이트인 데이터를 암호화하거나 복호화하는 연산량은 데이터 길이에 비례하므로 O(n)이다. 실제 처리량은 암호 알고리즘, 중앙 처리 장치(CPU)의 명령어 지원, 레코드 크기, 구현에 따라 달라진다.

순방향 비밀성

임시 키를 사용하는 ECDHE는 순방향 비밀성(Forward Secrecy)을 제공한다. 공격자가 나중에 서버 장기 개인 키를 얻더라도 과거 연결에서 사용한 임시 개인 키가 남아 있지 않으면 기록해 둔 과거 트래픽의 세션 키를 계산할 수 없다.

레코드 오버헤드

TLS는 응용 데이터를 레코드로 나누고 각 레코드에 헤더와 인증 태그를 추가한다. 작은 메시지를 매우 자주 보내면 실제 데이터에 비해 레코드와 네트워크 처리 오버헤드의 비율이 커질 수 있다.

6. 실무 사용 사례

웹 서비스의 HTTPS

브라우저와 웹 서버 사이의 HTTP 요청을 보호한다. 로그인 정보, 쿠키, 응답 본문뿐 아니라 요청 경로와 헤더도 TLS 내부에서 암호화된다.

다만 다음 정보까지 모두 숨겨지는 것은 아니다.

  • 목적지 IP 주소
  • 트래픽 크기와 전송 시점
  • 일부 환경에서 노출될 수 있는 서버 이름 정보
  • 도메인 이름 시스템(DNS) 질의 정보

역방향 프록시에서 TLS 종료

엔진엑스(Nginx), 로드 밸런서, 콘텐츠 전송 네트워크(CDN)가 TLS 종료(TLS Termination)를 담당할 수 있다.

text
브라우저
   │ HTTPS
   ▼
역방향 프록시
   │ HTTP 또는 HTTPS
   ▼
응용 서버

프록시 뒤쪽 네트워크도 신뢰할 수 없다면 응용 서버까지 TLS를 유지해야 한다. 외부 연결만 암호화하고 내부 연결을 평문으로 두면 내부 네트워크에서 데이터가 노출될 수 있다.

상호 TLS

일반적인 HTTPS에서는 클라이언트가 서버 인증서를 검증한다. 상호 TLS에서는 서버도 클라이언트 인증서를 요청하고 검증한다.

상호 TLS는 다음 환경에서 사용된다.

  • 내부 마이크로서비스 인증
  • 기업용 장치 인증
  • 서비스 메시(Service Mesh)
  • 제한된 관리 API
  • 기업 간 시스템 연동

상호 TLS는 서비스나 장치 신원을 강하게 확인할 수 있지만, 인증서 발급·갱신·폐기 자동화가 필요하다.

데이터베이스 연결

응용 서버와 데이터베이스 사이에서도 TLS를 사용할 수 있다. 연결 문자열에 TLS 사용 여부뿐 아니라 서버 인증서 검증 방식도 설정해야 한다.

암호화만 활성화하고 인증서 검증을 끄면 잘못된 데이터베이스 서버로 연결될 수 있다.

7. 주의할 점

인증서 검증을 끄지 않는다

다음 설정은 개발 중 오류를 숨길 수 있지만 운영 환경에서는 사용하면 안 된다.

typescript
rejectUnauthorized: false

이 설정은 인증서 체인과 호스트 이름 검증을 비활성화한다. 연결 내용은 암호화돼도 상대가 실제 서버인지 보장할 수 없다.

개인 키를 보호한다

서버 개인 키가 유출되면 공격자가 서버를 사칭할 수 있다.

  • 소스 코드 저장소에 커밋하지 않는다.
  • 파일 접근 권한을 제한한다.
  • 비밀 관리 시스템이나 하드웨어 보안 모듈(HSM, Hardware Security Module)을 사용한다.
  • 유출 시 인증서를 폐기하고 새 키와 인증서를 발급한다.
  • 여러 환경에서 같은 개인 키를 공유하지 않는다.

인증서 갱신을 자동화한다

인증서가 만료되면 클라이언트의 연결이 실패한다. 자동 발급과 갱신을 구성하고 만료 시점을 감시해야 한다. 새 인증서가 배포됐는지도 실제 외부 연결로 확인해야 한다.

오래된 프로토콜을 허용하지 않는다

보안 소켓 계층(SSL, Secure Sockets Layer)과 TLS 1.0, TLS 1.1은 사용하지 않는다. 서버와 클라이언트가 지원한다면 TLS 1.3을 우선 사용하고, 호환성이 필요하면 TLS 1.2를 함께 허용한다.

호스트 이름 검증 실패를 원인별로 처리한다

인증서 오류가 발생하면 다음 항목을 확인한다.

  1. 접속한 호스트 이름이 인증서의 주체 대체 이름에 있는가
  2. 인증서가 만료되지 않았는가
  3. 중간 인증서가 서버에 올바르게 설치됐는가
  4. 클라이언트의 신뢰 저장소가 올바른가
  5. 시스템 시간이 정확한가

오류를 해결하기 위해 검증 전체를 끄면 원래 문제와 중간자 공격을 구분할 수 없어진다.

TLS가 응용 프로그램 취약점을 해결하지는 않는다

TLS는 전송 중인 데이터를 보호한다. 다음 문제는 별도로 처리해야 한다.

  • 구조화 질의 언어 삽입(SQL Injection)
  • 사이트 간 스크립팅(XSS, Cross-Site Scripting)
  • 서버 측 요청 위조(SSRF, Server-Side Request Forgery)
  • 취약한 세션 관리
  • 서버 내부의 평문 로그
  • 권한 검사 누락

8. 핵심 정리

  • TLS는 기밀성, 무결성, 상대 인증을 제공한다.
  • 인증서는 도메인과 공개 키의 연결을 인증 기관의 서명으로 증명한다.
  • 공개 키 암호는 인증과 키 합의에 사용하고, 실제 데이터는 대칭 키로 암호화한다.
  • TLS 1.3은 일반적인 새 핸드셰이크에서 응용 데이터 전송 전 1회 왕복 시간이 필요하다.
  • 인증서 검증을 끄면 서버 신원을 확인할 수 없어 중간자 공격에 노출된다.
  • 연결 재사용과 세션 재개는 반복되는 핸드셰이크 비용을 줄인다.
  • TLS는 전송 구간을 보호하며, 응용 프로그램의 인증·인가와 보안 취약점은 별도로 해결해야 한다.