애플리케이션 수준 암호화: Redis용 Memorystore

이 문서에서는 Google Cloud의 Redis 사용자에게 Redis의 속도와 유연성을 저하하지 않으면서 악의적 공격을 완화할 수 있는 암호화 방법을 간략하게 설명합니다. 특히 이 문서에서는 Memorystore와 성능에 대한 영향을 최소화하고 키 순환에 유연성을 제공하는 보안 방식인 Cloud Key Management Service(Cloud KMS)를 사용하여 봉투 암호화 구현을 통해 민감한 정보를 보호하는 데 유용한 방법을 설명합니다.

이 문서와 연결된 가이드에서 Redis용 Memeorystore 저장소에 저장된 콘텐츠를 암호화하기 위해 Cloud Key Management Service API와 통신하는 애플리케이션을 만들어 이 방법을 적용합니다.

이 문서는 애플리케이션 개발자 및 보안 전문가를 대상으로 하며 자바 프로그래밍 및 보안 개념에 대한 기본 지식이 있다고 가정합니다. 이 문서의 모든 코드 스니펫은 자바로 작성됩니다.

Redis용 Memorystore

Redis용 Memorystore는 밀리초 미만의 데이터 액세스를 위한 애플리케이션 캐시를 빌드하도록 Redis의 인메모리 Datastore를 활용하여 완전 관리형 서비스를 제공합니다.

Redis의 핵심 보안 모델은 신뢰할 수 있는 클라이언트만 직접 액세스할 수 있는 환경 내에 인스턴스를 유지하는 데 사용됩니다. 이 보안 모델은 네트워크 인터페이스에 대한 액세스 잠금, 기본 인증 요구, 명령어 이름 난독과 같은 기본 보안 개념만 구현합니다. 비공개 IP와 ID 및 액세스 관리(IAM) 역할 기반 액세스 제어는 Redis 인스턴스의 보안과 보호를 강화합니다. 표준 고가용성 인스턴스는 항상 영역 간에 복제되며 99.9% 가용성 서비스수준계약(SLA)을 제공합니다.

밀리초 미만의 데이터 액세스를 위한 Redis 사용 사례

Redis에는 최신 보안 아키텍처에 사용되는 다양한 애플리케이션이 있습니다. 많은 기업에서 기본 보안 아키텍처는 대부분의 보안 요구사항을 충족합니다. 그러나 일부 업종에서는 보안 요구사항이 더 높습니다.

예를 들어 금융 서비스 산업의 머신러닝 애플리케이션을 생각해 보세요. 금융 트랜잭션에는 지연 시간이 짧은 추론을 위해 캐시되어야 하는 계정 세부정보가 포함됩니다. 짧은 지연 시간 요구사항으로 인해 데이터를 토큰화, 마스킹 또는 익명처리할 수 없습니다. 따라서 가능성이 거의 없는 공격 벡터로부터라도 데이터를 보호하고 노출 위험을 줄이는 데 유용한 추가 보안 레이어를 만드는 것이 중요합니다.

봉투 암호화 사용 시 고려사항

봉투 암호화의 일반적인 개념은 레이어에서 암호화를 결합하는 것입니다. 이 프로세스에서는 데이터 암호화 키(DEK) 한 개 이상을 사용하여 데이터를 암호화합니다. 그런 다음 키 암호화 키(KEK)로 DEK(원시 데이터가 아님)를 암호화합니다. 다음 다이어그램에서는 이 프로세스를 보여줍니다.

봉투 암호화 아키텍처

봉투 암호화는 대규모 데이터 암호화를 관리하는 프로세스를 간소화합니다. 또한 DEK가 아닌 KEK만 순환하므로 키 순환 오버헤드가 줄어듭니다. 이 방법에서는 DEK를 사용하여 원시 데이터를 한 번만 암호화합니다. DEK를 다시 암호화하면 Redis 데이터베이스의 전체 콘텐츠를 다시 암호화하는 것보다 처리 부하가 훨씬 적어집니다. 순환 주기 중에 DEK 키를 새로고치면 전체 암호화된 데이터 세트를 다시 처리해야 합니다. 하지만 봉투 암호화를 사용하면 키 순환의 권장사항을 따르는 동시에 암호화된 데이터의 전체 코퍼스를 다시 처리하는 오버헤드가 줄어듭니다.

다음 섹션에서는 Cloud KMS, Tink 라이브러리, Memorystore를 사용하여 봉투 암호화를 구현하는 방법을 설명합니다(코드 스니펫 포함). 이 문서의 코드 샘플은 tinkCryptoHelper 애플리케이션의 Git 저장소에서 가져온 샘플입니다.

Tink 및 Redis를 사용하여 키 관리

