Skip to content
ioob.dev
Go back

네트워크 기초 6편 — TLS/SSL

· 12분 읽기
Network 시리즈 (6/7)
  1. 네트워크 기초 1편 — OSI와 TCP/IP 모델
  2. 네트워크 기초 2편 — IP 주소와 서브넷
  3. 네트워크 기초 3편 — TCP와 UDP
  4. 네트워크 기초 4편 — DNS
  5. 네트워크 기초 5편 — HTTP와 HTTPS
  6. 네트워크 기초 6편 — TLS/SSL
  7. 네트워크 기초 7편 — 로드 밸런서와 프록시
Table of contents

Table of contents

TLS는 공개된 네트워크 위에 만드는 비밀 채널이다

5편 마지막에서 HTTPS가 HTTP를 TLS 위에 얹은 것이라고 했다. 그럼 TLS(Transport Layer Security — 두 호스트 사이에 암호화된 채널을 만드는 프로토콜)는 공개된 네트워크에서 어떻게 비밀 대화를 만들어낼까? 이 질문에 답하는 데 암호학의 기본기 몇 개만 있으면 된다.

TLS가 보장하는 것은 세 가지다.

이 세 가지를 하나로 묶어주는 게 TLS 핸드셰이크다. 이 편에서는 핸드셰이크가 하는 일을 원리부터 따라가며, 왜 이렇게 설계할 수밖에 없었는지를 풀어간다.

대칭 키 — 빠른데 전달이 문제다

메시지를 암호화하는 가장 단순한 방식은 대칭 키 암호다. 같은 키로 암호화하고 복호화한다. AES(Advanced Encryption Standard) 같은 현대 대칭 암호는 하드웨어 가속을 받아 기가비트급 처리량을 낸다. 빠르다.

flowchart LR
    A[Alice] -->|같은 키 K로 암호화| M[암호문]
    M -->|같은 키 K로 복호화| B[Bob]

문제는 그 키를 상대에게 어떻게 안전하게 전달하느냐다. 키 자체를 평문으로 보내면 도청자가 같이 주워 간다. 그렇다고 매번 오프라인으로 만나 키를 교환할 수도 없다. 이 딜레마가 오랫동안 암호학의 난제였다.

비대칭 키 — 느리지만 공개해도 안전하다

1970년대에 비대칭 키 암호가 등장했다. 공개키와 개인키 쌍을 만든다. 공개키로 암호화하면 개인키로만 복호화된다. RSA가 대표적이고, 요즘은 타원곡선 기반 ECDSA, Ed25519도 많이 쓴다.

flowchart LR
    PUB["공개키<br/>(누구에게나 공개)"] -->|암호화| M[암호문]
    M -->|복호화| PRIV["개인키<br/>(나만 안다)"]

공개키는 공개해도 상관없다. 그걸 알아도 개인키를 역산할 수 없기 때문이다. 그래서 공개키를 인터넷에 뿌려놓고, 누구든 그걸로 나에게 보낼 메시지를 암호화하게 할 수 있다. 키 전달 문제가 풀렸다.

그런데 비대칭 암호는 연산 비용이 크다. 대용량 데이터에 통째로 쓰기엔 너무 느리다. 그래서 TLS는 두 방식의 장점만 골라 쓴다.

TLS의 핵심 아이디어 — 두 암호를 조합한다

TLS의 설계 철학은 간단하다. 비대칭 암호로 대칭 키를 주고받고, 그 뒤부터 실제 데이터는 대칭 암호로 주고받는다. 키 교환은 한 번만 하면 되니 느려도 괜찮고, 이후의 데이터는 빠른 대칭 암호가 맡는다.

flowchart TB
    START[핸드셰이크 시작] --> ASYM["비대칭 암호<br/>(느리지만 키 교환 안전)"]
    ASYM --> SHARED["공유 세션 키 확립"]
    SHARED --> SYM["대칭 암호로<br/>모든 애플리케이션 데이터 암호화"]

이 “키 교환 → 대칭 암호”의 전환 지점이 핸드셰이크의 핵심이다. 핸드셰이크가 끝나면 클라이언트와 서버는 같은 세션 키를 공유한 상태가 된다. 그때부터의 HTTP 요청·응답은 전부 그 세션 키로 암호화된다.

인증서 — 공개키를 믿어도 되는가

