Kafka - 보안(2): 암호화, 인가, 감사


이 포스트에서는 아래의 내용에 대해 알아본다.

  • 주키퍼와 플래폼의 나머지 부분에 적용될 수 있는 추가적인 수단

목차


개발 환경

  • mac os
  • openjdk 17.0.12
  • zookeeper 3.9.2
  • apache kafka: 3.8.0 (스칼라 2.13.0 에서 실행되는 3.8.0 버전)

1. 암호화(encryption): 기밀성과 무결성

암호화는 데이터의 기밀성과 무결성을 보장하기 위해 사용된다.

기밀성(Confidentiality)

허가받지 않은 사용자가 정보에 접근하지 못하도록 보호하는 것
기밀성의 보호 대상은 데이터의 비공개성임
기밀성 보장을 위한 방법은 암호화, 접근 제어, 인증, 네트워크 보안(방화벽, VPN) 등이 있음

무결성(Integrity)

정보가 인가되지 않은 수정/손상/삭제없이 원래의 상태로 유지되는 것, 즉 데이터가 변경되거나 훼손되지 않았음을 보장
무결성의 보호 대상은 데이터의 정확성과 완전성임
무결성 보장을 위한 방법은 해시, 디지털 서명, 전송 중 데이터 보호(TLS/SSL 사용), 로그 감사 등이 있음

<카프카에서의 기밀성>

  • TLS/SSL 암호화
    • 클라이언트와 브로커 간, 브로커 간의 데이터를 암호화하여 네트워크에서 기밀성을 유지함

<카프카에서의 무결성>

  • 메시지 무결성 검증
    • 카프카는 메시지 무결성을 보장하기 위해 순환 중복 검사(CRC) 를 사용함
    • 클라이언트와 브로커는 데이터 전송 중 메시지가 변경되었는지 확인
  • ACL(Access Control List)
    • topic 과 데이터에 대한 비인가된 수정 및 삭제 방지

SSL, SASL_SSL 보안 프로토콜을 사용하는 카프카 리스너는 전송 계층으로 TLS 을 사용함으로써 안전하지 않은 네트워크를 통해 전송되는 데이터를 보호하는 암호화된 안전한 채널을 사용할 수 있게 해준다.

카프카 로그를 저장하는 디스크가 도난되는 경우에도 보안 사고를 방지하기 위해 전체 디스크 암호화나 볼륨 암호화를 사용하여 물리적인 저장 장치 자체를 암호화할 수 있다.

브로커 메모리에 남아있던 암호화되지 않은 데이터가 힙 덤프에서 보일 수도 있고, 디스크에 직접 접근 가능한 운영자가 직접 열어볼 수도 있으므로 보안 조건을 준수하기 위해서는 플랫폼 운영자가 어떠한 수단으로도 기밀 데이터에 접근할 수 없도록 할 필요가 있다.

전체 데이터 흐름이 암호화되는 종단 암호화를 구현하기 위해 커스텀 암호화 제공자를 카프카 클라이언트에 플러그인 형태로 설정할 수도 있다.


1.1. 종단 암호화(end-to-point encryption)

1. 시리얼 라이저 에서 본 것처럼 프로듀서에서 메시지를 카프카 로그에 저장되는 바이트 배열로 변환하기 위해 시리얼 라이저를 사용한다.

2. 디시리얼라이저 에서 본 것처럼 컨슈머에서 디시리얼라이저를 이용하여 바이트 배열을 다시 메시지로 변환한다.

시리얼라이저와 디시리얼라이저는 직렬화 도중에 메시지를 암호화하거나, 역직렬화 도중에 복호화를 수행할 수 있도록 암호화 라이브러리에 통합될 수 있다.
메시지 암호화는 보통 AES 와 같은 대칭 키 암호화 알고리즘을 사용하여 수행된다.
키 관리 시스템(KMS, Key Management System) 에 공유 키를 저장하여 프로듀서는 메시지 암호화를, 컨슈머는 메시지 복호화를 수행할 수 있다.
이 때 브로커는 암호화 키에 대해 접근할 필요가 없으며, 암호화되지 않은 원본 메시지를 볼 수도 없기 때문에 클라우드 환경에서 사용하기에 적합하다.
메시지 복호화에 필요한 암호나 암호화 매개변수는 메시지 헤더에 저장될 수 있고, 메시지의 무결성을 확인하기 위한 디지털 서명 또한 메시지 헤더에 첨부될 수 있다.

<종단 암호화 흐름>

  • 프로듀서를 사용하여 메시지를 보냄
  • 프로듀서는 KMS 에 저장된 암호화 키를 사용하여 메시지를 암호화함
  • 암호화된 메시지가 브로커에 전달되고, 브로커는 암호화된 메시지를 파티션 로그에 저장함
  • 브로커가 암호화된 메시지를 컨슈머에게 보냄
  • 컨슈머는 KMS 에 저장된 암호화 키를 사용하여 메시지를 복호화 함

이 때 프로듀서와 컨슈머에는 KMS 로부터 공유 키를 받아올 수 있는 자격 증명이 설정되어야 한다.

