Skip to content

Commit

Permalink
Merge pull request #1 from frank-bee/main
Browse files Browse the repository at this point in the history
Add variable recreate_missing_package
  • Loading branch information
JCapriotti authored Jul 14, 2022
2 parents c00ad70 + b8acaff commit e9357f2
Show file tree
Hide file tree
Showing 8 changed files with 127 additions and 32 deletions.
43 changes: 22 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,27 +36,28 @@ module "root_user" {

## Inputs

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_db_cluster_identifier"></a> [db_cluster_identifier](#input_db_cluster_identifier) | The DB cluster identifier | `string` | | yes |
| <a name="input_db_security_group_id"></a> [db_security_group_id](#input_db_security_group_id) | The security group ID for the database. Required for secret rotation. | `string` | `null` | no |
| <a name="input_engine"></a> [engine](#input_engine) | The database engine type | `string` | | yes |
| <a name="input_host"></a> [host](#input_host) | The host name of the database instance | `string` | | yes |
| <a name="input_master_secret_arn"></a> [master_secret_arn](#input_master_secret_arn) | The superuser credentials used to update another secret in the multiuser rotation strategy. Required when using `multipleuser` rotation strategy. | `string` | null | no |
| <a name="input_name_prefix"></a> [name_prefix](#input_name_prefix) | The prefix for names of created resources. | `string` | | yes |
| <a name="input_password"></a> [password](#input_password) | The password for the user. | `string` | | yes |
| <a name="input_port"></a> [port](#input_port) | The port number of the database instance. | `number` | | yes |
| <a name="input_rotation_days"></a> [rotation_days](#input_rotation_days) | The number of days between rotations. When set to `null` (the default) rotation is not configured. | `number` | `null` | no |
| <a name="input_rotation_lambda_env_variables"></a> [rotation_lambda_env_variables](#input_rotation_lambda_env_variables) | Optional environment variables for the rotation lambda; useful for integration with for certain layer providers. | `map(string)` | `{}` | no |
| <a name="input_rotation_lambda_handler"></a> [rotation_lambda_handler](#input_rotation_lambda_handler) | An optional lambda handler name; useful integration with for certain layer providers. | `string` | `null` | no |
| <a name="input_rotation_lambda_layers"></a> [rotation_lambda_layers](#input_rotation_lambda_layers) | Optional layers for the rotation lambda. | `list(string)` | `null` | no |
| <a name="input_rotation_lambda_policy_jsons"></a> [rotation_lambda_policy_jsons](#input_rotation_lambda_policy_jsons) | Additional policies to add to the rotation lambda; useful for integration with layer providers. | `list(string)` | `[]` | no |
| <a name="input_rotation_lambda_subnet_ids"></a> [rotation_lambda_subnet_ids](#input_rotation_lambda_subnet_ids) | The VPC subnets that the rotation lambda runs in. Required for secret rotation. | `list(string)` | `[]` | no |
| <a name="input_rotation_lambda_vpc_id"></a> [rotation_lambda_vpc_id](#input_rotation_lambda_vpc_id) | The VPC that the secret rotation lambda runs in. Required for secret rotation. | `string` | null | no |
| <a name="input_rotation_strategy"></a> [rotation_strategy](#input_rotation_strategy) | Specifies how the secret is rotated, either by updating credentials for the user itself (`single`) or by using a superuser's credentials to change another user's credentials (`multiuser`). | `string` | `single` | no |
| <a name="input_secret_recovery_window_days"></a> [secret_recovery_window_days](#input_secret_recovery_window_days) | The number of days that Secrets Manager waits before deleting a secret. | `number` | `0` | no |
| <a name="input_tags"></a> [tags](#input_tags) | Tags to use for created resources. | `map(string)` | `{}` | no |
| <a name="input_username"></a> [username](#input_username) | The username. | `string` | | yes |
| Name | Description | Type | Default | Required |
|--------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------|----------|:--------:|
| <a name="input_db_cluster_identifier"></a> [db_cluster_identifier](#input_db_cluster_identifier) | The DB cluster identifier | `string` | | yes |
| <a name="input_db_security_group_id"></a> [db_security_group_id](#input_db_security_group_id) | The security group ID for the database. Required for secret rotation. | `string` | `null` | no |
| <a name="input_engine"></a> [engine](#input_engine) | The database engine type | `string` | | yes |
| <a name="input_host"></a> [host](#input_host) | The host name of the database instance | `string` | | yes |
| <a name="input_master_secret_arn"></a> [master_secret_arn](#input_master_secret_arn) | The superuser credentials used to update another secret in the multiuser rotation strategy. Required when using `multipleuser` rotation strategy. | `string` | null | no |
| <a name="input_name_prefix"></a> [name_prefix](#input_name_prefix) | The prefix for names of created resources. | `string` | | yes |
| <a name="input_password"></a> [password](#input_password) | The password for the user. | `string` | | yes |
| <a name="input_port"></a> [port](#input_port) | The port number of the database instance. | `number` | | yes |
| <a name="input_rotation_days"></a> [rotation_days](#input_rotation_days) | The number of days between rotations. When set to `null` (the default) rotation is not configured. | `number` | `null` | no |
| <a name="input_rotation_lambda_env_variables"></a> [rotation_lambda_env_variables](#input_rotation_lambda_env_variables) | Optional environment variables for the rotation lambda; useful for integration with for certain layer providers. | `map(string)` | `{}` | no |
| <a name="input_rotation_lambda_handler"></a> [rotation_lambda_handler](#input_rotation_lambda_handler) | An optional lambda handler name; useful integration with for certain layer providers. | `string` | `null` | no |
| <a name="input_rotation_lambda_layers"></a> [rotation_lambda_layers](#input_rotation_lambda_layers) | Optional layers for the rotation lambda. | `list(string)` | `null` | no |
| <a name="input_rotation_lambda_policy_jsons"></a> [rotation_lambda_policy_jsons](#input_rotation_lambda_policy_jsons) | Additional policies to add to the rotation lambda; useful for integration with layer providers. | `list(string)` | `[]` | no |
| <a name="input_rotation_lambda_subnet_ids"></a> [rotation_lambda_subnet_ids](#input_rotation_lambda_subnet_ids) | The VPC subnets that the rotation lambda runs in. Required for secret rotation. | `list(string)` | `[]` | no |
| <a name="input_rotation_lambda_vpc_id"></a> [rotation_lambda_vpc_id](#input_rotation_lambda_vpc_id) | The VPC that the secret rotation lambda runs in. Required for secret rotation. | `string` | null | no |
| <a name="input_rotation_strategy"></a> [rotation_strategy](#input_rotation_strategy) | Specifies how the secret is rotated, either by updating credentials for the user itself (`single`) or by using a superuser's credentials to change another user's credentials (`multiuser`). | `string` | `single` | no |
| <a name="input_secret_recovery_window_days"></a> [secret_recovery_window_days](#input_secret_recovery_window_days) | The number of days that Secrets Manager waits before deleting a secret. | `number` | `0` | no |
| <a name="input_tags"></a> [tags](#input_tags) | Tags to use for created resources. | `map(string)` | `{}` | no |
| <a name="input_username"></a> [username](#input_username) | The username. | `string` | | yes |
| <a name="input_recreate_missing_package"></a> [recreate_missing_package](#input_recreate_missing_package) | Whether to recreate missing Lambda package if it is missing locally or not. | `bool` | true | no |

## Outputs

Expand Down
Binary file modified functions/postgresql-multiuser/_pg.cpython-37m-x86_64-linux-gnu.so
Binary file not shown.
108 changes: 97 additions & 11 deletions functions/postgresql-multiuser/lambda_function.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: MIT-0

import re
import boto3
import json
import logging
Expand All @@ -18,8 +19,8 @@ def lambda_handler(event, context):
This handler uses the master-user rotation scheme to rotate an RDS PostgreSQL user credential. During the first rotation, this
scheme logs into the database as the master user, creates a new user (appending _clone to the username), and grants the
new user all of the permissions from the user being rotated. Once the secret is in this state, every subsequent rotation
simply creates a new secret with the AWSPREVIOUS user credentials, adds any missing permissions that are in the current
secret, changes that user's password, and then marks the latest secret as AWSCURRENT.
simply creates a new secret with the AWSPREVIOUS user credentials, changes that user's password, and then marks the
latest secret as AWSCURRENT.
The Secret SecretString is expected to be a JSON string with the following format:
{
Expand Down Expand Up @@ -166,9 +167,9 @@ def set_secret(service_client, arn, token):
# 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'])))
"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 clone %s" % (pending_dict['username'], get_alt_username(current_dict['username'])))
"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']:
Expand Down Expand Up @@ -218,7 +219,7 @@ def set_secret(service_client, arn, token):
cur.execute(alter_role + " WITH PASSWORD %s", (pending_dict['password'],))

conn.commit()
logger.info("setSecret: Successfully created user %s in PostgreSQL DB for secret arn %s." % (pending_dict['username'], arn))
logger.info("setSecret: Successfully set password for %s in PostgreSQL DB for secret arn %s." % (pending_dict['username'], arn))
finally:
conn.close()

Expand Down Expand Up @@ -299,8 +300,9 @@ def finish_secret(service_client, arn, token):
def get_connection(secret_dict):
"""Gets a connection to PostgreSQL DB from a secret dictionary
This helper function tries to connect to the database grabbing connection info
from the secret dictionary. If successful, it returns the connection, else None
This helper function uses connectivity information from the secret dictionary to initiate
connection attempt(s) to the database. Will attempt a fallback, non-SSL connection when
initial connection fails using SSL and fall_back is True.
Args:
secret_dict (dict): The Secret Dictionary
Expand All @@ -316,12 +318,96 @@ def get_connection(secret_dict):
port = int(secret_dict['port']) if 'port' in secret_dict else 5432
dbname = secret_dict['dbname'] if 'dbname' in secret_dict else "postgres"

# Get SSL connectivity configuration
use_ssl, fall_back = get_ssl_config(secret_dict)

# if an 'ssl' key is not found or does not contain a valid value, attempt an SSL connection and fall back to non-SSL on failure
conn = connect_and_authenticate(secret_dict, port, dbname, use_ssl)
if conn or not fall_back:
return conn
else:
return connect_and_authenticate(secret_dict, port, dbname, False)


def get_ssl_config(secret_dict):
"""Gets the desired SSL and fall back behavior using a secret dictionary
This helper function uses the existance and value the 'ssl' key in a secret dictionary
to determine desired SSL connectivity configuration. Its behavior is as follows:
- 'ssl' key DNE or invalid type/value: return True, True
- 'ssl' key is bool: return secret_dict['ssl'], False
- 'ssl' key equals "true" ignoring case: return True, False
- 'ssl' key equals "false" ignoring case: return False, False
Args:
secret_dict (dict): The Secret Dictionary
Returns:
Tuple(use_ssl, fall_back): SSL configuration
- use_ssl (bool): Flag indicating if an SSL connection should be attempted
- fall_back (bool): Flag indicating if non-SSL connection should be attempted if SSL connection fails
"""
# Default to True for SSL and fall_back mode if 'ssl' key DNE
if 'ssl' not in secret_dict:
return True, True

# Handle type bool
if isinstance(secret_dict['ssl'], bool):
return secret_dict['ssl'], False

# Handle type string
if isinstance(secret_dict['ssl'], str):
ssl = secret_dict['ssl'].lower()
if ssl == "true":
return True, False
elif ssl == "false":
return False, False
else:
# Invalid string value, default to True for both SSL and fall_back mode
return True, True

# Invalid type, default to True for both SSL and fall_back mode
return True, True


def connect_and_authenticate(secret_dict, port, dbname, use_ssl):
"""Attempt to connect and authenticate to a PostgreSQL instance
This helper function tries to connect to the database using connectivity info passed in.
If successful, it returns the connection, else None
Args:
- secret_dict (dict): The Secret Dictionary
- port (int): The databse port to connect to
- dbname (str): Name of the database
- use_ssl (bool): Flag indicating whether connection should use SSL/TLS
Returns:
Connection: The pymongo.database.Database object if successful. None otherwise
Raises:
KeyError: If the secret json does not contain the expected keys
"""
# Try to obtain a connection to the db
try:
conn = pgdb.connect(host=secret_dict['host'], user=secret_dict['username'], password=secret_dict['password'], database=dbname, port=port,
connect_timeout=60)
if use_ssl:
# Setting sslmode='verify-full' will verify the server's certificate and check the server's host name
conn = pgdb.connect(host=secret_dict['host'], user=secret_dict['username'], password=secret_dict['password'], database=dbname, port=port,
connect_timeout=5, sslrootcert='/etc/pki/tls/cert.pem', sslmode='verify-full')
else:
conn = pgdb.connect(host=secret_dict['host'], user=secret_dict['username'], password=secret_dict['password'], database=dbname, port=port,
connect_timeout=5, sslmode='disable')
logger.info("Successfully established %s connection as user '%s' with host: '%s'" % ("SSL/TLS" if use_ssl else "non SSL/TLS", secret_dict['username'], secret_dict['host']))
return conn
except pg.InternalError:
except pg.InternalError as e:
if "server does not support SSL, but SSL was required" in e.args[0]:
logger.error("Unable to establish SSL/TLS handshake, SSL/TLS is not enabled on the host: %s" % secret_dict['host'])
elif re.search('server common name ".+" does not match host name ".+"', e.args[0]):
logger.error("Hostname verification failed when estlablishing SSL/TLS Handshake with host: %s" % secret_dict['host'])
elif re.search('no pg_hba.conf entry for host ".+", SSL off', e.args[0]):
logger.error("Unable to establish SSL/TLS handshake, SSL/TLS is enforced on the host: %s" % secret_dict['host'])
return None


Expand Down Expand Up @@ -422,7 +508,7 @@ def is_rds_replica_database(replica_dict, master_dict):
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)
logger.warning("Encountered error while verifying rds replica status: %s" % err)
return False
instances = describe_response['DBInstances']

Expand Down
Binary file modified functions/postgresql-multiuser/libcrypto.so.1.0.0
Binary file not shown.
Binary file modified functions/postgresql-multiuser/libpq.so.5
Binary file not shown.
Binary file modified functions/postgresql-multiuser/libssl.so.1.0.0
Binary file not shown.
2 changes: 2 additions & 0 deletions main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ module "rotation_lambda" {
}
}

recreate_missing_package = var.recreate_missing_package

attach_policy_jsons = true
policy_jsons = local.lambda_policies
number_of_policy_jsons = length(local.lambda_policies)
Expand Down
Loading

0 comments on commit e9357f2

Please sign in to comment.