개발을 하다가 보면 필연적으로 마주치는 문제가 있습니다. 바로 데이터베이스의 암호와 같은 민감한 데이터를 어떻게 저장할 것인가 하는 문제입니다. 코드에 하드코딩을 할 수는 없고, 다른 곳에 암호화를 해서 저장을 해도 결국 복호화를 할 또 다른 암호는 또 다른 어딘가에 있어야 하고… 그렇다고 이 과정을 무한히 반복할 수도 없겠지요.

옛날이나 지금도 가장 널리 사용되는, 안전하지 않은 방법은 바로 별도의 설정 파일을 이용하는 것입니다. 메인 코드에서 따로 분리된 설정 파일 안에 데이터베이스의 호스트, 포트, 계정과 암호 등을 적어놓고 배포하고자 하는 단계에 따라서 각 단계별 설정 파일을 함께 배포하는 방법입니다. 물론, 설정 파일은 암호화가 되어있지 않거나, 암호화가 되어 있다고 하더라도 결국 복호화를 위한 키는 코드에 포함될 수밖에 없습니다.

대부분의 경우에는 코드를 외부에서 볼 수 없도록 차단을 해놨을 것인데, 이것을 믿고 아래처럼 평문으로 된 정보를 그대로 소스와 함께 올리는 경우가 생각보다 많이 존재합니다. 사내 직원을 믿는 것은 좋지만, 클라우드 보안은 언제나 외칩니다. 아무도 믿지 말아라!

stage=test

test {
database {
url="jdbc:mysql://example.host:3306"
driver="org.mariadb.jdbc.Driver"
user="example-user"
password="example-password"
}
}

Code language: JavaScript (javascript)

이런 문제를 해결하기 위해서 암호를 중앙 관리해주는 Vault 서비스들이 등장했는데, 과연 AWS 같은 클라우드 서비스에서는 이것을 어떻게 구축하고, 이용할 수 있는지 그리고 정말로 안전한지 OpsNow의 소스를 좀 더 안전하게 만들어 나가는 과정을 따라가면서 살펴볼까 합니다. 참 많은 방법을 시도했었는데 몇 가지만 기억에 남네요. 😂

다양한 시도와 실패

메인 소스와 설정 파일 분리

가장 간단한 방법입니다. Git과 같은 SCM (Source Code Management) 툴에 올라갈 메인 소스와 별도로 설정 파일은 분리합니다. 그리고 SCM에는 올리지 않고 별도로 관리합니다. 여전히 문제는 많았습니다.

  • 개발 담당자가 직접 설정 파일을 로컬에서 관리해야 합니다.
  • 개발 담당자가 배포 과정에 직접 개입해야 합니다.
  • 배포 자동화를 위해서는 결국 설정 파일이 어딘가에 노출된 채로 있어야 합니다.

코드를 주로 관리하는 직원만 힘들어집니다. 빠르게 포기합니다.

설정 파일을 S3에 업로드

위와 유사한 방법입니다. 온프레미스에서는 별도로 시스템을 구축하기 전에는 해답이 없다는 것을 깨닫고, AWS의 IAM 권한을 이용하였습니다. S3에 설정 파일을 업로드하고 버킷 정책을 통해서 특정 IAM 권한 외에는 접근을 차단하는 것입니다.

{
"Version": "2012-10-17",
"Id": "ExamplePolicy",
"Statement": [
{
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": "arn:aws:s3:::example-bucket/path/to/password.txt",
"Condition": {
"StringNotLike": {
"aws:arn": [
"arn:aws:iam::123456789012:root",
"arn:aws:iam::123456789012:user/example-user"
] }
}
}
] }
Code language: JSON / JSON with Comments (json)

나름 나쁘지 않은 방법이었지만, 앞서 언급했던 일부 단점들이 그대로 넘어왔습니다.

  • 개발 담당자가 직접 설정 파일을 S3에서 관리해야 합니다.
  • 버킷 정책을 효과적으로 통제하기 어려웠습니다.
  • 정책을 직접 수정할 수 있는 권한을 가진 사람이 의외로 많았습니다.

