AWS

CloudFront 필드 레벨 암호화

misankim 2023. 3. 18. 00:30

CloudFront 에서 제공하는 추가 보안 기능인 필드 레벨 암호화에 대해 소개하고 간략히 시연하는 내용 공유드리려고 합니다.

1. 필드 레벨 암호화란

필드 레벨 암호화란 사용자가 제공한 민감한 정보(POST body 에 포함된 특정 필드)를 사용자에게 가까운 엣지에서 암호화하여 전체 애플리케이션 스택에서 암호화를 유지하고 데이터가 필요하고 이를 해독할 자격 증명을 보유한 애플리케이션(WAS)만 해당 정보를 볼 수 있도록 하는 클라우드프론트의 추가 보안 기능입니다.

2. 필드 레벨 암호화 설정

필드 레벨 암호화 설정은 간단한 편입니다.

1) RSA 키 페어 생성

다음 단계에서 생성한 키페어 중 공개키를 복사하여 CloudFront 콘솔에서 사용할 것이기 때문에 키 페어를 생성할 장소는 어디든 관계 없습니다. 비밀키의 경우 최종적으로 암호화된 필드를 해독할 WAS 혹은 해당 페이지 소스에 사용할 것입니다.

# 비밀키 생성
openssl genrsa -out private_key.pem 2048

# 비밀키를 기반으로 공개키 생성
openssl rsa -pubout -in private_key.pem -out public_key.pem

2) CloudFront에 퍼블릭 키 추가

클라우드프론트 콘솔 -> Key management -> Public keys -> Add public key

Key name - 임의로 지정
Key value - 위에서 생성한 public_key.pem 파일의 내용

3) 필드 레벨 암호화 프로필 생성

클라우드프론트 콘솔 -> Security -> Field-level encryption -> Create profile

Profile name - 임의로 지정
Public key name - 2) 에서 생성한 퍼블릭 키 이름
Provider name - openssl(키를 식별하는 데 도움이 될 구문, 애플리케이션이 데이터 필드를 해독할 때 필요)
Field name pattern to match - enc(암호화할 POST 필드, python 암복호화에 사용했던 소스를 이용할 것이며, 해당 소스에서 사용되는 POST 필드(enc, dec) 중 enc를 암호화하도록 하겠습니다.)


4) 구성 생성

클라우드프론트 콘솔 -> Security -> Field-level encryption -> Create configuration

Content type - application/x-www-form-urlencoded
Default profile - premisan-test


5) 필드 레벨 암호화 적용 전 테스트 페이지 확인

python 암호화 페이지를 만들었던 것을 활용하여 텍스트 입력을 받는 index.html 페이지와
index.html 페이지를 통해 넘겨받은 POST 필드를 출력하는 echo.py 페이지를 만들었습니다. 아래와 같이 index.html 페이지에서 "암호화" 텍스트 상자에 값을 입력하고 확인을 누르면
echo.py 페이지에서 단순히 해당 값을 출력합니다.


vim index.html

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-eOJMYsd53ii+scO/bJGFsiCZc+5NDVN2yr8+0RDqr0Ql0h+rP48ckxlpbzKgwra6" crossorigin="anonymous">
  </head>
  <body>
    <form action="echo.py" method="post">
    <div class="p-3">
      <label for="cryptsample1" class="form-label"> 암호화</label>
      <input type="text" name="enc" class="form-control" id="cryptsample1" placeholder="암호화할 텍스트 입력">
    </div>
    <div class="p-3">
      <label for="cryptsample2" class="form-label"> 복호화</label>
      <input type="text" name="dec" class="form-control" id="cryptsample2" placeholder="복호화할 텍스트 입력">
    </div>
    <div class="p-3">
      <input class="btn btn-outline-primary" type="submit" value="확인" />
    </div>
    </form>
  </body>
</html>
vim echo.py

#!/usr/bin/python3
import cgi, cgitb 

print ("Content-type: text/html")
print ()

form = cgi.FieldStorage() 
enc_text = form.getvalue('enc')
dec_text = form.getvalue('dec')

if enc_text is not None:
    print("enc_text= ",enc_text)
    print("")

if dec_text is not None:
    print("dec_text= ",dec_text)
    print("")

6) 캐시 동작에 구성 추가

이제 필드 레벨 암호화를 배포에 적용해보도록 하겠습니다.

6-1) Origin 수정

