컴퓨터 구조 운영체제 네트워크 자료구조 알고리즘 데이터베이스 내 학습
network · 07

HTTPS / TLS

네트워크TLS보안

HTTPS는 HTTP에 TLS(Transport Layer Security) 암호화를 적용한 보안 프로토콜입니다. TLS 핸드셰이크를 통해 서버 인증과 암호화 키 교환을 수행한 뒤, 이후 모든 HTTP 통신을 암호화해 도청·위변조·중간자 공격을 방지합니다.

핵심 개념

TLS 1.3 핸드셰이크

TLS 1.3은 1-RTT 핸드셰이크를 사용합니다. 클라이언트가 ClientHello(지원 암호 스위트, 키 공유)를 보내면, 서버가 ServerHello(선택된 암호 스위트, 키 공유), 인증서, Finished를 한 번에 응답합니다. 클라이언트는 서버의 Finished를 확인한 뒤 자신의 Finished와 함께 바로 애플리케이션 데이터를 보낼 수 있고, ServerHello 이후의 핸드셰이크 메시지도 암호화됩니다.

핵심 개념

공개키 암호화

서버는 공개키·개인키 쌍을 가집니다. 인증서에 공개키가 포함되어 있어 누구나 공개키로 암호화할 수 있지만, 복호화는 서버의 개인키로만 가능합니다. TLS 1.3에서는 공개키로 비밀을 암호화해 보내지 않고 ECDHE로 키를 합의하며, 서버는 개인키로 핸드셰이크에 서명해 신원을 증명합니다. 이후 데이터는 대칭키 암호화로 보냅니다.

핵심 개념

인증서와 CA

서버 인증서는 CA(Certificate Authority)가 서버의 신원을 보증하는 디지털 서명입니다. 브라우저는 신뢰하는 CA 목록을 내장하며, 인증서 체인을 검증해 서버의 신원을 확인합니다. Let's Encrypt로 무료 인증서를 발급받을 수 있습니다.

핵심 개념

ECDHE 키 교환

TLS 1.3은 ECDHE(Elliptic Curve Diffie-Hellman Ephemeral)로 키를 교환합니다. 클라이언트와 서버가 각각 임시 키 쌍을 생성하고 공개키를 교환해 같은 공유 비밀을 계산하고, 여기서 트래픽 키를 만듭니다. 세션마다 다른 임시 키를 쓰므로 순방향 비밀성(forward secrecy)을 얻습니다.

핵심 개념

대칭키 암호화 (AES-GCM)

핸드셰이크 후 실제 데이터는 AES-GCM 같은 대칭키 알고리즘으로 암호화합니다. 대칭키 방식은 공개키 방식보다 계산 비용이 훨씬 작습니다. AES-GCM은 암호화와 함께 무결성 검증(MAC)을 제공하는 인증 암호화(AEAD) 방식입니다.

핵심 개념

0-RTT (세션 재개)

TLS 1.3의 세션 재개 기능입니다. 이전 연결에서 받은 PSK(Pre-Shared Key)를 사용해 핸드셰이크 없이 첫 요청과 함께 데이터를 전송합니다. 단, 재전송 공격(Replay Attack)에 취약하고 순방향 비밀성도 없습니다(RFC 8446). HTTP에서는 클라이언트가 안전한 메서드의 요청만 early data로 보내고, 서버는 이를 거부하거나 핸드셰이크가 끝날 때까지 미루며 425 Too Early로 응답할 수 있습니다(RFC 8470).

TLS 1.2 vs TLS 1.3 — TLS 1.2는 2-RTT 핸드셰이크로 연결 지연이 큽니다. TLS 1.3은 1-RTT로 줄이고, 정적 RSA·DH 키 교환을 없애고 대칭 암호를 AEAD 방식만 남기도록 정리했습니다. RFC 8996은 TLS 1.0과 1.1을 사용하지 말라(MUST NOT)고 규정합니다.
SNI (Server Name Indication) — 하나의 IP에 여러 HTTPS 도메인을 호스팅할 때 필요합니다. ClientHello에 목적지 도메인명을 평문으로 포함해 서버가 올바른 인증서를 선택하게 합니다. ECH(Encrypted Client Hello)는 이 값까지 암호화하는 확장으로 RFC 9849에 정리되어 있습니다.
HSTS (HTTP Strict Transport Security) — 서버가 응답 헤더로 "이 도메인은 항상 HTTPS만 사용"을 브라우저에 지시합니다. 지정된 기간 동안 브라우저가 HTTP 요청을 자동으로 HTTPS로 업그레이드합니다. https를 http로 낮추는 SSL stripping 같은 중간자 공격을 막는 데 쓰입니다.
인증서 투명성 (CT) — CA가 발급한 모든 인증서를 공개 로그에 기록하는 시스템입니다. 잘못 발급된 인증서를 빠르게 발견할 수 있습니다. 브라우저는 CT 정책을 요구하는데, 예를 들어 Chrome은 공개 CT 로그 기록 증거가 없는 공개 신뢰 인증서를 받아들이지 않습니다.

