AWS Secrets Manager를 사용하여 IAM 액세스 키를 주기적으로 자동 교체하는 방안

7분 분량
콘텐츠 수준: 중급
3

해당 기사에서는 AWS Secrets Manager 보안 암호 교체를 사용하여 IAM 액세스 키를 주기적으로 교체하는 방안에 대해서 설명합니다.

액세스 키와 같은 장기 자격 증명이 있는 IAM 사용자를 사용하는 대신 임시 자격 증명을 사용하는 것을 권장합니다. 하지만, IAM 사용자의 장기 자격 증명이 필요한 특정 사용 사례가 있는 경우 액세스 키를 주기적으로 교체하는 것이 좋습니다. AWS Secrets ManagerAWS Lambda를 사용하여 주기적으로 IAM 액세스 키 교체를 달성할 수 있습니다. 더 나아가 Secrets Manager를 사용하면 액세스 키와 같은 자격증명을 애플리케이션 소스 코드에 하드 코딩 하지 않고 자격 증명 필요 시 보안 암호를 검색하여 사용하므로 보안 환경 개선과 자격 증명을 교체할 때 애플리케이션에 변경 사항을 배포하지 않아도 되므로 가용성 향상 측면에서 유용합니다. [1]

해당 기사의 최종 목적은 IAM 장기 자격증명인 액세스 키를 AWS Secrets Manager에 저장 후 자격증명 필요 시 보안 암호를 검색하여 자격증명을 사용합니다. 이 후, Secrets Manager 보안 암호 교체 옵션을 사용하여 주기적으로 자격증명을 교체합니다. 정상적으로 프로세스 완료 시, 보안 암호는 새로운 액세스 키로 교체되고 가장 오래 된 액세스 키는 삭제됩니다.

사전 요구 사항

  1. 액세스 키를 보유한 IAM 사용자
  2. 액세스 키를 저장하고 있는 보안 암호
  3. 액세스 키 교체를 위한 lambda 함수

단계 1. 액세스 키 확인

$ aws iam list-access-keys --user-name "Your User Name" 

  • 자신의 IAM 사용자에 대한 액세스 키를 교체하려면 다음 정책에 따른 권한이 있어야 합니다.
{
    "Version": "2012-10-17",
    "Statement": [{
        "Sid": "ManageOwnAccessKeys",
        "Effect": "Allow",
        "Action": [
            "iam:CreateAccessKey",
            "iam:DeleteAccessKey",
            "iam:GetAccessKeyLastUsed",
            "iam:GetUser",
            "iam:ListAccessKeys",
            "iam:UpdateAccessKey",
            "iam:TagUser"
        ],
        "Resource": "arn:aws:iam::*:user/${aws:username}"
    }]
}

단계 2. 다른 유형의 보안 암호 생성 [2]

다른 유형의 보안 암호 옵션을 사용하면 모든 유형의 서비스에 대한 자격 증명 또는 기타 정보를 저장할 수 있는 보안 암호를 생성 할 수 있습니다. 아래와 같은 Key / Value를 포함하는 보안 암호를 생성합니다.

Secret keySecret value
accesskeyYour Access Key
secretkeyYour Secret Key
usernameYour User Name
secretarnYour Secret ARN

  • secretarn은 보안 암호 생성 후 확인되는 ARN으로 편집합니다.

여기에 이미지 설명 입력

  • 보안 암호 검색

$ aws secretsmanager get-secret-value --secret-id "Your Secret Name"

여기에 이미지 설명 입력

단계 3. 액세스 키 교체를 위한 Lambda 함수 생성

Secrets Manager는 모든 유형의 암호에 대한 교체 기능을 생성할 수 있는 시작점으로 템플릿을 제공합니다. [3] 다음 예제 코드를 참고하실 수 있습니다.

