"""Secret Manager secrets backup.""" import logging import boto3 import sentry_sdk import config from connectors import datadog sentry_sdk.init( dsn=config.SENTRY_DSN, traces_sample_rate=1.0, ) logger = logging.getLogger() logger.setLevel(config.LOGGER_LEVEL) def assume_source_account_role(account_id): """ Assume IAM role in source account. Args: account_id (str): AWS account ID Returns: dict: dict of AWS credentials """ client = boto3.client('sts') assume_role_response = client.assume_role( RoleArn=f'arn:aws:iam::{account_id}:role/{config.CROSS_ACCOUNT_BACKUP_ROLE_NAME}', RoleSessionName=config.SERVICE_NAME, ExternalId=config.EXTERNAL_ID, DurationSeconds=3600 ) credentials = assume_role_response['Credentials'] return credentials def list_secrets(source_account_id): """ List Secrets Manager secrets for a given account. Args: source_account_id (str): AWS account ID Returns: list: list of Secrets Manager secret names """ credentials = assume_source_account_role(source_account_id) client = boto3.client( 'secretsmanager', region_name=config.AWS_REGION, aws_access_key_id=credentials['AccessKeyId'], aws_secret_access_key=credentials['SecretAccessKey'], aws_session_token=credentials['SessionToken'] ) secret_list = [] paginator = client.get_paginator('list_secrets') response_iterator = paginator.paginate(IncludePlannedDeletion=False) for response in response_iterator: if 'SecretList' in response: for secret in response['SecretList']: secret_list.append(secret['Name']) return secret_list def get_secret_value_sm(source_account_id, secret_name): """ Get Secrets Manager secret value for a given account. Args: source_account_id (str): AWS account ID secret_name (str): Secret name Returns: str: str secret value """ credentials = assume_source_account_role(source_account_id) client = boto3.client( 'secretsmanager', region_name=config.AWS_REGION, aws_access_key_id=credentials['AccessKeyId'], aws_secret_access_key=credentials['SecretAccessKey'], aws_session_token=credentials['SessionToken'] ) secret_value = client.get_secret_value(SecretId=secret_name)['SecretString'] return secret_value def get_secret_version_id(source_account_id, secret_name): """ Get Secrets Manager secret value for a given account. Args: source_account_id (str): AWS account ID secret_name (str): Secret name Returns: str: str secret value """ credentials = assume_source_account_role(source_account_id) client = boto3.client( 'secretsmanager', region_name=config.AWS_REGION, aws_access_key_id=credentials['AccessKeyId'], aws_secret_access_key=credentials['SecretAccessKey'], aws_session_token=credentials['SessionToken'] ) response = client.describe_secret(SecretId=secret_name) for key, value in response.get('VersionIdsToStages', {}).items(): if 'AWSCURRENT' in value: return key def get_s3_bucket_object_keys(client, bucket_name, source_account_id): """ List s3 bucket object keys. Args: client (obj): a boto3 client bucket_name (str): Backup bucket name Returns: list: list s3 bucket object keys """ object_keys = [] paginator = client.get_paginator('list_objects_v2') response_iterator = paginator.paginate(Bucket=bucket_name, Prefix=source_account_id) for response in response_iterator: if 'Contents' in response: for object_key in response['Contents']: object_keys.append(object_key['Key']) return object_keys def get_s3_metadata(client, bucket_name, s3_key): """ Get secret metadata(currentversionid) from s3 bucket. Args: client (obj): a boto3 client bucket_name (str): Backup bucket name s3_key (str): S3 key for the secret location Returns: str: str metadata(currentversionid) value """ s3_metadata = client.head_object(Bucket=bucket_name, Key=s3_key)['Metadata'] for key, value in s3_metadata.items(): if key == 'currentversionid': return value def put_secret_to_s3(client, bucket_name, secret_name, secret_value, source_account_id): """ Backup Secret Manager secrets to s3 bucket. Args: client (obj): a boto3 client bucket_name (str): Backup bucket name secret_name (str): S3 key for the secret location secret_value (str): Secret value """ client.put_object(Bucket=bucket_name, Key=f'{source_account_id}/{secret_name}', Body=secret_value, Metadata={'currentVersionID': get_secret_version_id(source_account_id, secret_name)}) def main(): """Backup Secret Manager secrets to s3 bucket.""" s3_client = boto3.client('s3', region_name=config.BACKUP_AWS_REGION) bucket_name = config.BUCKET_NAME account_id = config.ACCOUNT_ID_TO_BACKUP secret_names = list_secrets(account_id) object_keys = get_s3_bucket_object_keys(s3_client, bucket_name, account_id) try: for secret_name in secret_names: secret_version_id = get_secret_version_id(account_id, secret_name) if not secret_version_id: logging.info(f'Secret {account_id}/{secret_name} has no current version. Skipping...') elif f'{account_id}/{secret_name}' in object_keys: if get_s3_metadata(s3_client, bucket_name, f'{account_id}/{secret_name}') == secret_version_id: logging.info(f'Secret {account_id}/{secret_name} is not change') else: logging.info(f'Update value for secret {secret_name}') put_secret_to_s3(s3_client, bucket_name, secret_name, get_secret_value_sm(account_id, secret_name), account_id) else: logging.info(f'Creating a new secret {secret_name}') put_secret_to_s3(s3_client, bucket_name, secret_name, get_secret_value_sm(account_id, secret_name), account_id) logging.info(f'Secrets manager backup for account {account_id} is completed') datadog.publish_metric('secrets_manager_backup.success', 1) except Exception as error: logger.exception(str(error)) datadog.publish_metric('secrets_manager_backup.error', 1) raise error if __name__ == '__main__': main()