Skip to content

feat: warn against data sources with ephemeral alternatives #861

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
44 changes: 44 additions & 0 deletions docs/rules/aws_ephemeral_resources.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# aws_ephemeral_resources

Recommends using available [ephemeral resources](https://developer.hashicorp.com/terraform/language/resources/ephemeral/reference) instead of the original data source. This is only valid for Terraform v1.10+.

## Example

This example uses `aws_secretsmanager_random_password`, but the rule applies to all data sources with an ephemeral equivalent:

```hcl
data "aws_secretsmanager_random_password" "test" {
password_length = 50
exclude_numbers = true
}
```

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

Warning: [Fixable] "aws_secretsmanager_random_password" is a non-ephemeral data source, which means that all (sensitive) attributes are stored in state. Please use ephemeral resource "aws_secretsmanager_random_password" instead. (aws_ephemeral_resources)

on test.tf line 2:
2: data "aws_secretsmanager_random_password" "test"

```

## 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 ephemeral resources, which are not persisted to state. Other resources can refer to their values, but executing of the lookup is defered until the apply stage.

Using ephemeral resources mitigates the risk of a malicious actor obtaining privileged credentials by accessing Terraform state files directly. Prefer using them over the original data sources for sensitive data.

## How To Fix

Replace the data source with its ephemeral resource equivalent. Use resources with write-only arguments or in provider configuration to ensure that the sensitive value is not persisted to state.

In case of the previously shown `aws_secretsmanager_random_password` data source, replace `data` by `ephemeral`:

```hcl
ephemeral "aws_secretsmanager_random_password" "test" {
password_length = 50
exclude_numbers = true
}
```
90 changes: 90 additions & 0 deletions rules/ephemeral/aws_ephemeral_resources.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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"
)

// AwsEphemeralResourcesRule checks for data sources which can be replaced by ephemeral resources
type AwsEphemeralResourcesRule struct {
tflint.DefaultRule

replacingEphemeralResources []string
}

// NewAwsEphemeralResourcesRule returns new rule with default attributes
func NewAwsEphemeralResourcesRule() *AwsEphemeralResourcesRule {
return &AwsEphemeralResourcesRule{
replacingEphemeralResources: replacingEphemeralResources,
}
}

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

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

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

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

// Check checks if there is an ephemeral resource which can replace an data source
func (r *AwsEphemeralResourcesRule) Check(runner tflint.Runner) error {
for _, resourceType := range r.replacingEphemeralResources {
resources, err := GetDataSourceContent(runner, resourceType, &hclext.BodySchema{}, nil)
if err != nil {
return err
}

for _, resource := range resources.Blocks {
if err := runner.EmitIssueWithFix(
r,
fmt.Sprintf("\"%s\" is a non-ephemeral data source, which means that all (sensitive) attributes are stored in state. Please use ephemeral resource \"%s\" instead.", resourceType, resourceType),
resource.TypeRange,
func(f tflint.Fixer) error {
return f.ReplaceText(resource.TypeRange, "ephemeral")
},
); err != nil {
return fmt.Errorf("failed to call EmitIssueWithFix(): %w", err)
}
}
}

return nil
}

func GetDataSourceContent(r tflint.Runner, name string, schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, error) {
body, err := r.GetModuleContent(&hclext.BodySchema{
Blocks: []hclext.BlockSchema{
{Type: "data", LabelNames: []string{"type", "name"}, Body: schema},
},
}, opts)
if err != nil {
return nil, err
}

content := &hclext.BodyContent{Blocks: []*hclext.Block{}}
for _, resource := range body.Blocks {
if resource.Labels[0] != name {
continue
}

content.Blocks = append(content.Blocks, resource)
}

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

import (
"testing"

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

func Test_AwsEphemeralResources(t *testing.T) {
cases := []struct {
Name string
Content string
Expected helper.Issues
Fixed string
}{
{
Name: "basic aws_eks_cluster_auth",
Content: `
data "aws_eks_cluster_auth" "test" {
}
`,
Expected: helper.Issues{
{
Rule: NewAwsEphemeralResourcesRule(),
Message: `"aws_eks_cluster_auth" is a non-ephemeral data source, which means that all (sensitive) attributes are stored in state. Please use ephemeral resource "aws_eks_cluster_auth" instead.`,
Range: hcl.Range{
Filename: "resource.tf",
Start: hcl.Pos{Line: 2, Column: 1},
End: hcl.Pos{Line: 2, Column: 5},
},
},
},
Fixed: `
ephemeral "aws_eks_cluster_auth" "test" {
}
`,
},
}

rule := NewAwsEphemeralResourcesRule()

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())
}
}
12 changes: 12 additions & 0 deletions rules/ephemeral/ephemeral_resources_gen.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// This file generated by `generator/main.go`. DO NOT EDIT

package ephemeral

var replacingEphemeralResources = []string{
"aws_eks_cluster_auth",
"aws_kms_secrets",
"aws_lambda_invocation",
"aws_secretsmanager_random_password",
"aws_secretsmanager_secret_version",
"aws_ssm_parameter",
}
9 changes: 9 additions & 0 deletions rules/ephemeral/ephemeral_resources_gen.go.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// This file generated by `generator/main.go`. DO NOT EDIT

package ephemeral

var replacingEphemeralResources = []string{
{{- range $value := . }}
"{{ $value}}",
{{- end }}
}
16 changes: 15 additions & 1 deletion rules/ephemeral/generator/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"slices"
"strings"

tfjson "github.com/hashicorp/terraform-json"
Expand All @@ -25,8 +26,21 @@ func main() {
}
}

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

ephemeralResourcesAsDataAlternative := []string{}
// Iterate over all ephemeral resources in the AWS provider schema
for resourceName, _ := range awsProvider.EphemeralResourceSchemas {
if awsProvider.DataSourceSchemas[resourceName] != nil {
ephemeralResourcesAsDataAlternative = append(ephemeralResourcesAsDataAlternative, resourceName)
}
}

slices.Sort(ephemeralResourcesAsDataAlternative)

// Generate the ephemeral resources variable
utils.GenerateFile("../../rules/ephemeral/ephemeral_resources_gen.go", "../../rules/ephemeral/ephemeral_resources_gen.go.tmpl", ephemeralResourcesAsDataAlternative)
}

func findReplaceableAttribute(arguments []string, resource *tfjson.Schema) []writeOnlyArgument {
Expand Down
1 change: 1 addition & 0 deletions rules/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ var manualRules = []tflint.Rule{
NewAwsSecurityGroupRuleDeprecatedRule(),
NewAwsIAMRoleDeprecatedPolicyAttributesRule(),
ephemeral.NewAwsWriteOnlyArgumentsRule(),
ephemeral.NewAwsEphemeralResourcesRule(),
}

// Rules is a list of all rules
Expand Down
2 changes: 1 addition & 1 deletion tools/provider-schema/.terraform-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.1.2
1.11.2