본문 바로가기

네트워크/기타 네트워크 이야기

TLS SNI(HTTP SNI) 테스트 환경 구성 및 동작 방식의 이해

Today Keys : tls, sni, https, encrypt, certbot, aws, 인증서, ssl, server, name, indication


 이번 포스팅에서 TLS SNI 테스트 환경을 구성하고 테스트하면서 TLS SNI에 대한 동작 방식 이해를 해보는 포스팅입니다. 

 먼저 포스팅에 앞서서 용어부터 정리하면, 흔히 “HTTP SNI”라고 부르지만, SNI(Server Name Indication)는 HTTP 기능이 아니라 TLS ClientHello(핸드셰이크) 확장입니다. 
TLS 자체만으로는 “클라이언트가 어느 서버 이름(도메인)에 접속하려는지” 서버가 알 방법이 없어서, 가상호스팅(한 IP에 여러 HTTPS 사이트)에서 문제가 생기는데, SNI가 그 정보를 전달해줍니다.

 이번 포스팅에서는 blog..zigi.space / write.zigi.space 두 도메인을 같은 서버의 Public IPv4에 붙여 놓고, SNI가 도메인별로 다른 인증서를 선택하게 만드는지를  확인합니다.


DNS 레코드 설정

이번 포스팅에서 테스트 할, blog.zigispace.net 과 write.zigispace.net 레코드에 대해서

서버에 연결된 Public IPv4로 설정합니다. 

 

Amazon Linux 2023에 nginx  설치

sudo yum install -y nginx
sudo systemctl enable --now nginx

 

필요 디렉토리 생성

sudo mkdir -p /var/www/blog.zigispace.net \
             /var/www/write.zigispace.net \
             /var/www/letsencrypt

 

 

blog.zigispace.net 웹 페이지 생성

SNI에 따라서, 알맞은 인증서를 선택하여 웹 페이지가 열리는지 확인하기 위해서, 테스트용 웹 페이지를 만듭니다.

sudo tee /var/www/blog.zigispace.net/index.html > /dev/null <<'EOF'
<!doctype html>
<html>
<head><meta charset="utf-8"><title>blog.zigispace.net</title></head>
<body>
  <h1>HELLO from blog.zigispace.net</h1>
  <p>도메인/SNI에 따라 다른 사이트로 분기되는지 확인하는 테스트 페이지입니다.</p>
</body>
</html>
EOF

 

write.zigispace.net 웹 페이지 생성

SNI에 따라서, 알맞은 인증서를 선택하여 웹 페이지가 열리는지 확인하기 위해서, 테스트용 웹 페이지를 만듭니다.

sudo tee /var/www/write.zigispace.net/index.html > /dev/null <<'EOF'
<!doctype html>
<html>
<head><meta charset="utf-8"><title>write.zigispace.net</title></head>
<body>
  <h1>HELLO from write.zigispace.net</h1>
  <p>도메인/SNI에 따라 다른 사이트로 분기되는지 확인하는 테스트 페이지입니다.</p>
</body>
</html>
EOF

 

HTTP 가상 호스트 설정

도메인에 대한 인증서를 발급 받기 전에, 인증서가 정상적으로 발급 받기 위한 HTTP-01 검증을 위해서 

기본 HTTP(80)로, 두 도메인이 정상적으로 응답하기 위해서 HTTP 가상 호스트를 각각 설정합니다. 

sudo tee /etc/nginx/conf.d/sni-lab.conf > /dev/null <<'EOF'
# SNI/Host 관찰용 로그(HTTPS 이후 확인용)
log_format sni_lab '$remote_addr sni="$ssl_server_name" host="$host" request="$request" status=$status';
access_log /var/log/nginx/sni_lab_access.log sni_lab;

# blog.zigispace.net : HTTP
server {
  listen 80;
  server_name blog.zigispace.net;

  # ACME HTTP-01 챌린지 경로
  location /.well-known/acme-challenge/ {
    root /var/www/letsencrypt;
  }

  root /var/www/blog.zigispace.net;
  index index.html;
}

# write.zigispace.net : HTTP
server {
  listen 80;
  server_name write.zigispace.net;

  location /.well-known/acme-challenge/ {
    root /var/www/letsencrypt;
  }

  root /var/www/write.zigispace.net;
  index index.html;
}
EOF

sudo nginx -t
sudo systemctl reload nginx

 