비대칭 암호만으로는 아직 부족하다. 누군가 “나는 naver.com이야”라며 자기가 만든 공개키를 내밀었을 때, 이걸 어떻게 믿을 수 있을까? 그 공개키가 정말 naver.com의 것이라고 보증해주는 장치가 필요하다. 그게 인증서다.

인증서는 한 줄로 말하면 “이 공개키의 주인은 이 도메인이다”라는 보증서다. 보증하는 주체는 인증기관(CA, Certificate Authority)이다. 인증서에는 다음이 담긴다.

여기서 “서명” 이 핵심 장치다. CA가 자기 개인키로 인증서 내용을 서명한다. 누구든 CA의 공개키로 그 서명을 검증할 수 있다. 서명이 검증되면 “이 인증서는 진짜 이 CA가 발급한 게 맞다”고 확신할 수 있다.

실제 인증서를 눈으로 보려면 openssl을 쓴다.

openssl s_client -connect www.google.com:443 -showcerts < /dev/null 2>/dev/null \
  | openssl x509 -noout -issuer -subject -dates
# issuer=C=US, O=Google Trust Services, CN=WR2
# subject=CN=www.google.com
# notBefore=Feb 12 08:33:40 2026 GMT
# notAfter=May  7 08:33:39 2026 GMT

발급자(issuer)가 “Google Trust Services”고, 주체(subject)가 www.google.com이다. 유효기간도 확인된다. 90일 단위로 갱신되는 건 Let’s Encrypt·구글의 단명 인증서 정책 때문이다.

체인 — 한 번 믿으면 끝이 아니다

그럼 CA의 공개키는 또 누가 보증해주나? 여기서 체인이라는 개념이 필요하다. 브라우저나 OS는 미리 “이 CA들은 믿어라”는 루트 CA 목록을 내장하고 있다. Mozilla, Apple, Microsoft, Google이 각자 루트 스토어를 관리한다.

그런데 루트 CA는 너무 중요해서, 매번 인증서를 직접 발급하지 않는다. 중간 CA들에게 권한을 위임하고, 중간 CA가 실제 서버 인증서를 발급한다. 이 관계가 체인을 이룬다.

flowchart TB
    ROOT["루트 CA<br/>(브라우저/OS에 내장)"] -->|서명| INT["중간 CA<br/>(예: Lets Encrypt R3)"]
    INT -->|서명| LEAF["서버 인증서<br/>(예: example.com)"]

브라우저가 서버 인증서를 받으면, 체인을 거슬러 올라가며 각 단계의 서명을 검증한다. 루트까지 올라가 내장된 루트 CA 목록과 일치하면 신뢰가 완성된다. 어느 한 단계라도 서명이 깨지거나, 루트가 목록에 없으면 그 유명한 “연결이 비공개로 설정되어 있지 않습니다” 경고가 뜬다.

이 구조의 장점은 위임이다. 루트 CA의 키는 금고에 잠겨 있고, 중간 CA를 발행하는 데만 쓰인다. 중간 CA가 유출되면 그 중간 CA만 폐기하면 되지, 루트를 교체할 필요가 없다.

TLS 핸드셰이크 — 한 걸음씩

이제 핸드셰이크 전체 그림을 단계별로 따라가보자. TLS 1.2 기준이다(1.3은 뒤에서 따로 다룬다).

sequenceDiagram
    autonumber
    participant C as 클라이언트
    participant S as 서버
    C->>S: ClientHello<br/>- 지원하는 TLS 버전<br/>- 지원하는 암호 슈트(cipher suite)<br/>- 클라이언트 랜덤
    S-->>C: ServerHello<br/>- 선택한 버전과 암호 슈트<br/>- 서버 랜덤
    S-->>C: Certificate<br/>(서버 인증서 체인)
    S-->>C: ServerKeyExchange<br/>(DH/ECDHE 파라미터)
    S-->>C: ServerHelloDone
    Note over C: 인증서 체인 검증
    C->>S: ClientKeyExchange<br/>(키 교환 값)
    Note over C,S: 양측이 같은<br/>pre-master secret 계산
    Note over C,S: 랜덤 값들 + pre-master으로<br/>세션 키 유도
    C->>S: ChangeCipherSpec + Finished<br/>(여기부터 암호화)
    S-->>C: ChangeCipherSpec + Finished
    Note over C,S: 이후 모든 애플리케이션 데이터는<br/>세션 키로 대칭 암호화

