Skip to content

aws_write_only_arguments: recommend write-only arguments where available #860

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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
59 changes: 59 additions & 0 deletions docs/rules/aws_write_only_attributes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# aws_write_only_arguments

Recommends using available [write-only arguments](https://developer.hashicorp.com/terraform/language/resources/ephemeral/write-only) instead of the original sensitive attribute. This is only valid for Terraform v1.11+.

## Example

This example uses `aws_secretsmanager_secret_version`, but the rule applies to all resources with write-only arguments:

```hcl
resource "aws_secretsmanager_secret_version" "test" {
secret_string = var.secret
}
```

```
$ tflint
1 issue(s) found:

Warning: [Fixable] "secret_string" is a non-ephemeral attribute, which means this secret is stored in state. Please use "secret_string_wo". (aws_write_only_arguments)

on test.tf line 3:
3: secret_string = var.secret

```

## Why

By default, sensitive attributes are still stored in state, just hidden from view in plan output. Other resources are able to refer to these attributes. Current versions of Terraform also include support for write-only arguments, which are not persisted to state. Other resources cannot refer to their values.

Using write-only arguments mitigates the risk of a malicious actor obtaining privileged credentials by accessing Terraform state files directly. Prefer using them over the original sensitive attribute unless you need to refer to it in other blocks, such as a [root `output`](https://developer.hashicorp.com/terraform/language/values/outputs#ephemeral-avoid-storing-values-in-state-or-plan-files), that cannot be ephemeral.

## How To Fix

Replace the attribute with its write-only argument equivalent. Reference an ephemeral resource or ephemeral variable to ensure that the sensitive value is not persisted to state.

```hcl
ephemeral "random_password" "test" {
length = 32
override_special = "!#$%&*()-_=+[]{}<>:?"
}

resource "aws_secretsmanager_secret_version" "test" {
secret_string_wo = ephemeral.random_password.test.value
secret_string_wo_version = 1
}
```

```hcl
variable "test" {
type = string
ephemeral = true # Optional, non-ephemeral values can also be used for write-only arguments
description = "Input variable for a secret"
}

resource "aws_secretsmanager_secret_version" "test" {
secret_string_wo = var.test
secret_string_wo_version = 1
}
```
98 changes: 98 additions & 0 deletions rules/ephemeral/aws_write_only_arguments.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package ephemeral

import (
"fmt"

"github.com/terraform-linters/tflint-plugin-sdk/hclext"
"github.com/terraform-linters/tflint-plugin-sdk/tflint"
"github.com/terraform-linters/tflint-ruleset-aws/project"
"github.com/zclconf/go-cty/cty"
)

// AwsWriteOnlyArgumentsRule checks if a write-only argument is available for sensitive input attributes
type AwsWriteOnlyArgumentsRule struct {
tflint.DefaultRule

writeOnlyArguments map[string][]writeOnlyArgument
}

type writeOnlyArgument struct {
originalAttribute string
writeOnlyAlternative string
writeOnlyVersionAttribute string
}

// NewAwsWriteOnlyArgumentsRule returns new rule with default attributes
func NewAwsWriteOnlyArgumentsRule() *AwsWriteOnlyArgumentsRule {
return &AwsWriteOnlyArgumentsRule{
writeOnlyArguments: writeOnlyArguments,
}
}

// Name returns the rule name
func (r *AwsWriteOnlyArgumentsRule) Name() string {
return "aws_write_only_arguments"
}

// Enabled returns whether the rule is enabled by default
func (r *AwsWriteOnlyArgumentsRule) Enabled() bool {
return false
}

// Severity returns the rule severity
func (r *AwsWriteOnlyArgumentsRule) Severity() tflint.Severity {
return tflint.WARNING
}

// Link returns the rule reference link
func (r *AwsWriteOnlyArgumentsRule) Link() string {
return project.ReferenceLink(r.Name())
}

// Check checks whether the sensitive attribute exists
func (r *AwsWriteOnlyArgumentsRule) Check(runner tflint.Runner) error {
for resourceType, attributes := range r.writeOnlyArguments {
for _, resourceAttribute := range attributes {
resources, err := runner.GetResourceContent(resourceType, &hclext.BodySchema{
Attributes: []hclext.AttributeSchema{
{Name: resourceAttribute.originalAttribute},
},
}, nil)
if err != nil {
return err
}

for _, resource := range resources.Blocks {
attribute, exists := resource.Body.Attributes[resourceAttribute.originalAttribute]
if !exists {
continue
}

err := runner.EvaluateExpr(attribute.Expr, func(val cty.Value) error {
if !val.IsNull() {
if err := runner.EmitIssueWithFix(
r,
fmt.Sprintf("\"%s\" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only argument \"%s\".", resourceAttribute.originalAttribute, resourceAttribute.writeOnlyAlternative),
attribute.Expr.Range(),
func(f tflint.Fixer) error {
err := f.ReplaceText(attribute.NameRange, resourceAttribute.writeOnlyAlternative)
if err != nil {
return err
}
return f.InsertTextAfter(attribute.Range, fmt.Sprintf("\n %s = 1", resourceAttribute.writeOnlyVersionAttribute))
},
); err != nil {
return fmt.Errorf("failed to call EmitIssueWithFix(): %w", err)
}
}
return nil
}, nil)
if err != nil {
return err
}
}
}
}

return nil
}
71 changes: 71 additions & 0 deletions rules/ephemeral/aws_write_only_arguments_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package ephemeral

import (
"testing"

"github.com/hashicorp/hcl/v2"
"github.com/terraform-linters/tflint-plugin-sdk/helper"
)

func Test_AwsWriteOnlyAttribute(t *testing.T) {
cases := []struct {
Name string
Content string
Expected helper.Issues
Fixed string
}{
{
Name: "basic",
Content: `
resource "aws_secretsmanager_secret_version" "test" {
secret_string = "test"
}
`,
Expected: helper.Issues{
{
Rule: NewAwsWriteOnlyArgumentsRule(),
Message: `"secret_string" is a non-ephemeral attribute, which means this secret is stored in state. Please use write-only argument "secret_string_wo".`,
Range: hcl.Range{
Filename: "resource.tf",
Start: hcl.Pos{Line: 3, Column: 19},
End: hcl.Pos{Line: 3, Column: 25},
},
},
},
Fixed: `
resource "aws_secretsmanager_secret_version" "test" {
secret_string_wo = "test"
secret_string_wo_version = 1
}
`,
},
{
Name: "everything is fine",
Content: `
resource "aws_secretsmanager_secret_version" "test" {
secret_string_wo = "test"
secret_string_wo_version = 1
}
`,
Expected: helper.Issues{},
},
}

rule := NewAwsWriteOnlyArgumentsRule()

for _, tc := range cases {
filename := "resource.tf"
runner := helper.TestRunner(t, map[string]string{filename: tc.Content})

if err := rule.Check(runner); err != nil {
t.Fatalf("Unexpected error occurred: %s", err)
}
helper.AssertIssues(t, tc.Expected, runner.Issues)

want := map[string]string{}
if tc.Fixed != "" {
want[filename] = tc.Fixed
}
helper.AssertChanges(t, want, runner.Changes())
}
}
3 changes: 3 additions & 0 deletions rules/ephemeral/ephemeral.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
//go:generate go run -tags generators ./generator/main.go

package ephemeral
66 changes: 66 additions & 0 deletions rules/ephemeral/generator/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package main

import (
"strings"

tfjson "github.com/hashicorp/terraform-json"
utils "github.com/terraform-linters/tflint-ruleset-aws/rules/generator-utils"
)

type writeOnlyArgument struct {
OriginalAttribute string
WriteOnlyAlternative string
WriteOnlyVersionAttribute string
}

func main() {
awsProvider := utils.LoadProviderSchema("../../tools/provider-schema/schema.json")

resourcesWithWriteOnly := map[string][]writeOnlyArgument{}
// Iterate over all resources in the AWS provider schema
for resourceName, resource := range awsProvider.ResourceSchemas {
if arguments := writeOnlyArguments(resource); len(arguments) > 0 {
// gather sensitive attributes with write only argument alternatives
resourcesWithWriteOnly[resourceName] = findReplaceableAttribute(arguments, resource)
}
}

// Generate the write-only arguments variable to file
utils.GenerateFile("../../rules/ephemeral/write_only_arguments_gen.go", "../../rules/ephemeral/write_only_arguments_gen.go.tmpl", resourcesWithWriteOnly)
}

func findReplaceableAttribute(arguments []string, resource *tfjson.Schema) []writeOnlyArgument {
writeOnlyArguments := []writeOnlyArgument{}

for _, argument := range arguments {
// Check if the argument ends with "_wo" and if the original attribute without "_wo" suffix exists in the resource schema
attribute := strings.TrimSuffix(argument, "_wo")
versionAttribute := attribute + "_wo_version"
if strings.HasSuffix(argument, "_wo") && resource.Block.Attributes[attribute] != nil && resource.Block.Attributes[versionAttribute] != nil {
writeOnlyArguments = append(writeOnlyArguments, writeOnlyArgument{
OriginalAttribute: attribute,
WriteOnlyAlternative: argument,
WriteOnlyVersionAttribute: versionAttribute,
})
}
}

return writeOnlyArguments
}

func writeOnlyArguments(resource *tfjson.Schema) []string {
if resource == nil || resource.Block == nil {
return []string{}
}

writeOnlyArguments := []string{}

// Check if the resource has any write-only attributes
for name, attribute := range resource.Block.Attributes {
if attribute.WriteOnly {
writeOnlyArguments = append(writeOnlyArguments, name)
}
}

return writeOnlyArguments
}
55 changes: 55 additions & 0 deletions rules/ephemeral/write_only_arguments_gen.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// This file generated by `generator/main.go`. DO NOT EDIT

package ephemeral

var writeOnlyArguments = map[string][]writeOnlyArgument{
"aws_db_instance": {
{
originalAttribute: "password",
writeOnlyAlternative: "password_wo",
writeOnlyVersionAttribute: "password_wo_version",
},
},
"aws_docdb_cluster": {
{
originalAttribute: "master_password",
writeOnlyAlternative: "master_password_wo",
writeOnlyVersionAttribute: "master_password_wo_version",
},
},
"aws_rds_cluster": {
{
originalAttribute: "master_password",
writeOnlyAlternative: "master_password_wo",
writeOnlyVersionAttribute: "master_password_wo_version",
},
},
"aws_redshift_cluster": {
{
originalAttribute: "master_password",
writeOnlyAlternative: "master_password_wo",
writeOnlyVersionAttribute: "master_password_wo_version",
},
},
"aws_redshiftserverless_namespace": {
{
originalAttribute: "admin_user_password",
writeOnlyAlternative: "admin_user_password_wo",
writeOnlyVersionAttribute: "admin_user_password_wo_version",
},
},
"aws_secretsmanager_secret_version": {
{
originalAttribute: "secret_string",
writeOnlyAlternative: "secret_string_wo",
writeOnlyVersionAttribute: "secret_string_wo_version",
},
},
"aws_ssm_parameter": {
{
originalAttribute: "value",
writeOnlyAlternative: "value_wo",
writeOnlyVersionAttribute: "value_wo_version",
},
},
}
15 changes: 15 additions & 0 deletions rules/ephemeral/write_only_arguments_gen.go.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// This file generated by `generator/main.go`. DO NOT EDIT

package ephemeral

var writeOnlyArguments = map[string][]writeOnlyArgument{
{{- range $name, $value := . }}
"{{ $name }}": { {{- range $kk, $writeOnly := $value }}
{
originalAttribute: "{{ $writeOnly.OriginalAttribute }}",
writeOnlyAlternative: "{{ $writeOnly.WriteOnlyAlternative }}",
writeOnlyVersionAttribute: "{{ $writeOnly.WriteOnlyVersionAttribute }}",
},
},
{{- end -}}{{- end }}
}
2 changes: 2 additions & 0 deletions rules/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package rules
import (
"github.com/terraform-linters/tflint-plugin-sdk/tflint"
"github.com/terraform-linters/tflint-ruleset-aws/rules/api"
"github.com/terraform-linters/tflint-ruleset-aws/rules/ephemeral"
"github.com/terraform-linters/tflint-ruleset-aws/rules/models"
)

Expand Down Expand Up @@ -44,6 +45,7 @@ var manualRules = []tflint.Rule{
NewAwsSecurityGroupInlineRulesRule(),
NewAwsSecurityGroupRuleDeprecatedRule(),
NewAwsIAMRoleDeprecatedPolicyAttributesRule(),
ephemeral.NewAwsWriteOnlyArgumentsRule(),
}

// Rules is a list of all rules
Expand Down
Loading