import boto3
import json
import logging
import os
import time

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):
    arn = event['SecretId']
    token = event['ClientRequestToken']
    step = event['Step']
    # Setup the client
    secretsmanager_client = boto3.client('secretsmanager')
    # Make sure the version is staged correctly
    metadata = secretsmanager_client.describe_secret(SecretId=arn)
    logging.info(repr(metadata))
    versions = metadata['VersionIdsToStages']
    if token not in versions:
        logger.error("Secret version %s has no stage for rotation of secret %s." % (token, arn))
        raise ValueError("Secret version %s has no stage for rotation of secret %s." % (token, arn))
    if "AWSCURRENT" in versions[token]:
        logger.info("Secret version %s already set as AWSCURRENT for secret %s." % (token, arn))
        return
    elif "AWSPENDING" not in versions[token]:
        logger.error("Secret version %s not set as AWSPENDING for rotation of secret %s." % (token, arn))
        raise ValueError("Secret version %s not set as AWSPENDING for rotation of secret %s." % (token, arn))
    if step == "createSecret":
        logging.debug("createSecret %s" % arn)
        logging.info("for IAM user access keys secret creation is handled by IAM ")
    elif step == "setSecret":
        logging.debug("setSecret %s" % arn)
        current_dict = get_secret_dict(secretsmanager_client, arn, "AWSCURRENT", required_fields=['username'])
        username = current_dict['username']
        origin_dict = get_secret_dict(secretsmanager_client, current_dict['secretarn'], "AWSCURRENT")
        origin_iam_client = boto3.client('iam', aws_access_key_id=origin_dict['accesskey'], aws_secret_access_key=origin_dict['secretkey'])
        # load any pre-existing access keys. sorted by created descending. if the count is 2+ remove the oldest key
        existing_access_keys = sorted(origin_iam_client.list_access_keys(UserName=username)['AccessKeyMetadata'], key=lambda x: x['CreateDate'])
        if len(existing_access_keys) >= 2:
            logger.info("at least 2 access keys already exist. deleting the oldest version: %s" % existing_access_keys[0]['AccessKeyId'])
            origin_iam_client.delete_access_key(UserName=username, AccessKeyId=existing_access_keys[0]['AccessKeyId'])
        # request new access key and gather the response
        new_access_key = origin_iam_client.create_access_key(UserName=username)
        current_dict['accesskey'] = new_access_key['AccessKey']['AccessKeyId']
        current_dict['secretkey'] = new_access_key['AccessKey']['SecretAccessKey']
        logging.info('applying new secret value to AWSPENDING')
        # save the new access key to the pending secret
        secretsmanager_client.put_secret_value(SecretId=arn, ClientRequestToken=token, SecretString=json.dumps(current_dict), VersionStages=['AWSPENDING'])
    elif step == "testSecret":
        logging.debug("testSecret %s" % arn)
        # load the pending secret for testing
        pending_dict = get_secret_dict(secretsmanager_client, arn, "AWSPENDING", required_fields=['username'], token = token)
        # attempt to call an iam service using the credentials
        test_client = boto3.client('iam', aws_access_key_id=pending_dict['accesskey'], aws_secret_access_key=pending_dict['secretkey'])
        try:
            test_client.get_account_authorization_details()
        except test_client.exceptions.ClientError as e:
            # the test fails if and only if Authentication fails. Authorization failures are acceptable.
            if e.response['Error']['Code'] == 'AuthFailure':
                logging.error("Pending IAM secret %s in rotation %s failed the test to authenticate. exception: %s" % (arn, pending_dict['username'], repr(e)))
                raise ValueError("Pending IAM secret %s in rotation %s failed the test to authenticate. exception: %s" % (arn, pending_dict['username'], repr(e)))
    elif step == "finishSecret":
        logging.debug("finishSecret %s" % arn)
        # finalize the rotation process by marking the secret version passed in as the AWSCURRENT secret.
        metadata = secretsmanager_client.describe_secret(SecretId=arn)
        current_version = None
        for version in metadata["VersionIdsToStages"]:
            if "AWSCURRENT" in metadata["VersionIdsToStages"][version]:
                if version == token:
                    # The correct version is already marked as current, return
                    logger.info("finishSecret: Version %s already marked as AWSCURRENT for %s" % (version, arn))
                    return
                current_version = version
                break
        # finalize by staging the secret version current
        secretsmanager_client.update_secret_version_stage(SecretId=arn, VersionStage="AWSCURRENT", MoveToVersionId=token, RemoveFromVersionId=current_version)
        logger.info("finishSecret: Successfully set AWSCURRENT stage to version %s for secret %s." % (token, arn))
    else:
        raise ValueError("Invalid step parameter")

def get_secret_dict(secretsmanager_client, arn, stage, required_fields=[], token=None):
    # Only do VersionId validation against the stage if a token is passed in
    if token:
        secret = secretsmanager_client.get_secret_value(SecretId=arn, VersionId=token, VersionStage=stage)
    else:
        secret = secretsmanager_client.get_secret_value(SecretId=arn, VersionStage=stage)
    plaintext = secret['SecretString']
    secret_dict = json.loads(plaintext)
    # Run validations against the secret
    for field in required_fields:
        if field not in secret_dict:
            raise KeyError("%s key is missing from secret JSON" % field)
    # Parse and return the secret JSON string
    return secret_dict

  • Lambda 함수가 보안 암호를 교체하기 위해 실행 역할에 아래와 같은 권한 정책이 필요합니다. [4]
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "secretsmanager:DescribeSecret",
                "secretsmanager:GetSecretValue",
                "secretsmanager:PutSecretValue",
                "secretsmanager:UpdateSecretVersionStage"
            ],
            "Resource": "Your Secret ARN"
        },
        {
            "Effect": "Allow",
            "Action": [
                "secretsmanager:GetRandomPassword"
            ],
            "Resource": "*"
        }
    ]
}
  • AWS 관리형 키 aws/secretsmanager 이 아닌 KMS 키를 사용하여 보안 암호를 암호화 할 경우 Lambda 실행 역할에 다음 문서에 키 사용 권한 부여가 필요합니다. [5]

  • Lambda 리소스 기반 정책을 사용하여 AWS Secrets Manager가 함수를 호출하도록 허용합니다.