핸드셰이크에서 일어나는 일을 문장으로 풀어보자.

  1. ClientHello: 클라이언트가 “나는 TLS 1.2/1.3 지원하고, 이 암호 슈트들 중에서 고르자. 여기 랜덤 값 하나”라고 제안한다. 암호 슈트는 “키 교환 방식 + 대칭 암호 + 해시 함수”의 묶음이다
  2. ServerHello: 서버가 “그 중에 이걸로 하자. 내 랜덤도 받아라”고 답한다
  3. Certificate: 서버가 자기 인증서(체인 포함)를 보낸다. 클라이언트는 이걸 루트 CA까지 검증한다
  4. ServerKeyExchange: 전방향 비밀성(뒤에 설명)을 위한 디피-헬만 공개값을 보낸다
  5. ClientKeyExchange: 클라이언트도 자기 몫의 공개값을 보낸다
  6. 세션 키 유도: 양측이 각자 계산한 공유 비밀(pre-master secret)과 앞서 주고받은 랜덤 값들을 조합해 동일한 세션 키를 만든다. 이 계산은 양쪽에서 독립적으로 이뤄지지만 결과가 같다
  7. Finished: 양측이 지금까지의 메시지 전체에 대한 해시를 세션 키로 암호화해 보낸다. 이게 검증되면 핸드셰이크가 탈취되거나 변조되지 않았음이 확인된다

핸드셰이크 한 번에 2번의 왕복(2 RTT)이 든다. 이후의 모든 데이터는 세션 키로 대칭 암호화된다.

전방향 비밀성 — 과거를 지키는 장치

TLS 핸드셰이크에서 요즘 거의 필수로 쓰는 게 ECDHE(Elliptic Curve Diffie-Hellman Ephemeral — 매 연결마다 새로 만드는 일시적 키)다. “Ephemeral”이 핵심이다. 매 연결마다 일회성 키 쌍을 만들어 버린다.

이 설계가 보호하는 시나리오는 이렇다. 공격자가 오늘의 암호화된 트래픽을 몽땅 녹화해뒀다고 치자. 10년 뒤에 서버의 개인키를 어떻게든 탈취했다. 과거 트래픽을 복호화할 수 있을까? 없다. 세션 키는 그때그때 ECDHE로 만들어졌고, 그 일회성 키는 이미 버려졌기 때문이다. 이걸 전방향 비밀성(Forward Secrecy) 이라 부른다.

옛날 RSA 키 교환 방식은 서버 개인키가 유출되면 과거 트래픽이 통째로 풀렸다. 그래서 요즘의 TLS 설정은 ECDHE를 포함한 암호 슈트를 우선하게 되어 있다.

자기 서명 인증서 — 신뢰 체인의 예외

CA를 거치지 않고 직접 인증서를 만들 수도 있다. 자기 자신이 자기를 서명하는 자기 서명 인증서(self-signed certificate)다.

# RSA 개인키 생성
openssl genrsa -out server.key 2048

# 자기 서명 인증서 생성 (유효기간 1년)
openssl req -new -x509 -key server.key -out server.crt -days 365 \
  -subj "/CN=localhost"

이 인증서는 브라우저가 신뢰하지 않는다(루트 CA 목록에 없기 때문). 그래서 접속하면 경고가 뜬다. 개발 환경에서는 유용하지만 프로덕션에선 쓰지 않는다. 사내 인증서라도 내부 루트 CA를 직원들의 트러스트 스토어에 배포해서 쓴다.

Let’s Encrypt와 ACME — 인증서 발급을 자동화한다

2016년 이전까지 HTTPS의 가장 큰 장벽은 비용과 번거로움이었다. 상용 CA에 돈을 내고, 수동으로 CSR(Certificate Signing Request, 인증서 요청)을 만들고, 이메일 인증을 거쳐 파일을 받아 서버에 설치했다. 매년 이걸 반복해야 했다.

Let’s Encrypt는 이 흐름을 전부 자동화한 무료 CA다. 뒤에 있는 기술이 ACME 프로토콜(Automatic Certificate Management Environment — RFC 8555)이다. 클라이언트가 CA와 대화해 도메인 소유권을 증명하고 인증서를 받는 전 과정이 API로 정의돼 있다.

