HTTPS는 HTTP에 Security 기능을 추가한 프로토콜이다. 혹시 HTTP에 대한 이해가 필요하다면 아래 글을 먼저 읽는 것을 추천한다.
웹과 HTTP 동작방식 - 비지속/지속 연결 HTTP, 버전별 특징
[목차여기] Web은 온디맨드 방식으로 동작한다. 온디맨드(On Demand) 방식이란 클라이언트 프로그램에서 요구를 하면 그 요구를 전달해 주는 것이다. 그래서 필요할 때만 리소스를 가지고 올 수
proysm.tistory.com
😧 사담
2018년의 구글 Chrome을 시작으로 HTTP 페이지를 안전하지 않음으로 표시하여 대다수의 웹 서비스 업체는 HTTP에서 HTTPS로 전환하게 된다. 내가 인터넷에 글을 작성하기 시작했던 때가 2008년(초1)이었는데 그때는 HTTP라는 기술도 몰랐고 HTTPS가 없었던 시절이라고 생각하니 좀 놀라운 것 같다... 2018년이면 오래되지 않은 것인데 정말 신기하다.
1️⃣ HTTPS(HyperText Transfer Protocol Secure)
HTTPS는 HTTP에 TLS라는 보안을 결합하여 안전하게 전송하는 프로토콜이다. HTTP는 평문 통신을 사용하여 보안에 취약하지만 HTTPS는 데이터를 암호화한다.
🔐 TLS vs SSL
TLS 하면 항상 같이 언급되는 것이 SSL이다. 두 가지 모두 웹 통신에서 데이터를 암호화하고 보호하기 위한 프로토콜이다. SSL/TLS는 응용 계층(HTTP)과 전송계층(TCP) 사이에서 동작한다. SSL은 현재 SSL 2.0, 3.0이 알려진 여러 취약점 때문에 더 이상 사용되지 않으며, 거의 TLS로 대체되었다.
TLS 1.2 또는 TLS 1.3이 표준으로 사용되고 있다.
- TLS 1.2 : SHA-256, AES-GCM, ECC 지원
- TLS 1.3 : RSA 키 교환 제거, ECDHE, 0-RTT 지원
🙋🏻♀️ TLS는 누가 발급해 주나요?
HTTPS가 사용하는 TLS 인증서는 CA(Certificate Authority)가 발급한다. 무료로 TLS 인증서를 발급해 주는 기관 중에 Let’s Encrypt가 대표적이다. CA는 자체적으로 공개키와 비밀키를 가지고 있으며 이를 통해 데이터를 암호화하고 보호한다.
2️⃣ TLS 암호화 방식
TLS는 보안성을 극대화하기 위해 비대칭 암호화와 대칭 암호화를 결합한 하이브리드 방식을 사용한다.
- 비대칭 암호화 (예: RSA, ECDHE 등)
초기 핸드셰이크 과정에서 클라이언트와 서버가 서로의 공개키를 이용해 세션 키(Pre-Master Secret)를 안전하게 교환한다. 이 과정은 외부 공격자가 키 교환 과정을 엿볼 수 없도록 설계되어 있다. - 대칭 암호화 (예: AES-GCM, ChaCha20 등)
교환된 세션 키를 기반으로 실제 데이터 전송 시 빠르고 효율적인 대칭 암호화를 사용한다.
이렇게 비대칭 암호화를 통해 키를 안전하게 공유한 후, 대칭 암호화를 이용해 데이터 전송의 효율성과 보안을 동시에 확보할 수 있다.
TLS 1.2 핸드셰이크(RSA 방식)
세션 키를 클라이언트가 생성하고, 서버의 RSA 공개키로 암호화하여 전송하는 방식.
2-RTT
- 클라이언트가 서버에게 Client Hello를 보내며 지원 가능한 암호화 알고리즘 목록을 전송한다.
- 서버가 클라이언트에게 Server Hello를 보내며 선택한 암호화 알고리즘과 인증서를 전송한다.
- 클라이언트는 서버의 공개키로 세션키(Pre-Master Secret)를 암호화하여 서버에게 전송한다. (키 교환)
- 서버는 자신의 RSA 개인키(Private Key)로 복호화하여 세션 키를 얻는다.
- 세션 키를 이용해서 암호화(AES-GCM)로 데이터 송수신한다.
⚠️ TLS 1.2의 취약점
RSA를 이용한 키 교환은 전방 비밀성(Forward Secrecy)을 제공하지 않으므로, 서버의 개인키가 유출될 경우 과거의 모든 통신 기록이 위험에 노출될 수 있다. ECDHE나 DHE와 같이 임시 키를 사용하여 전방 비밀성을 보장하는 키 교환 방식을 사용하는 것이 권장된다.
* 전방 비밀성(Forward Secrecy)
완전 순방향 보안(Perfect Forward Secrecy)이라고도 말한다. 영어를 해석한 것이기 때문에 단어의 의미를 기억하는 것이 좋다.
⚠️ TLS 1.2는 느리다.
2-RTT 과정으로 인해 초기 연결 설정 속도가 느리다.
TLS 1.3 핸드셰이크
기본적으로 1-RTT이며, 과거 세션이 있는 경우 0-RTT가 가능하다.
- 클라이언트가 서버에게 Client Hello를 보내며 지원 가능한 암호화 알고리즘 목록 전송하고 키를 공유한다.
- 서버가 클라이언트에게 Server Hello를 보내며 선택한 암호화 알고리즘과 인증서를 전송한다.
- Application Data 전송을 시작한다. (암호화된 데이터 송수신)
💟 TLS 1.3의 짧아진 핸드셰이크
기본적으로 1-RTT 핸드셰이크를 사용하며, 이전 세션 정보가 있을 경우 0-RTT를 통한 빠른 재연결 지원한다. 또한, TLS 1.3은 모든 핸드셰이크 메시지를 암호화하여 중간자 공격(MITM)의 위험을 크게 줄였다.
💟 1.3의 전방 비밀성 보장
모든 키 교환 과정에서 전방 비밀성이 적용되어, 과거 통신이 서버의 키 유출로부터 보호된다.
💟 TLS 1.3의 암호화 알고리즘
오직 AEAD(Authenticated Encryption with Associated Data) 기반 암호 방식만 지원한다. (예: AES-GCM, ChaCha20-Poly1305)
* AEAD(Authenticated Encryption with Associated Data) 기반 암호 방식
암호화와 동시에 데이터의 무결성(변조 여부)과 인증을 보장하는 암호화 방법이다. 일반적인 암호화 방식은 데이터를 암호화하여 기밀성을 보장하지만, 별도로 데이터의 변조를 확인하기 위해 MAC(Message Authentication Code) 등을 추가로 사용해야 한다. 반면 AEAD는 이 두 가지를 하나의 연산으로 처리하여 보안성을 높이고 구현상의 실수를 줄일 수 있다.
📌 0-RTT가 가능한 이유
클라이언트가 이전에 접속한 서버와의 세션 정보를 저장한다. 이후 다시 연결할 때, 세션 키를 미리 사용하여 즉시 암호화된 데이터 전송 시작한다. 서버는 이전 세션을 확인하고 정상적으로 연결을 이어간다.
⚠️ 0-RTT는 재생 공격(replay attack) 위험이 존재한다.
0-RTT 기능은 재생 공격(replay attack) 위험이 존재하므로, 이를 사용할 때는 주의해야 한다. 0-RTT 데이터는 이전 세션의 키를 재사용하여 암호화되기 때문에, 서버가 해당 데이터가 이미 처리된 것인지 아닌지 추적하기 어려울 수 있다. 이 때문에 동일한 데이터가 여러 번 사용될 수 있는 재생 공격 위험이 존재한다.
👩🏻💻 실습 1 - curl을 통해 TLS 인증 과정 확인하기
curl 명령어를 날려서 TLS 인증 과정을 살펴볼 것이다. (실습 OS : MacOS)
`curl -vvv https://google.com`이라고 명령어를 입력하면 아래와 같이 코드가 출력된다.
curl -vvv https://google.com
* Host google.com:443 was resolved.
* IPv6: (none)
* IPv4: 142.250.206.206
* Trying 142.250.206.206:443...
* Connected to google.com (142.250.206.206) port 443
* ALPN: curl offers h2,http/1.1
* (304) (OUT), TLS handshake, Client hello (1):
* CAfile: /etc/ssl/cert.pem
* CApath: none
* (304) (IN), TLS handshake, Server hello (2):
* (304) (IN), TLS handshake, Unknown (8):
* (304) (IN), TLS handshake, Certificate (11):
* (304) (IN), TLS handshake, CERT verify (15):
* (304) (IN), TLS handshake, Finished (20):
* (304) (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / AEAD-CHACHA20-POLY1305-SHA256 / [blank] / UNDEF
* ALPN: server accepted h2
* Server certificate:
* subject: CN=*.google.com
* start date: Feb 3 08:36:16 2025 GMT
* expire date: Apr 28 08:36:15 2025 GMT
* subjectAltName: host "google.com" matched cert's "google.com"
* issuer: C=US; O=Google Trust Services; CN=WE2
* SSL certificate verify ok.
... 생략
📌 코드를 세부적으로 살펴보겠다.
우선 DNS를 통해 google.com의 IP를 얻어내고, IPv6 주소로 TCP 연결을 시도한다.
* Host google.com:443 was resolved.
* IPv6: 2404:6800:4005:81f::200e
* IPv4: 142.250.71.174
* Trying [2404:6800:4005:81f::200e]:443...
* Connected to google.com (2404:6800:4005:81f::200e) port 443
이후 Client hello를 통해 가능한 암호화 방식과 자신의 임시 공개키(1) 정보를 서버에 건네준다. 이때 서버의 인증서를 검증할 CAfile로는 /etc/ssl/cert.pem 경로의 파일을 이용한다.
* (304) (OUT), TLS handshake, Client hello (1):
* CAfile: /etc/ssl/cert.pem
* CApath: none
이제 서버도 클라이언트 측에게 선택한 암호화 방식과 자신의 임시 공개키(2)를 건네준다.
* (304) (IN), TLS handshake, Server hello (2):
추가로 서버는 클라이언트에게 자신의 신원을 증명할 인증서를 보내고, 인증서에 자신의 정적 공개키(3)를 담는다. 클라이언트는 CA 파일을 통해 인증하고, 서버의 정적 공개키를 얻을 수 있다
* (304) (IN), TLS handshake, Certificate (11):
* Server certificate:
* subject: CN=*.google.com
* start date: Jan 20 08:36:04 2025 GMT
* expire date: Apr 14 08:36:03 2025 GMT
* subjectAltName: host "google.com" matched cert's "google.com"
* issuer: C=US; O=Google Trust Services; CN=WR2
* SSL certificate verify ok.
자신의 신분을 입증할 CERT 증명을 하는데, 이는 위에서 있었던 핸드셰이크 메시지를 해싱하고 이를 자신의 정적 개인키(4)로 서명한다. 클라이언트는 방금 윗 단계에서 얻은 정적 공개키(3)를 통해 해싱된 값을 확인하고, 대화를 나눈 대상이 맞는지도 검증할 수 있다.
* (304) (IN), TLS handshake, CERT verify (15):
이를 마지막으로 서로가 대칭키(5)를 생성하고 대칭키를 기반으로 HTTPS 연결이 완료된다.
* (304) (IN), TLS handshake, Finished (20):
* (304) (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / AEAD-CHACHA20-POLY1305-SHA256 / [blank] / UNDEF
👩🏻💻 실습 2 - Nginx에서 HTTPS 적용해 보기
Newsee라는 프로젝트에서 Springboot 기반의 백엔드 코드를 NCP(Naver Cloud Platform)에서 배포했었다.
✈️ 프로젝트 배포 및 인증서 발급 과정
- 백엔드 코드를 Docker 파일로 빌드했다. 그리고 만들어진 도커 파일을 NCP Server에 push 했다.
- Server에 Nginx를 설치하고 도커 파일을 실행할 수 있도록 도커를 설치하였다. (Nginx는 웹 서버다)
- Cerbot을 이용해서 Let’s Encrypt의 SSL 인증서를 발급받았다. (Cerbot은 Let’s Encrypt의 SSL 인증서 발급을 도와주는 오픈소스 툴이다.)
- 발급된 인증서는 /etc/letsencrypt/live/{도메인이름} 경로에 저장되며, fullchain.pem과 privkey.pem 파일이 생성된다.
nginx.conf
Nginx 설정파일인 nginx.conf를 열어서 포트포워딩을 설정한다. 아래 코드는 내가 프토포워딩을 설정한 방법이다.
server {
listen 80;
server_name newsee.xyz;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name newsee.xyz;
# SSL 설정
ssl_certificate /etc/letsencrypt/live/newsee.xyz/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/newsee.xyz/privkey.pem;
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
✅ HTTP 트래픽 수신
- `listen 80` : HTTP(포트 80) 트래픽을 수신한다.
- `server_name newsee.xyz` : 해당 서버가 newsee.xyz 도메인에 대한 요청을 처리한다.
- `return 301 https://$host$request_uri` : 모든 HTTP 요청을 HTTPS(443 포트)로 리디렉션 한다.
✅ HTTPS 트래픽 수신
- `listen 443 ssl` : HTTPS(포트 443) 트래픽을 수신한다.
- `server_name newsee.xyz` : 해당 서버가 newsee.xyz 도메인에 대한 요청을 처리한다.
- `SSL 설정` : SSL 인증서와 SSL 인증서의 개인 키 경로를 정의한다. 이 설정을 통해 SSL 보안 연결이 활성화된다.
✅ 프록시 경로 설정 (Springboot 서버로 전달)
- `location / { ... }` : 웹사이트의 모든 요청을 Springboot 서버로 전달한다. 예를 들어 https://newsee.xyz/home을 요청하면 Nginx는 이것을 https://localhost:8080/home으로 전달한다. 왜 하필 8080이냐?라고 질문한다면 내가 아래와 같이 Springboot 프로젝트에서 포트번호를 8080으로 설정했기 때문이다.
Springboot 프로젝트의 application.yml
server:
port: 8080
글을 마치며..
단순히 HTTPS를 보안이 강화된 프로토콜이라고 알고 있었지만, 이 글을 작성하며 어떻게 보안을 강화하고 있는지 자세히 알게되었다. 그리고 2가지 실습을 진행해 보며 역시 글만을 읽는 것보다 실제로 손가락을 움직여서 코드를 두드려 보는 게 더 공부가 된다는 것을 느꼈다.
'Network' 카테고리의 다른 글
GSLB, CDN - 트래픽을 분산하는 방식 (+ DSR, AWS) (1) | 2025.02.18 |
---|---|
API 프로토콜 SOAP이란 - 개념, 실습 (4) | 2025.01.19 |
TCP의 신뢰적인 데이터 전송 원리(GBN, SR, 혼잡제어) (0) | 2025.01.17 |
인터넷과 IP (2) | 2025.01.16 |
[Network] 다중화(multiplexing)와 역다중화(demultiplexing) (2) | 2024.04.20 |