Skip to content
This repository has been archived by the owner on Jul 11, 2022. It is now read-only.

Commit

Permalink
add option to re-encrypt snapshot
Browse files Browse the repository at this point in the history
also fix new snapshot mode
also fix empty db name handling
  • Loading branch information
kichik committed Jun 12, 2020
1 parent 1a0f3a4 commit 92db549
Show file tree
Hide file tree
Showing 7 changed files with 85 additions and 13 deletions.
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ The step function does the following to create the snapshot:
1. Get a snapshot of the given database by either:
* Finding the latest snapshot for the given database
* Creating and waiting for a new fresh snapshot
1. Re-encrypt snapshot if KMS key is supplied
1. Create a temporary database from the snapshot
1. Wait for the database to be ready
1. Reset the master password on the temporary database to a random password
Expand Down Expand Up @@ -50,12 +51,16 @@ and deploy it as you normally would from the terminal or in the AWS CloudFormati
| Sanitization SQL statements | SQL statement used to sanitize the temporary database. Use this to remove any data you don't want in the final snapshot, or the trim the data for size. You can separate multiple statements with a semicolon. |
| List of AWS accounts to share snapshot with | A comma-separated list of AWS accounts to share the final snapshot with. These accounts will see the snapshot under the "Shared with me" tab in the RDS console. |
| Snapshot name format | Final snapshot name format. A new snapshot will be created periodically, so this should contain the date to provide uniqueness. Make sure it follows the [naming rules of AWS](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Limits.html). |
| KMS key id | Re-encrypt the snapshot with a different key. If left empty, it will be encrypted with the same key used for the original database. |
| Network | Network parameters are required to create the temporary database. Make sure to select at least two subnets that are associated with the selected VPC |

### Encryption

The new snapshot will be encrypted with the same key used by the original database. If the original database wasn't
encrypted, the snapshot won't be encrypted either.
encrypted, the snapshot won't be encrypted either. To add another step that changes the key, use the KMS key parameter.

See [AWS documentation](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_ShareSnapshot.html) for instructions
on giving other accounts access to the key.

### Known Limitations