필드 레벨 암호화를 적용할 클라우드프론트 배포 클릭 -> Origins and Origin Groups -> 해당 Origin 선택 -> Edit

Origin Protocol Policy - Match Viewer or HTTPS Only

6-2) Behavior 수정

필드 레벨 암호화를 적용할 클라우드프론트 배포 클릭 -> Behaviors -> 해당 Behavior 선택 -> Edit

Viewer Protocol Policy - Redirect HTTP to HTTPS or HTTPS Only
Allowed HTTP Methods - GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE
Field-level Encryption Config - 위에서 생성한 Configuration ID를 선택

7) 필드 레벨 암호화 적용 후 테스트 페이지 확인

필드 레벨 암호화를 적용하였으니 전 단계에서 생성한 테스트 페이지에 동일하게 값을 입력해봅니다. 아래와 같이 입력한 값이 클라우드프론트에서 오리진으로 넘어갈 때 암호화되어 오리진에 도착합니다. 아래는 "premisan"이라는 문자열이 아래와 같이 암호화된 것을 확인할 수 있습니다.

# 암호화된 문자
AYABePO1hHeQKs8xDqvd4Uc6NOwAAAABAAdvcGVuc3NsAA1wcmVtaXNhbi10ZXN0AQAR/ew2jK34+hG3J8zprNtsYY1tlwh3ngyD+xkjKijqeLW/0ji18OUzAMjUxZhTsbJEGTGIaJSv+1cdlTr1rD1hqX9rEyfyn+uewbMl245u7QbeujOPZTVyGo3C9UQe/Izpvoi7NbciE8IkTmMu9U8uicfHJDit3R0QD4hng5LCZjqmaKQZFy0FV2r4K76pcn9uj+kzYvmjGNB+H7O+VChUU+FcjBJfNMzfUQUZ2ZdvfIArb4dC1iaNhbNnSVeUYPhQmn6QeonL+sHLp4jcDpYI8RTx1/v0/Cz4+QUc9j8Om05oX4EaYrKMZzCN1ySNQK/bu1xqk71kt5Ai7lHdXATvAgAAAAAMAAAQAAAAAAAAAAAAAAAAAE6rkVOJMN9VZfitHHbd+Lf/////AAAAAQAAAAAAAAAAAAAAAQAAAAjPdbS54kCkVGqQemfd4mOBj+roJoGfKWc=


8) 어플리케이션에서 암호화된 필드 복호화

위와 같이 암호화된 문자는 WAS에서 기존 소스로는 해독이 불가하기 때문에 별도의 복호화 소스를 추가하여 다시 평문으로 해독합니다. 기존 echo.py 소스를 아래와 같이 수정합니다. 아래 내용 중 "provider_id", "PublicKeyName", "private_key_text" 변수의 경우 테스트 환경에 따라 다를 수 있기 때문에 적절히 수정합니다.

#!/usr/bin/python3
import cgi, cgitb 
import aws_encryption_sdk
from aws_encryption_sdk.internal.crypto import WrappingKey
from aws_encryption_sdk.key_providers.raw import RawMasterKeyProvider
from aws_encryption_sdk.identifiers import WrappingAlgorithm, EncryptionKeyType, CommitmentPolicy
from Crypto.PublicKey import RSA
import base64

print ("Content-type: text/html")
print ()

provider_id   = 'openssl' # 필드 레벨 암호화 프로파일의 Provider name 값 입력
PublicKeyName = 'premisan-test' # 필드 레벨 암호화 프로파일의 Public key name 값 입력

