diff --git a/SecretsManagerMongoDBRotationMultiUser/lambda_function.py b/SecretsManagerMongoDBRotationMultiUser/lambda_function.py index 91bfc6f4..441d3afe 100644 --- a/SecretsManagerMongoDBRotationMultiUser/lambda_function.py +++ b/SecretsManagerMongoDBRotationMultiUser/lambda_function.py @@ -154,6 +154,7 @@ def set_secret(service_client, arn, token): """ # First try to login with the pending secret, if it succeeds, return + current_dict = get_secret_dict(service_client, arn, "AWSCURRENT") pending_dict = get_secret_dict(service_client, arn, "AWSPENDING", token) conn = get_connection(pending_dict) if conn: @@ -161,9 +162,18 @@ def set_secret(service_client, arn, token): logger.info("setSecret: AWSPENDING secret is already set as password in MongoDB for secret arn %s." % arn) return + # Make sure the user from current and pending match + if get_alt_username(current_dict['username']) != pending_dict['username']: + logger.error("setSecret: Attempting to modify user %s other than current user or clone %s" % (pending_dict['username'], current_dict['username'])) + raise ValueError("Attempting to modify user %s other than current user or clone %s" % (pending_dict['username'], current_dict['username'])) + + # Make sure the host from current and pending match + if current_dict['host'] != pending_dict['host']: + logger.error("setSecret: Attempting to modify user for host %s other than current host %s" % (pending_dict['host'], current_dict['host'])) + raise ValueError("Attempting to modify user for host %s other than current host %s" % (pending_dict['host'], current_dict['host'])) + # Before we do anything with the secret, make sure the AWSCURRENT secret is valid by logging in to the db # This ensures that the credential we are rotating is valid to protect against a confused deputy attack - current_dict = get_secret_dict(service_client, arn, "AWSCURRENT") conn = get_connection(current_dict) if not conn: logger.error("setSecret: Unable to log into database using current credentials for secret %s" % arn) @@ -174,7 +184,8 @@ def set_secret(service_client, arn, token): master_arn = current_dict['masterarn'] master_dict = get_secret_dict(service_client, master_arn, "AWSCURRENT") if current_dict['host'] != master_dict['host']: - logger.warn("setSecret: Master database host %s is not the same host as current %s" % (master_dict['host'], current_dict['host'])) + logger.error("setSecret: Current database host %s is not the same host as master %s" % (current_dict['host'], master_dict['host'])) + raise ValueError("Current database host %s is not the same host as master %s" % (current_dict['host'], master_dict['host'])) # Now log into the database with the master credentials conn = get_connection(master_dict) @@ -191,6 +202,9 @@ def set_secret(service_client, arn, token): user_info = conn.command('usersInfo', current_dict['username']) conn.command("createUser", pending_dict['username'], pwd=pending_dict['password'], roles=user_info['users'][0]['roles']) logger.info("setSecret: Successfully set password for %s in MongoDB for secret arn %s." % (pending_dict['username'], arn)) + except errors.PyMongoError: + logger.error("setSecret: Error encountered when attempting to set password in database for user %s", pending_dict['username']) + raise ValueError("Error encountered when attempting to set password in database for user %s", pending_dict['username']) finally: conn.logout() @@ -288,9 +302,9 @@ def get_connection(secret_dict): ssl = False if 'ssl' in secret_dict: if type(secret_dict['ssl']) is bool: - ssl = secret_dict['ssl'] - else: - ssl = (secret_dict['ssl'].lower() == "true") + ssl = secret_dict['ssl'] + else: + ssl = (secret_dict['ssl'].lower() == "true") # Try to obtain a connection to the db try: diff --git a/SecretsManagerMongoDBRotationSingleUser/lambda_function.py b/SecretsManagerMongoDBRotationSingleUser/lambda_function.py index c05f5199..4133f7e5 100644 --- a/SecretsManagerMongoDBRotationSingleUser/lambda_function.py +++ b/SecretsManagerMongoDBRotationSingleUser/lambda_function.py @@ -146,22 +146,43 @@ def set_secret(service_client, arn, token): KeyError: If the secret json does not contain the expected keys """ - # First try to login with the pending secret, if it succeeds, return + try: + previous_dict = get_secret_dict(service_client, arn, "AWSPREVIOUS") + except (service_client.exceptions.ResourceNotFoundException, KeyError): + previous_dict = None + current_dict = get_secret_dict(service_client, arn, "AWSCURRENT") pending_dict = get_secret_dict(service_client, arn, "AWSPENDING", token) + + # First try to login with the pending secret, if it succeeds, return conn = get_connection(pending_dict) if conn: conn.logout() logger.info("setSecret: AWSPENDING secret is already set as password in MongoDB for secret arn %s." % arn) return + # Make sure the user from current and pending match + if current_dict['username'] != pending_dict['username']: + logger.error("setSecret: Attempting to modify user %s other than current user %s" % (pending_dict['username'], current_dict['username'])) + raise ValueError("Attempting to modify user %s other than current user %s" % (pending_dict['username'], current_dict['username'])) + + # Make sure the host from current and pending match + if current_dict['host'] != pending_dict['host']: + logger.error("setSecret: Attempting to modify user for host %s other than current host %s" % (pending_dict['host'], current_dict['host'])) + raise ValueError("Attempting to modify user for host %s other than current host %s" % (pending_dict['host'], current_dict['host'])) + # Now try the current password - conn = get_connection(get_secret_dict(service_client, arn, "AWSCURRENT")) - if not conn: + conn = get_connection(current_dict) + if not conn and previous_dict: # If both current and pending do not work, try previous - try: - conn = get_connection(get_secret_dict(service_client, arn, "AWSPREVIOUS")) - except service_client.exceptions.ResourceNotFoundException: - conn = None + conn = get_connection(previous_dict) + + # Make sure the user/host from previous and pending match + if previous_dict['username'] != pending_dict['username']: + logger.error("setSecret: Attempting to modify user %s other than previous valid user %s" % (pending_dict['username'], previous_dict['username'])) + raise ValueError("Attempting to modify user %s other than previous valid user %s" % (pending_dict['username'], previous_dict['username'])) + if previous_dict['host'] != pending_dict['host']: + logger.error("setSecret: Attempting to modify user for host %s other than previous host %s" % (pending_dict['host'], previous_dict['host'])) + raise ValueError("Attempting to modify user for host %s other than previous host %s" % (pending_dict['host'], previous_dict['host'])) # If we still don't have a connection, raise a ValueError if not conn: @@ -172,6 +193,9 @@ def set_secret(service_client, arn, token): try: conn.command("updateUser", pending_dict['username'], pwd=pending_dict['password']) logger.info("setSecret: Successfully set password for user %s in MongoDB for secret arn %s." % (pending_dict['username'], arn)) + except errors.PyMongoError: + logger.error("setSecret: Error encountered when attempting to set password in database for user %s", pending_dict['username']) + raise ValueError("Error encountered when attempting to set password in database for user %s", pending_dict['username']) finally: conn.logout() @@ -267,10 +291,10 @@ def get_connection(secret_dict): ssl = False if 'ssl' in secret_dict: if type(secret_dict['ssl']) is bool: - ssl = secret_dict['ssl'] - else: - ssl = (secret_dict['ssl'].lower() == "true") - + ssl = secret_dict['ssl'] + else: + ssl = (secret_dict['ssl'].lower() == "true") + # Try to obtain a connection to the db try: client = MongoClient(host=secret_dict['host'], port=port, connectTimeoutMS=5000, serverSelectionTimeoutMS=5000, ssl=ssl) diff --git a/SecretsManagerRDSMariaDBRotationMultiUser/lambda_function.py b/SecretsManagerRDSMariaDBRotationMultiUser/lambda_function.py index 8e7e2263..bd056dd0 100644 --- a/SecretsManagerRDSMariaDBRotationMultiUser/lambda_function.py +++ b/SecretsManagerRDSMariaDBRotationMultiUser/lambda_function.py @@ -152,17 +152,28 @@ def set_secret(service_client, arn, token): KeyError: If the secret json does not contain the expected keys """ - # First try to login with the pending secret, if it succeeds, return + current_dict = get_secret_dict(service_client, arn, "AWSCURRENT") pending_dict = get_secret_dict(service_client, arn, "AWSPENDING", token) + + # First try to login with the pending secret, if it succeeds, return conn = get_connection(pending_dict) if conn: conn.close() logger.info("setSecret: AWSPENDING secret is already set as password in MariaDB DB for secret arn %s." % arn) return + # Make sure the user from current and pending match + if get_alt_username(current_dict['username']) != pending_dict['username']: + logger.error("setSecret: Attempting to modify user %s other than current user or clone %s" % (pending_dict['username'], current_dict['username'])) + raise ValueError("Attempting to modify user %s other than current user or clone %s" % (pending_dict['username'], current_dict['username'])) + + # Make sure the host from current and pending match + if current_dict['host'] != pending_dict['host']: + logger.error("setSecret: Attempting to modify user for host %s other than current host %s" % (pending_dict['host'], current_dict['host'])) + raise ValueError("Attempting to modify user for host %s other than current host %s" % (pending_dict['host'], current_dict['host'])) + # Before we do anything with the secret, make sure the AWSCURRENT secret is valid by logging in to the db # This ensures that the credential we are rotating is valid to protect against a confused deputy attack - current_dict = get_secret_dict(service_client, arn, "AWSCURRENT") conn = get_connection(current_dict) if not conn: logger.error("setSecret: Unable to log into database using current credentials for secret %s" % arn) @@ -172,8 +183,10 @@ def set_secret(service_client, arn, token): # Now get the master arn from the current secret master_arn = current_dict['masterarn'] master_dict = get_secret_dict(service_client, master_arn, "AWSCURRENT") - if current_dict['host'] != master_dict['host']: - logger.warn("setSecret: Master database host %s is not the same host as current %s" % (master_dict['host'], current_dict['host'])) + if current_dict['host'] != master_dict['host'] and not is_rds_replica_database(current_dict, master_dict): + # If current dict is a replica of the master dict, can proceed + logger.error("setSecret: Current database host %s is not the same host as/rds replica of master %s" % (current_dict['host'], master_dict['host'])) + raise ValueError("Current database host %s is not the same host as/rds replica of master %s" % (current_dict['host'], master_dict['host'])) # Now log into the database with the master credentials conn = get_connection(master_dict) @@ -189,9 +202,8 @@ def set_secret(service_client, arn, token): cur.execute("SHOW GRANTS FOR %s", current_dict['username']) for row in cur.fetchall(): grant = row[0].split(' TO ') - new_grant = "%s TO %s" % (grant[0], pending_dict['username']) - new_grant_escaped = new_grant.replace('%','%%') # % is a special character in Python format strings. - cur.execute(new_grant_escaped + " IDENTIFIED BY %s", pending_dict['password']) + new_grant_escaped = grant[0].replace('%','%%') # % is a special character in Python format strings. + cur.execute(new_grant_escaped + " TO %s IDENTIFIED BY %s", (pending_dict['username'], pending_dict['password'])) conn.commit() logger.info("setSecret: Successfully set password for %s in MariaDB DB for secret arn %s." % (pending_dict['username'], arn)) finally: @@ -365,3 +377,42 @@ def get_alt_username(current_username): if len(new_username) > 80: raise ValueError("Unable to clone user, username length with _clone appended would exceed 80 characters") return new_username + +def is_rds_replica_database(replica_dict, master_dict): + """Validates that the database of a secret is a replica of the database of the master secret + + This helper function validates that the database of a secret is a replica of the database of the master secret. + + Args: + replica_dict (dictionary): The secret dictionary containing the replica database + + primary_dict (dictionary): The secret dictionary containing the primary database + + Returns: + isReplica : whether or not the database is a replica + + Raises: + ValueError: If the new username length would exceed the maximum allowed + """ + # Setup the client + rds_client = boto3.client('rds') + + # Get instance identifiers from endpoints + replica_instance_id = replica_dict['host'].split(".")[0] + master_instance_id = master_dict['host'].split(".")[0] + + try: + describe_response = rds_client.describe_db_instances(DBInstanceIdentifier=replica_instance_id) + except Exception as err: + logger.warn("Encountered error while verifying rds replica status: %s" % err) + return False + instances = describe_response['DBInstances'] + + # Host from current secret cannot be found + if not instances: + logger.info("Cannot verify replica status - no RDS instance found with identifier: %s" % replica_instance_id) + return False + + # DB Instance identifiers are unique - can only be one result + current_instance = instances[0] + return master_instance_id == current_instance.get('ReadReplicaSourceDBInstanceIdentifier') diff --git a/SecretsManagerRDSMariaDBRotationSingleUser/lambda_function.py b/SecretsManagerRDSMariaDBRotationSingleUser/lambda_function.py index c59f3861..a3cd8499 100644 --- a/SecretsManagerRDSMariaDBRotationSingleUser/lambda_function.py +++ b/SecretsManagerRDSMariaDBRotationSingleUser/lambda_function.py @@ -145,22 +145,43 @@ def set_secret(service_client, arn, token): KeyError: If the secret json does not contain the expected keys """ - # First try to login with the pending secret, if it succeeds, return + try: + previous_dict = get_secret_dict(service_client, arn, "AWSPREVIOUS") + except (service_client.exceptions.ResourceNotFoundException, KeyError): + previous_dict = None + current_dict = get_secret_dict(service_client, arn, "AWSCURRENT") pending_dict = get_secret_dict(service_client, arn, "AWSPENDING", token) + + # First try to login with the pending secret, if it succeeds, return conn = get_connection(pending_dict) if conn: conn.close() logger.info("setSecret: AWSPENDING secret is already set as password in MariaDB DB for secret arn %s." % arn) return + # Make sure the user from current and pending match + if current_dict['username'] != pending_dict['username']: + logger.error("setSecret: Attempting to modify user %s other than current user %s" % (pending_dict['username'], current_dict['username'])) + raise ValueError("Attempting to modify user %s other than current user %s" % (pending_dict['username'], current_dict['username'])) + + # Make sure the host from current and pending match + if current_dict['host'] != pending_dict['host']: + logger.error("setSecret: Attempting to modify user for host %s other than current host %s" % (pending_dict['host'], current_dict['host'])) + raise ValueError("Attempting to modify user for host %s other than current host %s" % (pending_dict['host'], current_dict['host'])) + # Now try the current password - conn = get_connection(get_secret_dict(service_client, arn, "AWSCURRENT")) - if not conn: + conn = get_connection(current_dict) + if not conn and previous_dict: # If both current and pending do not work, try previous - try: - conn = get_connection(get_secret_dict(service_client, arn, "AWSPREVIOUS")) - except service_client.exceptions.ResourceNotFoundException: - conn = None + conn = get_connection(previous_dict) + + # Make sure the user and host from previous and pending match + if previous_dict['username'] != pending_dict['username']: + logger.error("setSecret: Attempting to modify user %s other than last valid user %s" % (pending_dict['username'], previous_dict['username'])) + raise ValueError("Attempting to modify user %s other than last valid user %s" % (pending_dict['username'], previous_dict['username'])) + if previous_dict['host'] != pending_dict['host']: + logger.error("setSecret: Attempting to modify user for host %s other than previous host %s" % (pending_dict['host'], previous_dict['host'])) + raise ValueError("Attempting to modify user for host %s other than previous host %s" % (pending_dict['host'], previous_dict['host'])) # If we still don't have a connection, raise a ValueError if not conn: diff --git a/SecretsManagerRDSMySQLRotationMultiUser/lambda_function.py b/SecretsManagerRDSMySQLRotationMultiUser/lambda_function.py index a661816f..0b07e491 100644 --- a/SecretsManagerRDSMySQLRotationMultiUser/lambda_function.py +++ b/SecretsManagerRDSMySQLRotationMultiUser/lambda_function.py @@ -152,17 +152,28 @@ def set_secret(service_client, arn, token): KeyError: If the secret json does not contain the expected keys """ - # First try to login with the pending secret, if it succeeds, return + current_dict = get_secret_dict(service_client, arn, "AWSCURRENT") pending_dict = get_secret_dict(service_client, arn, "AWSPENDING", token) + + # First try to login with the pending secret, if it succeeds, return conn = get_connection(pending_dict) if conn: conn.close() logger.info("setSecret: AWSPENDING secret is already set as password in MySQL DB for secret arn %s." % arn) return + # Make sure the user from current and pending match + if get_alt_username(current_dict['username']) != pending_dict['username']: + logger.error("setSecret: Attempting to modify user %s other than current user or clone %s" % (pending_dict['username'], current_dict['username'])) + raise ValueError("Attempting to modify user %s other than current user or clone %s" % (pending_dict['username'], current_dict['username'])) + + # Make sure the host from current and pending match + if current_dict['host'] != pending_dict['host']: + logger.error("setSecret: Attempting to modify user for host %s other than current host %s" % (pending_dict['host'], current_dict['host'])) + raise ValueError("Attempting to modify user for host %s other than current host %s" % (pending_dict['host'], current_dict['host'])) + # Before we do anything with the secret, make sure the AWSCURRENT secret is valid by logging in to the db # This ensures that the credential we are rotating is valid to protect against a confused deputy attack - current_dict = get_secret_dict(service_client, arn, "AWSCURRENT") conn = get_connection(current_dict) if not conn: logger.error("setSecret: Unable to log into database using current credentials for secret %s" % arn) @@ -172,8 +183,10 @@ def set_secret(service_client, arn, token): # Now get the master arn from the current secret master_arn = current_dict['masterarn'] master_dict = get_secret_dict(service_client, master_arn, "AWSCURRENT") - if current_dict['host'] != master_dict['host']: - logger.warn("setSecret: Master database host %s is not the same host as current %s" % (master_dict['host'], current_dict['host'])) + if current_dict['host'] != master_dict['host'] and not is_rds_replica_database(current_dict, master_dict): + # If current dict is a replica of the master dict, can proceed + logger.error("setSecret: Current database host %s is not the same host as/rds replica of master %s" % (current_dict['host'], master_dict['host'])) + raise ValueError("Current database host %s is not the same host as/rds replica of master %s" % (current_dict['host'], master_dict['host'])) # Now log into the database with the master credentials conn = get_connection(master_dict) @@ -193,9 +206,8 @@ def set_secret(service_client, arn, token): cur.execute("SHOW GRANTS FOR %s", current_dict['username']) for row in cur.fetchall(): grant = row[0].split(' TO ') - new_grant = "%s TO '%s'" % (grant[0], pending_dict['username']) - new_grant_escaped = new_grant.replace('%','%%') # % is a special character in Python format strings. - cur.execute(new_grant_escaped) + new_grant_escaped = grant[0].replace('%','%%') # % is a special character in Python format strings. + cur.execute(new_grant_escaped + " TO %s", (pending_dict['username'],) ) # Set the password for the user and commit cur.execute("SELECT VERSION()") @@ -394,3 +406,42 @@ def get_password_option(version): return "%s" else: return "PASSWORD(%s)" + +def is_rds_replica_database(replica_dict, master_dict): + """Validates that the database of a secret is a replica of the database of the master secret + + This helper function validates that the database of a secret is a replica of the database of the master secret. + + Args: + replica_dict (dictionary): The secret dictionary containing the replica database + + primary_dict (dictionary): The secret dictionary containing the primary database + + Returns: + isReplica : whether or not the database is a replica + + Raises: + ValueError: If the new username length would exceed the maximum allowed + """ + # Setup the client + rds_client = boto3.client('rds') + + # Get instance identifiers from endpoints + replica_instance_id = replica_dict['host'].split(".")[0] + master_instance_id = master_dict['host'].split(".")[0] + + try: + describe_response = rds_client.describe_db_instances(DBInstanceIdentifier=replica_instance_id) + except Exception as err: + logger.warn("Encountered error while verifying rds replica status: %s" % err) + return False + instances = describe_response['DBInstances'] + + # Host from current secret cannot be found + if not instances: + logger.info("Cannot verify replica status - no RDS instance found with identifier: %s" % replica_instance_id) + return False + + # DB Instance identifiers are unique - can only be one result + current_instance = instances[0] + return master_instance_id == current_instance.get('ReadReplicaSourceDBInstanceIdentifier') diff --git a/SecretsManagerRDSMySQLRotationSingleUser/lambda_function.py b/SecretsManagerRDSMySQLRotationSingleUser/lambda_function.py index 46456af0..00de0777 100644 --- a/SecretsManagerRDSMySQLRotationSingleUser/lambda_function.py +++ b/SecretsManagerRDSMySQLRotationSingleUser/lambda_function.py @@ -145,22 +145,43 @@ def set_secret(service_client, arn, token): KeyError: If the secret json does not contain the expected keys """ - # First try to login with the pending secret, if it succeeds, return + try: + previous_dict = get_secret_dict(service_client, arn, "AWSPREVIOUS") + except (service_client.exceptions.ResourceNotFoundException, KeyError): + previous_dict = None + current_dict = get_secret_dict(service_client, arn, "AWSCURRENT") pending_dict = get_secret_dict(service_client, arn, "AWSPENDING", token) + + # First try to login with the pending secret, if it succeeds, return conn = get_connection(pending_dict) if conn: conn.close() logger.info("setSecret: AWSPENDING secret is already set as password in MySQL DB for secret arn %s." % arn) return + # Make sure the user from current and pending match + if current_dict['username'] != pending_dict['username']: + logger.error("setSecret: Attempting to modify user %s other than current user %s" % (pending_dict['username'], current_dict['username'])) + raise ValueError("Attempting to modify user %s other than current user %s" % (pending_dict['username'], current_dict['username'])) + + # Make sure the host from current and pending match + if current_dict['host'] != pending_dict['host']: + logger.error("setSecret: Attempting to modify user for host %s other than current host %s" % (pending_dict['host'], current_dict['host'])) + raise ValueError("Attempting to modify user for host %s other than current host %s" % (pending_dict['host'], current_dict['host'])) + # Now try the current password - conn = get_connection(get_secret_dict(service_client, arn, "AWSCURRENT")) - if not conn: + conn = get_connection(current_dict) + if not conn and previous_dict: # If both current and pending do not work, try previous - try: - conn = get_connection(get_secret_dict(service_client, arn, "AWSPREVIOUS")) - except service_client.exceptions.ResourceNotFoundException: - conn = None + conn = get_connection(previous_dict) + + # Make sure the user/host from previous and pending match + if previous_dict['username'] != pending_dict['username']: + logger.error("setSecret: Attempting to modify user %s other than previous valid user %s" % (pending_dict['username'], previous_dict['username'])) + raise ValueError("Attempting to modify user %s other than previous valid user %s" % (pending_dict['username'], previous_dict['username'])) + if previous_dict['host'] != pending_dict['host']: + logger.error("setSecret: Attempting to modify user for host %s other than previous host %s" % (pending_dict['host'], previous_dict['host'])) + raise ValueError("Attempting to modify user for host %s other than previous host %s" % (pending_dict['host'], previous_dict['host'])) # If we still don't have a connection, raise a ValueError if not conn: diff --git a/SecretsManagerRDSOracleRotationMultiUser/lambda_function.py b/SecretsManagerRDSOracleRotationMultiUser/lambda_function.py index a204c968..4f4565e0 100644 --- a/SecretsManagerRDSOracleRotationMultiUser/lambda_function.py +++ b/SecretsManagerRDSOracleRotationMultiUser/lambda_function.py @@ -152,17 +152,28 @@ def set_secret(service_client, arn, token): KeyError: If the secret json does not contain the expected keys """ - # First try to login with the pending secret, if it succeeds, return + current_dict = get_secret_dict(service_client, arn, "AWSCURRENT") pending_dict = get_secret_dict(service_client, arn, "AWSPENDING", token) + + # First try to login with the pending secret, if it succeeds, return conn = get_connection(pending_dict) if conn: conn.close() logger.info("setSecret: AWSPENDING secret is already set as password in Oracle DB for secret arn %s." % arn) return + # Make sure the user from current and pending match + if get_alt_username(current_dict['username']) != pending_dict['username']: + logger.error("setSecret: Attempting to modify user %s other than current user or clone %s" % (pending_dict['username'], current_dict['username'])) + raise ValueError("Attempting to modify user %s other than current user or clone %s" % (pending_dict['username'], current_dict['username'])) + + # Make sure the user from current and pending match + if current_dict['host'] != pending_dict['host']: + logger.error("setSecret: Attempting to modify user for host %s other than current host %s" % (pending_dict['host'], current_dict['host'])) + raise ValueError("Attempting to modify user for host %s other than current host %s" % (pending_dict['host'], current_dict['host'])) + # Before we do anything with the secret, make sure the AWSCURRENT secret is valid by logging in to the db # This ensures that the credential we are rotating is valid to protect against a confused deputy attack - current_dict = get_secret_dict(service_client, arn, "AWSCURRENT") conn = get_connection(current_dict) if not conn: logger.error("setSecret: Unable to log into database using current credentials for secret %s" % arn) @@ -172,8 +183,10 @@ def set_secret(service_client, arn, token): # Now get the master arn from the current secret master_arn = current_dict['masterarn'] master_dict = get_secret_dict(service_client, master_arn, "AWSCURRENT") - if current_dict['host'] != master_dict['host']: - logger.warn("setSecret: Master database host %s is not the same host as current %s" % (master_dict['host'], current_dict['host'])) + if current_dict['host'] != master_dict['host'] and not is_rds_replica_database(current_dict, master_dict): + # If current dict is a replica of the master dict, can proceed + logger.error("setSecret: Current database host %s is not the same host as/rds replica of master %s" % (current_dict['host'], master_dict['host'])) + raise ValueError("Current database host %s is not the same host as/rds replica of master %s" % (current_dict['host'], master_dict['host'])) # Now log into the database with the master credentials conn = get_connection(master_dict) @@ -184,21 +197,32 @@ def set_secret(service_client, arn, token): # Now set the password to the pending password cur = conn.cursor() + # Escape username via DBMS ENQUOTE_NAME + cur.execute("SELECT sys.DBMS_ASSERT.ENQUOTE_NAME(:username) FROM DUAL", username=pending_dict['username']) + escaped_username = cur.fetchone()[0] + + # Escape current username via DBMS ENQUOTE_NAME + cur.execute("SELECT sys.DBMS_ASSERT.ENQUOTE_NAME(:username) FROM DUAL", username=current_dict['username']) + escaped_current = cur.fetchone()[0] + + # Passwords cannot have double quotes in Oracle, remove any double quotes to allow the password to be properly escaped + pending_password = pending_dict['password'].replace("\"","") + # Check to see if the user already exists - cur.execute("SELECT USERNAME FROM DBA_USERS WHERE USERNAME='%s'" % pending_dict['username']) + cur.execute("SELECT USERNAME FROM DBA_USERS WHERE USERNAME=:username", username=pending_dict['username'].upper()) results = cur.fetchall() if len(results) > 0: # If user exists, just change their password - cur.execute("ALTER USER %s IDENTIFIED BY \"%s\"" % (pending_dict['username'], pending_dict['password'])) + cur.execute("ALTER USER %s IDENTIFIED BY \"%s\"" % (escaped_username, pending_password)) else: # If user does not exist, create the user with appropriate grants - cur.execute("CREATE USER %s IDENTIFIED BY \"%s\"" % (pending_dict['username'], pending_dict['password'])) + cur.execute("CREATE USER %s IDENTIFIED BY \"%s\"" % (escaped_username, pending_password)) for grant_type in ['ROLE_GRANT', 'SYSTEM_GRANT', 'OBJECT_GRANT']: try: - cur.execute("SELECT DBMS_METADATA.GET_GRANTED_DDL('%s', '%s') FROM DUAL" % (grant_type, current_dict['username'].upper())) + cur.execute("SELECT DBMS_METADATA.GET_GRANTED_DDL(:grant_type, :username) FROM DUAL", grant_type=grant_type, username=current_dict['username'].upper()) results = cur.fetchall() for row in results: - sql = row[0].read().strip(' \n\t').replace("\"%s\"" % current_dict['username'].upper(), "\"%s\"" % pending_dict['username']) + sql = row[0].read().strip(' \n\t').replace("%s" % escaped_current, "%s" % escaped_username) cur.execute(sql) except cx_Oracle.DatabaseError: # If we were unable to find any grants skip this type @@ -298,7 +322,9 @@ def get_connection(secret_dict): # Try to obtain a connection to the db try: - conn = cx_Oracle.connect(secret_dict['username'] + '/' + secret_dict['password'] + '@' + secret_dict['host'] + ':' + port + '/' + secret_dict['dbname']) + conn = cx_Oracle.connect(secret_dict['username'], + secret_dict['password'], + secret_dict['host'] + ':' + port + '/' + secret_dict['dbname']) return conn except (cx_Oracle.DatabaseError, cx_Oracle.OperationalError) : return None @@ -371,3 +397,42 @@ def get_alt_username(current_username): if len(new_username) > 30: raise ValueError("Unable to clone user, username length with _CLONE appended would exceed 30 characters") return new_username.upper() + +def is_rds_replica_database(replica_dict, master_dict): + """Validates that the database of a secret is a replica of the database of the master secret + + This helper function validates that the database of a secret is a replica of the database of the master secret. + + Args: + replica_dict (dictionary): The secret dictionary containing the replica database + + primary_dict (dictionary): The secret dictionary containing the primary database + + Returns: + isReplica : whether or not the database is a replica + + Raises: + ValueError: If the new username length would exceed the maximum allowed + """ + # Setup the client + rds_client = boto3.client('rds') + + # Get instance identifiers from endpoints + replica_instance_id = replica_dict['host'].split(".")[0] + master_instance_id = master_dict['host'].split(".")[0] + + try: + describe_response = rds_client.describe_db_instances(DBInstanceIdentifier=replica_instance_id) + except Exception as err: + logger.warn("Encountered error while verifying rds replica status: %s" % err) + return False + instances = describe_response['DBInstances'] + + # Host from current secret cannot be found + if not instances: + logger.info("Cannot verify replica status - no RDS instance found with identifier: %s" % replica_instance_id) + return False + + # DB Instance identifiers are unique - can only be one result + current_instance = instances[0] + return master_instance_id == current_instance.get('ReadReplicaSourceDBInstanceIdentifier') \ No newline at end of file diff --git a/SecretsManagerRDSOracleRotationSingleUser/lambda_function.py b/SecretsManagerRDSOracleRotationSingleUser/lambda_function.py index 35a2caf0..9b830c04 100644 --- a/SecretsManagerRDSOracleRotationSingleUser/lambda_function.py +++ b/SecretsManagerRDSOracleRotationSingleUser/lambda_function.py @@ -145,31 +145,60 @@ def set_secret(service_client, arn, token): KeyError: If the secret json does not contain the expected keys """ - # First try to login with the pending secret, if it succeeds, return + try: + previous_dict = get_secret_dict(service_client, arn, "AWSPREVIOUS") + except (service_client.exceptions.ResourceNotFoundException, KeyError): + previous_dict = None + current_dict = get_secret_dict(service_client, arn, "AWSCURRENT") pending_dict = get_secret_dict(service_client, arn, "AWSPENDING", token) + + # First try to login with the pending secret, if it succeeds, return conn = get_connection(pending_dict) if conn: conn.close() logger.info("setSecret: AWSPENDING secret is already set as password in Oracle DB for secret arn %s." % arn) return + # Make sure the user from current and pending match + if current_dict['username'] != pending_dict['username']: + logger.error("setSecret: Attempting to modify user %s other than current user %s" % (pending_dict['username'], current_dict['username'])) + raise ValueError("Attempting to modify user %s other than current user %s" % (pending_dict['username'], current_dict['username'])) + + # Make sure the host from current and pending match + if current_dict['host'] != pending_dict['host']: + logger.error("setSecret: Attempting to modify user for host %s other than current host %s" % (pending_dict['host'], current_dict['host'])) + raise ValueError("Attempting to modify user for host %s other than current host %s" % (pending_dict['host'], current_dict['host'])) + # Now try the current password - conn = get_connection(get_secret_dict(service_client, arn, "AWSCURRENT")) - if not conn: + conn = get_connection(current_dict) + if not conn and previous_dict: # If both current and pending do not work, try previous - try: - conn = get_connection(get_secret_dict(service_client, arn, "AWSPREVIOUS")) - except service_client.exceptions.ResourceNotFoundException: - conn = None + conn = get_connection(pending_dict) + + # Make sure the user/host from previous and pending match + if previous_dict['username'] != pending_dict['username']: + logger.error("setSecret: Attempting to modify user %s other than previous valid user %s" % (pending_dict['username'], previous_dict['username'])) + raise ValueError("Attempting to modify user %s other than previous valid user %s" % (pending_dict['username'], previous_dict['username'])) + if previous_dict['host'] != pending_dict['host']: + logger.error("setSecret: Attempting to modify user for host %s other than previous host %s" % (pending_dict['host'], previous_dict['host'])) + raise ValueError("Attempting to modify user for host %s other than previous host %s" % (pending_dict['host'], previous_dict['host'])) # If we still don't have a connection, raise a ValueError if not conn: logger.error("setSecret: Unable to log into database with previous, current, or pending secret of secret arn %s" % arn) raise ValueError("Unable to log into database with previous, current, or pending secret of secret arn %s" % arn) - # Now set the password to the pending password cur = conn.cursor() - sql="ALTER USER %s IDENTIFIED BY \"%s\"" % (pending_dict['username'], pending_dict['password']) + + # Escape username via DBMS ENQUOTE_NAME + cur.execute("SELECT sys.DBMS_ASSERT.enquote_name(:username) FROM DUAL", username=pending_dict['username']) + escaped_username = cur.fetchone()[0] + + # Passwords cannot have double quotes in Oracle, remove any double quotes to allow the password to be properly escaped + pending_password = pending_dict['password'].replace("\"","") + + # Now set the password to the pending password + sql="ALTER USER %s IDENTIFIED BY \"%s\"" % (escaped_username, pending_dict['password']) cur.execute(sql) conn.commit() logger.info("setSecret: Successfully set password for user %s in Oracle DB for secret arn %s." % (pending_dict['username'], arn)) @@ -263,7 +292,9 @@ def get_connection(secret_dict): # Try to obtain a connection to the db try: - conn = cx_Oracle.connect(secret_dict['username'] + '/' + secret_dict['password'] + '@' + secret_dict['host'] + ':' + port + '/' + secret_dict['dbname']) + conn = cx_Oracle.connect(secret_dict['username'], + secret_dict['password'], + secret_dict['host'] + ':' + port + '/' + secret_dict['dbname']) return conn except (cx_Oracle.DatabaseError, cx_Oracle.OperationalError) : return None diff --git a/SecretsManagerRDSPostgreSQLRotationMultiUser/lambda_function.py b/SecretsManagerRDSPostgreSQLRotationMultiUser/lambda_function.py index 7fd5b2bd..71c3cfe8 100644 --- a/SecretsManagerRDSPostgreSQLRotationMultiUser/lambda_function.py +++ b/SecretsManagerRDSPostgreSQLRotationMultiUser/lambda_function.py @@ -154,16 +154,27 @@ def set_secret(service_client, arn, token): """ # First try to login with the pending secret, if it succeeds, return + current_dict = get_secret_dict(service_client, arn, "AWSCURRENT") pending_dict = get_secret_dict(service_client, arn, "AWSPENDING", token) + conn = get_connection(pending_dict) if conn: conn.close() logger.info("setSecret: AWSPENDING secret is already set as password in PostgreSQL DB for secret arn %s." % arn) return + # Make sure the user from current and pending match + if get_alt_username(current_dict['username']) != pending_dict['username']: + logger.error("setSecret: Attempting to modify user %s other than current user clone %s" % (pending_dict['username'], get_alt_username(current_dict['username']))) + raise ValueError("Attempting to modify user %s other than current user clone %s" % (pending_dict['username'], get_alt_username(current_dict['username']))) + + # Make sure the host from current and pending match + if current_dict['host'] != pending_dict['host']: + logger.error("setSecret: Attempting to modify user for host %s other than current host %s" % (pending_dict['host'], current_dict['host'])) + raise ValueError("Attempting to modify user for host %s other than current host %s" % (pending_dict['host'], current_dict['host'])) + # Before we do anything with the secret, make sure the AWSCURRENT secret is valid by logging in to the db # This ensures that the credential we are rotating is valid to protect against a confused deputy attack - current_dict = get_secret_dict(service_client, arn, "AWSCURRENT") conn = get_connection(current_dict) if not conn: logger.error("setSecret: Unable to log into database using current credentials for secret %s" % arn) @@ -173,8 +184,10 @@ def set_secret(service_client, arn, token): # Now get the master arn from the current secret master_arn = current_dict['masterarn'] master_dict = get_secret_dict(service_client, master_arn, "AWSCURRENT") - if current_dict['host'] != master_dict['host']: - logger.warn("setSecret: Master database host %s is not the same host as current %s" % (master_dict['host'], current_dict['host'])) + if current_dict['host'] != master_dict['host'] and not is_rds_replica_database(current_dict, master_dict): + # If current dict is a replica of the master dict, can proceed + logger.error("setSecret: Current database host %s is not the same host as/rds replica of master %s" % (current_dict['host'], master_dict['host'])) + raise ValueError("Current database host %s is not the same host as/rds replica of master %s" % (current_dict['host'], master_dict['host'])) # Now log into the database with the master credentials conn = get_connection(master_dict) @@ -185,15 +198,21 @@ def set_secret(service_client, arn, token): # Now set the password to the pending password try: with conn.cursor() as cur: + # Get escaped usernames via quote_ident + cur.execute("SELECT quote_ident(%s)", (pending_dict['username'],)) + pending_username = cur.fetchone()[0] + cur.execute("SELECT quote_ident(%s)", (current_dict['username'],)) + current_username = cur.fetchone()[0] + # Check if the user exists, if not create it and grant it all permissions from the current role # If the user exists, just update the password cur.execute("SELECT 1 FROM pg_roles where rolname = %s", (pending_dict['username'],)) if len(cur.fetchall()) == 0: - create_role = "CREATE ROLE \"%s\"" % pending_dict['username'] + create_role = "CREATE ROLE %s" % pending_username cur.execute(create_role + " WITH LOGIN PASSWORD %s", (pending_dict['password'],)) - cur.execute("GRANT \"%s\" TO \"%s\"" % (current_dict['username'], pending_dict['username'])) + cur.execute("GRANT %s TO %s" % (current_username, pending_username)) else: - alter_role = "ALTER USER \"%s\"" % pending_dict['username'] + alter_role = "ALTER USER %s" % pending_username cur.execute(alter_role + " WITH PASSWORD %s", (pending_dict['password'],)) conn.commit() @@ -372,3 +391,42 @@ def get_alt_username(current_username): if len(new_username) > 63: raise ValueError("Unable to clone user, username length with _clone appended would exceed 63 characters") return new_username + +def is_rds_replica_database(replica_dict, master_dict): + """Validates that the database of a secret is a replica of the database of the master secret + + This helper function validates that the database of a secret is a replica of the database of the master secret. + + Args: + replica_dict (dictionary): The secret dictionary containing the replica database + + primary_dict (dictionary): The secret dictionary containing the primary database + + Returns: + isReplica : whether or not the database is a replica + + Raises: + ValueError: If the new username length would exceed the maximum allowed + """ + # Setup the client + rds_client = boto3.client('rds') + + # Get instance identifiers from endpoints + replica_instance_id = replica_dict['host'].split(".")[0] + master_instance_id = master_dict['host'].split(".")[0] + + try: + describe_response = rds_client.describe_db_instances(DBInstanceIdentifier=replica_instance_id) + except Exception as err: + logger.warn("Encountered error while verifying rds replica status: %s" % err) + return False + instances = describe_response['DBInstances'] + + # Host from current secret cannot be found + if not instances: + logger.info("Cannot verify replica status - no RDS instance found with identifier: %s" % replica_instance_id) + return False + + # DB Instance identifiers are unique - can only be one result + current_instance = instances[0] + return master_instance_id == current_instance.get('ReadReplicaSourceDBInstanceIdentifier') \ No newline at end of file diff --git a/SecretsManagerRDSPostgreSQLRotationSingleUser/lambda_function.py b/SecretsManagerRDSPostgreSQLRotationSingleUser/lambda_function.py index c23beb6a..db66ff3b 100644 --- a/SecretsManagerRDSPostgreSQLRotationSingleUser/lambda_function.py +++ b/SecretsManagerRDSPostgreSQLRotationSingleUser/lambda_function.py @@ -146,22 +146,44 @@ def set_secret(service_client, arn, token): KeyError: If the secret json does not contain the expected keys """ - # First try to login with the pending secret, if it succeeds, return + try: + previous_dict = get_secret_dict(service_client, arn, "AWSPREVIOUS") + except (service_client.exceptions.ResourceNotFoundException, KeyError): + previous_dict = None + current_dict = get_secret_dict(service_client, arn, "AWSCURRENT") pending_dict = get_secret_dict(service_client, arn, "AWSPENDING", token) + + # First try to login with the pending secret, if it succeeds, return conn = get_connection(pending_dict) if conn: conn.close() logger.info("setSecret: AWSPENDING secret is already set as password in PostgreSQL DB for secret arn %s." % arn) return + # Make sure the user from current and pending match + if current_dict['username'] != pending_dict['username']: + logger.error("setSecret: Attempting to modify user %s other than current user %s" % (pending_dict['username'], current_dict['username'])) + raise ValueError("Attempting to modify user %s other than current user %s" % (pending_dict['username'], current_dict['username'])) + + # Make sure the host from current and pending match + if current_dict['host'] != pending_dict['host']: + logger.error("setSecret: Attempting to modify user for host %s other than current host %s" % (pending_dict['host'], current_dict['host'])) + raise ValueError("Attempting to modify user for host %s other than current host %s" % (pending_dict['host'], current_dict['host'])) + # Now try the current password - conn = get_connection(get_secret_dict(service_client, arn, "AWSCURRENT")) + conn = get_connection(current_dict) if not conn: - # If both current and pending do not work, try previous - try: - conn = get_connection(get_secret_dict(service_client, arn, "AWSPREVIOUS")) - except service_client.exceptions.ResourceNotFoundException: - conn = None + if previous_dict: + # If both current and pending do not work, try previous + conn = get_connection(previous_dict) + + # Make sure the user/host from previous and pending match + if previous_dict['username'] != pending_dict['username']: + logger.error("setSecret: Attempting to modify user %s other than previous valid user %s" % (pending_dict['username'], previous_dict['username'])) + raise ValueError("Attempting to modify user %s other than previous valid user %s" % (pending_dict['username'], previous_dict['username'])) + if previous_dict['host'] != pending_dict['host']: + logger.error("setSecret: Attempting to modify user for host %s other than previous valid host %s" % (pending_dict['host'], previous_dict['host'])) + raise ValueError("Attempting to modify user for host %s other than current previous valid %s" % (pending_dict['host'], previous_dict['host'])) # If we still don't have a connection, raise a ValueError if not conn: @@ -171,7 +193,11 @@ def set_secret(service_client, arn, token): # Now set the password to the pending password try: with conn.cursor() as cur: - alter_role = "ALTER USER \"%s\"" % pending_dict['username'] + # Get escaped username via quote_ident + cur.execute("SELECT quote_ident(%s)", (pending_dict['username'],)) + escaped_username = cur.fetchone()[0] + + alter_role = "ALTER USER %s" % escaped_username cur.execute(alter_role + " WITH PASSWORD %s", (pending_dict['password'],)) conn.commit() logger.info("setSecret: Successfully set password for user %s in PostgreSQL DB for secret arn %s." % (pending_dict['username'], arn)) diff --git a/SecretsManagerRDSSQLServerRotationMultiUser/lambda_function.py b/SecretsManagerRDSSQLServerRotationMultiUser/lambda_function.py index 46ee6997..b02199b8 100644 --- a/SecretsManagerRDSSQLServerRotationMultiUser/lambda_function.py +++ b/SecretsManagerRDSSQLServerRotationMultiUser/lambda_function.py @@ -152,17 +152,28 @@ def set_secret(service_client, arn, token): KeyError: If the secret json does not contain the expected keys """ - # First try to login with the pending secret, if it succeeds, return + current_dict = get_secret_dict(service_client, arn, "AWSCURRENT") pending_dict = get_secret_dict(service_client, arn, "AWSPENDING", token) + + # First try to login with the pending secret, if it succeeds, return conn = get_connection(pending_dict) if conn: conn.close() logger.info("setSecret: AWSPENDING secret is already set as password in SQL Server DB for secret arn %s." % arn) return + # Make sure the user from current and pending match + if get_alt_username(current_dict['username']) != pending_dict['username']: + logger.error("setSecret: Attempting to modify user %s other than current user or clone %s" % (pending_dict['username'], current_dict['username'])) + raise ValueError("Attempting to modify user %s other than current user or clone %s" % (pending_dict['username'], current_dict['username'])) + + # Make sure the host from current and pending match + if current_dict['host'] != pending_dict['host']: + logger.error("setSecret: Attempting to modify user for host %s other than current host %s" % (pending_dict['host'], current_dict['host'])) + raise ValueError("Attempting to modify user for host %s other than current host %s" % (pending_dict['host'], current_dict['host'])) + # Before we do anything with the secret, make sure the AWSCURRENT secret is valid by logging in to the db # This ensures that the credential we are rotating is valid to protect against a confused deputy attack - current_dict = get_secret_dict(service_client, arn, "AWSCURRENT") conn = get_connection(current_dict) if not conn: logger.error("setSecret: Unable to log into database using current credentials for secret %s" % arn) @@ -172,8 +183,10 @@ def set_secret(service_client, arn, token): # Now get the master arn from the current secret master_arn = current_dict['masterarn'] master_dict = get_secret_dict(service_client, master_arn, "AWSCURRENT") - if current_dict['host'] != master_dict['host']: - logger.warn("setSecret: Master database host %s is not the same host as current %s" % (master_dict['host'], current_dict['host'])) + if current_dict['host'] != master_dict['host'] and not is_rds_replica_database(current_dict, master_dict): + # If current dict is a replica of the master dict, can proceed + logger.error("setSecret: Current database host %s is not the same host as/rds replica of master %s" % (current_dict['host'], master_dict['host'])) + raise ValueError("Current database host %s is not the same host as/rds replica of master %s" % (current_dict['host'], master_dict['host'])) # Now log into the database with the master credentials conn = get_connection(master_dict) @@ -403,12 +416,16 @@ def set_password_for_login(cursor, current_db, current_login, pending_dict): pymssql.OperationalError: If there are any errors running the SQL statements """ + # Get escaped pending user via QUOTENAME + cursor.execute("SELECT QUOTENAME(%s) AS QUOTENAME", (pending_dict['username'],)) + escaped_pending_username = cursor.fetchone()['QUOTENAME'] + # Check if the login exists, if not create it and grant it all permissions from the current user # If the user exists, just update the password cursor.execute("SELECT name FROM sys.server_principals WHERE name = %s", pending_dict['username']) if len(cursor.fetchall()) == 0: # Create the new login - create_login = "CREATE LOGIN %s" % pending_dict['username'] + create_login = "CREATE LOGIN %s" % escaped_pending_username cursor.execute(create_login + " WITH PASSWORD = %s", pending_dict['password']) # Only handle server level permissions if we are connected the the master DB @@ -416,13 +433,13 @@ def set_password_for_login(cursor, current_db, current_login, pending_dict): # Loop through the types of server permissions and grant them to the new login query = "SELECT state_desc, permission_name FROM sys.server_permissions perm "\ "JOIN sys.server_principals prin ON perm.grantee_principal_id = prin.principal_id "\ - "WHERE prin.name = '%s'" % current_login - cursor.execute(query) + "WHERE prin.name = %s" + cursor.execute(query, current_login) for row in cursor.fetchall(): if row['state_desc'] == 'GRANT_WITH_GRANT_OPTION': - cursor.execute("GRANT %s TO %s WITH GRANT OPTION" % (row['permission_name'], pending_dict['username'])) + cursor.execute("GRANT %s TO %s WITH GRANT OPTION" % (row['permission_name'], escaped_pending_username)) else: - cursor.execute("%s %s TO %s" % (row['state_desc'], row['permission_name'], pending_dict['username'])) + cursor.execute("%s %s TO %s" % (row['state_desc'], row['permission_name'], escaped_pending_username)) # We do not create user objects in the master database else: @@ -434,12 +451,15 @@ def set_password_for_login(cursor, current_db, current_login, pending_dict): # Check if the user exists. If not, create it cursor.execute("SELECT name FROM sys.database_principals WHERE name = %s", alt_user) if len(cursor.fetchall()) == 0: - cursor.execute("CREATE USER %s FOR LOGIN %s" % (alt_user, pending_dict['username'])) + # Get escaped alt user via QUOTENAME + cursor.execute("SELECT QUOTENAME(%s) AS QUOTENAME", (alt_user,)) + escaped_alt_username = cursor.fetchone()['QUOTENAME'] + cursor.execute("CREATE USER %s FOR LOGIN %s" % (escaped_alt_username, escaped_pending_username)) - apply_database_permissions(cursor, cur_user, pending_dict['username']) + apply_database_permissions(cursor, cur_user, escaped_pending_username) else: - alter_stmt = "ALTER LOGIN %s" % pending_dict['username'] + alter_stmt = "ALTER LOGIN %s" % escaped_pending_username cursor.execute(alter_stmt + " WITH PASSWORD = %s", pending_dict['password']) @@ -459,17 +479,21 @@ def set_password_for_user(cursor, current_user, pending_dict): pymssql.OperationalError: If there are any errors running the SQL statements """ + # Get escaped pending user via QUOTENAME + cursor.execute("SELECT QUOTENAME(%s) AS QUOTENAME", (pending_dict['username'],)) + escaped_pending_username = cursor.fetchone()['QUOTENAME'] + # Check if the user exists, if not create it and grant it all permissions from the current user # If the user exists, just update the password cursor.execute("SELECT name FROM sys.database_principals WHERE name = %s", pending_dict['username']) if len(cursor.fetchall()) == 0: # Create the new user - create_login = "CREATE USER %s" % pending_dict['username'] + create_login = "CREATE USER %s" % escaped_pending_username cursor.execute(create_login + " WITH PASSWORD = %s", pending_dict['password']) - apply_database_permissions(cursor, current_user, pending_dict['username']) + apply_database_permissions(cursor, current_user, escaped_pending_username) else: - alter_stmt = "ALTER USER %s" % pending_dict['username'] + alter_stmt = "ALTER USER %s" % escaped_pending_username cursor.execute(alter_stmt + " WITH PASSWORD = %s", pending_dict['password']) @@ -495,8 +519,8 @@ def apply_database_permissions(cursor, current_user, pending_user): query = "SELECT roleprin.name FROM sys.database_role_members rolemems "\ "JOIN sys.database_principals roleprin ON roleprin.principal_id = rolemems.role_principal_id "\ "JOIN sys.database_principals userprin ON userprin.principal_id = rolemems.member_principal_id "\ - "WHERE userprin.name = '%s'" % current_user - cursor.execute(query) + "WHERE userprin.name = %s" + cursor.execute(query, current_user) for row in cursor.fetchall(): sql_stmt = "ALTER ROLE %s ADD MEMBER %s" % (row['name'], pending_user) @@ -549,8 +573,8 @@ def apply_database_permissions(cursor, current_user, pending_user): "LEFT JOIN sys.symmetric_keys symkey ON symkey.symmetric_key_id = perm.major_id "\ "LEFT JOIN sys.certificates cert ON cert.certificate_id = perm.major_id "\ "LEFT JOIN sys.asymmetric_keys asymkey ON asymkey.asymmetric_key_id = perm.major_id "\ - "WHERE prin.name = '%s'" % current_user - cursor.execute(query) + "WHERE prin.name = %s" + cursor.execute(query, current_user) for row in cursor.fetchall(): # Determine which type of permission this is and create the sql statement accordingly if row['class'] == 0: # Database permission @@ -605,3 +629,42 @@ def apply_database_permissions(cursor, current_user, pending_user): # Execute the sql cursor.execute(sql_stmt) + +def is_rds_replica_database(replica_dict, master_dict): + """Validates that the database of a secret is a replica of the database of the master secret + + This helper function validates that the database of a secret is a replica of the database of the master secret. + + Args: + replica_dict (dictionary): The secret dictionary containing the replica database + + primary_dict (dictionary): The secret dictionary containing the primary database + + Returns: + isReplica : whether or not the database is a replica + + Raises: + ValueError: If the new username length would exceed the maximum allowed + """ + # Setup the client + rds_client = boto3.client('rds') + + # Get instance identifiers from endpoints + replica_instance_id = replica_dict['host'].split(".")[0] + master_instance_id = master_dict['host'].split(".")[0] + + try: + describe_response = rds_client.describe_db_instances(DBInstanceIdentifier=replica_instance_id) + except Exception as err: + logger.warn("Encountered error while verifying rds replica status: %s" % err) + return False + instances = describe_response['DBInstances'] + + # Host from current secret cannot be found + if not instances: + logger.info("Cannot verify replica status - no RDS instance found with identifier: %s" % replica_instance_id) + return False + + # DB Instance identifiers are unique - can only be one result + current_instance = instances[0] + return master_instance_id == current_instance.get('ReadReplicaSourceDBInstanceIdentifier') \ No newline at end of file diff --git a/SecretsManagerRDSSQLServerRotationSingleUser/lambda_function.py b/SecretsManagerRDSSQLServerRotationSingleUser/lambda_function.py index b1c801d9..b5e458f4 100644 --- a/SecretsManagerRDSSQLServerRotationSingleUser/lambda_function.py +++ b/SecretsManagerRDSSQLServerRotationSingleUser/lambda_function.py @@ -145,24 +145,43 @@ def set_secret(service_client, arn, token): KeyError: If the secret json does not contain the expected keys """ - # First try to login with the pending secret, if it succeeds, return + try: + previous_dict = get_secret_dict(service_client, arn, "AWSPREVIOUS") + except (service_client.exceptions.ResourceNotFoundException, KeyError): + previous_dict = None + current_dict = get_secret_dict(service_client, arn, "AWSCURRENT") pending_dict = get_secret_dict(service_client, arn, "AWSPENDING", token) + + # First try to login with the pending secret, if it succeeds, return conn = get_connection(pending_dict) if conn: conn.close() logger.info("setSecret: AWSPENDING secret is already set as password in SQL Server DB for secret arn %s." % arn) return + # Make sure the user from current and pending match + if current_dict['username'] != pending_dict['username']: + logger.error("setSecret: Attempting to modify user %s other than current user %s" % (pending_dict['username'], current_dict['username'])) + raise ValueError("Attempting to modify user %s other than current user %s" % (pending_dict['username'], current_dict['username'])) + + # Make sure the host from current and pending match + if current_dict['host'] != pending_dict['host']: + logger.error("setSecret: Attempting to modify user for host %s other than current host %s" % (pending_dict['host'], current_dict['host'])) + raise ValueError("Attempting to modify user for host %s other than current host %s" % (pending_dict['host'], current_dict['host'])) + # Now try the current password - current_dict = get_secret_dict(service_client, arn, "AWSCURRENT") conn = get_connection(current_dict) - if not conn: + if not conn and previous_dict: # If both current and pending do not work, try previous - try: - current_dict = get_secret_dict(service_client, arn, "AWSPREVIOUS") - conn = get_connection(current_dict) - except service_client.exceptions.ResourceNotFoundException: - conn = None + conn = get_connection(previous_dict) + + # Make sure the user/host from previous and pending match + if previous_dict['username'] != pending_dict['username']: + logger.error("setSecret: Attempting to modify user %s other than previous valid user %s" % (pending_dict['username'], previous_dict['username'])) + raise ValueError("Attempting to modify user %s other than previous valid user %s" % (pending_dict['username'], previous_dict['username'])) + if previous_dict['host'] != pending_dict['host']: + logger.error("setSecret: Attempting to modify user for host %s other than previous host %s" % (pending_dict['host'], previous_dict['host'])) + raise ValueError("Attempting to modify user for host %s other than previous host %s" % (pending_dict['host'], previous_dict['host'])) # If we still don't have a connection, raise a ValueError if not conn: @@ -172,6 +191,10 @@ def set_secret(service_client, arn, token): # Now set the password to the pending password try: with conn.cursor() as cursor: + # Get escaped username via QUOTENAME + cursor.execute("SELECT QUOTENAME(%s) AS QUOTENAME", (current_dict['username'],)) + escaped_username = cursor.fetchone()['QUOTENAME'] + # Get the current version and db cursor.execute("SELECT @@VERSION AS version") version = cursor.fetchall()[0]['version'] @@ -186,10 +209,10 @@ def set_secret(service_client, arn, token): # Set the user or login password (depending on database containment) if containment == 0: - alter_stmt = "ALTER LOGIN %s" % pending_dict['username'] + alter_stmt = "ALTER LOGIN %s" % escaped_username cursor.execute(alter_stmt + " WITH PASSWORD = %s OLD_PASSWORD = %s", (pending_dict['password'], current_dict['password'])) else: - alter_stmt = "ALTER USER %s" % pending_dict['username'] + alter_stmt = "ALTER USER %s" % escaped_username cursor.execute(alter_stmt + " WITH PASSWORD = %s OLD_PASSWORD = %s", (pending_dict['password'], current_dict['password'])) conn.commit() diff --git a/SecretsManagerRedshiftRotationMultiUser/lambda_function.py b/SecretsManagerRedshiftRotationMultiUser/lambda_function.py index ef2452e6..083ce4a3 100644 --- a/SecretsManagerRedshiftRotationMultiUser/lambda_function.py +++ b/SecretsManagerRedshiftRotationMultiUser/lambda_function.py @@ -153,17 +153,28 @@ def set_secret(service_client, arn, token): KeyError: If the secret json does not contain the expected keys """ - # First try to login with the pending secret, if it succeeds, return + current_dict = get_secret_dict(service_client, arn, "AWSCURRENT") pending_dict = get_secret_dict(service_client, arn, "AWSPENDING", token) + + # First try to login with the pending secret, if it succeeds, return conn = get_connection(pending_dict) if conn: conn.close() logger.info("setSecret: AWSPENDING secret is already set as password in Redshift DB for secret arn %s." % arn) return + # Make sure the user from current and pending match + if get_alt_username(current_dict['username']) != pending_dict['username']: + logger.error("setSecret: Attempting to modify user %s other than current user or clone %s" % (pending_dict['username'], get_alt_username(current_dict['username']))) + raise ValueError("Attempting to modify user %s other than current user or clone %s" % (pending_dict['username'], get_alt_username(current_dict['username']))) + + # Make sure the host from current and pending match + if current_dict['host'] != pending_dict['host']: + logger.error("setSecret: Attempting to modify user for host %s other than current host %s" % (pending_dict['host'], current_dict['host'])) + raise ValueError("Attempting to modify user for host %s other than current host %s" % (pending_dict['host'], current_dict['host'])) + # Before we do anything with the secret, make sure the AWSCURRENT secret is valid by logging in to the db # This ensures that the credential we are rotating is valid to protect against a confused deputy attack - current_dict = get_secret_dict(service_client, arn, "AWSCURRENT") conn = get_connection(current_dict) if not conn: logger.error("setSecret: Unable to log into database using current credentials for secret %s" % arn) @@ -174,7 +185,8 @@ def set_secret(service_client, arn, token): master_arn = current_dict['masterarn'] master_dict = get_secret_dict(service_client, master_arn, "AWSCURRENT") if current_dict['host'] != master_dict['host']: - logger.warn("setSecret: Master database host %s is not the same host as current %s" % (master_dict['host'], current_dict['host'])) + logger.error("setSecret: Current database host %s is not the same host as master %s" % (current_dict['host'], master_dict['host'])) + raise ValueError("Current database host %s is not the same host as master %s" % (current_dict['host'], master_dict['host'])) # Now log into the database with the master credentials conn = get_connection(master_dict) @@ -185,39 +197,44 @@ def set_secret(service_client, arn, token): # Now set the password to the pending password try: with conn.cursor() as cur: + # Get escaped usernames via quote_ident + cur.execute("SELECT quote_ident(%s)", (pending_dict['username'],)) + pending_username = cur.fetchone()[0] + # Check if the user exists, if not create it and grant it all permissions from the current role # If the user exists, just update the password cur.execute("SELECT usename FROM pg_user where usename = %s", (pending_dict['username'],)) if len(cur.fetchall()) == 0: - create_role = "CREATE USER %s" % pending_dict['username'] + create_role = "CREATE USER %s" % pending_username cur.execute(create_role + " WITH PASSWORD %s", (pending_dict['password'],)) # Grant the database permissions db_perm_types = ['CREATE', 'TEMPORARY', 'TEMP'] for perm in db_perm_types: - cur.execute("SELECT dat.datname FROM pg_database dat WHERE HAS_DATABASE_PRIVILEGE(%s, dat.datname , %s)", (current_dict['username'], perm)) + cur.execute("SELECT QUOTE_IDENT(dat.datname) as datname FROM pg_database dat WHERE HAS_DATABASE_PRIVILEGE(%s, dat.datname , %s)", (current_dict['username'], perm)) databases = [row.datname for row in cur.fetchall()] if databases: - cur.execute("GRANT %s ON DATABASE %s TO %s" % (perm, ','.join(databases), pending_dict['username'])) + cur.execute("GRANT %s ON DATABASE %s TO %s" % (perm, ','.join(databases), pending_username)) # Grant table permissions table_perm_types = ['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'REFERENCES'] for perm in table_perm_types: - cur.execute("SELECT tab.schemaname, tab.tablename FROM pg_tables tab WHERE HAS_TABLE_PRIVILEGE(%s, tab.schemaname + '.' + tab.tablename , %s) "\ - "AND tab.schemaname NOT IN ('pg_internal')", (current_dict['username'], perm)) + cur.execute("SELECT QUOTE_IDENT(tab.schemaname) as schemaname, QUOTE_IDENT(tab.tablename) as tablename FROM pg_tables tab WHERE "\ + "HAS_TABLE_PRIVILEGE(%s, QUOTE_IDENT(tab.schemaname) + '.' + QUOTE_IDENT(tab.tablename) , %s) AND tab.schemaname NOT IN ('pg_internal')", + (current_dict['username'], perm)) tables = [row.schemaname + '.' + row.tablename for row in cur.fetchall()] if tables: - cur.execute("GRANT %s ON TABLE %s TO %s" % (perm, ','.join(tables), pending_dict['username'])) + cur.execute("GRANT %s ON TABLE %s TO %s" % (perm, ','.join(tables), pending_username)) # Grant schema permissions table_perm_types = ['CREATE', 'USAGE'] for perm in table_perm_types: - cur.execute("SELECT schemaname FROM (SELECT DISTINCT schemaname FROM pg_tables) WHERE HAS_SCHEMA_PRIVILEGE(%s, schemaname, %s)", (current_dict['username'], perm)) + cur.execute("SELECT QUOTE_IDENT(schemaname) as schemaname FROM (SELECT DISTINCT schemaname FROM pg_tables) WHERE HAS_SCHEMA_PRIVILEGE(%s, schemaname, %s)", (current_dict['username'], perm)) schemas = [row.schemaname for row in cur.fetchall()] if schemas: - cur.execute("GRANT %s ON SCHEMA %s TO %s" % (perm, ','.join(schemas), pending_dict['username'])) + cur.execute("GRANT %s ON SCHEMA %s TO %s" % (perm, ','.join(schemas), pending_username)) else: - alter_role = "ALTER USER %s" % pending_dict['username'] + alter_role = "ALTER USER %s" % pending_username cur.execute(alter_role + " WITH PASSWORD %s", (pending_dict['password'],)) conn.commit() @@ -392,4 +409,4 @@ def get_alt_username(current_username): if current_username.endswith(clone_suffix): return current_username[:(len(clone_suffix) * -1)] else: - return current_username + clone_suffix + return current_username + clone_suffix \ No newline at end of file diff --git a/SecretsManagerRedshiftRotationSingleUser/lambda_function.py b/SecretsManagerRedshiftRotationSingleUser/lambda_function.py index 7543ff94..39448547 100644 --- a/SecretsManagerRedshiftRotationSingleUser/lambda_function.py +++ b/SecretsManagerRedshiftRotationSingleUser/lambda_function.py @@ -146,22 +146,43 @@ def set_secret(service_client, arn, token): KeyError: If the secret json does not contain the expected keys """ - # First try to login with the pending secret, if it succeeds, return + try: + previous_dict = get_secret_dict(service_client, arn, "AWSPREVIOUS") + except (service_client.exceptions.ResourceNotFoundException, KeyError): + previous_dict = None + current_dict = get_secret_dict(service_client, arn, "AWSCURRENT") pending_dict = get_secret_dict(service_client, arn, "AWSPENDING", token) + + # First try to login with the pending secret, if it succeeds, return conn = get_connection(pending_dict) if conn: conn.close() logger.info("setSecret: AWSPENDING secret is already set as password in Redshift DB for secret arn %s." % arn) return + # Make sure the user from current and pending match + if current_dict['username'] != pending_dict['username']: + logger.error("setSecret: Attempting to modify user %s other than current user %s" % (pending_dict['username'], current_dict['username'])) + raise ValueError("Attempting to modify user %s other than current user %s" % (pending_dict['username'], current_dict['username'])) + + # Make sure the host from current and pending match + if current_dict['host'] != pending_dict['host']: + logger.error("setSecret: Attempting to modify user for host %s other than current host %s" % (pending_dict['host'], current_dict['host'])) + raise ValueError("Attempting to modify user for host %s other than current host %s" % (pending_dict['host'], current_dict['host'])) + # Now try the current password - conn = get_connection(get_secret_dict(service_client, arn, "AWSCURRENT")) - if not conn: + conn = get_connection(current_dict) + if not conn and previous_dict: # If both current and pending do not work, try previous - try: - conn = get_connection(get_secret_dict(service_client, arn, "AWSPREVIOUS")) - except service_client.exceptions.ResourceNotFoundException: - conn = None + conn = get_connection(previous_dict) + + # Make sure the user/host from current and pending match + if previous_dict['username'] != pending_dict['username']: + logger.error("setSecret: Attempting to modify user %s other than previous valid user %s" % (pending_dict['username'], previous_dict['username'])) + raise ValueError("Attempting to modify user %s other than previous valid user %s" % (pending_dict['username'], previous_dict['username'])) + if previous_dict['host'] != pending_dict['host']: + logger.error("setSecret: Attempting to modify user for host %s other than previous host %s" % (pending_dict['host'], previous_dict['host'])) + raise ValueError("Attempting to modify user for host %s other than previous host %s" % (pending_dict['host'], previous_dict['host'])) # If we still don't have a connection, raise a ValueError if not conn: @@ -171,7 +192,11 @@ def set_secret(service_client, arn, token): # Now set the password to the pending password try: with conn.cursor() as cur: - alter_role = "ALTER USER %s" % pending_dict['username'] + # Get escaped username via quote_ident + cur.execute("SELECT quote_ident(%s)", (pending_dict['username'],)) + escaped_username = cur.fetchone()[0] + + alter_role = "ALTER USER %s" % escaped_username cur.execute(alter_role + " WITH PASSWORD %s", (pending_dict['password'],)) conn.commit() logger.info("setSecret: Successfully set password for user %s in Redshift DB for secret arn %s." % (pending_dict['username'], arn))