HTTPS에 대해서

HTTPS에 대해서 (About HTTPS)

HTTPS가 그냥 "켜져 있거나 꺼져 있거나" 하는 단순한 것이라고 생각하기 쉬워요. 하지만 사실 훨씬 더 복잡하답니다.

💡 팁: 급하시거나 깊이 파볼 필요가 없다면, 다음 섹션들로 넘어가서 각기 다른 기법으로 모든 걸 설정하는 단계별 안내를 따라가세요.

HTTPS의 기본 개념을 소비자 관점에서 배우고 싶다면, https://howhttps.works/ 를 확인해 보세요.

이제 개발자 관점에서 HTTPS에 대해 생각할 때 명심할 점 몇 가지를 살펴볼게요.

  • HTTPS를 위해서는 서버제3자(third party) 가 만들어 준 " 인증서(certificates) "를 보유해야 해요.
    • 그 인증서는 사실 제3자에게서 인증서를 "획득" 하는 것이지, "생성"하는 게 아니에요.
  • 인증서에는 유효기간(lifetime) 이 있어요.
    • 만료돼요.
    • 그러면 다시 제3자에게서 갱신, 즉 다시 획득해야 해요.
  • 연결의 암호화는 TCP 계층에서 일어나요.
    • HTTP 바로 아래의 계층이에요.
    • 그래서 인증서와 암호화 처리는 HTTP 이전에 이루어져요.
  • TCP는 "도메인"이라는 것을 모릅니다. 오직 IP 주소만 알아요.
    • 요청된 특정 도메인에 대한 정보는 HTTP 데이터 안에 들어 있어요.
  • HTTPS 인증서특정 도메인을 "증명"하지만, 프로토콜과 암호화는 어떤 도메인인지 알기 전에 TCP 계층에서 일어나요.
  • 기본적으로 이 말은 IP 주소 하나당 HTTPS 인증서를 딱 하나만 가질 수 있다는 뜻이에요.
    • 서버가 아무리 크고, 그 위에 올린 앱 하나하나가 아무리 작아도 마찬가지예요.
    • 하지만 이에 대한 해결책이 있어요.
  • HTTP 이전에 TCP 계층에서 암호화를 담당하는 TLS 프로토콜에는 SNI 라는 확장(extension) 이 있어요.
    • 이 SNI 확장 덕분에 (단일 IP 주소를 가진) 하나의 서버가 여러 HTTPS 인증서를 보유하고 여러 HTTPS 도메인/앱을 서빙할 수 있어요.
    • 이를 동작시키려면, 서버에서 실행되고 공인 IP 주소에서 수신을 듣는 하나의 컴포넌트(프로그램)가 서버의 모든 HTTPS 인증서를 보유해야 해요.
  • 보안 연결을 얻은 뒤에도, 통신 프로토콜은 여전히 HTTP예요.
    • HTTP 프로토콜로 보내지고 있음에도 내용물은 암호화되어 있어요.

서버(머신, 호스트 등)에서 하나의 프로그램/HTTP 서버가 실행되면서 HTTPS 관련 부분을 전부 관리하는 것은 아주 흔한 관행이에요: 암호화된 HTTPS 요청을 받아서, 같은 서버에서 실행 중인 실제 HTTP 앱(FastAPI 앱)에 복호화된 HTTP 요청을 보내고, 앱에서 HTTP 응답을 받아 적절한 HTTPS 인증서암호화한 뒤 HTTPS로 클라이언트에게 되돌려 보내는 방식이죠. 이런 서버를 흔히 TLS 종료 프록시(TLS Termination Proxy) 라고 불러요.

TLS 종료 프록시로 쓸 수 있는 몇 가지 옵션:

  • Traefik (인증서 갱신도 처리 가능)
  • Caddy (인증서 갱신도 처리 가능)
  • Nginx
  • HAProxy

출처: 공식문서

Let's Encrypt

Let's Encrypt 이전에는 이런 HTTPS 인증서를 신뢰할 수 있는 제3자에게서 사야 했어요.

이런 인증서 하나를 받으려면 절차가 번거롭고 서류 작업도 꽤 필요했으며, 인증서 값도 꽤 비쌌어요.

하지만 그때 Let's Encrypt 가 등장했어요.

