Skip to content

Commit

Permalink
Add secret manager replication support (ansible-collections#827)
Browse files Browse the repository at this point in the history
Add secret manager replication support

Signed-off-by: Eric Millbrandt eric.millbrandt@numerated.com
SUMMARY
Add support for regional secret replication.  The component now supports:

Creating a secret with a regional replica
Adding a region replica to a secret
Removing a region replica from a secret

ISSUE TYPE

Feature Pull Request

COMPONENT NAME
aws_secret
ADDITIONAL INFORMATION
https://aws.amazon.com/about-aws/whats-new/2021/03/aws-secrets-manager-provides-support-to-replicate-secrets-in-aws-secrets-manager-to-multiple-aws-regions/
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/secretsmanager.html

Reviewed-by: Eric Millbrandt <eric.millbrandt@numerated.com>
Reviewed-by: Markus Bergholz <git@osuv.de>
Reviewed-by: Mark Chappell <None>
Reviewed-by: Alina Buzachis <None>
Reviewed-by: Mark Woolley <mw@marknet15.com>
  • Loading branch information
emillbrandt-ngt committed Jan 31, 2023
1 parent 21f05b4 commit 6f76a88
Showing 1 changed file with 110 additions and 2 deletions.
112 changes: 110 additions & 2 deletions secretsmanager_secret.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,24 @@
- Specifies a user-provided description of the secret.
type: str
default: ''
replica:
description:
- Specifies a list of regions and kms_key_ids (optional) to replicate the secret to
type: list
elements: dict
version_added: 5.3.0
suboptions:
region:
description:
- Region to replicate secret to.
type: str
required: true
kms_key_id:
description:
- Specifies the ARN or alias of the AWS KMS customer master key (CMK) in the
destination region to be used (alias/aws/secretsmanager is assumed if not specified)
type: str
required: false
kms_key_id:
description:
- Specifies the ARN or alias of the AWS KMS customer master key (CMK) to be
Expand Down Expand Up @@ -196,10 +214,13 @@

class Secret(object):
"""An object representation of the Secret described by the self.module args"""
def __init__(self, name, secret_type, secret, resource_policy=None, description="", kms_key_id=None,
tags=None, lambda_arn=None, rotation_interval=None):
def __init__(
self, name, secret_type, secret, resource_policy=None, description="", kms_key_id=None,
tags=None, lambda_arn=None, rotation_interval=None, replica_regions=None,
):
self.name = name
self.description = description
self.replica_regions = replica_regions
self.kms_key_id = kms_key_id
if secret_type == "binary":
self.secret_type = "SecretBinary"
Expand All @@ -223,6 +244,15 @@ def create_args(self):
args["Description"] = self.description
if self.kms_key_id:
args["KmsKeyId"] = self.kms_key_id
if self.replica_regions:
add_replica_regions = []
for replica in self.replica_regions:
if replica["kms_key_id"]:
add_replica_regions.append({'Region': replica["region"],
'KmsKeyId': replica["kms_key_id"]})
else:
add_replica_regions.append({'Region': replica["region"]})
args["AddReplicaRegions"] = add_replica_regions
if self.tags:
args["Tags"] = ansible_dict_to_boto3_tag_list(self.tags)
args[self.secret_type] = self.secret
Expand Down Expand Up @@ -320,6 +350,35 @@ def put_resource_policy(self, secret):
self.module.fail_json_aws(e, msg="Failed to update secret resource policy")
return response

def remove_replication(self, name, regions):
if self.module.check_mode:
self.module.exit_json(changed=True)
try:
replica_regions = []
response = self.client.remove_regions_from_replication(
SecretId=name,
RemoveReplicaRegions=regions)
except (BotoCoreError, ClientError) as e:
self.module.fail_json_aws(e, msg="Failed to replicate secret")
return response

def replicate_secret(self, name, regions):
if self.module.check_mode:
self.module.exit_json(changed=True)
try:
replica_regions = []
for replica in regions:
if replica["kms_key_id"]:
replica_regions.append({'Region': replica["region"], 'KmsKeyId': replica["kms_key_id"]})
else:
replica_regions.append({'Region': replica["region"]})
response = self.client.replicate_secret_to_regions(
SecretId=name,
AddReplicaRegions=replica_regions)
except (BotoCoreError, ClientError) as e:
self.module.fail_json_aws(e, msg="Failed to replicate secret")
return response

def restore_secret(self, name):
if self.module.check_mode:
self.module.exit_json(changed=True)
Expand Down Expand Up @@ -424,12 +483,49 @@ def rotation_match(desired_secret, current_secret):
return True


def compare_regions(desired_secret, current_secret):
"""Compare secrets replication configuration
Args:
desired_secret: camel dict representation of the desired secret state.
current_secret: secret reference as returned by the secretsmanager api.
Returns: bool
"""
regions_to_set_replication = []
regions_to_remove_replication = []

if desired_secret.replica_regions is None:
return regions_to_set_replication, regions_to_remove_replication

if desired_secret.replica_regions:
regions_to_set_replication = desired_secret.replica_regions

for current_secret_region in current_secret.get("ReplicationStatus", []):
if regions_to_set_replication:
for desired_secret_region in regions_to_set_replication:
if current_secret_region["Region"] == desired_secret_region["region"]:
regions_to_set_replication.remove(desired_secret_region)
else:
regions_to_remove_replication.append(current_secret_region["Region"])
else:
regions_to_remove_replication.append(current_secret_region["Region"])

return regions_to_set_replication, regions_to_remove_replication


def main():
replica_args = dict(
region=dict(type='str', required=True),
kms_key_id=dict(type='str', required=False),
)

module = AnsibleAWSModule(
argument_spec={
'name': dict(required=True),
'state': dict(choices=['present', 'absent'], default='present'),
'description': dict(default=""),
'replica': dict(type='list', elements='dict', options=replica_args),
'kms_key_id': dict(),
'secret_type': dict(choices=['binary', 'string'], default="string"),
'secret': dict(default="", no_log=True),
Expand All @@ -454,6 +550,7 @@ def main():
module.params.get('secret_type'),
module.params.get('secret') or module.params.get('json_secret'),
description=module.params.get('description'),
replica_regions=module.params.get('replica'),
kms_key_id=module.params.get('kms_key_id'),
resource_policy=module.params.get('resource_policy'),
tags=module.params.get('tags'),
Expand Down Expand Up @@ -492,6 +589,7 @@ def main():
if not rotation_match(secret, current_secret):
result = secrets_mgr.update_rotation(secret)
changed = True

current_resource_policy_response = secrets_mgr.get_resource_policy(secret.name)
current_resource_policy = current_resource_policy_response.get("ResourcePolicy")
if compare_policies(secret.resource_policy, current_resource_policy):
Expand All @@ -500,6 +598,7 @@ def main():
else:
result = secrets_mgr.put_resource_policy(secret)
changed = True

if module.params.get('tags') is not None:
current_tags = boto3_tag_list_to_ansible_dict(current_secret.get('Tags', []))
tags_to_add, tags_to_remove = compare_aws_tags(current_tags, secret.tags, purge_tags)
Expand All @@ -509,6 +608,15 @@ def main():
if tags_to_remove:
secrets_mgr.untag_secret(secret.name, tags_to_remove)
changed = True

regions_to_set_replication, regions_to_remove_replication = compare_regions(secret, current_secret)
if regions_to_set_replication:
secrets_mgr.replicate_secret(secret.name, regions_to_set_replication)
changed = True
if regions_to_remove_replication:
secrets_mgr.remove_replication(secret.name, regions_to_remove_replication)
changed = True

result = camel_dict_to_snake_dict(secrets_mgr.get_secret(secret.name))
if result.get('tags', None) is not None:
result['tags_dict'] = boto3_tag_list_to_ansible_dict(result.get('tags', []))
Expand Down

0 comments on commit 6f76a88

Please sign in to comment.