HTTP로 서비스하면 안 되는 이유가 많지만, 우리 프로젝트는 결제와 얼굴 인증 데이터를 다루기 때문에 HTTPS가 필수였다. Let's Encrypt로 무료 SSL 인증서를 발급받았다.
HTTP는 평문 통신이다. 클라이언트와 서버 사이
서 누군가 네트워크를 도청하면(Man-in-the-Middle Attack) 데이터가 그대로 보인다. 비밀번호, JWT 토큰, 결제 정보가 모두 노출된다.
HTTPS는 HTTP + TLS(Transport Layer Security)다. TLS가 하는 일은 세 가지다:
데이터를 암호화해서 도청을 방지한다.
데이터가 중간에 변조되지 않았음을 보장한다.
이 서버가 진짜 j14d103.p.ssafy.io 인지 인증서로 증명한다.
TLS 핸드셰이크 과정을 간단히 보면,
1. 클라이언트가 서버에 접속 요청
2. 서버가 SSL인증서(공개 키 포함)를 전달
3. 클라이언트가 인증서를 CA(Certificate Authority)로 검증
4. 대칭키를 공개키로 암호화해서 서버에 전달
5. 이후 통신은 대칭키로 암호화
비대칭키(RSA)로 대칭키를 교환하고, 실제 데이터는 대칭키(AES)로 암호화하는 하이브리드 방식이다. RSA는 안전하지만 느리고, AES는 빠르지만 키 교환이 어렵다. 둘의 장점을 합친 것이다.
Certbot은 Let's Encrypt 인증서를 자동으로 발급/설치/갱신해주는 도구이다.
sudo apt-get install -y certbot python3-certbot-nginx
python3-certbot-nginx는 Nginx 전용 플러그인이다. 이게 있으면 Nginx 설정을 자동으로 수정해준다.
sudo certbot --nginx -d j14d103.p.ssafy.io
이 명령 하나면 다음을 전부 자동으로 해준다:
HTTP-01 챌린지가 뭐냐면, "이 도메인이 정말 네 거 맞아?"를 확인하는 과정이다. Let's Encrypt가 특정 토큰 파일을 웹서버에 올려놓으라고 요구하고, HTTP로 접근해서 파일이 있는지 확인한다. Certbot이 이 과정을 자동으로 처리한다. 그래서 80번 포트가 반드시 열려 있어야 한다.
근데 처음에 실패했다. 에러 메시지가 "Unable to find a matching server block"이었다.
원인은 Nginx 설정에서 servername ;으로 해놨던 거다. certbot이 도메인 이름으로 서버 블록을 찾는데, 와일드카드(_)로는 매칭이 안 된다.
sudo sed -i 's/server_name _;/server_name j14d103.p.ssafy.io;/' /etc/nginx/sites-available/naeda
sudo nginx -t && sudo systemctl reload nginx
이렇게 실제 도메인으로 바꿔준 다음 certbot을 다시 실행하니까 성공했다.
Let's Encrypt 인증서는 90일짜리다. certbot을 설치하면 systemd timer 또는 cron job이 자동으로 등록된다. 보통 하루에 2번 갱신을 시도하는데, 만료 30일 전부터 실제 갱신이 진행된다.
# 갱신 테스트 (실제로 갱신하지 않고 시뮬레이션만)
sudo certbot renew --dry-run
# 타이머 확인
sudo systemctl list-timers | grep certbot
--dry-run은 실제 갱신 없이 과정만 테스트하는 옵션이다. 이게 성공하면 실제 자동 갱신도 문제없을 것이다.
자동 갱신이 실패하는 케이스를 알아두면 좋다:
80번 포트가 막혀 있을 때 (HTTP-01 챌린지 실패)
Nginx가 죽어 있을 때
DNS 설정이 변경됐을 때
디스크가 꽉 찼을 때
server {
server_name j14d103.p.ssafy.io;
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/j14d103.p.ssafy.io/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/j14d103.p.ssafy.io/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
server {
if ($host = j14d103.p.ssafy.io) {
return 301 https://$host$request_uri;
}
listen 80;
server_name j14d103.p.ssafy.io;
return 404;
}
각 파일의 역할:
1. fullchain.pem: 서버 인증서 + 중간 인증서. 클라이언트에게 전달되는 인증서 체인이다.
2. privkey.pem: 서버 개인키. 이 파일이 유출되면 HTTPS가 무력화된다. 절대 외부에 노출하면 안 된다.
3. options-ssl-nginx.conf: TLS 프로토콜 버전, 암호화 스위트 등 보안 설정. TLS 1.0/1.1은 비활성화하고 TLS 1.2/1.3만 허용하는 게 권장이다.
4.ssl-dhparams.pem: Diffie-Hellman 파라미터. 키 교환 알고리즘의 안전성을 높이는 역할이다.
두 번째 server 블록은 HTTP(80)로 들어오는 모든 요청을 HTTPS(443)로 리다이렉트한다. 301은 영구 리다이렉트라서 브라우저가 캐시한다. 이후에는 브라우저가 자동으로 HTTPS로 접속한다.
한 단계 더 보안을 강화하려면 HSTS 헤더를 추가할 수 있다:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
이 헤더가 있으면 브라우저가 해당 도메인에는 무조건 HTTPS로만 접속한다. 301 리다이렉트를 거칠 필요도 없이 브라우저 레벨에서 강제한다. 첫 번째 HTTP 요청에서의 도청 위험까지 없애는 것이다.