Linux Foundation의 프로젝트로, HTTPS 인증서를 무료로 자동화된 방식으로 제공해요. 이 인증서들은 표준적인 암호화 보안을 모두 사용하며, 유효기간이 짧아요(약 3개월). 그래서 수명이 짧은 덕분에 보안이 오히려 더 좋아요.

도메인은 안전하게 검증되고 인증서는 자동으로 생성돼요. 덕분에 이 인증서들의 갱신도 자동화할 수 있어요.

핵심 아이디어는 이 인증서들의 획득과 갱신을 자동화해서, 무료로, 영원히, 안전한 HTTPS를 누릴 수 있게 하는 거예요.

개발자 관점의 HTTPS

개발자에게 중요한 아이디어를 주로 살펴보면서, HTTPS API가 단계별로 어떻게 생겼는지 예시를 들어볼게요.

도메인 이름

대개 도메인 이름획득하는 것부터 시작할 거예요. 그런 다음 (아마 그 클라우드 프로바이더의) DNS 서버에 설정하게 되죠.

클라우드 서버(가상 머신) 같은 걸 하나 얻으면, 그 서버는 고정된 공인 IP 주소를 갖게 돼요.

DNS 서버(들)에는 여러분의 도메인이 서버의 공인 IP 주소를 가리키도록 레코드("A record")를 설정해요.

이건 셋업할 때, 처음 한 번만 하면 될 거예요.

💡 팁: 이 도메인 이름 부분은 HTTPS보다 훨씬 앞선 얘기지만, 모든 게 도메인과 IP 주소에 의존하니 여기서 짚고 넘어갈게요.

DNS

이제 실제 HTTPS 부분에 집중해 볼게요.

먼저 브라우저가 DNS 서버에 도메인의 IP가 뭔지 물어봐요. 이 경우 someapp.example.com이 되겠죠.

DNS 서버는 브라우더에게 특정 IP 주소를 쓰라고 알려줘요. 그건 여러분이 DNS 서버에 설정한, 서버가 쓰는 공인 IP 주소예요.

TLS 핸드셰이크 시작

그러면 브라우저는 포트 443(HTTPS 포트)에서 그 IP 주소와 통신해요.

통신의 첫 부분은 클라이언트와 서버 사이의 연결을 수립하고, 사용할 암호화 키를 결정하는 등의 작업이에요.

클라이언트와 서버가 TLS 연결을 수립하기 위해 주고받는 이 상호작용을 TLS 핸드셰이크(TLS handshake) 라고 해요.

SNI 확장이 포함된 TLS

서버에서는 하나의 프로세스만 특정 IP 주소의 특정 포트에서 수신을 들을 수 있어요. 같은 IP 주소의 다른 포트에서 듣는 프로세스는 있을 수 있지만, IP 주소와 포트의 각 조합마다 단 하나만 가능해요.

TLS(HTTPS)는 기본적으로 특정 포트 443을 사용해요. 그래서 우리에게 필요한 것도 그 포트예요.

이 포트에서는 하나의 프로세스만 수신을 들을 수 있으니, 그 일을 하는 프로세스는 TLS 종료 프록시가 돼요.

TLS 종료 프록시는 하나 이상의 TLS 인증서(HTTPS 인증서)에 접근할 수 있어요.

앞서 다룬 SNI 확장을 사용해서, TLS 종료 프록시는 이 연결에 사용할 TLS(HTTPS) 인증서 중 어떤 것을 써야 하는지 확인해요. 클라이언트가 기대하는 도메인과 일치하는 인증서를 쓰는 거죠.

이 경우에는 someapp.example.com용 인증서를 사용해요.

