이번 스프린트3 동안 필수 요구사항인 HTTPS
를 도입해야 했다.
HTTPS 의 동작원리 등 이론적인 내용은 그린론이 작성해준 HTTPS 동작 원리 와 라쿤의 HTTPS 테코톡을 참고하였고, 실제 적용하는 부분에 있어서는 리차드가 써준 10분만에 끝내는 EC2 생성, NGINX 구성, SSL 적용 를 참고하였다.
이번에 요구사항을 보고 나서 "기능 구현도 하기 바쁜데..HTTPS를 왜 필수로 해야하는 걸까?" 라는 생각이 들었었다.
궁금해서 HTTP 완벽 가이드
라고 하는 책을 읽어보았다. 책에서는 온라인 쇼핑이나 인터넷뱅킹과 같은 웹 트랜잭션에서 중요한 일을 사람이 하기 때문에 웹은 안전한 방식의 HTTP가 필요하다고 한다.
하지만 우리 서비스의 경우, 스터디 관리를 위한 웹 페이지로 돈과 관련된 중요한 비즈니스를 처리하는 웹 사이트는 아니다. 그런데도 왜 HTTPS 가 필요할까?
HTTPS는 데이터를 암호화해서 전송하는 것 이외에, 데이터 무결성 또한 보장한다. 데이터는 전송 중 의도적으로 또는 비의도적으로 감지 및 탐지되지 않고 수정되거나 손상될 수 없다.
또한 우리의 클라이언트가 신뢰할 수 있게 된다. 즉, 사용자가 접속한 웹 사이트가 공격자로부터 보호받고 신뢰할 수 있음을 보장할 수 있다.
결론적으로 우리 서비스가 금전적인 비즈니스와 연관이 없다고 하더라도 우리 서비스의 사용자로 하여금 신뢰를 제공할 수 있고, 데이터의 무결성 등 서버와 제대로 통신하고 있다는 것을 보장하기 위해서라도 HTTPS를 해야하는 것이다.
물론 기존의 HTTP 보다는 한 단계(SSL)를 더 겨쳐야 하기 때문에 속도 측면에서 불리한 것 아니냐라고 이야기 할 수 있지만, HTTPS 의 경우 공개키 함호화 방식으로 한 번 통신한 이후에는 대칭키를 사용하기 때문에 속도 저하가 크지 않고, 이 보다 보안적인 측면에서의 가치가 충분하기 때문에 HTTPS 를 사용한다고 이야기할 수 있다.
HTTPS 는 기존의 HTTP 에서 애플리케이션 계층과 TCP 계층 사이에 보안 계층을 하나 둔 구조이다.
SSL(Secure Socket Layer)
혹은 전송 계층 보안(Transport Layer Security, TLS) 이라고 불리는 해당 계층은 응용계층에서 내려온 데이터를 암호화하여 TCP 계층으로 내려서 요청을 보내기 때문에 요청문 부분은 소켓을 타고 네트워크를 타는 사이에서 암호화 되어 있다.
반대로 해당 HTTP 메시지를 받는 입장에서는 TCP와 애플리케이션 계층 사이에서 복호화한다. 이러한 과정을 통해서 네트워크를 타고 패킷이 날아다니는 과정에서 공격자가 탈취를 하게 되더라도 내용이 암호화되어 있기 때문에 피해가 없게 되는 것이다.
이러한 구조 덕분에 만약 URL 이 HTTP 스킴을 갖고 있다면, 클라이언트는 서버에 80번 포트로 연결하고 평범한 HTTP 메시지를 전송한다. 반면 만약 URL 이 HTTPS 스킴을 갖고 있다면, 클라이언트는 서버에 443 포트로 연결하고 서버와 바이너리 포맷으로 된 몇몇 SSL 보안 매개변수를 교환하면서 '핸드쉐이크' 과정을 거치고, 암호화된 HTTP 명령이 뒤를 잇는다.
(SSL 트래픽은 바이너리 프로토콜이기 때문에 HTTP와는 완전히 다르다. 그 트래픽은 다른 포트 (SSL은 보통 443)로 전달된다. 만약 SSL과 HTTP 트래픽 모두가 80번 포트로 도착한다면, 대부분의 웹 브라우저는 바이너리 SSL 트래픽을 잘못된 HTTP로 해석하고 커넥션을 닫을 것이다.)
암호화되지 않은 HTTP 트랜잭션
1. 서버의 80포트로 TCP 커넥션 연결
2. TCP를 통해 보내진 HTTP 요청
3. TCP를 통해 보내진 HTTP 응답
4. TCP 커넥션 close
암호화된 HTTPS 트랜잭션
1. 서버의 443 포트로 TCP 커넥션 연결
2. SSL 보안 매개변수 핸드쉐이크
3. SSL과 HTTP 요청/TCP를 통해 보내진 암호화된 요청
4. SSL과 HTTP 요청/TCp를 통해 보내진 암호화된 응답
5. SSL close
5. TCP 커넥션 close
SSL(Secure Socket Layer) : 컴퓨터 네트워크에 통신 보안을 제공하기 위해 설계된 암호 규약으로 SSL는 과거의 명칭이고, 현재는 TLS(Transport Layer Security - 전송 계층 보안)라고 한다.
http와 https에 대해 알아보자
이제 TLS 계층에서 어떻게 암호화를 해서 서로 HTTP 통신을 하게 되는지 알아보자.
TLS(SSL) 이 어떻게 동작하는지 알기 전에 사전지식으로써 대칭키 암호화 방식과 공개키 암호화 방식에 대해서 알아야 한다.
대칭키 암호화
방법은 암호화 할 때 사용하는 키와 복호화할 때 같은 키를 사용하는 방식이다. 따라서 발송자와 수신자 모두 통신을 위해 비밀 키를 동일하게 공유한다. 따라서 간단하고 빠르다는 장점이 있지만, 비밀 키가 노출되면 암호화된 내용을 해독 가능하기 때문에 보안적인 측면에서 그렇게 좋은 방법은 아니라고 볼 수 있다.
반면 공개키 암호화
란 두개의 비대칭 키를 사용하는 방식이다. (서로 다른 2개의 키를 사용한다.) 하나는 메시지를 암호화하기 위한 것이며, 다른 하나는 메시지를 복호화하기 위한 키라고 이해하면 좋다. 이 때, 암호화를 하는 키는 모두에게 공개되어 있지만, 이 암호화된 암호문을 복호화하여 평문을 얻기 위해서는 복호화 키가 필요하고, 이는 자기 자신만이 가지는 구조이다.
http와 https에 대해 알아보자의 예시를 통해서 공개키 암호화 방식에 대해서 자세히 알아보자.
이렇게 되면 대칭키 암호화 방식에 비해서는 보안적인 측면에서 더 좋다라고 말할 수 있다. 하지만 위의 과정을 보아도 알 수 있다시피 대칭키 암호화 방식에 비해서 복잡하고 위 과정을 계속해서 수행하기에는 시간이 많이 걸린다는 단점이 존재하게 된다. 특히, Web 이라는 특성상 이렇게 시간이 오래걸린다는 단점은 사용자 입장에서 치명적으로 다가올 수 있다.
따라서 공개키 암호 방식의 알고리즘은 계산이 느리다는 단점과, 대칭키 암호화 방식은 보안에 비교적 취약하다는 단점을 서로 보완하기 위해서 대칭키 방식과 비대칭 방식을 혼합하여 사용한다.
즉, 두 호스트 사이의 안전한 통신 채널을 수립할 때는 공개키 암호화 방식
을 사용하고, 이렇게 만들어진 안전한 채널을 통해, 임의의 대칭키를 생성하고 교환하여 이후의 나머지 데이터를 암호화할 때는 속도가 빠른 대칭키를 사용
하는 방식이다.
마찬가지로 Tecoble 글의 예시로 자세히 알아보자.
앞서 보았던 것과 같이 암호화된 HTTP 메시지를 보내기 위해서는 클라이언트와 서버 사이에 SSL 핸드 쉐이크 과정이 필요하다.
통신을 하는 브라우저와 웹 서버가 서로 안호화 통신을 시작할 수 있도록 신분을 확인하고, 필요한 정보를 클라이어늩와 서버가 주거니 받거니 하는 과정
HTTPS를 위한 SSL/TLS 핸드셰이크 작동원리
SSL 핸드쉐이크는 다음의 과정을 거친다.
즉, 암호화된 HTTP 데이터가 네트워크를 통해서 오가기 전에 SSL은 통신을 시작하기 위해 상당한 양의 핸드쉐이크 데이터를 주고 받는다.
클라이언트가 먼저 서버에 접속한다. HTTP 는 TCP의 일종이므로 TCP 연결을 위한 3-Way 핸드쉐이크를 한 후에 클라이언트는 HTTPS 통신이 필요함을 알게 되고 클라이언트(브라우저)가 사용하는 SSL 혹은 TLS 버전 정보, 지원하는 암호화 방식 모음(cipher suite), 순간적으로 생성한 임의의 난수, 만약 이전에 SSL 핸드쉐이크가 완료된 상태면 그 때 생성된 세션 아이디(Session ID), 기타 확장 정보(extension)을 포함하여 전송하게 된다.
cipher suite이란, 보안의 궁극적 목표를 달성하기 위해 사용하는 방식을 패키지의 형태로 묶어놓은 것을 의미한다. 여기서 보안의 목표란 다음과 같다.
안전한 키 교환, 전달 대상 인증, 암호화 알고리즘, 메시지 무결성 확인 알고리즘
HTTPS를 위한 SSL/TLS 핸드셰이크 작동원리
서버는 앞서 온 클라이언트의 요청에 응답하면서, 다음 정보를 클라이언트에 제공한다.
이후 클라이언트는 서버의 SSL 인증서가 믿을만한 것인지를 확인한다. 대부분 브라우저에는 공신력 있는 CA들의 정보와 CA가 만든 공개키가 이미 등록되어 있다. 서버가 보낸 SSL 인증서가 정말 CA가 만든 것인지를 확인하기 위해 내장된 CA 공개키로 암호화된 인증서를 복호화하는데, 정상적으로 복호화가 되면 CA 가 발급한 것이 증명되는 것이다.
만약 등록된 CA가 아니거나 등록된 CA가 만든 인증서처럼 꾸민 것이라면 이 과정에서 브라우저는 경고를 띄우게 된다.
다음으로 클라이언트는 자신이 생성한 난수와 서버의 난수를 사용하여 premaster secret(암호화 통신에 사용될 대칭키, 대칭키이므로 절대로 노출되어서는 안된다.)을 만들어 서버에 전송한다.
서버는 비밀키로 브라우저가 보낸 premaster secret 값을 복호화하고 이를 master secret(복호화한 값) 값으로 저장한다. 이를 이용하여 클라이언트와 만들어진 연결에 고유한 값을 부여하기 위한 세션 키(session key)를 생성하고 세션 키는 대칭키 암호화에 사용한다.
이렇게 SSL 핸드쉐이크 과정을 종료하고 본격적으로 HTTPS 통신을 시작한다.
HTTPS 를 적용하기 위해서 EC2 인스턴스를 하나 만들고 거기에 NGINX 를 올렸다.
NGINX 는 Web Server 구축을 위한 소프트웨어이다. 따라서 NGINX를 웹 서버 소프트웨어라고도 한다. 우리가 흔히 알고 있는 Apache도 웹서버이다.
Apache는 쓰레드/프로세스 기반으로 요청을 처리하는데 요청 하나당 쓰레드 하나가 처리하는 구조로 사용자가 많아지면 CPU와 메모리 사용이 증가해 성능이 저하된다는 특징이 있다.
반면 NGINX는 비동기 이벤트 기반 방식으로 요청이 들어오면 어떤 동작을 해야하는지만 알려주고 다음 요청을 처리하는 방식으로 진행되어 응답이 빠르다.
우리는 리버스 프록시(Reverse Proxy) 목적으로 WAS 앞 단에 WS 를 둘 목적이었다. 따라서 Apache 보다는 비동기 이벤트 방식으로 동작하는 NGINX 가 적절했다.
여기서 리버스 프록시란 서버 앞 단에서 중계기능을 하는 서버이다. 클라이언트가 서버에 요청을 보낼 때 직접 WAS 서버로 요청을 보내는 것이 아니라, 리버스 프록시가 요청을 받고 적절한 WAS 서버로 요청을 대신해서 보내주게 된다. 이렇게 되면 클라이언트 입장에서는 리버스 프록시 서버를 호출하고 리버스 프록시 서버만을 알기 때문에 실제 서버를 숨길 수 있게 되고, 보안을 높일 수도 있다.
또한 이렇게 되면 로드밸런싱 을 해줄 수도 있다. 즉, 여러 서버를 두어서 서비스를 운영한다고 하면 부하를 분산시켜 적절한 서버로 요청을 보내게 하는 것이다. 하지만 우리의 경우에는 WAS 서버가 한 대이므로 이렇게 로드밸런싱 까지 필요하지 않았다.
우리는 이렇게 리버스 프록시로써 NGINX 를 올린 인스턴스를 하나 두고 클라이언트에서는 여기로 요청을 보내도록 하였다. 그리고 실제 서버와의 통신은 리버스 프록시가 대신 수행하게 된다. 이렇게 되면 Clinet <-> 리버스 프록시 사이에는 HTTPS 통신이 필요하지만, 리버스 프록시 <-> 실제 서버 사이에서는 HTTP 통신을 수행하게 된다.
또한 현재 우리는 개발 서버와 운영 서버 이렇게 2대를 운영하고 있다. 따라서 클라이언트에서 요청 도메인에 따라 2개의 서버로 분기할 수 있도록 처리해주도록 설계하였다.
참고 웹 서버와 NGINX
가장 먼저 NGINX
를 올려 리버스 프록시 역할
을 할 EC2 인스턴스를 하나 생성해주었다.
그리고 해당 EC2 인스턴스는 HTTPS 적용을 위해서 HTTPS(SSL) 기본 포트인 443 번을 열어주어야 하므로 다음과 같이 인바운드 규칙을 설정해준다.
이후 해당 WS 서버(NGINX가 올라가있는 인스턴스)에 도메인을 연결해주는 작업을 진행하였다.
가비아라고 하는 곳을 가장 많이 쓰는 것 같아 해당 사이트를 이용하였다.
마음에 드는 도메인을 하나 구매하고 사진과 같이 도메인 레코드를 등록한다.
http://www.moamoa.ne.kr
로 오는 요청에는 값/위치
에 등록한 우리의 실제 WS 서버(NGINX가 올라가 리버스 프록시 역할을 할 인스턴스)가 올라가 있는 EC2 인스턴스로 연결되게끔 IP 주소를 입력해주었다.
또한 @
는 빈값으로 대체되어 http://moamoa.ne.kr
로 오는 요청에도 동일하게 우리의 EC2 인스턴스로 연결되도록 하였다.
마지막으로 http://dev.moamoa.ne.kr
로 오는 요청에 대해서도 동일하게 요청이 오도록 설정해주었다.
(주의: dev와 같이 도메인명 맨 뒤에는 '.' 을 추가로 입력해야 한다.)
그리고 앞서 생성하였던 인스턴스에서 다음 명령어를 통해서 nginx 를 설치해준다.
sudo apt-get update //apt-get 업데이트
sudo apt-get install nginx -y //apt-get 을 통해 nginx 설치
위와 같이 nginx 를 설치하고 나면, NGINX 는 설치와 함께 80번 포트에 자동으로 가동되기 때문에 우리가 앞서 설정한 도메인으로 접속을 요청해보면 다음과 같이 Welcome to nginx!
라는 문구와 함께 제대로 구동되는 것을 확인할 수 있다.
(주의 요함을 보면 아직 HTTPS 적용이 안되어있는 것 또한 확인할 수 있다.)
다음으로는 nginx에 설정을 통해서 어떤 도메인으로 연결이 오면 어디로 요청을 보낼지에 대해서 설정해주어야 한다.
NGINX 기본 설정은 /etc/nginx/sites-available/default
파일에 설정되어 있고, 여기에 추가해주면 된다.
우리는 moamoa.ne.kr
도메인과 www.moamoa.ne.kr
도메인으로 오는 요청은 우리의 WAS 서버, 즉 스프링이 올라가 구동되고 있는 운영 서버
로 요청이 가도록 할 것이고, dev.moamoa.ne.kr
도메인으로 오는 요청에는 우리의 개발 서버
로 요청이 가도록 할 것이기 때문에 여기에는 개발용으로 만든 EC2 인스턴스의 주소로 설정해주어야한다.
위의 스크린샷과 같이 설정해주면 된다.
listen 80
은 80번 포트 소켓을 열어두고 있다는 의미로 해석할 수 있고, server_name 에 어떤 도메인으로 오는 요청에 대한 설정인지를 명시해주었다고 볼 수 있다.
그리고 access_log
과 error_log
를 통해서 액세스 로그와 에러 로그를 남기도록 하였다.
이후 lcoation /
path가 루트로 시작한다면, 즉 모든 요청에 대해서 중괄호 안에 설정해준 것과 같이 요청을 보내도록 하겠다라고 이해할 수 있다. 여기서 proxy_pass 에 우리의 운영 서버 (스프링이 올라가서 동작하고 있는 WAS 용 EC2 인스턴스 IP 주소) 주소를 기입해주었다.
여기서 NGINX 가 올라가 있는 WS 와 스프링이 올라가 있는 WAS가 같은 VPC 내에 구성되어 있기 때문에 private IP 를 명시해주었다.
아래도 마찬가지인데, 여기서는 proxy_pass
, 즉 요청을 보낼 서버의 주소를 우리의 개발 서버 EC2 인스턴스의 private IP 로 명시해주었다.
이후에는 sudo service nginx restart
명령어로 NGINX를 재구동해주면 제대로 적용이 될 것이다!
(포트번호는 8080 포트를 사용하였다. http://{privateIP}:8080
)
이렇게 설정해주고 나서, 우리의 개발서버의 API에 GET 요청을 보내보면 다음과 같이 JSON 응답이 오는 것을 확인할 수 있다.
이제 마지막으로 SSL
을 적용하여, 클라이언트와 리버스 프록시 사이에서 HTTPS 로 통신하도록 구성해주어야한다.
certbot을 이용하면 몇 번의 커멘드 입력으로 간단하게 끝낼 수 있다.
(Let's Encrypt
라는 비영리 기관을 통해 무료로 TLS 인증서를 발급받을 수 있고, 루트 도메인, 서브 도메인 까지 무료로 발급받을 수 있다. 그리고 이렇게 인증서를 발급받기 위해서 Certbot
을 사용한다 정도로만 우선 이해하고 있다.)
아래와 같이 우선 certbot
을 설치해준다.
(우분투 18.04 이하에서는 python-certbot-nginx
로 설치)
이후에는 SSL/TLS 인증서를 발급받아야 하는데, -d
옵션을 통해서 도메인을 나열하여 서브 도메인까지 여러 도메인에 대해서 인증서 발급이 가능하다.
우리팀의 경우 moamoa.ne.kr 과 함께 서브 도메인인 www.moamoa.ne.kr, dev.moamoa.ne.kr 에 대해서 인증서 발급을 받았다.
그리고 certbot은 SSL 을 적용하는데서 그치지 않고 자동으로 리뉴얼까지 해준다.
이후 NGINX 기본 설정 파일인 /etc/nginx/sites-available/default
를 보면 SSL 적용을 위해 수정된 못브을 확인할 수 있다.
마지막으로 다시 dev.moamoa.ne.kr/api/studies
로 요청을 보내면, 이전과 달리 주의 요함
표시가 없고 자물쇠가 잠긴 모습을 볼 수 있다. (크롬에서 자물쇠가 잠긴 표시는 HTTPS 로 통신을 하고 있음을 나타내어 준다.)
이렇게 NGINX 를 통해서 실제 WAS 서버로 요청이 가도록 분기하였기 때문에 요청을 보내는 도메인을 통해서 운영 서버와 개발 서버를 분리해줄 수 있게 되었다.
또한 이번에 그린론과 베루스가 맡은 로깅 기능
을 추가하면서 드디어 운영서버와 개발서버가 의미있게 분리될 수 있게 되었다.
따라서 DB에도 개발서버와 운영서버를 위해서 분리될 필요성이 있게 되었으나, 새로운 인스턴스를 따로 파서 DB 서버를 2대 두는 것은 낭비라고 판단되었고, 2개의 스킴으로 나누어서 관리하기로 결정하였다.
그러면서 든 생각은 DB 테이블의 변화가 생겼을 때, 이것을 적절하게 운영서버에 업데이트해주어야하는데, 동기화가 잘 이루어질까? 하는 고민이 들긴 하였으나, 아직 좋은 방법을 찾지는 못했다.