애플리케이션 레이어에서 암호화를 위해 Google은 암호화 키를 생성, 가져오기, 관리하고 중앙 집중식 클라우드 서비스에서 암호화 작업을 수행할 수 있도록 Cloud Key Management Service(Cloud KMS)를 제공합니다. 이러한 키를 사용하고 Cloud KMS를 직접 사용하거나 Cloud HSM 또는 Cloud 외부 키 관리자(Cloud EKM)를 사용하거나 다른 Google Cloud 서비스 내에서 고객 관리형 암호화 키(CMEK)를 통합하여 암호화 작업을 수행할 수 있습니다.

Cloud KMS는 키를 보유한 키링으로 구성된 계층 레이어를 중심으로 설계되었습니다. 키마다 여러 버전이 있을 수 있으며 이 중 하나에 기본 속성이 있습니다. 키링은 리전에 할당되며 권한을 상속하는 프로젝트의 일부입니다.

키는 이 계층 구조를 반영하는 URI를 통해 처리됩니다(예: 다음 패턴에서).

gcp-kms://projects/PROJECT_ID/locations/LOCATION/keyRings/KEYRING_ID/cryptoKeys/KEY_ID

이 패턴의 변수는 다음과 같이 정의됩니다.

규정 준수 추가 요구사항이 있는 경우 Google Cloud는 클라우드 호스팅 하드웨어 보안 모듈(HSM) 서비스인 Cloud HSM도 제공합니다(예: FIPS 140-2 레벨 3으로 인증된 하드웨어 환경에서 암호화 작업을 수행해야 하는 경우).

기본적으로 Google Cloud는 내부 키 관리 서비스(KMS)와 함께 봉투 암호화를 사용하여 저장된 미사용 고객 콘텐츠(일부 사소한 예외 포함)를 암호화합니다.

Tink 라이브러리

이 문서에서 설명하는 방법은 Tink 라이브러리와 Cloud KMS를 사용하여 암호화를 관리합니다. Tink는 Google의 암호화 전문가와 보안 엔지니어 그룹에서 작성한 암호화 API를 제공하는 다국어를 지원하는 크로스 플랫폼 라이브러리입니다. Tink의 목적은 쉽게 사용할 수 있는 안전한 API를 제공하고 일반적인 암호화 문제를 줄이는 것입니다. Tink 설계 시 고려사항에 대한 자세한 내용은 이 Real World Crypto 토크를 리슨하고 포함된 슬라이드를 참조하세요.

키 관리 수명 주기

작업 데이터의 암호화 및 복호화를 시작하기 전에 먼저 Redis 클라이언트 애플리케이션의 키 관리 수명 주기 구성요소를 구성해야 합니다. 다음 다이어그램에서는 키와 데이터 간의 관계를 간략하게 보여줍니다.

KMS 수명 주기 구성요소에는 키와 IAM 역할이 포함됩니다.

이 수명 주기에서 Redis 데이터베이스에 저장된 모든 데이터는 단일 DEK로 암호화됩니다. 프로세스의 첫 번째 단계는 DEK 생성입니다. Tink는 키 컬렉션을 키 세트로 구성한 후 다음 코드 샘플에 표시된 대로 KeysetHandle 객체로 래핑합니다.

KeysetHandle k = KeysetHandle.generateNew
    (AeadKeyTemplates.createAesGcmKeyTemplate(256 / 8));

키 세트의 모든 키는 단일 기본에 해당합니다. 이 문서와 관련 가이드에서는 권장사항으로 GCM(Galois/Counter Mode)의 AES를 사용합니다.

DEK에 대한 액세스를 사용 설정하려면 DEK를 안전하게 저장해야 합니다. 이 문서에서는 Cloud KMS를 사용하여 KEK로 DEK를 암호화합니다. Tink 라이브러리에서 Cloud KMS가 사용하는 KEK는 이 문서 앞의 URI로 표시된 대로 특정 버전이 있고 특정 키링에 포함되며 특정 위치에 있고 특정 프로젝트와 연결된 키입니다.

다른 Google Cloud 리소스에서 격리된 프로젝트에 Cloud KMS KEK를 만드는 것이 좋습니다. 이 방법을 사용하면 해당 프로젝트의 소유자 IAM 역할이 있는 사용자나 서비스 계정만 Cloud KMS의 키에 액세스하고 데이터를 복호화할 수 있습니다. 권한 및 액세스 관리 권장사항은 업무분장을 참조하세요.

Cloud KMS에서 KEK에 대한 액세스 권한을 제공하려면 KEK에서 암호화 및 복호화 작업을 수행할 수 있는 권한이 있는 서비스 계정을 사용합니다. 자세한 내용은 올바른 IAM 역할 선택을 참조하세요. Google Cloud Console 또는 gcloud iam service-accounts keys create 명령어를 사용하여 Cloud Shell을 통해 서비스 계정의 사용자 인증 정보를 가져올 수 있습니다. Tink를 사용하여 KMS에 액세스하려면 다음 코드 샘플과 같이 Cloud KMS 클라이언트를 인스턴스화하도록 서비스 계정 사용자 인증 정보(kmsCredentialsFilename)를 제공합니다.

