HTTPS는 HTTP의 요청과 응답을 암호화하여 전달하기 위해 사용한다. HTTP는 암호화 되어 있지 않으므로, 데이터가 중간에 가로채지거나 변조될 수 있는 취약점을 해결하기 위해 도입되었다. 또한 사용자간의 인증을 위해서도 사용한다. HTTPS는 공개 키 기반 구조(PKI)를 사용하여 신원을 확인하고 데이터를 암호화한다.
웹 사이트에 접속했을 때, 웹 사이트가 개인 키를 가지고 있으며, 브라우저는 해당 웹 사이트의 공개 키가 포함된 인증서를 검증한다. 인증서는 신뢰할 수 있는 인증 기관(CA)에 의해 발급된다. 브라우저는 인증서를 사용하여 웹 서버의 신원을 확인한다.
브라우저에 "등록"된 것은 실제로 인증서의 유효성을 확인하기 위한 루트 인증서 목록이다. 웹 서버의 인증서가 이 루트 인증서들 중 하나에 의해 서명되었다면, 브라우저는 그 인증서를 신뢰한다.
HTTPS는 HTTP 프로토콜 상위에 TLS 암호화를 구현한 것이라고 할 수 있다. TLS는 응용 계층과 전송 계층 사이에서 작동하여 안전한 보안 채널을 형성해 주는 역할을 한다.
HTTPS는 다음과 같이 동작한다:
HTTPS의 장점은 다음과 같다.
HTTP의 성능 향상을 목적으로 설계된 새로운 프로토콜인 HTTP/2는 대부분의 상황에서 HTTPS 연결을 전제로 한다. 이를 통해 웹 페이지 속도를 빠르게 하고, 다수의 리소스를 효율적으로 전송할 수 있도록 해 준다.
TLS는 넷스케이프의 SSL을 기반으로 한 보안 프로토콜이다. SSL과 TLS는 사실상 같은 것을 의미한다고 보면 된다. 주로 HTTP를 암호화할 때 사용한다. 인증서를 통해 상호가 적당한 사용자인지 검증하고, 암호화 알고리즘을 통해 암호화된 키를 교환한 후 암호화 통신을 진행한다.
TLS는 세 가지 역할을 한다.
TLS는 공개키를 사용하여 암호화를 진행한다. (공개키 암호화) 공개 키는 서버의 인증서를 통해 클라이언트 장치와 공유된다. 인증서는 CA(인증 기관) 에 의해 암호화되어 서명되며, 브라우저에는 신뢰하는 CA의 목록이 있다. 이 CA의 목록에 저장되지 않은 곳에서 인증서가 발급되었다면, https가 신뢰되지 않는다고 뜬다. (그 빨간색 그거)
서버와 클라이언트는 교환된 공개 키와 개인 키를 통해 세션 키라는 새로운 키에 동의하여 통신을 암호화한다. 세션 키는 이 하나의 통신 세션만을 위해 존재하는 일회용 대칭 키이다.
TLS 프로토콜은 여러 버전(예: TLS 1.0, 1.1, 1.2, 1.3 등)이 있으며, 각각의 버전은 보안 강도와 호환성 측면에서 차이가 있다.
TLS를 사용하는 통신 프로세스를 TLS Handshake 라고 한다. 서버와 클라이언트가 서로를 인식하고, 검증하고, 안전하게 통신할 수 있도록 하는 과정이다.
ClientHello
Handshake 프로세스는 클라이언트가 서버에게 Hello 메시지를 보내면서 시작된다. 이 때 메시지에는 클라이언트가 지원하는 TLS 버전, 클라이언트가 지원하는 암호화 방식, 세션을 재개하기 위한 ID(이전 연결에서 사용됨), 그리고 무작위 난수가 포함되어 있다.
ServerHello
서버는 ClientHello에 대해 ServerHello 메시지로 응답한다. 이 때 메시지에는 선택된 TLS 버전, 서버에서 선택한 암호화 방식, 세션 ID, 그리고 서버에서 생성한 무작위 난수가 포함되어 있다.
Certificate
서버는 자신의 인증서를 클라이언트에게 보내 클라이언트가 서버의 신뢰성을 검증할 수 있도록 한다. 인증서에는 서버의 공개 키와 인증서 발급 기관(CA)의 서명이 포함되어 있다. 클라이언트는 자신의 웹 브라우저에 등록된 “신뢰할 수 있는 인증서” 목록에 있는 공개 키로 인증서 해시 값을 비교하여 서버를 신뢰할 수 있는지 검증한다.
ServerHelloDone
서버가 인증서와 필요한 모든 메시지를 보낸 후, 서버는 ServerHelloDone 메시지를 클라이언트에게 전송하여 서버가 클라이언트에게 보낼 메시지를 모두 보냈다고 한다.
ClientKeyExchange
인증서의 무결성이 검증되었다면, 클라이언트는 클라이언트의 난수와 서버의 난수를 조합하여 대칭 키(세션 키)를 생성한다. 그리고 대칭 키를 서버의 공개 키로 암호화하고, 암호화된 키를 서버에 전송한다. 이를 통해 양측은 통신에 사용할 공유된 비밀을 가지게 된다.
자세히 설명하자면, 클라이언트는 키 교환에 필요한 정보를 서버에 제공한다. 이 정보를 사전 마스터 시크릿(pre-master secret) 이라고 하며, 절대로 노출이 되어서는 안 된다. 이 값을 클라이언트가 서버의 공개 키로 암호화 하여 전송한다. 그러면 서버는 개인 키로 복호화하고, 이렇게 되면 서로가 사전 마스터 시크릿을 공유한다. 이 값을 사용해서 세션에 사용할 키를 만드는 것이다.
ChangeCipherSpec
양측은 이후의 메시지가 협상된 암호화 설정을 사용하여 전송될 것임을 알린다.
Finished
핸드셰이크 과정 동안 교환된 모든 메시지의 무결성을 검증하며, 이 시점부터 양측은 안전한 통신을 시작한다.
클라이언트-인터넷망-서비스-파드로 연결된 구조에서, 클라이언트부터 파드까지 end-to-end로 https 암호화가 걸린다. 문제는 서버 입장에서 end-to-end 암호화는 전혀 안전하지 않다. https는 클라이언트가 서버를 인증하는 과정인데, 서버 입장에서는 클라이언트를 확인하지 못한다(인증을 할 수 없다). 따라서 서버 입장에서는 클라이언트의 요청을 수행할 수 밖에 없는데, 악성 요청도 그대로 수행하게 된다.
이를 방지하기 위해서 쓰이는 것이 WAF지만, 결국 암호화 된 데이터를 중간에서 풀어볼 수 없어 공격인지 아닌지 확인할 수 없다. 이를 위해서 사용하는 것이 TLS Termination Proxy이다. 클라이언트와 프록시 서버 간에 HTTPS 연결을 종료하고, 프록시 서버와 내부 서비스 간에는 HTTP 또는 새로운 HTTPS 연결을 사용하는 방식이다. 프록시 서버에서 암호화를 해제하여 요청을 검사할 수 있으며, 내부 네트워크에서는 HTTPS가 필요하지 않거나, 필요한 경우 새로운 HTTPS 세션을 시작할 수 있다.
클라이언트와 서버 사이에 프록시 서버가 있다. 프라이빗 네트워크 안에서는 사실 접근 제어만 잘 된다는 가정 하에 https로 통신할 필요가 없다. 프록시 서버에서 tls 종료 프록시를 통해 https를 종료시키고, 내부에서는 http를 종료한 상태에서 통신한다. 내부적으로 https 통신을 한다고 해도, tls 통신 프록시를 통해 한 번 종료시키고 다시 https로 통신한다.
쿠버네티스 환경에서는 ingress를 사용하여 쉽게 TLS Termination Proxy를 구현할 수 있다.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp-ing-tls-term
spec:
tls:
# - hosts:
# - myapp.example.com #실습할 때는 없애는 것이 편함
secretName: myapp-tls-secret #이곳의 인증서와 키를 가져온다.
rules:
# - host: myapp.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp-svc-np
port:
number: 80
예시는 클라이언트와 Ingress 간의 HTTPS 연결을 종료하고, Ingress와 내부 서비스 간에는 HTTP를 사용하도록 설정하는 방법을 보여준다. 이 설정을 통해 Ingress에서 TLS 인증서를 관리하며, 내부 파드는 HTTPS 관련 설정이나 키를 관리할 필요가 없게 된다. 암호화 통신은 클라이언트와 인그레스 사이에서만 하기 때문에 파드의 nginx는 암호를 위한 설정과 인증키가 필요 없기 때문이다. 따라서 사용자가 훨씬 유연하게 사용할 수 있다.
PKI는 공개 키 기반 구조(Public Key Infrastructure)를 의미한다. PKI는 디지털 인증서와 공개 키 암호화를 활용하여 인터넷 상에서 사용자, 서버 및 조직의 신원을 안전하게 인증하고, 데이터의 무결성 및 기밀성을 보장한다.
기존의 암호화는 대칭 키(암호화 키와 복호화 키가 같음) 알고리즘을 사용하여 정보를 교환했다. 대칭 키 암호화는 구현이 빠르고 속도가 빠르다는 장점이 있지만, 키를 스니핑(패킷을 엿보는 행위)나 스푸핑(속여서 공격) 등의 해킹 기법에 당하기 쉽다는 단점이 있었다. 이를 해결하기 위해 암호화 키와 복호화 키를 따로 가져가는 기술이 등장했고, 이에 사용하는 것이 PKI 이다.
PKI를 이용한 정보 교환은 두 가지 상황을 가정할 수 있다:
공개 키의 경우에는 공개되어 있기 때문에, 해당 키가 신뢰할 수 있는 키인지 확인하기 어렵다. 이 때 사용되는 것이 바로 인증 기관(CA) 이다. CA는 인증서를 발행하고 관리하는 공개 키에 대한 공신력 있는 기관이다. 인증 정책을 수립하고 사용자의 공개 키와 신원 정보를 결합하여 인증서를 발급하며, 이를 통해 사용자의 신원을 보증한다.
CA는 개인 키와 공개 키 쌍을 만들고, 개인 키에 대한 인증서를 발급한다. 이러한 인증서들의 표준 규칙이 X.509 형식이다. 이 때 인증서의 최상위에 존재하는, 일반적으로 웹 브라우저에 내장되어 있는 ‘누구나 신용할 만한’ 인증서 발급 기관에서 발급한 인증서들을 Root CA라고 한다.
인증서는 트리 구조를 가진다. 예를 들어, 네이버의 인증서 구조를 보자.
네이버는 Root CA인 DigiCert로 부터 인증받은 하위 인증서인 DigiCert TLS~CA1 으로부터 인증을 받았다. 보통은 이처럼 3계층 구조로 이루어진다. DigiCert가 Root CA이고, DigiCert TLS~CA1 역시 인증기관이다. 이것을 중간 인증 기관(Intermidiate CA) 라고 부른다. 이 ICA에 대한 신뢰를 입증하기 위해 다음과 같은 구성을 한다.
이처럼, X.509 인증서는 신뢰 체인을 구성할 수 있다. 최상위의 루트 CA가 중간 CA의 인증서에 서명하고, 이 중간 CA는 다시 사용자 또는 서버의 인증서에 서명한다. 이 체인을 통해 최종 사용자의 인증서가 루트 CA에 의해 간접적으로 신뢰된다. 이것을 Chain of Trust라고 부른다.
X.509는 ITU-T에서 책정한 표준 형식이다. X.509는 매우 엄격한 수직 구조를 띄고 있어, 하나의 인증 기관(Root CA)를 정점으로 하는 트리 구조를 띄고 있다.
X.509 규격의 인증서는 공개되어 있고, OpenSSL 등을 통해 누구나 만들 수 있다. 이러면 인증서에 대한 신뢰의 문제가 발생한다. 이래서 등장한 것이 바로 CA라고 볼 수 있다.
X.509 인증서는 다음과 같은 주요 구성 요소를 포함한다:
HAProxy가 설치되어 있다는 것을 전제로 한다.
인증서의 경로는 /etc/haproxy/tls 에 있다.
openssl genrsa -out test.com.key 2048
openssl req -new -key test.com.key -out test.com.csr
openssl x509 -req -days 365 -in test.com.csr -signkey test.com.key -out test.com.crt
를 통해 crt 파일을 생성한다.
openssl rsa -in test.com.key -text > key.pem
openssl x509 -inform PEM -in test.com.crt > crt.pem
openssl pkcs12 -export -in test.com.crt -inkey test.com.key -out cert.p12
openssl pkcs12 -in cert.p12 -nodes -out cert.pem
.pem 파일로 만든다.
vi /etc/haproxy/haporxy.cfg
global
ssl-default-bind-options no-sslv3
ssl-default-bind-ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS
ssl-default-server-options no-sslv3
ssl-default-server-ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS
tune.ssl.cachesize 100000
tune.ssl.lifetime 600
tune.ssl.default-dh-param 2048
frontend https
bind *:443 ssl crt /etc/haproxy/tls/cert.pem
default_backend https_test
option forwardfor
backend https_test
balance roundrobin
server app1 192.168.0.26:80 check
server app2 192.168.0.36:80 check
정상적으로 설정되었는지 확인한다.
haproxy -f /etc/haproxy/haproxy.cfg -c
configure valid ok가 나오면 완료.