HTTPS는 무엇을 더하나

평문 HTTP는 중간 경로의 누구나 내용을 읽고 바꿀 수 있습니다. HTTPS는 HTTP를 TLS 위에서 보내 기밀성(도청 방지), 무결성(위변조 탐지), 서버 인증(진짜 서버인지 확인)을 더합니다. TLS는 SSL에서 이어진 프로토콜로, 현재 널리 쓰는 TLS 1.3은 2018년 RFC 8446으로 표준화되었습니다.

숫자로 확인하기

연결 지연(단순화) — 요청을 보내기 전까지 TCP에 1 RTT가 들고, TLS 1.2 핸드셰이크는 2 RTT, TLS 1.3은 1 RTT입니다. RTT가 100ms이면 요청 전 대기가 TLS 1.2에서 300ms, TLS 1.3에서 200ms입니다. 재개한 0-RTT 연결은 TCP 1 RTT만 남아 100ms입니다.
디피-헬만 장난감 예제 — 소수 p=23, 기준값 g=5로 두고 클라이언트의 비밀 a=6, 서버의 비밀 b=15를 정합니다. 클라이언트는 5⁶ mod 23 = 8을, 서버는 5¹⁵ mod 23 = 19를 보냅니다. 클라이언트는 19⁶ mod 23 = 2, 서버는 8¹⁵ mod 23 = 2로 같은 공통 비밀 2를 얻습니다. 도청자가 본 값은 23, 5, 8, 19뿐입니다. 실제 TLS는 훨씬 큰 수나 타원 곡선을 씁니다.
레코드 크기 — TLS 1.3 레코드 하나의 평문 최대 길이는 2¹⁴ = 16,384바이트이고, 이보다 큰 데이터는 여러 레코드로 나뉩니다.
키 공유 크기 — 타원 곡선 X25519의 공개키는 32바이트입니다. 같은 보안 수준의 RSA 공개키는 훨씬 큽니다.

자주 하는 오해

HTTPS면 안전한 사이트다 — HTTPS는 통신 경로를 보호하고 도메인의 주인임을 확인해 줄 뿐입니다. 피싱 사이트도 자기 도메인의 인증서를 받아 HTTPS를 쓸 수 있습니다.
데이터 전체를 공개키로 암호화한다 — 공개키 방식은 키 합의와 신원 증명에 쓰고, 실제 데이터는 합의한 키로 대칭 암호(AEAD)를 씁니다.
HTTPS면 어느 사이트를 방문했는지 완전히 숨겨진다 — 경로와 본문은 가려지지만 IP 주소는 보이고, SNI와 DNS 질의가 평문이면 도메인 이름도 드러날 수 있습니다.
HSTS는 첫 방문부터 보호한다 — 브라우저는 HSTS 헤더를 한 번 받아야 그 도메인을 기억합니다. 첫 방문의 보호는 브라우저에 미리 등록하는 preload 목록에 달려 있습니다.

면접에서 이렇게 물어요

면접 질문

HTTPS 연결은 어떻게 맺어지나요?

TCP 연결 뒤에 클라이언트가 ClientHello로 지원 암호 스위트와 키 공유를 보냅니다. 서버는 ServerHello와 인증서, 서명, Finished로 응답합니다. 클라이언트가 인증서를 검증하고 Finished를 보내면 합의한 키로 HTTP 메시지를 암호화해 주고받습니다.

면접 질문

왜 대칭키와 공개키를 함께 쓰나요?

공개키 방식은 사전에 비밀을 공유하지 않아도 되지만 계산 비용이 큽니다. 대칭 암호는 가볍고 빠르지만 같은 키를 안전하게 나눠 가져야 합니다. 그래서 공개키 방식으로 키를 합의하고 데이터는 대칭 암호로 보냅니다.

면접 질문

브라우저는 인증서를 어떻게 검증하나요?

서버가 보낸 체인을 따라 올라가 브라우저가 신뢰하는 루트 CA에 닿는지, 각 서명이 올바른지, 유효 기간 안인지, 인증서의 이름이 접속한 도메인과 맞는지 확인합니다.

면접 질문

순방향 비밀성(forward secrecy)이란 무엇인가요?

세션마다 임시 키로 키를 합의해서, 나중에 서버의 개인키가 유출되어도 과거에 녹화해 둔 통신 내용을 풀 수 없는 성질입니다. TLS 1.3은 ECDHE 같은 임시 키 교환을 기본으로 씁니다.