여기에 이미지 설명 입력

단계 4. Secrets Manager 교체 구성

자동 교체를 켜면 cron() 또는 rate() 표현식을 사용하여 보안 암호 교체 일정을 설정할 수 있습니다. rate 표현식을 사용하면 시간 또는 일 간격으로 반복되는 교체 일정을 생성할 수 있으며, cron 표현식을 사용하면 보다 세부적인 교체 일정을 생성할 수 있습니다. 또한, Secrets Manager 교체 일정은 UTC 표준 시간대를 사용하며, 보안 암호를 4시간 마다 교체할 수 있습니다. [6]

여기에 이미지 설명 입력

단계 5. 보안 암호 즉시 교체 및 확인

교체 구성 완료 시, 보안 암호를 즉시 교체 할 수 있습니다.

여기에 이미지 설명 입력

정상적으로 교체가 성공되면, 보안 암호가 교체되고 오래 된 액세스 키는 삭제됩니다. 운영방식에 따라 이전 사용하던 액세스 키를 비 활성화 또는 삭제하는 방안을 추가 구현할 수 있습니다.

여기에 이미지 설명 입력

$ aws secretsmanager get-secret-value --secret-id "Your Secret Name"

여기에 이미지 설명 입력

$ aws iam list-access-keys --user-name "Your User Name"

여기에 이미지 설명 입력 


관련 정보

[1] AWS Secrets Manager란 무엇인가요? - https://docs.aws.amazon.com/ko_kr/secretsmanager/latest/userguide/intro.html

[2] AWS Secrets Manager 보안 암호 생성 - https://docs.aws.amazon.com/ko_kr/secretsmanager/latest/userguide/create_secret.html

[3] AWS Secrets Manager 교체 함수 템플릿 - https://docs.aws.amazon.com/ko_kr/secretsmanager/latest/userguide/reference_available-rotation-templates.html#OTHER_rotation_templates

[4] Lambda 교체 함수 실행 역할에 대한 정책 - https://docs.aws.amazon.com/ko_kr/secretsmanager/latest/userguide/rotating-secrets-required-permissions-function.html#rotating-secrets-required-permissions-function-example

[5] 고객 관리형 키에 대한 정책 설명 - https://docs.aws.amazon.com/ko_kr/secretsmanager/latest/userguide/rotating-secrets-required-permissions-function.html#rotating-secrets-required-permissions-function-cust-key-example

[6] Secrets Manager 교체의 예약 표현식 - https://docs.aws.amazon.com/ko_kr/secretsmanager/latest/userguide/rotate-secrets_schedule.html

3 댓글

좋은 글 감사합니다.

AWS
지원 엔지니어
답글을 게시함 9달 전

좋은글 감사합니다. 위와 같은 SecretManger & Lambda를 활용한 AccessKey 자동갱신을 설정할 때 월1회 동작한다고 가정했을때 비용이 어떻게 될까요?

Owen
답글을 게시함 8달 전

안녕하세요 Owen님,

Secrets Manager와 Lambda 비용에 대해 말씀드리면 아래와 같습니다. CMK를 사용하면 추가 비용이 발생할 수 있으며, 고객님의 호출 건수에 따라 계산이 필요할 것으로 생각됩니다.

AWS Secrets Manager 요금 - https://aws.amazon.com/ko/secrets-manager/pricing/

1개의 보안 암호: 월 0.40USD

API 호출 1만 건 당: 0.05USD

AWS Lambda 요금 - https://aws.amazon.com/ko/lambda/pricing/

요청 1백만 건당: 0.20 USD

또한 AWS 요금 계산기를 사용하여 예상 비용을 생성하실 수 있으며, Account & billing으로 Support 케이스를 생성하여 비용을 전문적으로 상담해주는 엔지니어의 도움을 받으실 수 있습니다.

AWS 요금 계산기 - https://calculator.aws/#/

지원 사례 및 사례 관리 생성 - https://docs.aws.amazon.com/ko_kr/awssupport/latest/user/case-management.html

감사합니다.

profile pictureAWS
지원 엔지니어
답글을 게시함 8달 전