아무래도 개발계는 권한이 후하게 제공되다 보니까 이런 일이 생깁니다.

환경 변수로 민감한 데이터 제공

이번에는 아예 프로그램을 실행할 때, 환경 변수에 민감한 정보를 제공하는 것입니다. JAR 프로그램은 단 한 번 시도해보고 바로 포기했습니다. 단점을 논할 필요조차 없을 정도로 비효율의 극에 다다른 방법입니다.

export DB_HOST=example.host
export DB_PORT=3306
export DB_USER=example-user
export DB_PASS=example-password

java -jar my_application.jar arguments
Code language: Bash (bash)

사실, 이와 같은 시도를 한 이유는 AWS Lambda를 위한 것이었습니다. 앞서 IAM을 이용한 것에서 한 발 더 나아간 것인데, Lambda에서는 KMS (Key Management Service)를 이용하여 환경 변수를 암호화된 상태로 제공할 수 있었기 때문입니다.

전송 중 암호화 구성으로 값을 **** 처럼 암호화 할 수 있습니다.

그러나 Lambda에서도 포기하게 된 이유는 다음과 같습니다.

  • 복호화 로직을 넣어야 하는데, 코드가 길어져서 효율이 너무 떨어졌습니다.
  • 환경 변수의 관리가 제대로 이루어지지 않았습니다.
  • IaC로 넘어가면서 환경 변수도 코드로 제공되면서 의미가 무색해졌습니다.

그래도 여기서 힌트를 얻었습니다. 모든 민감한 정보들을 권한에 따라 분리해서 넣어놓고 KMS로 암호화 합니다. 그리고 각각의 정보들에 대한 권한 관리가 이루어진다면, 원하는 정보만 중앙 집중식으로 가져다가 쓸 수 있을 것 같습니다.

IAM 기반의 권한 관리

Parameter Store

AWS에서는 각종 매개 변수를 한 곳에서 모아서 관리가 가능하도록 해주는 Systems Manager의 Parameter Store라는 기능이 있습니다. 특히, 민감한 정보에 대해서는 KMS의 키를 이용해서 암호화를 해놓을 수 있습니다. 아래에서 SecureString이 바로 그러한 항목들입니다.

AWS에서 관리되는 매개 변수. 여러 서비스에서 공통으로 안전하게 이용하기 좋습니다.

매개 변수에 대한 접근 권한은 2단계로 관리할 수 있습니다. 매개 변수 자체에 대한 읽기 권한과 KMS 키를 이용한 복호화 권한이 모두 있어야만 실제 값을 알 수 있습니다. 그리고 실제 코드에서는 아래와 같이 사용할 수 있습니다.

import boto3

# AWS Service Clients
ssm = boto3.client('ssm')

# SSM parameters, 파라미터가 많아지면 get_parameters를 통해서 특정 경로의 매개 변수를 전체 호출 가능합니다.
cost_db_secret = ssm.get_parameter(Name=f'/COST/DEV/DB/MAIN/SECRET', WithDecryption=True)['Parameter']['Value'] cost_db_endpoint = ssm.get_parameter(Name=f'/COST/DEV/DB/MAIN/ENDPOINT/READ')['Parameter']['Value'] cost_db_database = ssm.get_parameter(Name=f'/COST/DEV/DB/MAIN/DATABASE')['Parameter']['Value'] cost_db_username = ssm.get_parameter(Name=f'/COST/DEV/DB/MAIN/USERNAME')['Parameter']['Value'] cost_db_password = ssm.get_parameter(Name=f'/COST/DEV/DB/MAIN/PASSWORD', WithDecryption=True)['Parameter']['Value']
Code language: Python (python)