def decrypt_data():
    class SIFPrivateMasterKeyProvider(RawMasterKeyProvider):
        provider_id = provider_id

        def __new__(cls, *args, **kwargs):
            obj = super(SIFPrivateMasterKeyProvider, cls).__new__(cls)
            return obj

        def __init__(self, private_key_id, private_key_text):
            RawMasterKeyProvider.__init__(self)

            private_key = RSA.importKey(private_key_text)
            self._key = private_key.exportKey()

            RawMasterKeyProvider.add_master_key(self, private_key_id)

        def _get_raw_key(self, key_id):
            return WrappingKey(
                wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1,
                wrapping_key=self._key,
                wrapping_key_type=EncryptionKeyType.PRIVATE
            )

    def DecryptField(private_key, field_data):
        # add padding if needed base64 decoding
        field_data = field_data + '=' * (-len(field_data) % 4)
        # base64-decode to get binary ciphertext
        ciphertext = base64.b64decode(field_data)
        # decrypt ciphertext into plaintext
        client = aws_encryption_sdk.EncryptionSDKClient(
            commitment_policy=CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT
        )
        plaintext, header = client.decrypt(
            source=ciphertext,
            key_provider=sif_private_master_key_provider
        )
        return plaintext

    # 생성한 비밀키 파일의 내용을 복사하여 붙여넣기
    private_key_text = '''
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAoj4Tmo6OGvA4rBsh6UA0LO7tGLtLf96WTfLQGKG2Ev9pOnWr
r3JSbdVlupc/wtCIs/aPqC6I4vIbE7i2d7wjI7zCdrCSPUbj8lcxr/kB1nkSbsCS
XrqH+sA4UaBsbdNVrXOEZXwKd2nhDOmu42MjBBtPX3p/9wzbL4pNkqW9Et8drwHY
...
7BmTrXcCgYEAxI66RkWMJRqjYqsjn2+RmGO1m5mMkDHwF0yWcohe9Z81grTQNifS
I5Dk5h5fk5IaVJakeG4mDc+KDox+a0PPi3fmBmsVSBA/l2f8Y5hy00WsR8TVWseZ
899hl3pBH6avNnVzCJfYOY93eZvIkbEj3iPK+tatk5NidP+PcGz3EiM=
-----END RSA PRIVATE KEY-----
'''.strip()
    sif_private_master_key_provider = SIFPrivateMasterKeyProvider(PublicKeyName, private_key_text)
    return DecryptField(private_key_text, enc_text_raw).decode("utf-8")

form = cgi.FieldStorage() 
enc_text_raw = form.getvalue('enc')
enc_text = decrypt_data ()
dec_text = form.getvalue('dec')

if enc_text is not None and enc_text is not "":
    print("enc_text_raw= ",enc_text_raw) # 필드 레벨 암호화로 암호화된 텍스트
    print("")
    print("enc_text= ",enc_text) # 필드 레벨 암호화 복구된 텍스트
    print("")

if dec_text is not None:
    print("dec_text= ",dec_text) # 복호화되기 전 텍스트 출력
    print("")

다시 페이지 접속 시 "premisan"이라는 값을 입력하면 클라우드프론트에서 "enc" 필드가 암호화되어 WAS로 전달(enc_text_raw 변수)되고 이 값을 다시 WAS 에서 복호화(enc_text 변수)하여 출력합니다.


9) 텍스트 암/복호화 스크립트에 응용하기

기존 텍스트 암/복호화 스크립트에 응용하면 아래와 같이 작동합니다.

사용자 -> 클라우드프론트 - (enc 필드 암호화) -> 오리진 - (enc 필드 복호화 후 기존 암호화 스크립트의 SimpleEnDecrypt 클래스로 텍스트 암호화) -> 클라우드프론트 -> 사용자

아래 소스 내용 중 "provider_id", "PublicKeyName", "private_key_text" 변수의 경우 테스트 환경에 따라 다를 수 있기 때문에 적절히 수정합니다.




#!/usr/bin/python3
import os
from cryptography.fernet import Fernet
import cgi, cgitb 
import aws_encryption_sdk
from aws_encryption_sdk.internal.crypto import WrappingKey
from aws_encryption_sdk.key_providers.raw import RawMasterKeyProvider
from aws_encryption_sdk.identifiers import WrappingAlgorithm, EncryptionKeyType, CommitmentPolicy
from Crypto.PublicKey import RSA
import base64

print ("Content-type: text/html")
print ()

provider_id   = 'openssl' # 필드 레벨 암호화 프로파일의 Provider name 값 입력
PublicKeyName = 'premisan-test' # 필드 레벨 암호화 프로파일의 Public key name 값 입력