KmsClient kmsClient = new
    GcpKmsClient().withCredentials(kmsCredentialsFilename);

KMS에서 KEK로 암호화된 DEK가 있는 키 세트는 파일에 작성됩니다(이 경우 로컬 파일 시스템의 JSON 파일에 작성).

k.write(JsonKeysetWriter.withFile(new File(keysetFilename)),
    kmsClient.getAead(keyResourceIdUri));

Redis 데이터 암호화 및 복호화

키 관리 수명 주기를 시작하고 DEK를 안전하게 저장하면 Redis 클라이언트 애플리케이션은 다음 다이어그램과 같이 DEK를 사용하여 Redis 값을 암호화 및 복호화할 수 있습니다.

Redis용 Memorystore는 DEK를 사용하여 데이터에서 암호화 프로세스를 수행합니다.

Tink를 사용하여 저장된 DEK로 암호화 및 복호화 작업을 수행하면 다음과 같이 작동합니다.

  1. Aead 기본은 KeysetHandle 객체에서 검색됩니다.
  2. encrypt(또는 decrypt) 메서드가 실행되어 암호화할 데이터를 제공합니다.

다음 스니펫에서는 암호화 및 복호화 함수를 수행하는 코드를 보여줍니다.

Aead aead = k.getPrimitive(Aead.class);
byte[] ciphertext = aead.encrypt(clearText.getBytes("UTF-8"), aa);
String decryptedText = new String(aead.decrypt(ciphertext, aa), "UTF-8");

Aead.encrypt 메서드(관련 데이터와 함께 인증된 암호화)에 전달되는 관련 데이터(aa)는 컨텍스트(예: 레코드 ID) 암호화를 바인딩합니다. 연결된 데이터는 비밀번호 암호화에 사용되는 salt 인수와 동일하다고 간주될 수 있습니다. 이 방법을 사용하면 데이터의 신뢰성과 무결성을 강화할 수 있습니다.

이 Redis 클라이언트 애플리케이션의 경우 관련 데이터는 암호화 및 복호화를 수행하는 모듈에서 정의된 상수일 수 있습니다. Redis 클라이언트 애플리케이션이 시작하면 Redis에 저장할 데이터를 암호화하기 위해 DEK를 로드하고 Cloud KMS의 KEK를 사용하여 복호화해야 합니다.

Tink를 사용하여 초기화 프로세스 중에 생성 및 저장된 DEK 키 세트를 로드합니다. Tink는 다음 코드 스니펫과 같이 Cloud KMS의 키 URI를 통해 제공되는 KEK를 사용하여 복호화를 처리합니다.

File keyset = new File(keysetFilename);
if (keyset.exists()) {
    KeysetHandle k =
        KeysetHandle.read(JsonKeysetReader.withFile(keyset),
            kmsClient.getAead(keyResourceIdUri));

    Aead aead = k.getPrimitive(Aead.class);
    String decText = new String(aead.decrypt(ciphertext, aa));
}

Redis와 암호화 혼합

이전 섹션에서는 Tink를 사용하여 Redis 데이터베이스를 참조하지 않고 데이터를 암호화하고 복호화하는 방법을 설명했습니다. Redis와 암호화를 통합하는 방법은 간단합니다. 이 문서와 관련된 가이드에서 Jedis를 클라이언트 자바 라이브러리로 사용합니다. 코드 몇 줄만 사용하여 키-값 쌍을 전달하고 암호화할 수 있습니다. 이 코드를 모든 데이터 스토리지에 적용할 수 있습니다. 다음 코드 샘플에서는 Redis에서 이 코드를 적용하는 방법을 보여줍니다.

void set(Iterator<Map.Entry<String, String>> kvs) throws
             NullPointerException, GeneralSecurityException, IOException {
    try (Pipeline p = jedisPool.getResource().pipelined()) {
      while (kvs.hasNext()) {
        Map.Entry<String, String> r = kvs.next();
        p.set(r.getKey(), cryptoHelper.encrypt(r.getValue()));
      }
      p.sync();
    }
}

List<String> get(String... keys) throws UnsupportedEncodingException, NullPointerException,
GeneralSecurityException, IOException {
    Jedis j = jedisPool.getResource();
    List<String> values = j.mget(keys);
    List<String> l = new ArrayList<String>();
    for (String v : values) {
      l.add(cryptoHelper.decrypt(v));
    }
    return l;
}

키는 일반 텍스트로 유지되고 값만 DEK로 암호화됩니다. 따라서 민감한 정보를 Redis 키에 포함하지 않아야 합니다. 값을 암호화하는 한 가지 단점은 기본 Redis 함수를 통해 중첩된 값 구조를 쿼리할 수 없다는 점입니다. 데이터 액세스 레이어를 설계할 때 이 특성을 고려해야 합니다.

다음 단계