FINLOG
securityzero-trustmicroservices

내부 서비스 호출은 왜 검증해야 할까

앞선 글에서는 금융권 보안이 왜 제로트러스트를 말하기 시작했는지, 그리고 여신 시스템을 설계하면서 왜 Keycloak 같은 인증 서버를 두는 쪽으로 판단했는지 정리했다.

그 다음에 자연스럽게 남는 질문이 있다.

사용자 인증까지 끝났다면, 내부 서비스 호출은 그냥 믿어도 되는 걸까?

예전에는 이 질문에 비교적 쉽게 답했을 것 같다.
같은 시스템 안에 있고, 같은 네트워크 안에 있고, gateway도 통과했으면 어느 정도 믿어도 된다고 생각하기 쉽다.

그런데 여신 시스템을 기준으로 다시 보면 이 판단은 꽤 위험하다.

gateway를 통과했다고 끝난 게 아니다

외부 요청이 gateway-service를 통과했다는 건 적어도 한 번은 요청을 검증했다는 뜻이다.
하지만 그게 내부 서비스에서의 신뢰를 자동으로 보장해주지는 않는다.

이유는 간단하다.

  • gateway가 검증한 건 주로 외부 사용자 요청
  • 내부 서비스가 봐야 하는 건 그 요청이 지금 이 서비스에서 허용 가능한지

같은 토큰을 들고 오더라도 서비스마다 관심사가 다르다.

예를 들어 customer-service는 고객 본인 여부를 더 중요하게 볼 수 있고,
account-service는 계좌 소유권이나 계좌 상태를 더 중요하게 볼 수 있고,
lending-service는 대출 상태 전이나 실행 가능 여부를 더 중요하게 볼 수 있다.

즉 gateway에서 한 번 인증됐다는 사실만으로는 충분하지 않다.
각 서비스가 자기 기준으로 다시 판단해야 한다.

내부 서비스 요청은 생각보다 더 위험할 수 있다

보통 외부 요청은 경계가 뚜렷해서 경계 장비나 gateway에서 집중적으로 보게 된다.
반면 내부 요청은 같은 네트워크 안에서 오간다는 이유로 상대적으로 신뢰를 받기 쉽다.

문제는 실제 사고가 항상 외부에서만 시작되지는 않는다는 점이다.

  • 운영자 계정 탈취
  • 내부 서비스 취약점 악용
  • 잘못 배포된 서비스
  • 과도한 권한을 가진 서비스 계정
  • 서비스 간 권한 분리가 없는 구조

이런 상황에서는 내부 서비스 호출이 오히려 더 위험한 경로가 될 수 있다.

내부망에 있다는 이유만으로 호출을 받아주면, 한 번 침해된 서비스가 다른 서비스로 권한을 계속 확장할 수 있다.
이 지점에서 제로트러스트가 말하는 "침해를 전제로 한 설계"가 의미를 갖는다.

여신 시스템에서는 특히 더 그렇다

여신 시스템은 기능 단위로 보면 꽤 자연스럽게 서비스가 나뉜다.

  • customer-service
  • account-service
  • lending-service
  • batch-service

문제는 기능이 나뉜다고 해서 보안 책임도 자동으로 정리되지는 않는다는 점이다.

예를 들어 대출 실행 과정에서 lending-serviceaccount-service를 호출한다고 해보자.
이 호출은 단순한 네트워크 연결이 아니라 실제로 고객의 자산 흐름과 연결될 수 있다.

이때 확인해야 하는 건 단순히 "호출이 왔다"가 아니다.

  • 누가 호출했는가
  • 어떤 권한으로 호출했는가
  • 왜 이 정보를 요청하는가
  • 어느 범위의 데이터에 접근하려는가
  • 같은 요청이 반복된 건 아닌가

내부 요청이라는 이유만으로 이걸 건너뛰면, 서비스가 나뉘어 있어도 사실상 하나의 큰 신뢰 영역처럼 움직이게 된다.

예시: lending-service가 account-service를 호출하는 경우

이 예시는 이번에 구조를 생각하면서 제일 많이 떠올린 장면이었다.

대출 실행 전에는 보통 계좌와 관련된 확인이 필요하다.

  • 이 계좌가 실제 존재하는가
  • 이 고객의 계좌가 맞는가
  • 현재 계좌 상태가 정상인가

이런 확인을 위해 lending-serviceaccount-service를 호출하는 건 자연스럽다.
그런데 여기서 "같은 내부망 서비스니까 허용"이라고 해버리면 너무 느슨하다.

여기서 더 필요한 건 이런 기준이다.

1. 호출 주체 확인