Expand Down
2 changes: 1 addition & 1 deletion architecture.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion architecture.xml
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<mxfile modified="2020-06-09T22:42:10.591Z" host="app.diagrams.net" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36" etag="m2sORilSoxg_TCqdTmIj" version="13.2.1" type="device"><diagram id="LGMa1gm817sAuc-iM5Mb" name="Page-1">7Vnbbts4EP0aPzbQXeljLcXdAimwrQu0fTJoiZa4oUgtNYrtfn2HEmVbkpO4gDdOsHEChHM4ImfIOYdUPHGjYvNRkTL/LFPKJ46VbiZuPHEcxwoc/KORbYvYtu+3SKZYarA9MGe/qAEtg9YspVXPEaTkwMo+mEghaAI9jCgl1323leT9WUuS0REwTwgfo99ZCnmLXvvWHv+LsizvZrYt01OQztkAVU5SuT6A3JuJGykpoW0Vm4hyvXrdurTPzR7o3QWmqIBTHijy6N91BTLPo/wDy5xPQfjxndmee8Jrk/AcaInIrBYJMClM7LDtFqSUTECzqP4Uf3HOyJr42BNp68rxB8DQDvuAPbb0GH1gaId9wB4Obw/mt4cBHgAjqze8NZjfOggQf92prIEzQaNd+VkIZoqkDLclklwqxIQUuHrTHAqOlo3Ndc6AzkuS6FVdI3cQW0kBhgC209lm4fWoWEClbhebTJPtiqwr7ypTsi6bKT8hBY72LrC5qHBbFyuzqdViLdXdiutynFag5B3tQp04bhQ7TuDpABjngxTuqQKG7PjAWaZnA6knJ8bidAV6RMyLiey2sWLXMrkcmyIlVU5Tk6CpRZyCbh4scntHHRQdKgsKaosu3QOe1z7S6Y0h33rP3cALWiw/5K1nHInRi2w39J5S2DCs+gOG2faIQjRFiTGmVJDLTArCb/boFLdNpLt12fvcSr3eTQX9QwG2plxIDbJfX7hcavvDPN8YP7WBlWzMeHPYGW97W6ADfHwDMB9Zq4Q+krhr1JqojMJTEjTeUEU5AXbfj+Psu+OO9G/iBFwX8RIbmW5EXNbpdwJJjo7fWEFV54Iz7rxGm3xUGAY0wJ+ZDvYhwTgkILpPI9v1gxFljXOPSh1Pb8mS8r9lxRotd+OlBJDFk0ROMBbMs1dST8kTqco20RXb6DiO6xW9x6EXgMu4WJKq8TsD7bsz1rDes0esD6/HpO+ws1eV41yC82fkru2cSF47vCR7uzAfo++MiRQHB3JHdeaClFWOV67/nMLYNwuvbyzvoC9miiaGiULv8JjjseVHuKhHjuVV83k1RFe0rbL2WjJF89gFhZNimZIznf1hXwXc4MIq4L12FQhPPcLdi6pAeMIhrigBLQBAC/16kxIg+vx5Fh3w4hA7/0wH8ONN3/9vdECl1XlEwLVemAgEr1wEnJPv8f4lRcA54Sb/tRboMf9y2/wTSSdPBPLk1/OowNtt4EkVWGGNaZk+ixJ47gtTguvXrgT+qUpw0ZeCLszHlODbBd4G3m4Bz3cL8IMXxv0TrqjzHNN7e0V9cUV5zlfU4P1z1SWa+6+Wmr6Db+jcm98=</diagram></mxfile>
<mxfile modified="2020-06-12T20:07:34.506Z" host="app.diagrams.net" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36" etag="XL3oBVjKomztn0jymFRy" version="13.2.3" type="device"><diagram id="LGMa1gm817sAuc-iM5Mb" name="Page-1">7Vldc+I2FP01PG7G3yaPix3SzKTbdtmZtE+MsIWtRpZcWQTYX98rWwYbO0C2LJBpCDPRPbrWx9U9R5IZ2EG2uhcoT3/lMaYDy4hXAzscWJZleBb8U8i6QkzTdSskESTW2BaYkO9Yg4ZGFyTGRctRck4lydtgxBnDkWxhSAi+bLvNOW33mqMEd4BJhGgXfSKxTCt06Bpb/BdMkrTu2TR0TYZqZw0UKYr5sgHZdwM7EJzLqpStAkxV9Oq4VM+NX6ndDExgJo95IEuDf5aF5GkapJ9JYj14/v0nvTwviC70hCcS54CMFyyShDM9drmuA5JzwmQZVHcEX+gzMAYu1ATKurHcHWDX9tuA2bVUG21g1/bbgLnbvLnTv7k7wAbQsVrNGzv9G40Bwtce8YWkhOFgk34GgIlAMYFlCTjlAjDGGURvlMqMgmVCcZkSiSc5ilRUl8AdwOacSU0A06ptHXjVKiRQrsrZKlFku0HLwrlJBF/kZZcPQIHe2ikUpwUs63SuF7WYLrl4nlOVjqNCCv6M66EOLDsILctz1AAIpTtTeMFCEmDHZ0oS1ZvkqnOkLYrnUrUI8yIseSyt0Db0XPq6iFGR4lhPsJvROslVr3jVgHSG32OeYSnW4FLXOk71SK03mnzLLXd9x6uwtMlbRzsirRfJpuktpaCgWfUGhplmh0I4BonRJhcy5QlniN5t0REsG4s3cdn6PHIV7zKD/sZSrnW6oIXk7fyCCIr1n/r50vhLGZDJ2gxXzcpwfWgJCr4QEd4zT1uLMxIJlocVR8Vg74IKTJEkL20ZPvnq2B39G1geVUk8g0KiCgHli/gJySgFx28kw6J2gR43Xp1F7hWGHRrA31gN9jXBaBIQ3EeBabteh7LauUWlmqePaIbp77wgpZbb4YxLybODRI5gLDDPVkodkidU5NVE52SlxtGvV/gFmp5KCON0horS7wS0r/dYzXrH7LJ+2CV9jf2XrHLzL4mxevqyttNoyB/Ib5Pwa51V56V8L69+jMum1SVz70Qvyt16lPvIOyYshsYlesZq4gzlRQoHrp9OYKgb+8M7w2nUhUTgSPOQqQXuMjw03MD0+zblefl5NzQXuEqy6lAyArPveEJRNovRiXZ+v60Btnc2Dejf55zLicAPcd4/dgO3r2oHr8e9dwsXGEklABJn6nITI4nU7nMWHXBCHyrfpgPwcUa3/xsdEHFxGhFwzOF1iYD3vkTAOvoU716VCFhHnOO/Lhh4TP54LF8hqWggBjz5fh4V+DgNHFSBOSSdkumTKIHrWNelBMN3pgTusUrgX5cSuIeV4NsFbgMfp4DznQI8z7su7h9xRJ2kMOOPK+rVJeUpr6j+7cWuqL1vby54Q33ra6rX3z4dsUVVV8Qz7Ej7Rrn3aIo/YRaJdb6h/UzUdUvVe/nroYCwW8YzXnedPraw61CL5hbW0YWeJH/9hfZt+3esnycVYG5/gy7rGj/l23f/Ag==</diagram></mxfile>
31 changes: 29 additions & 2 deletions cfm.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ def add_task_state(state_name: str, task_arn: str, next_state_name: str, catch_s
"new_snapshot": "${NewSnapshot}",
"shared_accounts": ["${ShareAccountsJoined}"],
"snapshot_format": "${SnapshotFormat}",
"kms": "${KMS}",
}

