generated from terraform-linters/tflint-ruleset-template
-
Notifications
You must be signed in to change notification settings - Fork 76
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
bendrucker
merged 15 commits into
terraform-linters:master
from
aristosvo:feat/warn-against-non-ephemeral-secret-attributes
Apr 24, 2025
Merged
Changes from 14 commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
f017f28
Validate against secret attributes
aristosvo 1a5f215
fix: turn it into one rule for all write-only attributes
aristosvo 97cfaa9
feat: also suggest the managed password solutions available
aristosvo 092ef59
docs: update docs based on suggested alternatives
aristosvo 6852f20
fix: suggestion 1
aristosvo b0b5312
fix: suggestion 2
aristosvo 23ef504
fix: suggestion 3
aristosvo 962d77b
fix: suggestion 4
aristosvo a5c5b1c
fix: disabled by default, DB Instance added, wo attribute -> wo argum…
aristosvo 2ea69dd
feat: update for auto gen
aristosvo e051181
refactor: moving files around + DO NOT EDIT comment
aristosvo 52316d5
refactor: rename package to `ephemeral`
aristosvo 6780682
fix: add *_wo_version attribute via autofix as well
aristosvo 2b61e58
refactor: only generate write only arguments map, reduce tests
aristosvo 0057405
fix: adjust for comments
aristosvo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
package ephemeral | ||
|
||
import ( | ||
"testing" | ||
|
||
"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".`, | ||
}, | ||
}, | ||
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.AssertIssuesWithoutRange(t, tc.Expected, runner.Issues) | ||
aristosvo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
want := map[string]string{} | ||
if tc.Fixed != "" { | ||
want[filename] = tc.Fixed | ||
} | ||
helper.AssertChanges(t, want, runner.Changes()) | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
//go:build generators | ||
aristosvo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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 | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
}, | ||
}, | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.