정말 lending-service가 호출한 게 맞는가.
서비스 이름만 헤더로 보내는 정도로는 부족하다.
적어도 토큰이나 인증서를 통해 호출 주체를 식별할 수 있어야 한다.

2. 권한 확인

lending-service라고 해서 account-service의 모든 내부 기능을 호출할 수 있으면 안 된다.
예를 들어 계좌 소유권 검증은 가능하더라도, 잔액 조정 같은 기능은 전혀 다른 권한이어야 한다.

3. 요청 목적과 범위 확인

호출 주체가 맞고 권한도 있어도, 모든 데이터에 접근하도록 열어두면 안 된다.
정말 필요한 데이터만, 필요한 API만 열어줘야 한다.

4. 감사 가능성

누가 언제 어떤 내부 API를 호출했고, 그 결과가 어땠는지 남아야 한다.
금융 시스템에서는 외부 사용자 행동만 추적하면 끝나는 게 아니다. 내부 서비스 호출도 나중에 설명 가능해야 한다.

결국 내부 API도 최소 권한으로 쪼개야 한다

이걸 생각하다 보면 서비스 간 호출도 자연스럽게 더 세분화된 API 설계로 가게 된다.

예를 들어 이런 식이다.

  • 계좌 소유권 검증 API
  • 계좌 상태 조회 API
  • 잔액 변경 API
  • 대출 상태 검증 API

이걸 전부 "내부 API" 하나로 뭉쳐두고 권한을 한꺼번에 열어두는 방식은 위험하다.
각 API가 하는 일이 다르면, 요구하는 권한도 달라야 한다.

그래야 한 서비스가 침해돼도 다른 서비스의 모든 기능으로 곧바로 확장되지 않는다.

그래서 서비스 간 인증이 필요하다

이 지점에서 사용자 인증과는 다른 종류의 인증이 필요해진다.

사용자 토큰은 말 그대로 사용자 신원을 위한 것이다.
하지만 내부 서비스 호출에서는 서비스 자체의 신원도 확인해야 한다.

그래서 보통 이런 구성이 따라온다.

  • 서비스 계정
  • 서비스 토큰
  • 서비스별 권한 scope
  • 필요하면 mTLS

이걸 붙이는 이유는 단순히 "보안을 세게 하자"가 아니다.
내부 호출을 좀 더 설명 가능한 요청으로 만들기 위해서다.

예를 들어 "누가 이 API를 호출했는가"라는 질문에,

  • 내부망 IP
  • 컨테이너 이름

정도로 답하는 것보다,

  • lending-service 서비스 계정
  • account.verify-owner scope
  • 특정 고객의 특정 계좌 확인 목적

이렇게 답할 수 있는 구조가 훨씬 낫다.

인증 서버를 붙여도 서비스의 보안 책임은 남는다

이쯤 되면 Keycloak 같은 인증 서버를 두는 이유도 조금 더 분명해진다.

인증 서버는 사용자와 서비스 계정의 신원을 관리하고, 토큰을 발급하고, 역할과 클라이언트를 정리해준다.
하지만 그 이후의 판단은 여전히 서비스가 해야 한다.

예를 들면 이런 것들이다.

  • 고객 본인 여부 판단
  • 계좌 소유권 판단
  • 대출 상태 전이 허용 여부
  • 중복 요청 차단
  • 감사로그 적재

즉 내부 서비스 호출 검증은 "인증 서버가 있으니 끝"이 아니라, 서비스가 자기 경계를 어디까지 믿을지 다시 정의하는 문제에 가깝다.

정리

마이크로서비스 구조에서는 서비스가 나뉘어 있다는 사실만으로 안전해지지 않는다.
오히려 서비스 간 호출이 많아질수록 "어느 요청을 어디까지 신뢰할 것인가"를 더 자주 판단해야 한다.

여신 시스템에서는 그 판단이 더 중요하다.
고객 정보, 계좌 정보, 대출 상태, 상환 처리, 연체 전환처럼 실제로 민감하고 영향이 큰 기능이 많기 때문이다.

그래서 내부 서비스 호출도 검증 대상으로 보는 게 맞다.
gateway를 통과했는지, 같은 네트워크 안에 있는지보다 더 중요한 건 누가, 어떤 권한으로, 왜 이 요청을 보내는지를 설명할 수 있느냐는 점이다.

이번에 정리하면서 남은 핵심은 이거였다.

제로트러스트는 외부 요청을 의심하는 데서 끝나지 않고, 내부 서비스 경계에서도 신뢰를 줄이는 설계로 이어져야 한다.

이제 다음 단계는 조금 더 구체적이다.
사용자 인증, 서비스 계정, 권한 scope, 감사로그, 멱등성 같은 걸 실제 구조 안에서 어떻게 나눠 가져갈지 정리하는 일이다.