Guest User

Untitled

a guest
Dec 14th, 2018
122
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 11.16 KB | None | 0 0
  1. import boto3
  2. import json
  3. import logging
  4. import os
  5.  
  6. logger = logging.getLogger()
  7. logger.setLevel(logging.INFO)
  8.  
  9.  
  10. def lambda_handler(event, context):
  11. """Secrets Manager Rotation Template
  12.  
  13. This is a template for creating an AWS Secrets Manager rotation lambda
  14.  
  15. Args:
  16. event (dict): Lambda dictionary of event parameters. These keys must include the following:
  17. - SecretId: The secret ARN or identifier
  18. - ClientRequestToken: The ClientRequestToken of the secret version
  19. - Step: The rotation step (one of createSecret, setSecret, testSecret, or finishSecret)
  20.  
  21. context (LambdaContext): The Lambda runtime information
  22.  
  23. Raises:
  24. ResourceNotFoundException: If the secret with the specified arn and stage does not exist
  25.  
  26. ValueError: If the secret is not properly configured for rotation
  27.  
  28. KeyError: If the event parameters do not contain the expected keys
  29.  
  30. """
  31. arn = event['SecretId']
  32. token = event['ClientRequestToken']
  33. step = event['Step']
  34.  
  35. # Setup the client
  36. service_client = boto3.client('secretsmanager', endpoint_url=os.environ['SECRETS_MANAGER_ENDPOINT'])
  37.  
  38. # Make sure the version is staged correctly
  39. metadata = service_client.describe_secret(SecretId=arn)
  40. if not metadata['RotationEnabled']:
  41. logger.error("Secret %s is not enabled for rotation" % arn)
  42. raise ValueError("Secret %s is not enabled for rotation" % arn)
  43. versions = metadata['VersionIdsToStages']
  44. if token not in versions:
  45. logger.error("Secret version %s has no stage for rotation of secret %s." % (token, arn))
  46. raise ValueError("Secret version %s has no stage for rotation of secret %s." % (token, arn))
  47. if "AWSCURRENT" in versions[token]:
  48. logger.info("Secret version %s already set as AWSCURRENT for secret %s." % (token, arn))
  49. return
  50. elif "AWSPENDING" not in versions[token]:
  51. logger.error("Secret version %s not set as AWSPENDING for rotation of secret %s." % (token, arn))
  52. raise ValueError("Secret version %s not set as AWSPENDING for rotation of secret %s." % (token, arn))
  53.  
  54. if step == "createSecret":
  55. create_secret(service_client, arn, token)
  56.  
  57. elif step == "setSecret":
  58. set_secret(service_client, arn, token)
  59.  
  60. elif step == "testSecret":
  61. test_secret(service_client, arn, token)
  62.  
  63. elif step == "finishSecret":
  64. finish_secret(service_client, arn, token)
  65.  
  66. else:
  67. raise ValueError("Invalid step parameter")
  68.  
  69.  
  70. def create_secret(service_client, arn, token):
  71. """Create the secret
  72.  
  73. This method first checks for the existence of a secret for the passed in token. If one does not exist, it will generate a
  74. new secret and put it with the passed in token.
  75.  
  76. Args:
  77. service_client (client): The secrets manager service client
  78.  
  79. arn (string): The secret ARN or other identifier
  80.  
  81. token (string): The ClientRequestToken associated with the secret version
  82.  
  83. Raises:
  84. ResourceNotFoundException: If the secret with the specified arn and stage does not exist
  85.  
  86. """
  87. # Make sure the current secret exists
  88. current_dict = get_secret_dict(service_client, arn, "AWSCURRENT")
  89.  
  90. # Now try to get the secret version, if that fails, put a new secret
  91. try:
  92. get_secret_dict(service_client, arn, "AWSPENDING")
  93. logger.info("createSecret: Successfully retrieved secret for %s." % arn)
  94. except service_client.exceptions.ResourceNotFoundException:
  95. iam_user = current_dict['smtpUser']
  96. iam = boto3.client('iam')
  97.  
  98. # Remove the oldest key if there are multiple
  99. logger.debug("Retrieving keys for user %s" % iam_user)
  100. keys = iam.list_access_keys(UserName=iam_user)['AccessKeyMetadata']
  101. logger.debug("Found %s key(s)" % len(keys))
  102.  
  103. if len(keys) > 1:
  104. oldest_key = keys[0]
  105. for key in keys:
  106. if key['CreateDate'] < oldest_key['CreateDate']:
  107. logger.debug("%s is older than %s" % (key, oldest_key))
  108. oldest_key = key
  109. key_id_to_delete = oldest_key['AccessKeyId']
  110. key_date_to_delete = oldest_key['CreateDate']
  111. iam.delete_access_key(UserName=iam_user, AccessKeyId=key_id_to_delete)
  112. logger.info("Deleted old key %s from %s" % (key_id_to_delete, key_date_to_delete))
  113.  
  114. # Generate a new access key
  115. logger.info("createSecret: Creating new access key for %s." % iam_user)
  116. newKey = iam.create_access_key(UserName=iam_user)['AccessKey']
  117. current_dict['smtpAccessKeyId'] = newKey['AccessKeyId']
  118. current_dict['smtpSecretKey'] = newKey['SecretAccessKey']
  119. current_dict['smtpUsername'] = newKey['AccessKeyId']
  120. current_dict['smtpPassword'] = generate_ses_password(newKey['SecretAccessKey'])
  121.  
  122. # Put the secret
  123. service_client.put_secret_value(SecretId=arn, ClientRequestToken=token, SecretString=json.dumps(current_dict), VersionStages=['AWSPENDING'])
  124. logger.info("createSecret: Successfully put secret for ARN %s and version %s." % (arn, token))
  125.  
  126. def generate_ses_password(secret_key, charset='utf-8'):
  127. import hmac #required to compute the HMAC key
  128. import hashlib #required to create a SHA256 hash
  129. import base64 #required to encode the computed key
  130.  
  131. # These variables are used when calculating the SMTP password. You shouldn't
  132. # change them.
  133. message = 'SendRawEmail'
  134. version = '\x02'
  135.  
  136. # Compute an HMAC-SHA256 key from the AWS secret access key.
  137. signatureInBytes = hmac.new(secret_key.encode(charset),message.encode(charset),hashlib.sha256).digest()
  138. # Prepend the version number to the signature.
  139. signatureAndVersion = version.encode(charset) + signatureInBytes
  140. # Base64-encode the string that contains the version number and signature.
  141. smtpPassword = base64.b64encode(signatureAndVersion)
  142. # Decode the string and return it
  143. return smtpPassword.decode(charset)
  144.  
  145. def set_secret(service_client, arn, token):
  146. """Set the secret
  147.  
  148. This method should set the AWSPENDING secret in the service that the secret belongs to. For example, if the secret is a database
  149. credential, this method should take the value of the AWSPENDING secret and set the user's password to this value in the database.
  150.  
  151. Args:
  152. service_client (client): The secrets manager service client
  153.  
  154. arn (string): The secret ARN or other identifier
  155.  
  156. token (string): The ClientRequestToken associated with the secret version
  157.  
  158. """
  159. # This is where the secret should be set in the service
  160. # NB This is not applicable for access keys, since they are created and set in one operation
  161.  
  162. def test_secret(service_client, arn, token):
  163. """Test the secret
  164.  
  165. This method should validate that the AWSPENDING secret works in the service that the secret belongs to.
  166.  
  167. Args:
  168. service_client (client): The secrets manager service client
  169.  
  170. arn (string): The secret ARN or other identifier
  171.  
  172. token (string): The ClientRequestToken associated with the secret version
  173.  
  174. """
  175. # This is where the secret should be tested against the service
  176. pending_dict = get_secret_dict(service_client, arn, "AWSPENDING")
  177. iam_user = pending_dict['smtpUser']
  178. logger.info("Checking that the pending key is valid for user %s" % iam_user)
  179. iam = boto3.client('iam')
  180. logger.debug("Retrieving keys for user %s" % iam_user)
  181. keys = iam.list_access_keys(UserName=iam_user)['AccessKeyMetadata']
  182. logger.debug("Found %s key(s)" % len(keys))
  183. pending_key = pending_dict['smtpAccessKeyId']
  184. for key in keys:
  185. if key["AccessKeyId"] == pending_key:
  186. logger.info("Pending access key %s was found on the user" % pending_key)
  187. return
  188. raise ValueError("Unable to find pending access key on user %s" % iam_user)
  189.  
  190. def finish_secret(service_client, arn, token):
  191. """Finish the secret
  192.  
  193. This method finalizes the rotation process by marking the secret version passed in as the AWSCURRENT secret.
  194.  
  195. Args:
  196. service_client (client): The secrets manager service client
  197.  
  198. arn (string): The secret ARN or other identifier
  199.  
  200. token (string): The ClientRequestToken associated with the secret version
  201.  
  202. Raises:
  203. ResourceNotFoundException: If the secret with the specified arn does not exist
  204.  
  205. """
  206. # First describe the secret to get the current version
  207. metadata = service_client.describe_secret(SecretId=arn)
  208. current_version = None
  209. for version in metadata["VersionIdsToStages"]:
  210. if "AWSCURRENT" in metadata["VersionIdsToStages"][version]:
  211. if version == token:
  212. # The correct version is already marked as current, return
  213. logger.info("finishSecret: Version %s already marked as AWSCURRENT for %s" % (version, arn))
  214. publish_update(service_client, arn)
  215. return
  216. current_version = version
  217. break
  218.  
  219. # Finalize by staging the secret version current
  220. service_client.update_secret_version_stage(SecretId=arn, VersionStage="AWSCURRENT", MoveToVersionId=token, RemoveFromVersionId=current_version)
  221. logger.info("finishSecret: Successfully set AWSCURRENT stage to version %s for secret %s." % (version, arn))
  222.  
  223. publish_update(service_client, arn)
  224.  
  225. def publish_update(service_client, arn):
  226. logger.info("Notifying consumers that the secret has updated")
  227.  
  228. current_dict = get_secret_dict(service_client, arn, "AWSCURRENT")
  229. topic_arn = current_dict['snsTopic']
  230. if not topic_arn:
  231. logger.warn("No topic ARN provided; unable to notify")
  232. return
  233. sns = boto3.client('sns')
  234. result = sns.publish(TopicArn=topic_arn, Message='Updated %s' % arn)
  235. logger.info("Notification sent as message %s" % result['MessageId'])
  236.  
  237. def get_secret_dict(service_client, arn, stage, token=None):
  238. """Gets the secret dictionary corresponding for the secret arn, stage, and token
  239.  
  240. This helper function gets credentials for the arn and stage passed in and returns the dictionary by parsing the JSON string
  241.  
  242. Args:
  243. service_client (client): The secrets manager service client
  244.  
  245. arn (string): The secret ARN or other identifier
  246.  
  247. token (string): The ClientRequestToken associated with the secret version, or None if no validation is desired
  248.  
  249. stage (string): The stage identifying the secret version
  250.  
  251. Returns:
  252. SecretDictionary: Secret dictionary
  253.  
  254. Raises:
  255. ResourceNotFoundException: If the secret with the specified arn and stage does not exist
  256.  
  257. ValueError: If the secret is not valid JSON
  258.  
  259. """
  260. required_fields = ['smtpUser', 'smtpAccessKeyId', 'smtpSecretKey']
  261.  
  262. # Only do VersionId validation against the stage if a token is passed in
  263. if token:
  264. secret = service_client.get_secret_value(SecretId=arn, VersionId=token, VersionStage=stage)
  265. else:
  266. secret = service_client.get_secret_value(SecretId=arn, VersionStage=stage)
  267. plaintext = secret['SecretString']
  268. secret_dict = json.loads(plaintext)
  269.  
  270. # Run validations against the secret
  271. for field in required_fields:
  272. if field not in secret_dict:
  273. raise KeyError("%s key is missing from secret JSON" % field)
  274.  
  275. # Parse and return the secret JSON string
  276. return secret_dict
Add Comment
Please, Sign In to add comment