Skip to content

Commit 4bf25ea

Browse files
committed
Initial Version.
1 parent f33d427 commit 4bf25ea

File tree

1 file changed

+182
-0
lines changed

1 file changed

+182
-0
lines changed
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
################################################################################
2+
# This program is used to rotate a AWS secret manager secret, and update the
3+
# corresponding FSxN file system with the new secret. It depends on the
4+
# secret having the following tags associted with the secret:
5+
# fsxId: The ID of the FSx file system to update.
6+
# region: The region in which the FSx file system is located.
7+
################################################################################
8+
9+
import boto3
10+
import logging
11+
12+
charactersToExcludeInPassword = '/"\'\\'
13+
14+
################################################################################
15+
# THis function is used to get the value of a tag from a list of tags.
16+
################################################################################
17+
def getTagValue(tags, key):
18+
for tag in tags:
19+
if tag['Key'] == key:
20+
return tag['Value']
21+
return None
22+
23+
################################################################################
24+
# This function is used to create a new version of a Secret Manager secret
25+
# asscoiated with the supplied token. It will first check to see if a secret
26+
# already exists, and if not, it will create a new secret with version stage
27+
# set to AWSPENDING.
28+
################################################################################
29+
def create_secret(secretsClient, arn, token):
30+
global logger
31+
#
32+
# Make sure the current secret exists
33+
secretsClient.get_secret_value(SecretId=arn, VersionStage="AWSCURRENT")
34+
#
35+
# Now try to get the secret version, if that fails, put a new secret
36+
try:
37+
secretsClient.get_secret_value(SecretId=arn, VersionId=token, VersionStage="AWSPENDING")
38+
logger.info(f"create_secret: Secret already exist secret for ARN {arn} with VersionId {token}.")
39+
except secretsClient.exceptions.ResourceNotFoundException:
40+
#
41+
# Generate a random password.
42+
passwd = secretsClient.get_random_password(ExcludeCharacters=charactersToExcludeInPassword, PasswordLength=8, IncludeSpace=False)
43+
#
44+
# Put the secret.
45+
secretsClient.put_secret_value(SecretId=arn, ClientRequestToken=token, SecretString=passwd['RandomPassword'], VersionStages=['AWSPENDING'])
46+
logger.info(f"create_secret: Successfully put secret for ARN {arn} with ClientRequestToken {token} and VersionStage = 'AWSPENDING'.")
47+
48+
################################################################################
49+
# This functino is used to set the password of an FSxN file system based
50+
# on a secret stored in the Secrets Manager pointed to by the supplied secret
51+
# ARN with VersionId equal to token and VersionStage equal to AWSPENDING.
52+
################################################################################
53+
def set_secret(secretsClient, arn, token):
54+
global logger
55+
56+
try:
57+
secretValueResponse = secretsClient.get_secret_value(SecretId=arn, VersionStage="AWSPENDING", VersionId=token)
58+
except ClientError as e:
59+
logger.error(f"Unable to retrieve secret for {arn} in VersionStage = 'AWSPENDING'. Error={e}")
60+
#
61+
# Pass the exception on so the Secret Manager will know that the rotate failed.
62+
raise Exception(e)
63+
64+
password = secretValueResponse['SecretString']
65+
#
66+
# Get the FSx file system ID and region from the secret's tags.
67+
secretMetadata = secretsClient.describe_secret(SecretId=arn)
68+
tags = secretMetadata['Tags']
69+
fsxId = getTagValue(tags, 'fsxId')
70+
fsxRegion = getTagValue(tags, 'region')
71+
if fsxId is None or fsxRegion is None:
72+
message=f"Unable to retrieve the FSx file system ID ('fsxId') or region ('region') tags from secret {arn}."
73+
logger.error(message)
74+
raise Exception(message) # Signal to the Secrets Manager that the rotation failed.
75+
#
76+
# Update the FSx file system with the new password.
77+
fsxClient = boto3.client(service_name='fsx', region_name=fsxRegion)
78+
fsxClient.update_file_system(OntapConfiguration={"FsxAdminPassword": password}, FileSystemId=fsxId)
79+
logger.info(f"Successfully set the FSxN ({fsxId}) password to secret stored in {arn} with a VersionStage = 'AWSPENDING'.")
80+
81+
################################################################################
82+
# Usually this function would be used to test that the service has been updated
83+
# to use the new password. However, since the FSx file system is not accessible
84+
# from this Lambda function, unless it is running within the FSxN's VPC, there
85+
# is no way to test that the password has been set correctly.
86+
################################################################################
87+
def test_secret(secretsClient, arn, token):
88+
global logger
89+
return
90+
91+
################################################################################
92+
# This function is used to finalize the secret rotation process. it does this
93+
# by marking the secret version passed in as the AWSCURRENT secret.
94+
################################################################################
95+
def finish_secret(secretsClient, arn, token):
96+
global logger
97+
#
98+
# First get the current version.
99+
metadata = secretsClient.describe_secret(SecretId=arn)
100+
current_version = None
101+
for version in metadata["VersionIdsToStages"]:
102+
if "AWSCURRENT" in metadata["VersionIdsToStages"][version]:
103+
if version == token:
104+
#
105+
# The new version is already marked as current.
106+
logger.info(f"finishSecret: Version {version} already marked as AWSCURRENT for {arn}")
107+
return
108+
current_version = version
109+
break
110+
#
111+
# Finalize by staging the secret version current
112+
secretsClient.update_secret_version_stage(SecretId=arn, VersionStage="AWSCURRENT", MoveToVersionId=token, RemoveFromVersionId=current_version)
113+
logger.info(f"finishSecret: Successfully set AWSCURRENT stage to version {token} for secret {arn}.")
114+
115+
116+
################################################################################
117+
# This is the main entry point for the Lambda function. It expects the following
118+
# parameters:
119+
# event['SecretId']: The ARN of the secret to rotate.
120+
# event['ClientRequestToken']: The ClientRequestToken associated with the secret version.
121+
# event['Step']: The rotation step (createSecret, setSecret, testSecret, or finishSecret).
122+
#
123+
################################################################################
124+
def lambda_handler(event, context):
125+
global logger
126+
127+
arn = event['SecretId']
128+
token = event['ClientRequestToken']
129+
step = event['Step']
130+
131+
logger = logging.getLogger()
132+
logger.setLevel(logging.INFO)
133+
#
134+
# Set the logging level higher for these noisy modules to mute thier messages.
135+
logging.getLogger("boto3").setLevel(logging.WARNING)
136+
logging.getLogger("botocore").setLevel(logging.WARNING)
137+
138+
logger.info(f'arn={arn}, token={token}, step={step}.')
139+
#
140+
# Create a client to the secrets manager service.
141+
secretsClient = boto3.client('secretsmanager')
142+
#
143+
# Make sure the version is staged correctly.
144+
metadata = secretsClient.describe_secret(SecretId=arn)
145+
if not metadata['RotationEnabled']:
146+
message = f"Secret {arn} is not enabled for rotation."
147+
logger.error(message)
148+
raise Exception(message)
149+
#
150+
# If rotation is enabled, then a version is created with a version-id
151+
# equal to the token.
152+
versions = metadata['VersionIdsToStages']
153+
if token not in versions:
154+
message = f"Secret version {token} has no stage for rotation of secret {arn}."
155+
logger.error(message)
156+
raise Exception(message)
157+
#
158+
# Now check that the version hasn't already been promoted to AWSCURRENT and if not
159+
# that a AWSPENDING staging exist.
160+
if "AWSCURRENT" in versions[token]:
161+
logger.info(f"Secret version {token} already set as AWSCURRENT for secret {arn}.")
162+
return
163+
elif "AWSPENDING" not in versions[token]:
164+
message = f"Secret version {token} not set as AWSPENDING for rotation of secret {arn}."
165+
logger.error(message)
166+
raise Exception(message)
167+
#
168+
# At this point we are ready to process the request.
169+
if step == "createSecret":
170+
create_secret(secretsClient, arn, token)
171+
172+
elif step == "setSecret":
173+
set_secret(secretsClient, arn, token)
174+
175+
elif step == "testSecret":
176+
test_secret(secretsClient, arn, token)
177+
178+
elif step == "finishSecret":
179+
finish_secret(secretsClient, arn, token)
180+
181+
else:
182+
raise ValueError(f"Invalid step parameter '{step}'.")

0 commit comments

Comments
 (0)