def decrypt_data():
    class SIFPrivateMasterKeyProvider(RawMasterKeyProvider):
        provider_id = provider_id

        def __new__(cls, *args, **kwargs):
            obj = super(SIFPrivateMasterKeyProvider, cls).__new__(cls)
            return obj

        def __init__(self, private_key_id, private_key_text):
            RawMasterKeyProvider.__init__(self)

            private_key = RSA.importKey(private_key_text)
            self._key = private_key.exportKey()

            RawMasterKeyProvider.add_master_key(self, private_key_id)

        def _get_raw_key(self, key_id):
            return WrappingKey(
                wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1,
                wrapping_key=self._key,
                wrapping_key_type=EncryptionKeyType.PRIVATE
            )

    def DecryptField(private_key, field_data):
        # add padding if needed base64 decoding
        field_data = field_data + '=' * (-len(field_data) % 4)
        # base64-decode to get binary ciphertext
        ciphertext = base64.b64decode(field_data)
        # decrypt ciphertext into plaintext
        client = aws_encryption_sdk.EncryptionSDKClient(
            commitment_policy=CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT
        )
        plaintext, header = client.decrypt(
            source=ciphertext,
            key_provider=sif_private_master_key_provider
        )
        return plaintext

    # 생성한 비밀키 파일의 내용을 복사하여 붙여넣기
    private_key_text = '''
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAoj4Tmo6OGvA4rBsh6UA0LO7tGLtLf96WTfLQGKG2Ev9pOnWr
r3JSbdVlupc/wtCIs/aPqC6I4vIbE7i2d7wjI7zCdrCSPUbj8lcxr/kB1nkSbsCS
XrqH+sA4UaBsbdNVrXOEZXwKd2nhDOmu42MjBBtPX3p/9wzbL4pNkqW9Et8drwHY
...
7BmTrXcCgYEAxI66RkWMJRqjYqsjn2+RmGO1m5mMkDHwF0yWcohe9Z81grTQNifS
I5Dk5h5fk5IaVJakeG4mDc+KDox+a0PPi3fmBmsVSBA/l2f8Y5hy00WsR8TVWseZ
899hl3pBH6avNnVzCJfYOY93eZvIkbEj3iPK+tatk5NidP+PcGz3EiM=
-----END RSA PRIVATE KEY-----
'''.strip()
    sif_private_master_key_provider = SIFPrivateMasterKeyProvider(PublicKeyName, private_key_text)
    return DecryptField(private_key_text, enc_text_raw).decode("utf-8")

class SimpleEnDecrypt:
    def __init__(
            self, key=None):
        self.key = b'XXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX='
        self.f = Fernet(self.key)

    def encrypt(self, data, is_out_string=True):
        if isinstance(data, bytes):
            ou = self.f.encrypt(data)  # 바이트형태이면 바로 암호화
        else:

            ou = self.f.encrypt(data.encode('utf-8'))  # 인코딩 후 암호화
        if is_out_string is True:
            return ou.decode('utf-8')  # 출력이 문자열이면 디코딩 후 반환
        else:
            return ou

    def decrypt(self, data, is_out_string=True):
        if isinstance(data, bytes):
            ou = self.f.decrypt(data)  # 바이트형태이면 바로 복호화
        else:
            ou = self.f.decrypt(data.encode('utf-8'))  # 인코딩 후 복호화
        if is_out_string is True:
            return ou.decode('utf-8')  # 출력이 문자열이면 디코딩 후 반환
        else:
            return ou

simpleEnDecrypt = SimpleEnDecrypt()

form = cgi.FieldStorage() 
enc_text_raw = form.getvalue('enc')
enc_text = decrypt_data ()
dec_text = form.getvalue('dec')

if enc_text is not None and enc_text is not "":
    encrypt_text = simpleEnDecrypt.encrypt(enc_text)
    print("enc_text_raw= ",enc_text_raw) # 필드 레벨 암호화로 암호화된 텍스트
    print("")
    print("enc_text= ",enc_text) # 필드 레벨 암호화 복구된 텍스트
    print("")
    print(encrypt_text) # 암호화 출력
    print("")

if dec_text is not None:
    decrypt_text = simpleEnDecrypt.decrypt(dec_text)
    print("dec_text= ",dec_text) # 복호화되기 전 텍스트 출력
    print("")
    print(decrypt_text) # 복호화 출력
    print("")

이렇게 클라우드프론트의 추가 보안 기능인 필드 레벨 암호화를 통해 POST body 데이터의 특정 필드를 암호화하여 오리진으로 전송하고, 오리진에서 해당 암호화된 필드를 복호화하여 처리하는 방법을 알아보았습니다. 소스를 수정해야하는 점이 조금 까다로워 실제 사용하는 케이스가 있을지는 잘 모르겠지만, AWS에서 클라우드프론트 서비스가 제공하는 보안 관련 기능으로 자주 보이는 기능이라 알아두면 좋을 것 같습니다.