HTTPS / 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).
HTTPS는 무엇을 더하나
평문 HTTP는 중간 경로의 누구나 내용을 읽고 바꿀 수 있습니다. HTTPS는 HTTP를 TLS 위에서 보내 기밀성(도청 방지), 무결성(위변조 탐지), 서버 인증(진짜 서버인지 확인)을 더합니다. TLS는 SSL에서 이어진 프로토콜로, 현재 널리 쓰는 TLS 1.3은 2018년 RFC 8446으로 표준화되었습니다.
숫자로 확인하기
자주 하는 오해
면접에서 이렇게 물어요
면접 질문
HTTPS 연결은 어떻게 맺어지나요?
TCP 연결 뒤에 클라이언트가 ClientHello로 지원 암호 스위트와 키 공유를 보냅니다. 서버는 ServerHello와 인증서, 서명, Finished로 응답합니다. 클라이언트가 인증서를 검증하고 Finished를 보내면 합의한 키로 HTTP 메시지를 암호화해 주고받습니다.
면접 질문
왜 대칭키와 공개키를 함께 쓰나요?
공개키 방식은 사전에 비밀을 공유하지 않아도 되지만 계산 비용이 큽니다. 대칭 암호는 가볍고 빠르지만 같은 키를 안전하게 나눠 가져야 합니다. 그래서 공개키 방식으로 키를 합의하고 데이터는 대칭 암호로 보냅니다.
면접 질문
브라우저는 인증서를 어떻게 검증하나요?
서버가 보낸 체인을 따라 올라가 브라우저가 신뢰하는 루트 CA에 닿는지, 각 서명이 올바른지, 유효 기간 안인지, 인증서의 이름이 접속한 도메인과 맞는지 확인합니다.
면접 질문
순방향 비밀성(forward secrecy)이란 무엇인가요?
세션마다 임시 키로 키를 합의해서, 나중에 서버의 개인키가 유출되어도 과거에 녹화해 둔 통신 내용을 풀 수 없는 성질입니다. TLS 1.3은 ECDHE 같은 임시 키 교환을 기본으로 씁니다.
실제 시스템에서는
openssl s_client -connect 호스트:443에 -showcerts를 붙이면 서버가 보낸 인증서 목록을(검증된 체인은 아닙니다), -brief를 붙이면 연결 매개변수 요약을 볼 수 있습니다.
ssl_protocols와 ssl_certificate 지시어로, Spring Boot는 server.ssl.* 속성으로 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 핸드셰이크의 주요 목적은?
Q2
TLS 핸드셰이크의 주요 목적은?
Q3
HTTPS가 HTTP보다 안전한 이유는?
Q4
HTTPS가 HTTP보다 안전한 이유는?
Q5
TLS 인증서 검증 과정에서 확인하는 것은?
Q6
TLS 인증서 검증 과정에서 확인하는 것은?