실제 시스템에서는

openssl로 확인 — openssl s_client -connect 호스트:443에 -showcerts를 붙이면 서버가 보낸 인증서 목록을(검증된 체인은 아닙니다), -brief를 붙이면 연결 매개변수 요약을 볼 수 있습니다.
인증서 발급 자동화 — 인증서 발급과 갱신을 자동화하는 ACME 프로토콜은 RFC 8555로 표준화되어 있고, Let's Encrypt가 대표적인 구현입니다.
서버 설정 — nginx는 ssl_protocols와 ssl_certificate 지시어로, Spring Boot는 server.ssl.* 속성으로 TLS를 설정합니다. 로드 밸런서나 CDN 앞단에서 TLS를 끝내고 뒤쪽은 평문으로 두는 구성도 흔합니다.

함께 보면 좋은 토픽

TCP/IP — TLS가 올라가는 TCP 연결 설정을 봅니다.
HTTP — TLS 위에서 오가는 HTTP 메시지를 봅니다.
DNS — DNS 질의 보호(DoH, DoT)와 SNI의 관계를 봅니다.
로드 밸런서 — TLS 종료를 어디서 하는지 비교합니다.
CDN — 엣지에서 TLS를 끝내는 구조를 봅니다.

참고 자료

Java — Spring Boot HTTPS 설정 및 TLS 인증서 적용

## application.yml — TLS 설정
##
## server:
##   port: 443
##   ssl:
##     enabled: true
##     key-store: classpath:keystore.p12
##     key-store-password: ${SSL_KEY_STORE_PASSWORD}
##     key-store-type: PKCS12
##     protocol: TLS
##     enabled-protocols: TLSv1.3,TLSv1.2

import org.springframework.boot.web.embedded.tomcat.*;
import org.springframework.context.annotation.*;
import javax.net.ssl.*;
import java.net.http.*;
import java.net.URI;

/* ── HTTPS 리다이렉트 설정 ─────────────────────────────── */
@Configuration
public class HttpsConfig {

    // HTTP(8080) → HTTPS(443) 자동 리다이렉트
    @Bean
    public TomcatServletWebServerFactory servletContainer() {
        var factory = new TomcatServletWebServerFactory();
        factory.addAdditionalTomcatConnectors(httpConnector());
        return factory;
    }

    private Connector httpConnector() {
        var connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL);
        connector.setScheme("http");
        connector.setPort(8080);
        connector.setSecure(false);
        connector.setRedirectPort(443); // HTTP → HTTPS 리다이렉트
        return connector;
    }
}

/* ── HSTS 헤더 설정 ────────────────────────────────────── */
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .requiresChannel(ch -> ch.anyRequest().requiresSecure())
            .headers(headers -> headers
                .httpStrictTransportSecurity(hsts -> hsts
                    .maxAgeInSeconds(31536000) // HSTS 1년
                    .includeSubDomains(true)
                    .preload(true)
                )
            );
        return http.build();
    }
}

/* ── Java HTTP Client로 HTTPS 요청 ─────────────────────── */
public class HttpsClient {
    public static void main(String[] args) throws Exception {
        var client = HttpClient.newBuilder()
            .version(HttpClient.Version.HTTP_2)
            // TLS 1.3 자동 협상, 인증서 검증 포함
            .build();

        var request = HttpRequest.newBuilder()
            .uri(URI.create("https://example.com"))
            .GET()
            .build();

        var response = client.send(request,
            HttpResponse.BodyHandlers.ofString());

        System.out.println("상태: " + response.statusCode());
        System.out.println("버전: " + response.version());
    }
}

Spring Boot에서 HTTPS를 설정하려면 PKCS12 형식의 키스토어를 준비하고 application.yml에 경로를 설정합니다.
실제 운영에서는 Let's Encrypt로 발급한 인증서를 certbot이 자동 갱신합니다.
HSTS 헤더는 브라우저에 "이 도메인은 항상 HTTPS만 사용하라"고 지시합니다.
max-age=31536000은 1년간 강제 HTTPS를 의미합니다. preload=true로 설정하면 브라우저에 사전 등록할 수 있습니다.
Java 11+의 HttpClient는 TLS 1.3을 자동으로 협상하며 인증서 검증을 기본으로 수행합니다.
개발 환경에서 자체 서명 인증서를 사용할 때만 검증을 비활성화합니다.
키스토어 비밀번호는 절대 소스 코드나 설정 파일에 하드코딩하지 않고 환경 변수나 시크릿 매니저를 사용합니다.

Python — TLS 핸드셰이크 상세 정보 확인

import ssl
import socket
import json

