분산 서비스에서 쉽고 확장 가능하게 인증처리하기

웹/게임 서비스 구조의 변화


2000년대 초/중반에 웹 서비스나 게임 서비스를 만든다고 하면 영속적인 데이터 저장소 (데이터베이스나 SAN와 애 플리케이션 계층처럼 2계층이나 데이터 저장소 – 비지니스 로직 – 프리젠테이션의 3계층 형식으로 구성하는게 흔했다. 하지만 요즘은 여러 곳에서 경험하고 있다시피, 여러 개의 작은 서비스로 쪼개서 서비스끼리 통신하는 형태로 구성하 는 마이크로서비스로 만드는 경우가 많아졌으며, AWS 같은 클라우드 플랫폼들은 이런 구조를 권장한다.

즉, 유저와 서비스가 통신하는 것으로 끝나는게 아니라 유저와 서비스 1, 2, … 그리고 서비스 1과 서비스 2, … N 이 통 신하게 되는 것이다. 그런데 서버/클라이언트 구조의 모노리딕 서비스를 만들던 시절이랑 똑같은 문제는 여전히 남아 있다.

  • 서비스에 접속한 유저/다른 서비스가 주장하는 유저/서비스가 맞는가 (authentication)
  • 해당 유저/서비스가 요청한 기능을 실행해도 좋은가 (authorization)

많은 경우,

  • 서비스에 접속한 유저 (이제부터 유저는 다른 서비스와 상호 교환가능한 단어로 생각하자) 가 그 유저가 맞는지를 인증 한다. 즉 ID/PW를 데이터베이스 비교하거나, Kerberos 나 LDAP 같은 외부 인증 서비스에 확인한다.
  • 유저가 요청한 내용이 허용되는 내용인지를 유저의 종류나 역할 (role) 정보를 데이터베이스에서 확인하거나, 외 부 서비스 (역시나 Kerberos나 LDAP 류)에 묻는다.

같은 구현을 만든다. 이렇게 만든 서비스가 규모가 커가면 이 부분에서 병목이 생기리란 점은 쉽게 예측 가능하다. 우선 저런 인증 및 권한 시스템 자체에 부하가 걸린다. 비슷하게, 데이터베이스에 권한 시스템을 만든다면 데이터베이스 의 부하가 오를 것이다. 다음으로, 간과하기 쉬운 부분인데 지연 시간이 오르기 시작한다. 10G를 넘어 40G 이상이 대 세가 되어가는 데이터 센터 내의 통신이 빠르다곤 하지만 네트워크 요청의 응답 지연 시간은 여전히 분산 시스템 전체 의 성능에서 꽤 중요한 요소다. 마이크로 서비스 구조는 그 자체가 다른 시스템에 뭔가를 요청하는 시간이 들 수 밖에 없고, 이로 인한 지연 시간은 다음과 같은 경우 모두에서 올 수 있다 — 그리고 모두 성능에 악영향을 준다:

  • 네트워크 사용이 짧은 시간에 몰리는 특성
  • 하드웨어/OS 오버헤드 (스위치, 라우터, …, OS 의 네트워크 스택)
  • 다른 서비스 (데이터베이스, Kerberos, …) 에 보낸 요청이 대기열에서 기다리는 시간
  • 인증 서비스가 정말로 외부에 있는 경우 외부 인증 서비스까지 다녀오는 시간:

auth-token-traditional

물론 이 중 많은 부분은 최적화하거나 개선할 수 있고, 적절한 캐시 계층은 많은 부하를 최소화하리라 생각된다. 그렇지만, 인증 / 권한 확인에 들어가는 부하와 지연 시간을 제거할 방법은 없을까?

인증토큰


많은 시스템들에서 토큰 기반의 인증/권한관리 시스템을 구현한다. 토큰에는 대략 다음과 같은 정보가 포함된다.

  • 토큰 소유자가 누구인지
  • 토큰 소유자가 무엇을 할 수 있는지 (혹은 그 중에 이 토큰으로 무엇을 할 수 있는지)
  • 누가/언제 토큰을 만들었는지 (그리고 언제 만료되는지)

토큰을 전달받은 마이크로 서비스는 토큰의 정보를 이용해서,

  • 인증 처리: 토큰 소유자가 누구인지가 토큰에 들어있다.
  • 권한 확인: 토큰 소유자가 할 수 있는 일이 무엇인지 들어있다.

즉, 인증/권한 서비스에 접근하지 않고도 토큰을 들여다보는 것만으로 토큰을 받은 서비스가 알아서 할 수 있게 된다.

auth-token

토큰을 어째서 신뢰해도 좋은가?

위에서 설명한 토큰의 내용 중에 의도적으로 포함하지 않은 부분이 있다. 인증 토큰이 유효하다는 걸 증명하기 위해서 메시지 인증 코드 (Message Authentication Code) 를 붙인다. 이 MAC 코드는 암호화를 하는 것은 아니고, MAC이 붙어있는 메시지가 변경 사항이 없다 는 점만 확인할 수 있다. 그래서 토큰의 신뢰성은 a. MAC 알고리즘을 믿을 수 있 는가? b. 토큰을 다른 유저가 접근할 수 없는가? 두 가지 요소가 결정짓는다.

우선 MAC 알고리즘 부분을 살펴보자. 크게 두 종류의 알고리즘이 있는데, 토큰을 어떤 주체와 주고 받을지에 따라서 선택할 수 있는 알고리즘이 다르다. 같은 운영 주체 안에서만 쓴다면 비밀 값 을 하나 정해서 안에서 공유할 수 있다. 이 경우에는 암호학적인 해시함수를 이용한 HMAC 을 이용한다. 보통 SHA1, SHA-2 (SHA-256, SHA-512, …) 나 심지 어 MD5 알고리즘을 이용한다. 해시 값 자체를 이용하는게 아니라 조금 다른 생성 방식을 쓰기 때문에 아직 MD5가 현역 인 거의 마지막 분야다. 그리고 몇 번의 해시 함수와 XOR 연산 외에는 없기 때문에 엄청나게 빠르다.

하지만 여러 운영 주체가 토큰을 교환하는 경우가 있다. 예를 들어서 Google 이 발급한 토큰을 사용해서 내 서비스에 접근하는데, Google 과 내 서비스가 특정 비밀 키를 합의하기는 어렵다. 그래서 HMAC 말고 공개적으로 확인할 수 있 는 다른 대안이 필요하다. 이 경우에는 전자 서명 알고리즘을 이용한다. 즉, 토큰 내용의 해시 값에 암호학적인 서명을 추가한다. (암호학적인 해싱 + RSA, DSA, ECDSA, 혹은 EdDSA 등의 공개키 알고리즘으로 서명) 다만 이 경우 서명과 서명 확인 모두 일반적인 해싱보다 훨씬 (대략 수십에서 수천배 정도)느리다.

알고리즘의 선택에 따라 HMAC 자체의 구조 혹은 공개키 알고리즘이 제공하는 보안성 만큼 믿을 수 있다. 만약 해당 토큰이 노출되면 토큰 자체의 기밀성은 없기 때문에 토큰을 탈취한 쪽이 인증/권한을 뺏어갈 수 있다.

사례 분석: OpenStack Keystone

IaaS 혹은 그 이상을 구축할 수 있게 해주는 오픈소스 구현체인 OpenStack 에는 Keystone 이라는 인증 및 권한 처리 를 담당하는 서비스가 있다. Ubuntu Linux 와 유사하게 매년 두 번 릴리즈하는데, 2012년의 두 개의 릴리즈 (Essex, Folsom) 동안에는 토큰을 사용하긴 하지만 토큰 자체를 항상 Keystone API에 확인받는 구조였다. 즉, 위에서 언급했 던 외부 서비스에 확인하는 방식으로 동작했다. 오픈스택 자체가 상호연관된 수많은 마이크로서비스로 만들어져있고, 하나의 API 서비스가 여러 개의 인스턴스가 분산 환경에서 동작하기에 Keystone API와 이 API가 사용하는 데이터베 이스 단의 부하가 꽤 컸다. 실제로 VM을 띄우는 노드가 수십개 수준일 때에도 상당한 양의 CPU 싸이클이 Keystone API와 데이터베이스에서 소진되는 걸 경험했다.

그래서 이 다음 릴리즈인 2013년의 Grizzly 버전에서는 공개키 인프라스트럭처 (PKI) 기반의 토큰 기능을 제공하기 시작했다. 예상할 수 있는 바처럼, Keystone 에서 토큰을 발급하고나면, Nova 나 Neutron 같은 다른 OpenStack 서비스에서는 공개키 서명을 확인해서 토큰을 검증하기 때문에 전반적인 API호출 수가 줄어든다.

개념 확장하기


인증 토큰은 단순하면서도 강력한 개념이다. 그래서 같은 개념을 사용해서 네트워크 기반 서비스에서 활용하는 일이 적지 않다.

좀 더 안전한 쿠키 / 클라이언트 세션

웹 사이트에서 보낸 쿠키를 클라이언트에서 변조해도 웹 사이트에서 이를 확인할 방법이 없다. 그래서 쿠키 데이터를 직렬화 (serialize) 할 때 MAC으로 HMAC 이나 전자 서명을 추가한다. 웹 서버에서 다시 쿠키를 받았을 때, MAC 내 용과 쿠키 내용을 비교하고, 이 내용에 대해서 변조된게 있는지 확인하는 방식으로 처리한다. 그리고 이런 기능을 이용 하면 분산 서버들이 공유 캐시나 공유 데이터베이스, 혹은 로드밸런서 수준의 sticky session 을 사용하지 않아도 세션 을 만들 수 있다.

실제로 여러 웹 프레임워크에서 이런 기능을 제공한다:

  • Python itsdangerous: MAC과 만료 시한을 포함한 데이터를 만들 수 있게 도와주는 라이브러리. 해당 기능을 이 용해서 flask 의 세션을 구현한다.
  • Rails: Rails의 세션 구현 역시 이런 기능을 이용한다.

이런 구현이 널리 쓰이는 것은 공유 세션 저장소를 만드는 것은 데이터베이스를 쓰거나 memcached 나 redis를 써도 해당 부분에 대해서 고가용성을 확보하면서 성능을 올리는게 그리 쉬운 일이 아니라서 그렇다.

메일에 보내는 URL에 특정 정보를 포함시키기

일부 이메일 리스트 등에서 탈퇴 처리를 위한 링크를 보낼 때가 있다. 해당 링크에는 서명 혹은 암호화를 하고 만료 시한을 지정한 정보를 지정하는 경우가 있다. 즉, 링크 내용의 일부를 디코딩하고, 암호화한 경우 복호화까지 하면,

  • 유저가 하려는 행동을 알 수 있다. 예를 들어 이메일 리스트에서 탈퇴, 혹은 패스워드 재설정
  • 유저에게 특정 시간까지만 이를 허용한다: 만료 시한을 데이터의 일부분으로 넣으면 기간이 지난 링크는 사용할 수 없다.
  • 유저가 의도한 유저인지 알 수 있다. MAC을 포함해서 보내면 데이터를 변조한 경우 알 수 있다.

분산 서비스에 토큰을 적용해보자


인증 토큰을 이용하면,

  • 공유 저장소의 사용을 줄이거나 (클라이언트 세션, 이메일 내의 링크 데이터)
  • 서비스 요소간의 (불필요한) 통신을 줄이거나 (서비스 내 인증 토큰)
  • 분산 처리를 쉽게 하거나 (특정 서버와 계속 통신하지 않아도 되는 인증 토큰)

하는 등의 이득을 볼 수 있다. 다만 인증 토큰의 무효화 처리 (revocation) 은 쉽지 않으니, 만료 처리나 블랙리스트 처 리를 구현해서 좀 더 편하게 분산 서비스를 만들어봅시다.

아이펀팩토리 김진욱 CTO

답글 남기기

댓글을 게시하려면 다음의 방법 중 하나를 사용하여 로그인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Google+ photo

Google+의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

%s에 연결하는 중