가장 흔한 소유권 증명 방식은 HTTP-01 챌린지다.

sequenceDiagram
    participant ACME as ACME 클라이언트(certbot)
    participant CA as Let's Encrypt
    participant WEB as 웹서버<br/>example.com
    ACME->>CA: 도메인 example.com 인증서 요청
    CA-->>ACME: 토큰 X를 /.well-known/acme-challenge/ 에 올려라
    ACME->>WEB: /.well-known/acme-challenge/X 에 토큰 배치
    CA->>WEB: http://example.com/.well-known/acme-challenge/X 조회
    WEB-->>CA: 토큰 X 반환
    CA->>ACME: 소유권 확인 완료, 인증서 발급

CA가 직접 도메인으로 HTTP 요청을 보낸다. 그 도메인을 통제하는 사람만 응답을 줄 수 있으니, 소유권이 간접 증명된다. DNS-01 챌린지는 같은 원리를 DNS TXT 레코드로 한다. 와일드카드 인증서(*.example.com)는 DNS-01이 필수다.

발급 명령은 한 줄로 끝난다.

# Nginx를 자동 설정해주는 certbot
sudo certbot --nginx -d example.com -d www.example.com

이 한 줄 안에서 키 생성, CSR 작성, 챌린지 수행, 인증서 다운로드, Nginx 설정 수정, 자동 갱신 cron 등록까지 전부 처리된다. 웹 인프라에서 cert-manager가 Kubernetes 네이티브하게 같은 역할을 하고, Traefik이나 Caddy 같은 최신 프록시는 아예 내장하고 있다. HTTPS가 기본값이 된 배경에 이 자동화가 있다.

TLS 1.3 — 더 빠르고 더 단순하게

2018년에 표준화된 TLS 1.3은 TLS 1.2와 철학이 다르다. 안전하지 않은 옵션을 과감하게 걷어내고, 핸드셰이크도 단순화했다.

바뀐 것들을 보자.

sequenceDiagram
    participant C as 클라이언트
    participant S as 서버
    Note over C,S: TLS 1.3 1-RTT 핸드셰이크
    C->>S: ClientHello + Key Share
    S-->>C: ServerHello + Key Share<br/>+ Certificate (암호화)<br/>+ Finished
    C->>S: Finished
    C->>S: 암호화된 HTTP 요청<br/>(두 번째 왕복부터 가능)

HTTP/3가 아예 QUIC 내부에 TLS 1.3을 내장해 버린 것도 이 단순함 덕분이다. 새 연결 수립이 빨라지면 체감 성능이 크게 좋아진다.

요약 — TLS는 결국 조합이다

여기까지 따라왔다면 TLS가 한 가지 마법이 아니라, 여러 암호학 기법을 상황에 맞게 조합한 결과라는 걸 알 수 있다. 비대칭 암호로 키를 교환하고, 대칭 암호로 데이터를 보호하고, 디지털 서명과 인증서 체인으로 신원을 확인하고, 해시와 MAC으로 무결성을 지킨다. 각각은 오래된 기술이지만, 조합의 방식과 프로토콜의 엄밀함이 TLS를 오늘날의 표준으로 만들었다.

그리고 이 보안 계층은 이제 “옵션”이 아니다. 모든 웹 서비스의 기본 전제다. Let’s Encrypt와 ACME가 비용과 수고를 걷어갔고, TLS 1.3과 QUIC이 성능 부담을 덜었다. 암호화하지 않을 이유가 거의 남지 않았다.


다음 편에서는 이 암호화된 HTTP 트래픽이 실제 서버 앞에서 어떻게 분배되는지를 다룬다. L4와 L7 로드밸런서, 리버스 프록시와 포워드 프록시, 라운드 로빈부터 ip-hash까지의 알고리즘, CDN이 트래픽을 가로채는 원리, 그리고 nginx·HAProxy·Envoy·Cloudflare 같은 실무 제품들을 짚고 시리즈를 마무리한다.

7편: 로드 밸런서와 프록시


Related Posts

Share this post on:

Comments

Loading comments...


Previous Post
네트워크 기초 5편 — HTTP와 HTTPS
Next Post
네트워크 기초 7편 — 로드 밸런서와 프록시