# TODO create add_choice()
Expand All @@ -275,8 +276,24 @@ def add_task_state(state_name: str, task_arn: str, next_state_name: str, catch_s
}

add_state("TakeSnapshot", "WaitForSnapshot", catch_state="ErrorCleanup")
add_waiting_state("WaitForSnapshot", "CreateTempDatabase", catch_state="ErrorCleanup")
add_state("FindLatestSnapshot", "CreateTempDatabase", catch_state="ErrorCleanup")
add_waiting_state("WaitForSnapshot", "ShouldEncrypt", catch_state="ErrorCleanup")
add_state("FindLatestSnapshot", "ShouldEncrypt", catch_state="ErrorCleanup")

states["ShouldEncrypt"] = {
"Type": "Choice",
"Choices": [
{
"Variable": "$.kms",
"StringEquals": "",
"Next": "CreateTempDatabase",
},
],
"Default": "Encrypt",
}

add_state("Encrypt", "WaitForEncrypt", catch_state="ErrorCleanup")
add_waiting_state("WaitForEncrypt", "CreateTempDatabase", catch_state="ErrorCleanup")

add_state("CreateTempDatabase", "WaitForTempDatabase", catch_state="ErrorCleanup")
add_waiting_state("WaitForTempDatabase", "SetTempPassword", catch_state="ErrorCleanup")
add_state("SetTempPassword", "WaitForPassword", catch_state="ErrorCleanup")
Expand Down Expand Up @@ -375,10 +392,20 @@ def generate_main_template():
"Options", Type="List<String>", Default="")
add_parameter(template, "SnapshotFormat", "Snapshot name format using Python .format() function",
"Options", Type="String", Default="{database_identifier:.42}-sanitized-{date:%Y-%m-%d}")
add_parameter(template, "KMS", "KMS key id to re-encrypt snapshots (leave empty to not encrypt)",
"Options", Type="String", Default="")
add_parameter(template, "VpcId", "VPC for temporary database", "Network", Type="AWS::EC2::VPC::Id")
add_parameter(template, "SubnetIds", "Subnets for temporary database (at least two)", "Network",
Type="List<AWS::EC2::Subnet::Id>")

template.add_condition(
"KmsEmpty",
troposphere.Equals(
troposphere.Ref("KMS"),
""
)
)

