Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 36 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,20 @@ Our initial version is built to handle [SOPS secrets](https://github.com/getsops

This module can be included as a child module, where needed, to fetch secrets and provide them in an abstract manner.

# Usage
## Usage

Copy `exports/secrets.sops.tf` to your project by running the following command:
Copy `exports/secrets.mixin.tf` to your project by running the following command:

```sh
curl -sL https://raw.githubusercontent.com/masterpointio/terraform-secrets-helper/main/exports/secrets.sops.tf -o secrets.sops.tf
curl -sL https://raw.githubusercontent.com/masterpointio/terraform-secrets-helper/main/exports/secrets.mixin.tf -o secrets.mixin.tf
```

The mixin incorporates the invocation of this module, so you simply need to configure the required `secret_mapping` variable and then reference it within your code.

See the full example in [examples/complete](https://github.com/masterpointio/terraform-secrets-helper/tree/main/examples/complete)

### SOPS Secrets

```hcl
secret_mapping = [{
name = "db_password"
Expand All @@ -39,9 +41,38 @@ output "db_password" {
}
```

### AWS SSM Parameter Store Secrets

```hcl
secret_mapping = [{
name = "api_token"
type = "ssm"
path = "/myapp/prod/api_token"
}]
```

### Mixed Sources

You can combine both SOPS and SSM secrets in the same configuration:

```hcl
secret_mapping = [
{
name = "db_password"
type = "sops"
file = "secrets.yaml"
},
{
name = "api_token"
type = "ssm"
path = "/myapp/prod/api_token"
}
]
```

# Future Enhancements

While the current version is specific to SOPS, future mixins will support other secret providers like SSM Parameter Store, Vault, and more. The future mixins will include all necessary provider configuration for themselves, making the process of integrating with different secret providers seamless.
The module currently supports SOPS and AWS SSM Parameter Store. Future versions may add support for other secret providers like HashiCorp Vault, AWS Secrets Manager, and more.

<!-- prettier-ignore-start -->
<!-- markdownlint-disable MD013 -->
Expand Down Expand Up @@ -76,7 +107,7 @@ No modules.

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_secret_mapping"></a> [secret\_mapping](#input\_secret\_mapping) | The list of secret mappings the application will need.<br/>This creates secret values for the component to consume at `local.secrets[name]`. | <pre>list(object({<br/> name = string<br/> type = optional(string, "sops")<br/> path = optional(string, null)<br/> file = string<br/> }))</pre> | `[]` | no |
| <a name="input_secret_mapping"></a> [secret\_mapping](#input\_secret\_mapping) | The list of secret mappings the application will need.<br/>This creates secret values for the component to consume at `local.secrets[name]`.<br/>For SOPS secrets: use type="sops" (default), file="path/to/sops/file.yaml", and name matching a key in the SOPS file.<br/>For SSM secrets: use type="ssm" and path="/path/to/ssm/parameter". | <pre>list(object({<br/> name = string<br/> type = optional(string, "sops")<br/> path = optional(string, null)<br/> file = optional(string, null)<br/> }))</pre> | `[]` | no |

## Outputs

Expand Down
54 changes: 54 additions & 0 deletions exports/secrets.mixin.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# tflint-ignore-file: terraform_required_version

# Reference your secrets using the module output
locals {
# tflint-ignore: terraform_unused_declarations
secrets = module.secrets.all
}

module "secrets" {
# checkov:skip=CKV_TF_1: For now we use Terraform registry source, not git. If switching to git, we should use a commit hash.
source = "masterpointio/helper/secrets"
version = "1.1.0"
secret_mapping = var.secret_mapping
}

variable "secret_mapping" {
type = list(object({
name = string
type = string
path = optional(string, null)
file = optional(string, null)
}))
default = []
description = <<-EOT
The list of secret mappings the application will need.
This creates secret values for the component to consume at `local.secrets[name]`.
For SOPS secrets: use type="sops", file="path/to/sops/file.yaml", and name matching a key in the SOPS file.
For SSM secrets: use type="ssm" and path="/path/to/ssm/parameter".
EOT

validation {
condition = alltrue([
for mapping in var.secret_mapping :
contains(["sops", "ssm"], mapping.type)
])
error_message = "Secret type must be either 'sops' or 'ssm'."
}

validation {
condition = alltrue([
for mapping in var.secret_mapping :
mapping.type == "sops" ? mapping.file != null : true
])
error_message = "SOPS secrets require 'file' attribute."
}

validation {
condition = alltrue([
for mapping in var.secret_mapping :
mapping.type == "ssm" ? mapping.path != null : true
])
error_message = "SSM secrets require 'path' attribute."
}
}
26 changes: 0 additions & 26 deletions exports/secrets.sops.tf

This file was deleted.

91 changes: 91 additions & 0 deletions tests/locals.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,94 @@ run "test_multiple_files_and_secrets" {
error_message = "All secret keys should be unique"
}
}

run "test_ssm_secrets" {
command = plan

variables {
secret_mapping = [
{
name = "ssm_secret"
type = "ssm"
path = "/app/secret"
},
{
name = "another_ssm_secret"
type = "ssm"
path = "/app/another_secret"
}
]
}

assert {
condition = length(local.ssm_secret_mapping) == 2
error_message = "Should have 2 SSM secret mappings"
}

assert {
condition = (
length(local.ssm_paths) == 2
&& contains(local.ssm_paths, "/app/secret")
&& contains(local.ssm_paths, "/app/another_secret")
)
error_message = "Should have both SSM paths in ssm_paths"
}

assert {
condition = length(local.ssm_secrets) == 2
error_message = "Should have 2 secrets in ssm_secrets map"
}

assert {
condition = length(local.sops_secret_mapping) == 0
error_message = "Should have no SOPS secret mappings"
}
}

run "test_mixed_sops_and_ssm" {
command = plan

variables {
secret_mapping = [
{
name = "db_password"
type = "sops"
file = "database.yaml"
},
{
name = "api_token"
type = "ssm"
path = "/app/api_token"
},
{
name = "api_key"
type = "sops"
file = "api.yaml"
}
]
}

assert {
condition = length(local.sops_secret_mapping) == 2
error_message = "Should have 2 SOPS secret mappings"
}

assert {
condition = length(local.ssm_secret_mapping) == 1
error_message = "Should have 1 SSM secret mapping"
}

assert {
condition = length(local.secrets) == 3
error_message = "Should have 3 total secrets"
}

assert {
condition = (
contains(keys(local.secrets), "db_password")
&& contains(keys(local.secrets), "api_token")
&& contains(keys(local.secrets), "api_key")
)
error_message = "All secret names should be present in the merged secrets"
}
}
58 changes: 58 additions & 0 deletions tests/outputs.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,61 @@ run "test_output_empty_secrets" {
error_message = "Output should be an empty map when no secrets are configured"
}
}

run "test_output_ssm_secrets" {
command = plan

variables {
secret_mapping = [
{
name = "ssm_secret"
type = "ssm"
path = "/app/secret"
}
]
}

assert {
condition = output.all["ssm_secret"] == "mock-ssm-value"
error_message = "ssm_secret should have the expected value from mocked SSM data"
}

assert {
condition = length(output.all) == 1
error_message = "Output should contain exactly 1 secret"
}
}

run "test_output_mixed_sops_and_ssm" {
command = plan

variables {
secret_mapping = [
{
name = "db_password"
type = "sops"
file = "database.yaml"
},
{
name = "ssm_token"
type = "ssm"
path = "/app/token"
}
]
}

assert {
condition = output.all["db_password"] == "supersecret123"
error_message = "db_password should have the expected value from mocked SOPS data"
}

assert {
condition = output.all["ssm_token"] == "mock-ssm-value"
error_message = "ssm_token should have the expected value from mocked SSM data"
}

assert {
condition = length(output.all) == 2
error_message = "Output should contain exactly 2 secrets"
}
}
28 changes: 27 additions & 1 deletion variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,37 @@ variable "secret_mapping" {
name = string
type = optional(string, "sops")
path = optional(string, null)
file = string
file = optional(string, null)
}))
default = []
description = <<-EOT
The list of secret mappings the application will need.
This creates secret values for the component to consume at `local.secrets[name]`.
For SOPS secrets: use type="sops" (default), file="path/to/sops/file.yaml", and name matching a key in the SOPS file.
For SSM secrets: use type="ssm" and path="/path/to/ssm/parameter".
EOT

validation {
condition = alltrue([
for mapping in var.secret_mapping :
contains(["sops", "ssm"], mapping.type)
])
error_message = "Secret type must be either 'sops' or 'ssm'."
}

validation {
condition = alltrue([
for mapping in var.secret_mapping :
mapping.type == "sops" ? mapping.file != null : true
])
error_message = "SOPS secrets require 'file' attribute."
}

validation {
condition = alltrue([
for mapping in var.secret_mapping :
mapping.type == "ssm" ? mapping.path != null : true
])
error_message = "SSM secrets require 'path' attribute."
}
}