Certbot 설치

Certbot은 Let's Encrypt 같은 ACME 기반 인증 기관에서 TLS 인증서를 발급,설치,갱신하도록 도와주는 자동화 도구로, 

이번 포스팅에서는 Let's Encrypt로 무료 TLS 인증서를 받기 위해서 사용합니다.

Certbot 사이트에서 설치 가이드 링크 ( Link ) 를 참조하여, Certbot을 설치합니다.

sudo yum install -y python3 augeas-libs

sudo python3 -m venv /opt/certbot/
sudo /opt/certbot/bin/pip install --upgrade pip

# certbot + webroot 사용(nginx 플러그인 없어도 됨)
sudo /opt/certbot/bin/pip install certbot

sudo ln -sf /opt/certbot/bin/certbot /usr/local/bin/certbot

 

Let's Encrypt 인증서 발급

Let's Encrypt 인증서는 무료로 TLS 서버 인증서를 발급하는 인증 기관(CA)입니다. 

certbot을 이용해서, 미리 만들어둔 인증서별 디렉토리에 blog.zigispace.net와 write.zigispace.net 인증서를 발급 받습니다.

sudo certbot certonly --webroot \
  -w /var/www/letsencrypt \
  -d blog.zigispace.net

sudo certbot certonly --webroot \
  -w /var/www/letsencrypt \
  -d write.zigispace.net

 

HTTPS 가상 호스트 구성과 HTTP를 HTTPS로 redirect 설정

정상적 인증서 발급까지 완료되면, 

이제 가상호스트 구성에서 HTTP를 HTTPS로 redirect 하도록 하고,

HTTPS에 대한 인증서 정보를 도메인 별로 각각 설정합니다. 

설정을 완료하면, nginx를 다시 재기동합니다.

sudo tee /etc/nginx/conf.d/sni-lab.conf > /dev/null <<'EOF'
log_format sni_lab '$remote_addr sni="$ssl_server_name" host="$host" request="$request" status=$status';
access_log /var/log/nginx/sni_lab_access.log sni_lab;

# HTTP -> HTTPS (챌린지 경로는 예외로 열어둠)
server {
  listen 80;
  server_name blog.zigispace.net;

  location /.well-known/acme-challenge/ {
    root /var/www/letsencrypt;
  }

  return 301 https://$host$request_uri;
}

server {
  listen 80;
  server_name write.zigispace.net;

  location /.well-known/acme-challenge/ {
    root /var/www/letsencrypt;
  }

  return 301 https://$host$request_uri;
}

# HTTPS: blog.zigispace.net
server {
  listen 443 ssl;
  server_name blog.zigispace.net;

  ssl_certificate     /etc/letsencrypt/live/blog.zigispace.net/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/blog.zigispace.net/privkey.pem;

  root /var/www/blog.zigispace.net;
  index index.html;
}

# HTTPS: write.zigispace.net
server {
  listen 443 ssl;
  server_name write.zigispace.net;

  ssl_certificate     /etc/letsencrypt/live/write.zigispace.net/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/write.zigispace.net/privkey.pem;

  root /var/www/write.zigispace.net;
  index index.html;
}
EOF

sudo nginx -t
sudo systemctl reload nginx

 

브라우저로 도메인 별로 접속 확인

이제 브라우저 별로 blog.zigispace.net과 wrtie.zigispace.net으로 접속하면

각각의 도메인 인증서를 통해 웹페이지가 정상적으로 열린 것을 확인 할 수 있습니다. 

 

openssl 명령어로 SNI(servername) 옵션 값에 따라서, 동일한 목적지 IP와 서비스 포트로 호출한 것이 servername에 설정한 값에 맞춰서 각각의 인증서를 선택하는 것을 확인할 수 있습니다.

echo | openssl s_client -connect <서버_PUBLIC_IP>:443 -servername blog.zigispace.net 2>/dev/null \
 | openssl x509 -noout -subject -issuer

 

실제 이 과정에 대해서 패킷을 wireshark로 떠보면 아래와 같습니다.

 

3way handshake 이후에, TLS v1.3 Client Hello로 SNI 값으로 도메인 정보를 전송하게 되고, 

이후, ServeHello로 서버가 TLS 파라미터(버전/암호스위트 등)를 선택해 응답하고, 

이후에는 요청과 응답이 TLS로 암호화 되어서, Application Data로 표기됩니다.