클라이언트는 그 TLS 인증서를 만든 주체(이 경우 Let's Encrypt, 자세한 건 나중에)를 이미 신뢰하고 있어서, 인증서가 유효한지 검증할 수 있어요.

그런 다음 인증서를 사용해 클라이언트와 TLS 종료 프록시는 TCP 통신의 나머지 부분을 어떻게 암호화할지 결정해요. 이것으로 TLS 핸드셰이크 부분이 끝나요.

이후 클라이언트와 서버는 암호화된 TCP 연결을 갖게 되고, TLS가 바로 그걸 제공해 줘요. 그 연결을 사용해 실제 HTTP 통신을 시작할 수 있게 되죠.

그게 바로 HTTPS예요. 순수한(암호화되지 않은) TCP 연결 대신 안전한 TLS 연결 안에서 이루어지는 평범한 HTTP일 뿐이에요.

💡 팁: 통신의 암호화가 HTTP 계층이 아니라 TCP 계층에서 일어난다는 점을 눈여겨보세요.

HTTPS 요청

이제 클라이언트와 서버(정확히는 브라우저와 TLS 종료 프록시)가 암호화된 TCP 연결을 갖게 됐으니, HTTP 통신을 시작할 수 있어요.

그래서 클라이언트는 HTTPS 요청을 보내요. 그건 그냥 암호화된 TLS 연결을 통한 HTTP 요청일 뿐이에요.

요청 복호화

TLS 종료 프록시는 합의된 암호화 방식을 사용해 요청을 복호화하고, 평문(복호화된) HTTP 요청을 앱을 실행하는 프로세스(예: FastAPI 앱을 실행하는 Uvicorn 프로세스)로 전달해요.

HTTP 응답

앱은 요청을 처리하고 평문(암호화되지 않은) HTTP 응답을 TLS 종료 프록시로 보내요.

HTTPS 응답

그러면 TLS 종료 프록시는 앞서 합의한 암호화 방식(someapp.example.com 인증서로 시작한)을 사용해 응답을 암호화하고 브라우저로 되돌려 보내요.

이어서 브라우저는 응답이 유효하고 올바른 암호화 키로 암호화됐는지 등을 검증해요. 그런 다음 응답을 복호화하고 처리해요.

클라이언트(브라우저)는 앞서 HTTPS 인증서를 사용해 합의한 암호화를 쓰고 있기 때문에, 응답이 올바른 서버에서 온 것임을 알 수 있어요.

여러 앱

같은 서버(또는 여러 서버)에는 여러 앱이 있을 수 있어요. 예를 들어 다른 API 프로그램이나 데이터베이스라든지요.

특정 IP와 포트를 처리하는 프로세스(우리 예시의 TLS 종료 프록시)는 하나뿐이지만, 다른 앱/프로세스들도 공인 IP와 포트의 조합을 쓰지 않는 한 서버(들)에서 실행될 수 있어요.

이런 방식으로 TLS 종료 프록시는 여러 도메인에 대한 HTTPS와 인증서를 여러 앱을 위해 처리하고, 각 경우에 요청을 올바른 앱으로 전달할 수 있어요.

인증서 갱신

어느 시점에는 각 인증서가 (획득 후 약 3개월 뒤에) 만료돼요.

그때 또 다른 프로그램이(어떤 경우엔 별도 프로그램, 어떤 경우엔 같은 TLS 종료 프록시일 수도 있어요) Let's Encrypt와 통신해서 인증서(들)를 갱신해요.


Forwarded 헤더

TLS 종료 프록시를 사용하는 실제 설정에서는, 앱이 실제 클라이언트의 IP와 같은 정보를 알 수 있도록 몇 가지를 설정해야 할 수 있어요.

예를 들어, 애플리케이션 서버가 신뢰하는 프록시에서만 통신을 받는다면, --forwarded-allow-ips="*"로 설정해서 들어오는 모든 IP를 신뢰하게 만들 수 있어요. 그러면 앱은 프록시가 사용하는 IP에서 오는 요청만 받게 되니까요.

이렇게 하면 앱은 자신의 공인 URL이 무엇인지, HTTPS를 쓰는지, 도메인이 무엇인지 등을 알 수 있어요.

이건 예를 들어 리다이렉트를 제대로 처리할 때 유용해요.

💡 팁: 더 자세한 내용은 Behind a Proxy - Enable Proxy Forwarded Headers 문서에서 확인하세요.

요약 (Recap)

HTTPS를 갖는 것은 매우 중요하고, 대부분의 경우 꽤 결정적이에요. 개발자로서 HTTPS에 대해 들여야 하는 수고의 대부분은 그냥 이 개념들과 그것들이 어떻게 동작하는지를 이해하는 것뿐이에요.

하지만 개발자 관점의 HTTPS 기본 정보만 알면, 여러 가지 도구를 조합하고 설정해서 모든 걸 간단히 관리할 수 있어요.

다음 장들에서는 FastAPI 앱을 위한 HTTPS 설정의 구체적인 사례 몇 가지를 보여드릴게요. 🔒

더 알아보기 (Learn more)