Skip to content

Commit 8ccfd71

Browse files
authored
Merge pull request #5 from thoughtbot/import-rds-posgres-login
Add rds-postgres-login module to create additional postgres users in RDS
2 parents ee1f27d + 8450c62 commit 8ccfd71

8 files changed

Lines changed: 813 additions & 0 deletions

File tree

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# RDS Postgres Admin Login
2+
3+
Creates a login to an RDS Postgres instance and automatically rotates the
4+
password.
5+
6+
An active, admin username and password must be provided in an existing secret.
7+
This admin user will be used to create and rotate credentials.
8+
9+
During rotation, the secret will toggle between primary and alternate usernames
10+
to avoid the scenario where the password is changed but hasn't been propagated
11+
to all users yet. This means that each password will remain active for two
12+
rotations.
13+
14+
Example:
15+
16+
```
17+
module "rds_readonly_password" {
18+
source = "git@github.com:thoughtbot/flightdeck-addons.git//aws/rds-postgres-login?ref=main"
19+
20+
admin_login_kms_key_id = module.rds_admin_password.kms_key_arn
21+
admin_login_secret_arn = module.rds_admin_password.secret_arn
22+
database = module.database.primary
23+
subnet_ids = module.network_data.private_subnet_ids
24+
username = "readonly"
25+
vpc_id = module.network_data.vpc_id
26+
27+
grants = [
28+
"GRANT USAGE ON SCHEMA public TO %s",
29+
"GRANT SELECT ON ALL TABLES IN SCHEMA public TO %s"
30+
]
31+
}
32+
33+
module "rds_admin_password" {
34+
source = "git@github.com:thoughtbot/flightdeck-addons.git//aws/rds-postgres-admin-login?ref=main"
35+
36+
database = module.database.primary
37+
initial_password = module.database.initial_password
38+
subnet_ids = module.network_data.private_subnet_ids
39+
username = module.database.admin_username
40+
vpc_id = module.network_data.vpc_id
41+
}
42+
```
43+
44+
<!-- BEGIN_TF_DOCS -->
45+
## Requirements
46+
47+
| Name | Version |
48+
|------|---------|
49+
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 0.14.0 |
50+
| <a name="requirement_aws"></a> [aws](#requirement\_aws) | ~> 4.0 |
51+
52+
## Providers
53+
54+
| Name | Version |
55+
|------|---------|
56+
| <a name="provider_aws"></a> [aws](#provider\_aws) | 4.26.0 |
57+
58+
## Modules
59+
60+
| Name | Source | Version |
61+
|------|--------|---------|
62+
| <a name="module_rotation"></a> [rotation](#module\_rotation) | github.com/thoughtbot/terraform-aws-secrets//secret-rotation-function | v0.4.0 |
63+
| <a name="module_secret"></a> [secret](#module\_secret) | github.com/thoughtbot/terraform-aws-secrets//secret | v0.4.0 |
64+
65+
## Resources
66+
67+
| Name | Type |
68+
|------|------|
69+
| [aws_iam_policy.access_admin_login](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource |
70+
| [aws_iam_role_policy_attachment.access_admin_login](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource |
71+
| [aws_security_group.function](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource |
72+
| [aws_security_group_rule.function_egress](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource |
73+
| [aws_iam_policy_document.access_admin_login](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
74+
| [aws_kms_key.admin_login](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/kms_key) | data source |
75+
76+
## Inputs
77+
78+
| Name | Description | Type | Default | Required |
79+
|------|-------------|------|---------|:--------:|
80+
| <a name="input_admin_login_kms_key_id"></a> [admin\_login\_kms\_key\_id](#input\_admin\_login\_kms\_key\_id) | ARN of the KMS key used to encrypt the admin login | `string` | n/a | yes |
81+
| <a name="input_admin_login_secret_arn"></a> [admin\_login\_secret\_arn](#input\_admin\_login\_secret\_arn) | ARN of a SecretsManager secret containing admin login | `string` | `null` | no |
82+
| <a name="input_admin_principals"></a> [admin\_principals](#input\_admin\_principals) | Principals allowed to peform admin actions (default: current account) | `list(string)` | `null` | no |
83+
| <a name="input_alternate_username"></a> [alternate\_username](#input\_alternate\_username) | Username for the alternate login used during rotation | `string` | `null` | no |
84+
| <a name="input_database"></a> [database](#input\_database) | The database instance for which a login will be managed | <pre>object({<br> address = string<br> arn = string<br> engine = string<br> identifier = string<br> name = string<br> port = number<br> })</pre> | n/a | yes |
85+
| <a name="input_grants"></a> [grants](#input\_grants) | List of GRANT statements for this user | `list(string)` | n/a | yes |
86+
| <a name="input_read_principals"></a> [read\_principals](#input\_read\_principals) | Principals allowed to read the secret (default: current account) | `list(string)` | `null` | no |
87+
| <a name="input_secret_name"></a> [secret\_name](#input\_secret\_name) | Override the name for this secret | `string` | `null` | no |
88+
| <a name="input_subnet_ids"></a> [subnet\_ids](#input\_subnet\_ids) | Subnets in which the rotation function should run | `list(string)` | n/a | yes |
89+
| <a name="input_tags"></a> [tags](#input\_tags) | Tags to be applied to created resources | `map(string)` | `{}` | no |
90+
| <a name="input_trust_tags"></a> [trust\_tags](#input\_trust\_tags) | Tags required on principals accessing the secret | `map(string)` | `{}` | no |
91+
| <a name="input_username"></a> [username](#input\_username) | The username for which a login will be managed | `string` | n/a | yes |
92+
| <a name="input_vpc_id"></a> [vpc\_id](#input\_vpc\_id) | VPC in which the rotation function should run | `string` | n/a | yes |
93+
94+
## Outputs
95+
96+
| Name | Description |
97+
|------|-------------|
98+
| <a name="output_policy_json"></a> [policy\_json](#output\_policy\_json) | Required IAM policies |
99+
| <a name="output_secret_arn"></a> [secret\_arn](#output\_secret\_arn) | ARN of the secrets manager secret containing credentials |
100+
| <a name="output_secret_name"></a> [secret\_name](#output\_secret\_name) | Name of the secrets manager secret containing credentials |
101+
<!-- END_TF_DOCS -->
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
module "secret" {
2+
source = "github.com/thoughtbot/terraform-aws-secrets//secret?ref=v0.4.0"
3+
4+
admin_principals = var.admin_principals
5+
description = "Postgres password for: ${local.full_name}"
6+
name = coalesce(var.secret_name, local.full_name)
7+
read_principals = var.read_principals
8+
resource_tags = var.tags
9+
trust_tags = var.trust_tags
10+
11+
initial_value = jsonencode({
12+
dbname = var.database.name
13+
engine = var.database.engine
14+
host = var.database.address
15+
password = ""
16+
port = tostring(var.database.port)
17+
username = var.username
18+
})
19+
}
20+
21+
module "rotation" {
22+
source = "github.com/thoughtbot/terraform-aws-secrets//secret-rotation-function?ref=v0.4.0"
23+
24+
handler = "lambda_function.lambda_handler"
25+
role_arn = module.secret.rotation_role_arn
26+
runtime = "python3.8"
27+
secret_arn = module.secret.arn
28+
security_group_ids = [aws_security_group.function.id]
29+
source_file = "${path.module}/rotation/lambda_function.py"
30+
subnet_ids = var.subnet_ids
31+
32+
dependencies = {
33+
postgres = "${path.module}/rotation/postgres.zip"
34+
}
35+
36+
variables = {
37+
ADMIN_LOGIN_SECRET_ARN = var.admin_login_secret_arn
38+
ALTERNATE_USERNAME = coalesce(var.alternate_username, "${var.username}_alt")
39+
GRANTS = jsonencode(var.grants)
40+
PRIMARY_USERNAME = var.username
41+
}
42+
}
43+
44+
resource "aws_security_group" "function" {
45+
description = "Security group for rotating ${local.full_name}"
46+
name = "${var.database.identifier}-rotation"
47+
tags = var.tags
48+
vpc_id = var.vpc_id
49+
}
50+
51+
resource "aws_security_group_rule" "function_egress" {
52+
cidr_blocks = ["0.0.0.0/0"]
53+
description = "Allow all egress"
54+
from_port = 0
55+
protocol = "-1"
56+
security_group_id = aws_security_group.function.id
57+
to_port = 0
58+
type = "egress"
59+
}
60+
61+
resource "aws_iam_role_policy_attachment" "access_admin_login" {
62+
policy_arn = aws_iam_policy.access_admin_login.arn
63+
role = module.secret.rotation_role_name
64+
}
65+
66+
resource "aws_iam_policy" "access_admin_login" {
67+
name = local.full_name
68+
policy = data.aws_iam_policy_document.access_admin_login.json
69+
}
70+
71+
data "aws_iam_policy_document" "access_admin_login" {
72+
statement {
73+
sid = "ReadAdminLogin"
74+
actions = [
75+
"secretsmanager:DescribeSecret",
76+
"secretsmanager:GetSecretValue"
77+
]
78+
resources = [var.admin_login_secret_arn]
79+
}
80+
81+
statement {
82+
sid = "DecryptReadAdminLogin"
83+
actions = [
84+
"kms:Decrypt"
85+
]
86+
resources = [data.aws_kms_key.admin_login.arn]
87+
}
88+
89+
statement {
90+
sid = "DescribeDatabase"
91+
actions = [
92+
"rds:DescribeDBInstances"
93+
]
94+
resources = [var.database.arn]
95+
}
96+
}
97+
98+
data "aws_kms_key" "admin_login" {
99+
key_id = var.admin_login_kms_key_id
100+
}
101+
102+
locals {
103+
full_name = join("-", ["rds-postgres", var.database.identifier, var.username])
104+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
TFLINTRC := ../../.tflint.hcl
2+
MODULEFILES := $(wildcard *.tf)
3+
4+
.PHONY: default
5+
default: checkfmt validate docs lint
6+
7+
.PHONY: checkfmt
8+
checkfmt: .fmt
9+
10+
.PHONY: fmt
11+
fmt: $(MODULEFILES)
12+
terraform fmt
13+
@touch .fmt
14+
15+
.PHONY: validate
16+
validate: .validate
17+
18+
.PHONY: docs
19+
docs: README.md
20+
21+
.PHONY: lint
22+
lint: .lint
23+
24+
.lint: $(MODULEFILES) .lintinit
25+
tflint --config=$(TFLINTRC)
26+
@touch .lint
27+
28+
.lintinit: $(TFLINTRC)
29+
tflint --init --config=$(TFLINTRC)
30+
@touch .lintinit
31+
32+
README.md: $(MODULEFILES)
33+
terraform-docs markdown table . --output-file README.md
34+
35+
.fmt: $(MODULEFILES)
36+
terraform fmt -check
37+
@touch .fmt
38+
39+
.PHONY: init
40+
init: .init
41+
42+
.init: versions.tf
43+
terraform init -backend=false
44+
@touch .init
45+
46+
.validate: .init $(MODULEFILES) $(wildcard *.tf.example)
47+
echo | cat - $(wildcard *.tf.example) > test.tf
48+
if AWS_DEFAULT_REGION=us-east-1 terraform validate; then \
49+
rm test.tf; \
50+
touch .validate; \
51+
else \
52+
rm test.tf; \
53+
false; \
54+
fi
55+
56+
.PHONY: clean
57+
clean:
58+
rm -rf .fmt .init .lint .lintinit .terraform .terraform.lock.hcl .validate
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
output "policy_json" {
2+
description = "Required IAM policies"
3+
value = module.secret.policy_json
4+
}
5+
6+
output "secret_arn" {
7+
description = "ARN of the secrets manager secret containing credentials"
8+
value = module.secret.arn
9+
}
10+
11+
output "secret_name" {
12+
description = "Name of the secrets manager secret containing credentials"
13+
value = module.secret.name
14+
}

0 commit comments

Comments
 (0)