매개 변수를 호출하는, 위 소스가 포함된 Lambda 함수가 적절한 권한이 둘 중에 단 하나라도 없다면 정상적으로 데이터베이스 암호를 읽을 수가 없습니다. 이제 민감한 데이터를 어떻게 관리할 것인지에 대한 부담이 많이 줄어들었습니다. 그럼에도 단점은 존재합니다.

  • AWS의 IAM에 대한 철저한 관리가 필요합니다.
    • 이건 클라우드에서 숙명과 같습니다. 받아들입니다.
  • 보안 규정에 따라 주기적인 암호 변경이 필요할 수 있습니다.

Secrets Manager

AWS의 Secrets Manager는 말 그대로 암호 관리에 최적화된 서비스입니다. 아니, 이미 Parameter Store를 소개했는데 유사한 서비스가 더 있다고요? 심지어 Secrets Manager는 Parameter Store에 비해서 비용도 비쌉니다. 그럼에도 불구하고 사용해야만 하는 이유는 존재합니다.

Secrets Manager는 AWS의 RDS 등에 대한 호스트, 데이터베이스, 사용자 계정 등에 대한 정보를 하나로 묶어서 관리합니다. 그리고 이 모든 것을 한 번에 암호화 시켜놓습니다. 기존에 Parameter Store에서는 각각 관리하기 때문에 매개 변수를 등록할 때도 4~5개의 정보를 각각 등록해야 했습니다. 아무래도 번거로운 것이 사실입니다.

게다가, Secrets Manager에서는 30일, 60일, 90일 주기로 계정의 암호를 자동으로 변경할 수 있습니다!

암호 변경은 자기 자신을 변경하거나, 마스터 계정을 통해 서브 계정을 변경할 수 있습니다.

암호 교체 구성을 선택하고 계정 정보에 맞는 구성을 고르게 되면 자동으로 암호 교체를 위한 Lambda 함수를 배포하게 됩니다. 저희는 암호 교체 구성 템플릿을 커스터마이즈하여 사용하기 위해 별도로 템플릿을 만들어서 사용하고 있습니다.

  • 단일 유저 교체 구성은 자기 자신으로 로그인하여 암호를 변경하는 방식으로 이루어지고, 서비스 계정일 경우에는 데이터베이스 접근에 순단이 발생할 수 있습니다.
  • 멀티 유저 교체 구성은 암호 변경 권한이 있는 마스터 계정이 제공된 또 다른 test 계정의 암호를 변경하는 방식입니다. Secrets Manager는 초기에 test 계정 정보를 제공하는데, 암호 교체 시기에 마스터 계정은 test 계정의 복제본인 test_clone을 생성하고 test_clone 계정 정보를 제공하게 됩니다. 이렇게 하면 두 계정이 모두 살아있어서 기존 연결과 신규 연결 모두 가능하여 순단이 발생하지 않습니다. 다음 암호 교체 시기에는 test 계정 암호를 변경하고 해당 정보를 제공함과 동시에 test_clone은 그대로 둠으로써 순단을 다시 방지합니다.

완벽합니다! 보안 준수 규정을 충족하기 위해서 알맞은 서비스입니다. 사용법도 Parameter Store와 유사합니다.

import boto3

# AWS Service Clients
secretsmanager = boto3.client('secretsmanager')

# Secrets Manager
secrets = client.get_secret_value(SecretId="DEV/COST/RDS/MAIN")
Code language: Python (python)

맺음말

자, 이렇게 로컬에서 관리하던 설정 값들이 클라우드로 옮겨졌습니다. 그리고 권한 관리는 매개 변수 값 자체와 KMS 두 단계를 거치는 IAM을 통해서 이루어집니다. 이 방식을 이용하면 개발자는 값이 무엇인지 알 필요도 없이 그저 민감한 데이터를 불러와서 사용하기만 하면 됩니다.

저의 추천은 다음과 같습니다. 변하지 않는 설정 값이라면 Parameter Store로, 주기적인 변경이 필요한 값이라면 Secrets Manager로 저장하고 사용하시면 됩니다. 이렇게 사용하시면 비용 효율적으로 설정 값들을 어떻게 관리할 것인지 고민하지 않고 편하게 사용하실 수 있습니다. 권한 관리는 IAM 관리자에게 떠넘기세요! 😝