[ 살펴보기 ] Nginx - TLS Certificate
![[ 살펴보기 ] Nginx - TLS Certificate](https://cdn.hashnode.com/res/hashnode/image/upload/v1729389630654/bee48901-100f-4bb1-b01f-6c2cd4bbab25.jpeg)
우선 용어를 정리하자. TLS는 Transport Layer Security의 약자로서 SSL( Secure Socket Layer )의 취약점을 개선한 upgrade version이다. 현재 SSL은 deprecated 되었으므로 대부분의 경우 SSL나 SSL/TLS라는 용어는 TLS를 뜻한다. ( Reference - What’s the Difference Between SSL and TLS? )
Client와 server가 HTTPS로 통신하기 위해선 server 측에서 client에 전달할 TSL 인증서가 필요하다. TLS 인증서는 인증 기관에 따라 비용이 발생할 수 있지만 Let’s encrypt와 같은 인증 기관을 통해 무료로 발급 받을 수도 있다. 혹은 단순 테스트 용도라면 다른 인증 기간을 통하지 않고 self-signed 인증서를 생성해 사용할 수도 있다.
Browsers는 서버가 전달한 인증서가 신뢰할 수 있는 인증기관을 통해 발급된 인증서인지 확인하고 만약 인증서가 self-signed 인증서와 같이 신뢰할 수 있는 인증기관에서 발급한 인증서가 아니면 페이지 접근 전에 경고나 페이지 접근을 위해 추가 설정을 요구한다.
해당 포스트는 무료로 신뢰할 수 있는 TLS 인증서를 발급해주는 Certificate Authority인 Let’s encrypt를 통해 nginx 서버에서 TLS 인증서를 발급 받고 client가 HTTPS로 통신을 할 수 있게 설정하는 법을 살펴본다. 그리고 간단한 테스트를 하는 상황을 위해 self-signed 인증서를 발급하는 방법도 살펴본다.
아래의 포스트 내용은 Ubuntu 22.04 환경에서 진행되었습니다.
Nginx 설정
우선 nginx server block을 설정한다. server_name에 등록한 도메인을 설정하고 location block에 request를 처리하는 필요 설정을 추가한다. 아래 예제에선 domain 이름이 example.com이라고 가정한다.
해당 포스트에서는 NodeJS 서버가 실제 reqeust를 처리하는 환경에서 테스트를 진행하므로 proxy_pass를 통해 NodeJS 서버가 실행되고 있는 port로 request를 전달해준다.
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name example.com;
location / {
proxy_pass http://localhost:3000;
}
}
Certbot
Certbot라는 tool을 사용하면 let’s encrypt에서 TLS 인증서의 발급 및 갱신을 보다 쉽게 처리할 수 있다. 다음 명령어를 통해 certbot을 설치한다.
sudo snap install --classic certbot
그리고 다음과 같이 symbolic link를 생성하여 certbot command를 사용할 수 있게 준비한다.
sudo ln -s /snap/bin/certbot /usr/bin/certbot
이제 다음 명령어를 실행하면 인증서 발급과 인증서 적용에 필요한 nginx 설정까지 자동으로 업데이트 해준다. -d option에는 인증서를 발급할 domain 정보를 기입해준다.
sudo certbot --nginx -d example.com
위의 명령어를 입력하면 email을 묻는 message와 이용약관에 대한 동의를 얻는 message를 볼 수 있다. email 주소와 이용약관 동의에 Y를 기입하여 진행하면 다음과 같은 메시지가 뜨는 것을 볼 수 있다.
Successfully received certificate.
Certificate is saved at: /etc/.../fullchain.pem
Key is saved at: /etc/.../privkey.pem
...
인증서 발급 및 nginx configuration 설정 업데이트가 완료되었다면 이제 기존 domain을 https를 통해 접속해보면 정상적으로 접속이 되는 것을 확인할 수 있다.
만약 AWS EC2와 같은 cloud service를 통해 테스트 중이라면 인증서를 발급할 때 EC2 instance에 적용된 inbound rule에 따라 connection timeout으로 인해 인증서 발급에 실패할 수도 있으므로 인증서 발급할 때는 inbound rule을 잠시 변경해준다.
또한 instance에 적용된 inbound rule이 https type의 traffic을 허용하는지도 체크하자.
이제 certbot이 인증서를 발급받고나서 자동으로 수정한 nginx 설정 내용을 살펴보자. 수정된 내용은 아래와 정확히 동일하지 않을 수 있다.
server {
listen [::]:443 ssl ipv6only=on; # managed by Certbot
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/.../fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/.../privkey.pem; # managed by Certbot
include /etc/.../options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/.../ssl-dhparams.pem; # managed by Certbot
server_name example.com;
location / {
proxy_pass http://localhost:3000;
}
}
server {
listen 80 default_server;
listen [::]:80 default_server;
if ($host = example.com) {
return 301 https://$host$request_uri;
} # managed by Certbot
server_name example.com;
return 404; # managed by Certbot
}
위의 예제에서 볼 수 있듯이 certbot으로 추가된 사항은 managed by certbot이라는 주석이 함께 추가된다. 기존의 80 port를 listen하던 server block은 이제 443 port를 listen하는 server block으로 변경되고 ssl 인증서 경로 및 기타 ssl 관련 configuration이 설정된 파일을 include한다.
그리고 그 아래에 80 port를 listen하는 server block은 이제 http로 request가 들어오면 301 status code를 통해 https server block으로 redirect하는 역할을 한다.
Certificate 갱신
Let’s encrypt를 통해 발급받은 인증서는 90일간 유효하다. 그렇기에 인증서 유효기간이 끝나기 전에 인증서를 갱신해야 하며 우리가 앞서 사용한 certbot은 인증서 갱신 작업까지 자동으로 해준다.
Certbot을 통해 인증서를 발급 받았다면 certbot이 매일 인증서의 유효기간을 체크하고 유효기간이 2주 정도 남았을 때 인증서 갱신 작업을 자동으로 수행한다. ( systemd timer인 certbot.renew.timer와 systemd service인 certbot.renew.service를 통해 자동 갱신을 수행한다 )
만약 어떠한 이유로 인증서 갱신 작업에 실패하면 인증서를 발급하는 과정에 기입했던 email로 인증서 만료기간 관련 안내가 전달된다.
Certbot을 통해 직접 인증서 갱신을 실행하고 싶다면 아래와 같이 실행할 수 있다.
sudo certbot renew
만약 실제 변화를 적용하지 않고 인증서 갱신 과정에 문제가 없는지만 테스트 해보고 싶다면 --dry-run option을 사용한다.
sudo certbot renew --dry-run
Self-Signed Certificate
만약 간단한 테스트용으로 인증서를 발급하는 상황이라면 굳이 Let’s encrypt와 같은 인증 기관을 통해 인증서를 발급 받지 않고 자체적으로 인증서를 발급해 사용할 수도 있다. 이렇게 인증 기관을 통해 인증서를 발급하는게 아닌 스스로 발급한 인증서를 self-signed 인증서라고 한다.
현재 따로 등록한 domain은 없고 AWS EC2에 deploy한 application server를 위한 self-signed 인증서를 발급하는 과정을 살펴보자.
우선 openssl을 통해 TLS 인증서를 생성한다. Ubuntu에서 테스트 중이라면 대부분 openssl은 미리 설치되어 있지만 설치되어 있지 않다면 설치하여 사용한다.
우선 RSA private key를 생성한다.
sudo openssl genrsa -out /etc/ssl/private/nginx-testkey.key 2048
genrsa : RSA private key를 생성하기 위한 sub command.
-out : private key가 생성될 경로와 이름을 지정한다.
2048 : private key를 생성할 때 사용되는 bit의 수를 지정한다. 예제에선 2048 bits private key를 생성한다.
이제 Certificate Sgning Request ( CSR ) file을 생성한다. 아래 명령어를 실행하면 인증서에 필요한 정보를 물어보는 prompt가 나타난다. 테스트 용도이므로 대부분의 정보는 임의로 기입해도 무방하며 Common Name은 테스트할 domain name이 있다면 해당 domain name을 기입하고 등록한 domain name이 없다면 appliation이 배포된 EC2 instance의 public ip를 기입한다.
sudo openssl req -new -key /etc/ssl/private/nginx-testkey.key -out /etc/ssl/certs/nginx-testreq.csr
req : CSR을 생성하기 위해 사용되는 sub command.
-new : 새로운 CSR을 생성할 때 사용하는 option.
-key : CSR을 생성할 때 사용될 private key를 지정한다.
-out : CSR을 생성할 경로와 이름을 지정한다.
우리는 self-signed certificate을 위해 CSR file을 생성했으므로 생성한 CSR file을 CA( 인증기관 )를 통해 인증 받고 CA에서 인증서를 발급받는 과정 없이 위에서 생성한 key와 CSR file을 통해 바로 self-signed certificate을 생성한다. 아래와 같이 x509 sub command를 통해 signed certificate을 생성할 수 있다.
sudo openssl x509 -req -days 90 -in /etc/ssl/certs/nginx-testreq.csr -signkey /etc/ssl/private/nginx-testkey.key -out /etc/ssl/certs/nginx-testcert.crt
-req : x509 sub command는 signed certificate를 생성하기 위해 default로 certificate을 input으로 받는다. 하지만 우리는 위에서 생성한 CSR file을 input으로 사용하기 때문에 -req option을 통해 CSR file을 input으로 사용하도록 설정해준다.
-days : certificate의 유효 기간을 설정한다.
-in : signed certificate 생성을 위한 input certificate을 지정한다. 우리의 경우에는 certificate이 아닌 CSR file을 지정한다.
-signkey : signing에 사용될 private key를 지정한다. 이전 단계에서 CSR을 생성할 때 지정했던 private key를 지정한다.
-out : signing 처리가 된 signed certificate이 생성될 경로와 이름을 지정한다.
정보를 모두 입력하면 유효기간이 90일인 self-signed 인증서가 지정한 -out option에 지정한 경로에 생성된다.
이제 보다 안전한 server-client key exchange를 위한 설정을 위해 openssl을 통해 DH ( Diffie-Hellman ) parameters를 생성한다.
sudo openssl dhparam -out /etc/nginx/dhparam.pem 4096
dhparam : Diffie-Hellman parameters를 생성하는 sub command.
-out : 생성된 DH parameters가 저장될 경로와 파일명.
4096 : 생성될 DH parameters의 bit size.
Nginx ssl와 관련된 설정을 위해 새로운 설정 파일을 만든다. 예제에선 /etc/nginx/snippets 경로에 ssl-test.conf라는 파일을 생성한다. 그리고 해당 파일에 아래와 같이 ssl관련 설정을 추가한다.
ssl_protocols TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_dhparam /etc/nginx/dhparam.pem;
ssl_ciphers EECDH+AESGCM:EDH+AESGCM;
ssl_ecdh_curve secp384r1;
ssl_session_timeout 10m;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
위에 사용된 option은 다음과 같다.
ssl_protocols : 사용할 protocol을 설정한다.
ssl_prefer_server_ciphers : on으로 설정되면 TLS protocol을 사용할 때 server ciphers가 client ciphers보다 선호된다.
ssl_dhparam : PH parameters 파일을 설정한다.
ssl_ciphers : HTTPS connection을 형성할 때 어떤 TLS ciphers가 사용될 수 있는지 설정한다. 예제에서는
EECDH+AESGCM와EDH+AESGCM두 개의 cipher suites이:로 구분되어 있으며 client가EECDH을 지원하지 않으면 두 번째 cipher suite인EDH+AESGCM가 사용된다.ssl_ecdh_curve : ECDHE cipher에 사용되는 curve를 설정한다.
ssl_session_timeout : client가 SSL session을 재사용할 수 있는 시간을 설정한다. 위의 예제에선 10분으로 설정하고 있다.
ssl_session_cache : SSL session cache의 type과 size를 설정한다. shared type을 설정하면 session cache가 모든 worker process와 공유된다. 위의 예제에선 type은 share, shared cache의 이름은 SSL, 사이즈는 10 megabyte로 설정하고 있다.
ssl_session_tickets : TLS session tickets을 통한 TLS session resumption 허용 여부를 설정한다.
ssl_stapling : OCSP ( Online Certificate Status Protocol ) stapling 적용 여부를 설정한다. Online Certificate Status Protocol를 통해 server에서 발생한 인증서가 폐기된 인증서인지 등 인증서의 상태를 확인할 수 있다.
Client에서 HTTPS를 통해 server와 통신할 때 client는 server에서 받은 인증서가 폐기된 인증서인지 등을 확인해야 하는데 이 때 client가 Certificate Authority ( CA )에 request를 보내 확인할 수 도 있지만 이는 client request의 latency가 늘어날 수 있다.
그렇기에 nginx ssl_stapling 기능을 활성화하면 server에서 주기적으로 CA에 request를 보내 인증서 상태를 검증하고 TLS negotiation 과정에서 client에 인증서와 CA에서 검증된 인증서 상태 정보를 client에 전달하므로 client는 HTTPS로 server와 통신할 때 추가로 CA에 인증서 상태 확인 request를 보낼 필요가 없어진다.
ssl_stapling_verify : Server에서 CA에 인증서 상태 점검을 요청하고 하고 돌아오는 OCSP response가 올바른 response인지 검사할 것인지 설정한다.
resolver : Domain name resolution을 위해 사용할 name server를 설정한다. stapling 기능을 활성화 하면 OCSP response hostname resolution을 위해 resolver directive도 설정해야 한다. 위의 예제에선 google public dns를 사용하고 있다. (
8.8.8.8,8.8.4.4) Nginx는 default로 resolution 결과를 response TTL에 설정된 기간동안 cache 하지만 valid option을 통해 cache 기간을 조정할 수 있다. 위의 예제에선 300초간 cache 한다.resolver_timeout : Domain resolution의 timeout 시간을 설정한다.
add_header X-Frame-Options : Browser가 response로 보내지는 page를 iframe으로 render하지 못하도록 한다. 현재는 해당 header보다
Content-Security-Policyheader의 사용이 권장된다. ( Reference - MDN X-Frame-Options )add_header X-Content-Type-Options : Response header에 response content의 mime type이 설정되어 있지 않거나 특정 경우 browser가 response content의 mime type을 추론하여 사용하는데 이를 mime type sniffing이라고 한다. 해당 header는 sniffing을 방지하고 언제나 response header의 content-type에 설정된 mime type을 따르도록 browser에게 알린다.
이제 위의 과정에서 생성한 인증서와 key, 그리고 ssl 관련 설정을 가진 conf 파일을 server block에 설정해야 한다.
server {
listen 443 ssl;
listen [::]:443 ssl;
ssl_certificate /etc/ssl/certs/nginx-testcert.crt;
ssl_certificate_key /etc/ssl/private/nginx-testkey.key;
include snippets/ssl-test.conf;
root /home/user/service;
index index.html;
server_name example.com; # domain 또는 ip 주소
location / {
try_files $uri $uri/ =404;
}
}
그리고 이제 일반 http protocol로 들어온 request를 https server block으로 redirection 해준다.
server {
listen 80;
listen [::]:80;
server_name example.com; # domain 또는 ip 주소
return 301 https://$server_name$request_uri;
}
이제 self-signed 인증서를 테스트할 준비가 완료 되었다. sudo ngint -t를 통해 configuration에 문제가 없는지 확인하고 sudo systemctl restart nginx 명령을 통해 nginx를 재시작 해준다. 이제 server block에 설정한 domain이나 ip로 접속해 self-signed 인증서를 통한 https connection을 테스트 해볼 수 있다.
주의할 점은 self-signed 인증서는 신뢰할 수 있는 인증 기관에서 발급 받은 인증서가 아니기에 self-signed 인증서를 제공하는 사이트에 접근하려고 하면 browser는 다음과 비슷한 에러를 보여준다.
현재 우리는 단순 테스트 중이므로 큰 문제는 없기에 아래 Advanced 버튼을 통해 접근을 허용하여 페이지에 접근할 수 있다.

![[ 살펴보기 ] RDB - Relationships](https://cdn.hashnode.com/res/hashnode/image/upload/v1739711556668/48dc9e84-a621-42aa-9c9f-5fc5c436f0ec.jpeg)
![[ 살펴보기 ] MySQL - Data types](https://cdn.hashnode.com/res/hashnode/image/upload/v1739593589113/530f8704-4d27-42c9-a451-bb5c63150b99.jpeg)
![[ 살펴보기 ] TypeORM - Transactions, Migration](https://cdn.hashnode.com/res/hashnode/image/upload/v1739106042581/980b8133-61d4-406a-a026-65be9c28eace.jpeg)
![[ 살펴보기 ] TypeORM - Relations](https://cdn.hashnode.com/res/hashnode/image/upload/v1738666874402/b688bd0b-b6bb-4f43-87d8-c1b46b59f1b7.jpeg)
![[ 살펴보기 ] TypeORM - Basics](https://cdn.hashnode.com/res/hashnode/image/upload/v1738666803591/bef5df17-7dc7-4123-ae55-004d5042df39.jpeg)