보안 강화를 위해 주기적으로 키를 회전시켜 주는 것이 좋다.
이는 무차별 암호 대입 공격(Brute-force Attack) 에 대해서 방어가 가능하다.
예전 키로 암호화된 메시지의 보존 기한동안 예전 키와 새로운 키, 둘 다 사용하여 메시지를 읽을 수 있어야 하는데 대부분의 KMS 시스템은 키 회전 시 일정 기간 동안 예전 키를 사용할 수 있도록 해주는 기능이 기본적으로 있기 때문에 대칭 키 암호화를 사용하는 카프카 클라이언트에서는 별도의 처리를 할 필요가 없다.

로그 압착이 설정되어 있는 토픽의 경우 예전 키를 사용해서 암호화된 메시지가 오래 남아있을 수 있기 때문에 오래된 메시지를 다시 암호화해야 할 수도 있는데 이 때 새로운 메시지와 섞이는 것을 방지하기 위해 이 작업이 이루어지는 동안에는 프로듀서와 컨슈머가 연결되어 있으면 안된다.

암호화된 메시지 압축

메시지를 암호화 한 후에 압축하는 것은 암호화 전에 압축하는 것에 비해 저장 공간 측면에서 아무런 이점이 없음
시리얼라이저를 사용하여 암호화 전에 압축을 할 수도 있고, 애플리케이션 코드에서 메시지를 쓰기 전에 압축을 할 수도 있음
어느 쪽에 되었건 압축은 아무런 이점없이 오버헤드만 추가로 발생시키므로 카프카에서는 압축 옵션을 끄는 것이 좋음
즉, TLS 암호화를 활성화하고, 압축은 메시지가 암호화되기 전에 수행하는 것이 좋음

안전하지 않은 전송 계층을 통해 전송되는 메시지의 경우 암호화된 메시지를 압축할 때 발생하는 알려진 보안 취약점이 고려되어야 함

대부분 메시지 키는 메시지 밸류만큼 민감한 정보를 포함하지 않기 때문에 암호화를 필요로 하지 않지만, 어떤 경우에는 암호화되지 않은 키가 규제 조건에 맞지 않을 수 있다.

키는 파티셔닝과 로그 압착 작업에 사용되기 때문에 키 값을 변환하더라도 요구되는 해시 동등성을 유지해야 한다.
즉, 암호화에 사용되는 매개변수가 바뀌어도 키 값에서는 동일한 해시값이 나와야한다.
이를 위한 한 가지 방법은 키 값 원본의 해시값을 메시지 키에 저장하고, 암호화된 키 값은 메시지 밸류나 헤더에 저장하면 된다.
카프카는 키와 밸류를 별도로 직렬화하기 때문에 프로듀서 인터셉터를 사용하여 이러한 변환을 수행할 수 있다.


2. 인가(Authorization): authorizer.class.name

인가는 사용자가 자원에 대해 어떠한 작업을 수행할 수 있는지를 결정하는 절차이다.

카프카 브로커는 커스터마이즈 가능한 권한 부여자를 사용하여 접근 제어를 관리한다.

클라이언트에서 브로커로의 연결이 맺어질 때마다 브로커는 클라이언트를 인증하고, 클라이언트의 신원을 가리키는 KafkaPrincipal 을 해당 연결에 결부시킨다.
그리고 요청이 처리될 때마다 브로커는 연결에 결부된 보안 주체가 해당 요청을 수행할 수 있는 권한이 있는지 검증한다.

카프카는 authorizer.class.name 옵션을 통해 활성화할 수 있는 AclAuthorizer 권한 부여자를 기본적으로 제공한다.
ACL 은 카프카 사용자와 관련되어 동작하기 때문에 클라이언트와 브로커 간 인증을 위해 SASL/SSL 을 설정해야 한다.

server.properties

# 권한 부여 클래스 설정
authorizer.class.name=kafka.security.authorizer.AclAuthorizer

# ACL 검사를 우회할 수 있는 관리자 계정 지정
super.users=User:admin

2.1. AclAuthorizer

AclAuthorizer 는 접근 제어 목록(ACL) 을 사용하여 카프카 자원에 대한 접근을 세밀하게 제어할 수 있도록 해준다.

ACL 은 주키퍼에 저장되는데 요청을 빠르게 인가할 수 있게 하기 위해서 모든 브로커의 메모리에 캐시된다.

ACL 저장소 변경(__cluster_metatata)

카프카 3.3.0 부터 주키퍼가 없는 완전한 KRaft 모드가 지원되면서 ACL 을 포함한 모든 메타데이터는 Kafka 자체의 내부 토픽에 저장됨
메타데이터는 __cluster_metadata 라는 내부 토픽에 저장됨

<각 ACL 설정의 요소>

  • 자원 유형
    • Cluster | Topic | Group | TransactionalId | DelegationToken
  • 패턴 유형
    • Literal | Prefixed
  • 자원 이름
    • 자원 이름, prefix 혹은 와일드카드
  • 작업(operation)
    • Describe | Create | Delete | Alter | Read | Write | DescribeConfigs | AlterConfigs
  • 권한 유형(permission type)
    • Allow | Deny
    • Deny 가 Allow 보다 우선함
  • 보안 주체(principal)
    • 카프카의 보안 주체는 <유형>:<이름> 의 형태로 표현됨
    • 예) User:Assu
    • 모든 사용자에게 권한을 부여하려면 User:* 를 사용하면 됨
  • 호스트
    • 클라이언트의 IP 주소 혹은 모든 호스트에 대해 권한을 부여할 경우에는 와일드카드

