aws iam get-credential-report --query 'Content' --output text | base64 -d | cut -d, -f1,9,10,11,14,15,16
[알림] 다양한 방법이 있을 수 있으나 본 항목에서는 Secret Manager와 Lambda를 활용한 방법을 안내
관리 콘솔에서 [Secret Manager] 검색 → [새 보안 암호 저장] 클릭
Access key를 저장할 Secret Manager 생성(키/값에는 임시 값 입력)
[자동 교체 구성] 비활성화 설정 후 [다음] 클릭
관리 콘솔에서 [IAM] 검색 → [정책] 메뉴 → [정책 생성] 클릭
JSON(1)을 활용하여 JSON 정책 생성
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "SecretsManagerReadWritePermission",
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret",
"secretsmanager:PutSecretValue",
"secretsmanager:UpdateSecretVersionStage",
"secretsmanager:ListSecretVersionIds"
],
"Resource": [
"SECRETS MANAGER의 ARN"
]
},
{
"Sid": "KMSWritePermission",
"Effect": "Allow",
"Action": [
"kms:Decrypt",
"kms:Encrypt"
],
"Resource": [
"aws/secretsmanager의 ARN"
]
},
{
"Sid": "SecretsManagerKMSListingPermission",
"Effect": "Allow",
"Action": [
"kms:ListKeys",
"secretsmanager:ListSecrets"
],
"Resource": "*"
}
]
}
IAM 서비스에서 [역할] 메뉴 → [역할 만들기] 클릭
사용 사례 설정
JSON(1)을 활용해 만든 정책과 [AWSLambdaBasicExecutionRole] 정책 할당
관리 콘솔에서 [Lambda] 검색 → [함수 생성] 클릭
함수 생성 설정
Lambda(1)을 활용해 함수 작성 → [Deploy] 클릭
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']
master_dict = get_secret_dict(secretsmanager_client, current_dict['masterarn'], "AWSCURRENT")
master_iam_client = boto3.client('iam', aws_access_key_id=master_dict['accesskey'], aws_secret_access_key=master_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(master_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'])
master_iam_client.delete_access_key(UserName=username, AccessKeyId=existing_access_keys[0]['AccessKeyId'])
# request new access key and gather the response
new_access_key = master_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
JSON(2)를 활용해 IAM 계정이 자신의 패스워드와 Access Key를 변경하도록 하는 정책 생성
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"iam:ListUsers",
"iam:GetAccountPasswordPolicy"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"iam:*AccessKey*",
"iam:ChangePassword",
"iam:GetUser",
"iam:*ServiceSpecificCredential*",
"iam:*SigningCertificate*"
],
"Resource": [
"arn:aws:iam::*:user/${aws:username}"
]
}
]
}
Access Key를 자동으로 교체할 IAM 계정에 JSON(2)를 활용해 만든 정책 할당
[AWSLambda_FullAccess] 정책을 할당한 IAM 계정의 AWS CLI 접속
Command(2) 명령어 실행
aws lambda add-permission --function-name [lambda의 ARN] --principal secretsmanager.amazonaws.com --action lambda:InvokeFunction --statement-id SecretsManagerAccess
관리 콘솔에서 [Secret Manager] 검색 → 생성한 보안 암호 클릭
[보안 암호값 검색] 클릭 → [편집] 클릭
보안 암호 값 편집
[교체 편집] 클릭 → 교체 구성 설정
[저장] 시 Access Key는 자동으로 교체되며, 변경 주기 이전에 교체를 원할 경우 [보안 암호 즉시 교체] 클릭
Secret Manager에서 Access Key 변경 확인
CloudWatch Log groups에서 Access Key 변경 확인