# ── TLS 연결 정보 상세 확인 ─────────────────────────────
def inspect_tls(host: str, port: int = 443):
    context = ssl.create_default_context()

    with socket.create_connection((host, port)) as sock:
        with context.wrap_socket(sock, server_hostname=host) as ssock:

            # TLS 버전 확인
            print(f"TLS 버전: {ssock.version()}")        # TLSv1.3

            # 협상된 암호 스위트
            cipher = ssock.cipher()
            print(f"암호 스위트: {cipher[0]}")         # TLS_AES_256_GCM_SHA384
            print(f"프로토콜: {cipher[1]}")            # TLSv1.3
            print(f"키 길이: {cipher[2]} bits")       # 256

            # 서버 인증서 정보
            cert = ssock.getpeercert()
            subject = dict(x[0] for x in cert['subject'])
            issuer  = dict(x[0] for x in cert['issuer'])

            print(f"도메인: {subject.get('commonName')}")
            print(f"발급자: {issuer.get('organizationName')}")
            print(f"만료일: {cert['notAfter']}")

            # SAN (Subject Alternative Names)
            sans = cert.get('subjectAltName', [])
            print(f"SAN: {[s[1] for s in sans]}")

# ── 인증서 체인 확인 ────────────────────────────────────
def check_cert_chain(host: str):
    context = ssl.create_default_context()
    context.check_hostname = True
    context.verify_mode    = ssl.CERT_REQUIRED

    with socket.create_connection((host, 443)) as sock:
        with context.wrap_socket(sock, server_hostname=host) as ssock:
            # DER 형식 인증서 체인
            der_certs = ssock.getpeercert(binary_form=True)
            print(f"인증서 체인 크기: {len(der_certs)} bytes")

# ── 실행
inspect_tls("example.com")
# $ openssl s_client -connect example.com:443 -tls1_3 로도 확인 가능

ssl.create_default_context()는 시스템의 CA 번들을 사용해 인증서를 검증합니다.
check_hostname=True와 verify_mode=CERT_REQUIRED가 기본값으로, 인증서 검증을 건너뛰지 않는 것이 중요합니다.
cipher() 메서드로 실제 협상된 암호 스위트를 확인할 수 있습니다.
TLS 1.3에서는 TLS_AES_256_GCM_SHA384 또는 TLS_CHACHA20_POLY1305_SHA256 중 하나가 선택됩니다.
SAN(Subject Alternative Names)은 하나의 인증서가 여러 도메인을 커버하는 데 사용됩니다.
와일드카드(*.example.com)도 SAN에 포함됩니다.
openssl s_client 명령은 TLS 핸드셰이크 전체 과정을 터미널에서 디버깅할 때 가장 유용한 도구입니다.

Q1

TLS 핸드셰이크의 주요 목적은?

이후 통신을 암호화하려면 먼저 키를 안전하게 나눠 가져야 합니다.
TLS 핸드셰이크는 암호화 알고리즘 협상, 인증서 검증, 세션 키 교환 등을 통해 이후 통신에 사용할 안전한 암호화 채널을 준비하는 과정입니다.

Q2

TLS 핸드셰이크의 주요 목적은?

이후 통신을 암호화하려면 먼저 키를 안전하게 나눠 가져야 합니다.
TLS 핸드셰이크는 암호화 알고리즘 협상, 인증서 검증, 세션 키 교환 등을 통해 이후 통신에 사용할 안전한 암호화 채널을 준비하는 과정입니다.

Q3

HTTPS가 HTTP보다 안전한 이유는?

'S'는 Secure(보안)를 의미합니다.
HTTPS는 HTTP 통신을 TLS/SSL로 암호화해, 중간에서 데이터를 가로채도 내용을 알 수 없게 하고 변조 여부도 감지할 수 있게 합니다.

Q4

HTTPS가 HTTP보다 안전한 이유는?

'S'는 Secure(보안)를 의미합니다.
HTTPS는 HTTP 통신을 TLS/SSL로 암호화해, 중간에서 데이터를 가로채도 내용을 알 수 없게 하고 변조 여부도 감지할 수 있게 합니다.

Q5

TLS 인증서 검증 과정에서 확인하는 것은?

가짜 서버가 아닌지 확인해야 합니다.
클라이언트는 서버가 제시한 인증서가 신뢰할 수 있는 CA(인증기관)에 의해 발급되었고 도메인이 일치하는지 검증해, 중간자 공격 등을 방지합니다.

Q6

TLS 인증서 검증 과정에서 확인하는 것은?

가짜 서버가 아닌지 확인해야 합니다.
클라이언트는 서버가 제시한 인증서가 신뢰할 수 있는 CA(인증기관)에 의해 발급되었고 도메인이 일치하는지 검증해, 중간자 공격 등을 방지합니다.