AclAuthorizer 는 일치하는 동작에 Deny 가 하나도 설정되어 있지 않은 동시에 최소한 1개의 Allow ACL 이 설정되어 있을 경우에 동작을 허가한다.

위에서 Describe 작업 권한은 Read, Write, Alter, Delete 권한 중 하나라도 부여되어 있을 경우 암묵적으로 부여된다.
DescribeConfigs 권한은 AlterConfigs 권한이 부여되어 있을 경우 암묵적으로 부여된다.

  • 브로커
    • 컨트롤러에 대한 요청과 레플리카 읽기 요청(Replica Fetch Request) 요청을 수행하기 위해 Cluster:ClusterAction 접근 권한을 부여받아야 함
    • 레플리카 읽기 요청
      • 팔로워 레플리카가 저장되어 있는 브로커가 리더 레플리카가 저장된 브로커의 변경 사항을 받아오기 위해 보내는 요청
  • 프로듀서
    • 토픽 생성을 위해 Topic:Write 권한을 부여받아야 함
    • 트랜잭션 기능 없이 멱등적 쓰기 기능을 사용할 경우 Cluster:IdempotenWrite 권한을 부여받아야 함
    • 트랜잭션 기능을 사용하는 프로듀서는 트랜잭션 ID 에 접근하기 위해 TransactionalId:Write 권한과 컨슈머 그룹에 오프셋을 커밋하기 위한 Group:Read 권한을 부여받아야 함
  • 컨슈머
    • 토픽에서 메시지를 읽어오기 위해 Topic:Read 권한을 부여받아야 함
    • 그룹 관리나 오프셋 관리 기능을 사용하는 경우 컨슈머 그룹에 접근하기 위해 Group:Read 권한을 부여받아야 함

ACL 을 관리하는 방법은 kafka-acls.sh 툴을 이용하여 관리한다.

AclAuthorizer 는 기존 클러스터에 처음으로 인가 기능을 추가하는 상황에서 ACL 관리를 편하게 하기 위해 넓은 범위의 자원이나 보안 주체에 대해 권한을 부여하는 2개의 설정 옵션을 제공한다.

super.users=User:Assu;User:Silby
allow.everyone.if.no.acl.found=true

super.users 는 어떠한 제한도 없이 모든 자원에 대해 모든 작업을 수행할 권리를 가지며, Deny ACL 로 접근을 불허할 수 없다.
만일 위에서 Assu의 자격 증명이 위조된 경우 super.users 에서 삭제한 뒤 모든 브로커를 재시작해서 변경점을 적용하는 것 외엔 방법이 없다.

allow.everyone.if.no.acl.found 이 활성화되어 있으면 모든 사용자들은 ACL 없이도 모든 자원에 대해 접근 권한을 부여받는다.
이 옵션은 처음으로 인가는 설정하거나, 아직 개발 단계일 때는 유용하지만 프로덕션 환경에서는 적절하지 않다.


3. 감사

감사와 디버깅을 목적으로 상세한 log4j 로그를 생성하도록 카프카 브로커 설정이 가능하다.
감사 목적의 로그를 남기기 위해서는 kafka.authorizer.logger 로거를, 요청 로그를 남기기 위해서는 kafka.request.logger 로거를 설정해주면 된다.
각각 로그 레벨과 보존 기한은 따로따로 잡아주어야 하며, 이러한 로그들을 분석하고 시각화하기 위해 엘라스틱 스택과 같은 프레임워크를 사용하면 편하다.

감사 목적의 로그 예로는 거부된 접근은 info 레벨로 로그를 남기고, 성공한 접근은 debug 레벨로 로그를 남긴다.

카프카는 kafka-configs.sh 툴을 이용하여 로깅 레벨을 동적으로 변경할 수 있는 기능을 제공한다.

아래는 카프카 클러스터의 {broker-id} 브로커의 kafka.request.logger 로거의 레벨을 trace 로 변경하는 예시이다.

$ bin/kafka-configs.sh --bootstrap-server {kafka-broder}:9092 \
-- entity-type broker-loggers --entity-name {broker-id} \
--alter --add-config kafka.request.logger=TRACE

아래는 현재 설정되어 있는 로거들을 확인하는 예시이다.

$ bin/kafka-configs.sh --bootstrap-server {kafka-broker}:9092 \
--entity-type broker-loggers ==entity-name {broker-id} \
--describe

kafka-configs.sh 에 대한 내용은 추후 다룰 예정입니다. (p. 335)


참고 사이트 & 함께 보면 좋은 사이트

본 포스트는 김병부 저자의 O’REILLY 카프카 핵심 가이드 2판를 기반으로 스터디하며 정리한 내용들입니다.






© 2020.08. by assu10

Powered by assu10