SSL/TLS? HTTPS란?
SSL/TLS
- SSL(Secure Sockets Layer)
- TLS(Transport Layer Security) - SSL의 후계자이다.
- SSL/TLS는 브라우저와 서버가 통신할 때 암호화를 지원하는 보안 프로토콜이다.
- SSL 인증서 (일컬어 TLS 또는 SSL /TLS 인증서)는 웹 사이트의 ID를 공개 키와 개인 키로 구성된 암호화 키 쌍에 바인딩하는 디지털 문서이다. 사용자는 인증서에 포함된 공개 키를 사용해 데이터를 암호화하여 전송할 수 있고, 서버는 안전하게 보관되어있는 개인 키를 사용자의 데이터를 복호할 때나 웹 페이지 및 기타 문서를 디지털 서명하는 데 사용할 수 있다.
- SSL 인증서의 역할은 두 가지이다. 클라이언트가 접속한 서버가 신뢰 할 수 있는 서버임을 보장하는 것과 SSL 통신에 사용할 공개키를 클라이언트에게 제공하는 것이다.
HTTPS
- HTTPS는 기존 HTTP(HtperText Transfer Protocol)에 Secure를 붙인 용어이다. 말그대로 HTTP에 보안을 추가한 것인데, HTTP통신을 안전하게 보호하기 위한 구조로 개선되었다.
- HTTP가 80번 포트를 사용하는 반면, HTTPS는 443번 포트를 할당받았다.
HTTPS를 설정해야하는 이유?
서비스에 대한 신뢰성
- 접속하는 사이트가 신뢰할 수 있는 사이트인지 알려준다. 처음 홈페이지에 들어갔는데 아래와 같은 창이 뜨면 이용자가 서비스를 사용하고 싶은 마음이 뚝 떨어질 것이다. (2018년부터 구글은 SSL 인증서가 없는 사이트에게 불이익을 주기 시작했다. Chrome 브라우저에 '안전하지 않음'을 표시한다.)
보안
- 웹사이트에서 주고 받는 정보를 암호화한다. HTTPS는 통신 시에 정보를 암호화해주기 때문에, 아래 그림에서와 같이 해커가 중간에 정보를 훔쳐보아도 암호화되어 있어 정보를 알 수 없다.
- 따라서 사용자 회원가입과 로그인이 있는 페이지라면, HTTPS를 통해 보안을 강화할 수 있다.
SSL 인증을 받기 위해 해야하는 일 3가지
1. 도메인 구매
2. 인증서 만들기
3. NGINX 및 docker-compose 설정하기(Docker 환경 사용)
위의 3가지를 실제 프로젝트에서 적용하는 과정과 함께 알아보자!
1. 도메인 구매
- 가비아를 이용해 도메인을 구매할 수 있다.
- 실제 프로젝에서 사용하기 위해 1년 동안 moyeomoeyo.com 을 15,000원 정도에 구매했다!
IP 연결하기
- @ 와 www 를 기본 호스트로 넣어준다.
- 클라이언트와 서버를 따로 배포했기때문에
- 기본 호스트에는 클라이언트의 공인 IP 주소를 넣어줬다.
- nest 서버를 서브도메인을 추가하기 위해 api 에 추가해줬다.
- 호스트를 추가하기 전에 DNS가 무엇인지 이해가 필요하다. 아래 내용을 확인해보자.
DNS 레코드(타입) 종류
A
- (Host) : 주소/호스트 레코드
- 정규화된 도메인 이름/호스트명(FQDN)을 IPv4에 연결한다.
AAAA
- 주소 레코드
- 호스트를 IPv6에 연결한다.
CNAME
- (Canonical NAME): 별칭 레코드
- 실제 호스트명(A레코드)과 연결되는 별칭,별명을 정의 한다.
MX
- (Mail Exchange) : 메일 교환 레코드
- 메일서버(사서함)에 도달할 수 있는 라우팅정보(메일서버)를 제공한다.
SRV
- (SeRVice) : 서비스 위치 레코드;
- 비슷한 TCP/IP 서비스를 제공하는 다수의 서버 위치 정보를 제공한다.
PTR
- (PoinTeR) : 포인터 리소스 레코드;
- 다른 DNS레코드를 가리킴, 역방향 조회에서 A레코드를 가리킬때 사용한다.
SOA
- (Start Of Authority) : 권한시작 레코드;
- DNS영역의 주 DNS 서버를 정의하며 일련번호를 통해 영역의 변경사항을 기록한다. 또한 보조영역의 새로고침 및 다시시도 간격등을 정의하고, 영역의 기본 TTL 값을 정의한다.
NS
- (Name Server) : 네임 서버 레코드;
- 영역을 풀이할 수 있는 DNS서버의 목록을 가지고 있다.
2. OpenSSL을 이용한 인증서 발급
- 자료가 가장 많은 Let’s Encrypt 사용해보자.
Let’s Encrypt란?
- CA 중 하나로, 무료로 SSL 인증서를 발급해준다.
- 유효기간이 90일이므로 기간이 다 되면 재발급 받아서 사용해야 한다는 특징이 있다.
Certbot도 알아야 한다.
- Let's Encrypt 에서 SSL인증서를 발급받는 자동화 툴이다.
- 오픈소스이며, 도커에 공식 이미지가 올라와 있어서 컨테이너 방식으로 편하게 사용할 수 있다.
설치!
- letsencrypt과 certbot 의존성을 이용해 문제를 해결해보자!
- 리눅스 서버 환경에서 아래와 같이 설치를 진행해준다.
sudo apt update
sudo apt upgrade -y
sudo apt-get install letsencrypt -y
sudo apt install certbot python3-certbot-nginx
아래와 같이 standalone 방식으로 인증서를 받을 수 있다.
- 도메인으로 인증서를 발급받아보자.
- 명령어를 실행하면 이메일을 작성하라 한다.
- 긴급 통보 혹은 키 복구를 위해 사용되니 실제 메일 받을 이메일을 적으면 된다.
- 이후 이용약관에 동의하라는 < Agree > 버튼이 나오면 선택한다.
- 도메인 이름 하나하나 넣어주는 것이 좋다.
sudo letsencrypt certonly -a standalone -d [발급받을 도메인]
- 위 사항을 완료하면 /etc/letsencrypt/live/도메인/ 디렉토리에 pem 파일이 생긴다.
- cert.pem : 도메인 인증서
- chain.pem : Let's Encrypt chain 인증서
- fullchain.pem : cert.pem 과 chain 인증서 합본
- privkey.pem : 개인키
하지만 Let’s Encrypt의 단점이 있다.
- 바로 90일마다 인증서가 만료된다는 것이다. ⇒ 배포 작업에서의 수동 체크를 의미하며, 배포자동화를 한 의미가 약간 퇴색된다.
- 배포 과정의 휴먼 이슈를 줄이기 위해 CI/CD를 하고 있는데 인증서 갱신은 수동으로 한다? 만약 인증서 갱신을 잊어서 HTTPS 배포가 터지면 어떻게 할 것인가? 있을 수 없는 일이었다. 그래서 정보를 더 찾아봤다.
- 프로젝트 환경(docker + nginx)에 딱 알맞는 오픈소스가 있었다.
Nginx and Let's Encrypt with Docker in Less Than 5 Minutes
https://github.com/wmnnd/nginx-certbot
- Let’s Encrypt를 NGINX 도커 환경에서 인증받을 수 있게 도와주는 보일러플레이트이다.
- 셸 스크립트를 이용해 인증서를 가져와 갱신할 수 있도록 도와준다.
- init-letsencrypt.sh 의 내용(위의 오픈소스에서 확인할 수 있습니다.)
#!/bin/bash
if ! [ -x "$(command -v docker-compose)" ]; then
echo 'Error: docker-compose is not installed.' >&2
exit 1
fi
domains=(example.org www.example.org)
rsa_key_size=4096
data_path="인증서를 저장할 경로 지정"
email="당신의 이메일 주소!" # Adding a valid address is strongly recommended
staging=0 # Set to 1 if you're testing your setup to avoid hitting request limits
if [ -d "$data_path" ]; then
read -p "Existing data found for $domains. Continue and replace existing certificate? (y/N) " decision
if [ "$decision" != "Y" ] && [ "$decision" != "y" ]; then
exit
fi
fi
if [ ! -e "$data_path/conf/options-ssl-nginx.conf" ] || [ ! -e "$data_path/conf/ssl-dhparams.pem" ]; then
echo "### Downloading recommended TLS parameters ..."
mkdir -p "$data_path/conf"
curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf > "$data_path/conf/options-ssl-nginx.conf"
curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem > "$data_path/conf/ssl-dhparams.pem"
echo
fi
echo "### Creating dummy certificate for $domains ..."
path="/etc/letsencrypt/live/$domains"
mkdir -p "$data_path/conf/live/$domains"
docker-compose run --rm --entrypoint "\
openssl req -x509 -nodes -newkey rsa:$rsa_key_size -days 1\
-keyout '$path/privkey.pem' \
-out '$path/fullchain.pem' \
-subj '/CN=localhost'" certbot
echo
echo "### Starting nginx ..."
docker-compose up --force-recreate -d nginx
echo
echo "### Deleting dummy certificate for $domains ..."
docker-compose run --rm --entrypoint "\
rm -Rf /etc/letsencrypt/live/$domains && \
rm -Rf /etc/letsencrypt/archive/$domains && \
rm -Rf /etc/letsencrypt/renewal/$domains.conf" certbot
echo
echo "### Requesting Let's Encrypt certificate for $domains ..."
#Join $domains to -d args
domain_args=""
for domain in "${domains[@]}"; do
domain_args="$domain_args -d $domain"
done
# Select appropriate email arg
case "$email" in
"") email_arg="--register-unsafely-without-email" ;;
*) email_arg="--email $email" ;;
esac
# Enable staging mode if needed
if [ $staging != "0" ]; then staging_arg="--staging"; fi
docker-compose run --rm --entrypoint "\
certbot certonly --webroot -w /var/www/certbot \
$staging_arg \
$email_arg \
$domain_args \
--rsa-key-size $rsa_key_size \
--agree-tos \
--force-renewal" certbot
echo
echo "### Reloading nginx ..."
docker-compose exec nginx nginx -s reload
이제 NGINX와 Docker Compose를 설정한 후 스크립트를 실행시키기만 하면 내 홈페이지를 HTTPS로 배포할 수 있다!
3. Docker-Compose와 NGINX 설정
Docker-Compose 사용을 위해 먼저 해줘야하는 설정
- 주의할 점이 있다. certbot을 지원해주는 버전이 정해져있기 때문에 docker compose version 3.3 이하로 해야한다는 단점이 있다. 그 이상 버전은 오류를 발생시킨다.
- docker compose 명령어가 아닌 docker-compose 명령어에만 작동이 가능하기 때문에 과거의 docker-compose를 설치해줘야한다는 문제가 있다.
sudo curl -L "https://github.com/docker/compose/releases/download/1.28.5/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose # chmod 를 통해서 실행이 가능하게 세팅
docker-compose -v # docker-compose 명령이 제대로 먹히는 지 확인한다.
백엔드의 Docker-Compse
version: '3.3'
services:
moyeo-nginx:
container_name: moyeo-nginx
image: nginx
restart: always
volumes:
- ./dev/nginx:/etc/nginx/conf.d
- /letsencrypt/certbot/conf:/etc/letsencrypt
- /letsencrypt/certbot/www:/var/www/certbot
ports:
- 80:80
- 443:443
depends_on:
- moyeo-server
networks:
- backbone
command: '/bin/sh -c ''while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g "daemon off;"'''
certbot:
container_name: certbot
image: certbot/certbot
restart: unless-stopped
volumes:
- /letsencrypt/certbot/conf:/etc/letsencrypt
- /letsencrypt/certbot/www:/var/www/certbot
depends_on:
- moyeo-nginx
networks:
- backbone
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
- volume 설정을 통해 서버에 있는 인증서를 docker 환경과 연결해줘야한다.
- /letsencrypt/certbot/conf:/etc/letsencrypt
- /letsencrypt/certbot/www:/var/www/certbot
- 위 설정은 certbot과 nginx가 동일해야하고 /letsencrypt/certbot/ 부분은 init-letsencrypt.sh 의 인증서를 저장할 경로와도 맞춰줘야한다.
- ./dev/nginx:/etc/nginx/conf.d
- 위의 내용은 아래에 나올 NGINX 설정을 Docker 환경에 연동시켜주는 부분이다.
- 특이점이 있다. nginx의 command와 certbot의 entrypoint는 왜 있는 걸까?
command: '/bin/sh -c ''while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g "daemon off;"'''
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
- 인증서가 만료될 때쯤 자동으로 SSL 인증서 갱신을 위한 커맨드를 추가해놓은 것이다.
- nginx의 Background(nginx 환경에서 nginx가 돌면서 다른 프로세스를 실행시킬 수 있는 상태)에서 6시간마다 구성(및 인증서)을 다시 로드하고, daemon off 를 통해 Foreground(nginx 환경에서 nginx가 돌면 아무것도 실행할 수 없는 상태)에서 nginx를 실행한다.
- certbot은 12시간마다 갱신하는 것처럼 짜여있고, 이는 Let’s Encrypt의 권장사항이라고 한다. Let’s Encrypt는 매주 발급받을 수 있는 인증서가 한정되어있기 때문에 해당 논리가 인증서를 발급받는데 해를 끼치진 않을까 걱정할 수도 있다.
- certbot 문서에 따르면 반드시 certbot이 12시간마다 인증서를 갱신하는 것은 아니라고 한다. 30일 이내에 인증서가 만료될 예정이면 갱신하고 그렇지 않으면 갱신하지 않는 식으로 프로그램이 돌아간다고 하니, 안심하도록 하자! (아래 자료를 참고)
- User Guide - Certbot 2.0.0 documentation
- It is normal to use "cerbot renew" every 12 hours?
백엔드의 NGINX 설정
upstream backend-server {
server moyeo-server:3000;
}
server {
listen 80 default_server;
server_name api.moyeomoyeo.com;
server_tokens off;
# certbot이 발급한 challenge 파일을 nginx가 서빙
location /.well-known/acme-challenge/ {
allow all;
root /var/www/certbot;
}
# 모든 http(80포트) 요청을 https로 리다이렉팅
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
server_name api.moyeomoyeo.com;
server_tokens off;
ssl_certificate /etc/letsencrypt/live/api.moyeomoyeo.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.moyeomoyeo.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_pass http://backend-server;
proxy_http_version 1.1;
}
}
- 로드밸런싱 설정을 위해 upstream 을 넣어놨지만, Container가 하나이기때문에 별의미는 없어보인다.
먼저 80번 포트를 설정해보자.
# certbot이 발급한 challenge 파일을 nginx가 서빙
location /.well-known/acme-challenge/ {
allow all;
root /var/www/certbot;
}
Now we can make nginx serve the challenge files from certbot! Add this to the first (port 80) section of our nginx configuration (data/nginx/app.conf )
- 오픈 소스 블로그에 Challenge 파일에 대한 자세한 설명은 없고, Let's Encrypt의 공식문서에 그 내용이 존재한다.
When you get a certificate from Let’s Encrypt, our servers validate that you control the domain names in that certificate using “challenges,” as defined by the ACME standard. Most of the time, this validation is handled automatically by your ACME client, but if you need to make some more complex configuration decisions, it’s useful to know more about them. If you’re unsure, go with your client’s defaults or with HTTP-01.
- Let’s Encrypt는 “챌린지”라는 것을 사용해서 도메인 이름을 제어하는지 검증한다고 한다.
- 찾아보니 아래 챌린지의 유형은 HTTP-01 Challenge로 가장 일반적인 챌린지 유형이다.
- 서버는 http://<YOUR_DOMAIN>/.well-known/acme-challenge/<TOKEN> 형태로 토큰을 저장하게 된다.
- 챌린지 파일은 토큰을 포함하고 있으며 해당 토큰이 있어야 Let’s Encrypt에서 올바른 응답을 받고 유효성 검증에 성공할 수 있다고 한다. 말그대로 필수적인 세팅이다!
- 추가적으로 80번 포트로 들어오게 되면 HTTPS로 자동 리다이렉트되도록 설정했다.
# 모든 http(80포트) 요청을 https로 리다이렉팅
location / {
return 301 https://$host$request_uri;
}
다음은 443 port이다. 인증부터 확인해보자.
ssl_certificate /etc/letsencrypt/live/api.moyeomoyeo.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.moyeomoyeo.com/privkey.pem;
- let’s encrypt의 인증서를 발급받은 경로의 fullchain과 privkey를 연결해줬다.
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
- certbot으로 자동 관리되는 친구들이다.
- include에 명시되어 있는 /etc/letsencrypt/options-ssl-nginx.conf 파일을 열어보면 아래와 같이 SSL 관련 설정들이 작성되어 있다. 내가 한 것이 아니라 오픈소스가 다 해주는 것이다.
# This file contains important security parameters. If you modify this file
# manually, Certbot will be unable to automatically provide future security
# updates. Instead, Certbot will print and log an error message with a path to
# the up-to-date file that you will need to refer to when manually updating
# this file.
ssl_session_cache shared:le_nginx_SSL:10m;
ssl_session_timeout 1440m;
ssl_session_tickets off;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";
- ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
- dhparam에 사용할 pem key를 지정한다. dhparam을 사용하면 client와 server간의 key를 exchange할 때 perfect security를 보장한다고 한다.
이제 upstream으로 만들어놓은 로드밸런서로 던져주기만 하면 된다.
location / {
proxy_pass http://backend-server;
proxy_http_version 1.1;
}
OpenSSL 인증에서 마주한 이슈
1. 인증서를 너무 많이 발급받아 정지를 당한 적이 있었다.
- 공식문서에 따르면 주당 5회까지 중복 발급이 가능하다고 되어있다.
- 이전 버전의 코드 디렉토리를 전부 삭제하고 다시 클론받는 식으로 CD를 진행했는데, 인증서를 코드 디렉토리에 넣은 실수를 범했다.
- 인증서가 삭제되고 계속 다시 발급되다 보니 자연스럽게 정지를 당했다.
- 인증받고 나면 init-script를 다시 실행하지 말자.
2. 네이버 certificate manger 등록
DST Root CA X3 Expiration (September 2021)
- 네이버에 인증서를 보관해 안정적으로 관리하려고 시도했었다.
- DST Root CA X3 루트 인증서가 전통적으로 사용되고 있었고 이 방식을 시도하고 있었다.
- 하지만 해당 방식이 2021-09-30에 만료되어 더 이상 사용할 수 없는 방식이 되었다고 한다.
- 이제 ISRG Root X1 인증서를 사용해야하는데 상당히 번거로워 6주 안에 프로젝트를 마쳐야하는 입장에서는 비용이 너무 많이 든다고 생각되어 포기했다.
실제 프로젝트 확인하러가기
Github Repo: 모여모여 프로젝트 레포지토리
Reference
SSL/TLS: https://velog.io/@dong5854/HTTPS와-SSL인증서-대칭키-공개키비대칭키
구글 크롬 samesite 이슈: https://coding-factory.tistory.com/843
HTTP vs HTTPS: https://blog.wishket.com/http-vs-https-차이-알면-사이트의-레벨이-보인다/
AMP 작동하는 방식: https://developers.google.com/search/docs/crawling-indexing/amp/about-amp?hl=ko
AMP 공식문서: https://support.google.com/tagmanager/answer/9205783?hl=ko
let’s encrypt 공식문서 - challenge files: https://letsencrypt.org/docs/challenge-types/
certbot은 정말 12시간마다 갱신할까?: https://stackoverflow.com/questions/73435012/it-is-normal-to-use-cerbot-renew-every-12-hours