troposphere.rds.DBSubnetGroup(
"SubnetGroup", template,
DBSubnetGroupDescription="Temporary database used for RDS-sanitize-snapshots",
Expand Down
15 changes: 10 additions & 5 deletions gen-cfm.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ def validate_subnets(ctx, param, value):
@click.option("--share-account", help="AWS account identifiers to share snapshots with", multiple=True)
@click.option("--new-snapshot", help="Take a new snapshot instead of using the latest available", is_flag=True)
@click.option("--snapshot_format", help="Snapshot name snapshot_format")
def deploy(profile, stack_name, database, vpc, subnet, sql, share_account, new_snapshot, snapshot_format):
@click.option("--kms", help="KMS ARN to encrypt snapshots")
def deploy(profile, stack_name, database, vpc, subnet, sql, share_account, new_snapshot, snapshot_format, kms):
# this is more for testing and not really for user consumption...
stack_template = generate_main_template()

Expand Down Expand Up @@ -85,10 +86,14 @@ def deploy(profile, stack_name, database, vpc, subnet, sql, share_account, new_s

if snapshot_format:
parameters.append({
{
"ParameterKey": "SnapshotFormat",
"ParameterValue": snapshot_format,
}
"ParameterKey": "SnapshotFormat",
"ParameterValue": snapshot_format,
})

if kms:
parameters.append({
"ParameterKey": "KMS",
"ParameterValue": kms,
})

if _stack_exists(cf, stack_name):
Expand Down
17 changes: 16 additions & 1 deletion iam.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@
"Sid": "OriginalDB",
"Effect": "Allow",
"Action": [
troposphere.If("KmsEmpty", troposphere.NoValue, "rds:CopyDBSnapshot"),
"rds:DescribeDBInstances",
"rds:DescribeDBSnapshots",
"rds:CreateDBSnapshot",
],
"Resource": [
troposphere.Sub(
"arn:${AWS::Partition}:rds:${AWS::Region}:${AWS::AccountId}:db:${Db}"),
# TODO only allow access to all snapshots when using latest snapshot option
troposphere.Sub(
"arn:${AWS::Partition}:rds:${AWS::Region}:${AWS::AccountId}:snapshot:*"),
]
Expand Down Expand Up @@ -70,7 +72,20 @@
]
}
}
}
},
troposphere.If(
"KmsEmpty",
troposphere.NoValue,
{
"Sid": "Copy",
"Effect": "Allow",
"Action": [
"kms:CreateGrant",
"kms:DescribeKey",
],
"Resource": troposphere.Ref("KMS"),
}
),
]
}
)
Expand Down
24 changes: 22 additions & 2 deletions lambda/rds.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ def decorator(f):
def initialize(state, uid):
orig_db = rds_client.describe_db_instances(DBInstanceIdentifier=state["db_identifier"])["DBInstances"][0]
state["engine"] = orig_db["Engine"]
state["temporary_snapshot_id"] = state["db_identifier"][:55] + "-" + secrets.token_hex(5)
state["temp_snapshot_id"] = state["db_identifier"][:55] + "-" + secrets.token_hex(5)
state["temp_snapshot_id2"] = state["db_identifier"][:55] + "-" + secrets.token_hex(5)
state["temp_db_id"] = state["db_identifier"][:55] + "-" + secrets.token_hex(5)
state["target_snapshot_id"] = state["db_identifier"][:55] + "-" + secrets.token_hex(5)
tsid = state["target_snapshot_id"] = state["snapshot_format"].format(
Expand All @@ -61,6 +62,11 @@ def initialize(state, uid):
# https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Limits.html
raise ValueError(f"Invalid snapshot id generated from format - {tsid}")

if state["kms"] and orig_db["DBInstanceClass"] in ["db.m1.small", "db.m1.medium", "db.m1.large", "db.m1.xlarge",
"db.m2.xlarge", "db.m2.2xlarge", "db.m2.4xlarge", "db.t2.micro"]:
# https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.Encryption.html
raise ValueError("Instance type doesn't support encryption.")


@state_function("FindLatestSnapshot")
def find_latest_snapshot(state, uid):
Expand Down Expand Up @@ -98,6 +104,7 @@ def take_final_snapshot(state, uid):

@state_function("WaitForSnapshot")
@state_function("WaitForFinalSnapshot")
@state_function("WaitForEncrypt")
def wait_for_snapshot(state, uid):
snapshot = rds_client.describe_db_snapshots(DBSnapshotIdentifier=state["snapshot_id"])["DBSnapshots"][0]
status = snapshot["Status"]
Expand All @@ -107,6 +114,19 @@ def wait_for_snapshot(state, uid):
raise NotReady()


@state_function("Encrypt")
def encrypt(state, uid):
old_snapshot_id = state["snapshot_id"]
state["snapshot_id"] = state["temp_snapshot_id2"]

rds_client.copy_db_snapshot(
SourceDBSnapshotIdentifier=old_snapshot_id,
TargetDBSnapshotIdentifier=state["snapshot_id"],
KmsKeyId=state["kms"],
Tags=_tags(uid),
)


@state_function("CreateTempDatabase")
def create_temp_database(state, uid):
rds_client.restore_db_instance_from_db_snapshot(
Expand Down Expand Up @@ -141,7 +161,7 @@ def set_temp_password(state, uid):
"port": str(db["Endpoint"]["Port"]),
"user": db["MasterUsername"],
"password": secrets.token_hex(32),
"database": db["DBName"],
"database": db.get("DBName", ""),
}

rds_client.modify_db_instance(
Expand Down

0 comments on commit 92db549

Please sign in to comment.