Skip to content

Commit d4d96c6

Browse files
authored
feat: warn against data sources with ephemeral alternatives (#861)
* feat: add suggestions to replace data sources with ephemeral alternatives * fix: implement remarks * fix: doc generation fix * fix: resolve comments
1 parent aec7f30 commit d4d96c6

10 files changed

+231
-12
lines changed

docs/rules/README.md

+7-5
Original file line numberDiff line numberDiff line change
@@ -61,24 +61,26 @@ These rules enforce best practices and naming conventions:
6161
|Rule|Description|Enabled by default|
6262
| --- | --- | --- |
6363
|[aws_acm_certificate_lifecycle](aws_acm_certificate_lifecycle.md)|Disallow adding `aws_acm_certificate` resource without setting `create_before_destroy = true` in `lifecycle` block ||
64-
|[aws_db_instance_previous_type](aws_db_instance_previous_type.md)|Disallow using previous generation instance types||
6564
|[aws_db_instance_default_parameter_group](aws_db_instance_default_parameter_group.md)|Disallow using default DB parameter group||
66-
|[aws_elasticache_cluster_previous_type](aws_elasticache_cluster_previous_type.md)|Disallow using previous node types||
65+
|[aws_db_instance_previous_type](aws_db_instance_previous_type.md)|Disallow using previous generation instance types||
6766
|[aws_elasticache_cluster_default_parameter_group](aws_elasticache_cluster_default_parameter_group.md)|Disallow using default parameter group||
68-
|[aws_elasticache_replication_group_previous_type](aws_elasticache_replication_group_previous_type.md)|Disallow using previous node types||
67+
|[aws_elasticache_cluster_previous_type](aws_elasticache_cluster_previous_type.md)|Disallow using previous node types||
6968
|[aws_elasticache_replication_group_default_parameter_group](aws_elasticache_replication_group_default_parameter_group.md)|Disallow using default parameter group||
70-
|[aws_instance_previous_type](aws_instance_previous_type.md)|Disallow using previous generation instance types||
69+
|[aws_elasticache_replication_group_previous_type](aws_elasticache_replication_group_previous_type.md)|Disallow using previous node types||
70+
|[aws_ephemeral_resources](aws_ephemeral_resources.md)|Recommends using available ephemeral resources instead of the original data source. This is only valid for Terraform v1.10+.||
7171
|[aws_iam_policy_attachment_exclusive_attachment](aws_iam_policy_attachment_exclusive_attachment.md)|Consider alternative resources to `aws_iam_policy_attachment`||
7272
|[aws_iam_policy_document_gov_friendly_arns](aws_iam_policy_document_gov_friendly_arns.md)|Ensure `iam_policy_document` data sources do not contain `arn:aws:` ARN's||
7373
|[aws_iam_policy_gov_friendly_arns](aws_iam_policy_gov_friendly_arns.md)|Ensure `iam_policy` resources do not contain `arn:aws:` ARN's||
7474
|[aws_iam_role_deprecated_policy_attributes](aws_iam_role_deprecated_policy_attributes.md)|Disallow using deprecated policy attributes of `aws_iam_role`||
7575
|[aws_iam_role_policy_gov_friendly_arns](aws_iam_role_policy_gov_friendly_arns.md)|Ensure `iam_role_policy` resources do not contain `arn:aws:` ARN's||
76+
|[aws_instance_previous_type](aws_instance_previous_type.md)|Disallow using previous generation instance types||
7677
|[aws_lambda_function_deprecated_runtime](aws_lambda_function_deprecated_runtime.md)|Disallow deprecated runtimes for Lambda Function||
78+
|[aws_provider_missing_default_tags](aws_provider_missing_default_tags.md)|Require specific tags for all AWS providers default tags||
7779
|[aws_resource_missing_tags](aws_resource_missing_tags.md)|Require specific tags for all AWS resource types that support them||
7880
|[aws_s3_bucket_name](aws_s3_bucket_name.md)|Ensures all S3 bucket names match the naming rules||
7981
|[aws_security_group_inline_rules](aws_security_group_inline_rules.md)|Disallow `ingress` and `egress` arguments of the `aws_security_group` resource||
8082
|[aws_security_group_rule_deprecated](aws_security_group_rule_deprecated.md)|Disallow using `aws_security_group_rule` resource||
81-
|[aws_provider_missing_default_tags](aws_provider_missing_default_tags.md)|Require specific tags for all AWS providers default tags||
83+
|[aws_write_only_arguments](aws_write_only_arguments.md)|Recommends using available write-only arguments instead of the original sensitive attribute. This is only valid for Terraform v1.11+.||
8284

8385
### SDK-based Validations
8486

docs/rules/README.md.tmpl

+7-5
Original file line numberDiff line numberDiff line change
@@ -61,24 +61,26 @@ These rules enforce best practices and naming conventions:
6161
|Rule|Description|Enabled by default|
6262
| --- | --- | --- |
6363
|[aws_acm_certificate_lifecycle](aws_acm_certificate_lifecycle.md)|Disallow adding `aws_acm_certificate` resource without setting `create_before_destroy = true` in `lifecycle` block |✔|
64-
|[aws_db_instance_previous_type](aws_db_instance_previous_type.md)|Disallow using previous generation instance types|✔|
6564
|[aws_db_instance_default_parameter_group](aws_db_instance_default_parameter_group.md)|Disallow using default DB parameter group|✔|
66-
|[aws_elasticache_cluster_previous_type](aws_elasticache_cluster_previous_type.md)|Disallow using previous node types|✔|
65+
|[aws_db_instance_previous_type](aws_db_instance_previous_type.md)|Disallow using previous generation instance types|✔|
6766
|[aws_elasticache_cluster_default_parameter_group](aws_elasticache_cluster_default_parameter_group.md)|Disallow using default parameter group|✔|
68-
|[aws_elasticache_replication_group_previous_type](aws_elasticache_replication_group_previous_type.md)|Disallow using previous node types|✔|
67+
|[aws_elasticache_cluster_previous_type](aws_elasticache_cluster_previous_type.md)|Disallow using previous node types|✔|
6968
|[aws_elasticache_replication_group_default_parameter_group](aws_elasticache_replication_group_default_parameter_group.md)|Disallow using default parameter group|✔|
70-
|[aws_instance_previous_type](aws_instance_previous_type.md)|Disallow using previous generation instance types|✔|
69+
|[aws_elasticache_replication_group_previous_type](aws_elasticache_replication_group_previous_type.md)|Disallow using previous node types|✔|
70+
|[aws_ephemeral_resources](aws_ephemeral_resources.md)|Recommends using available ephemeral resources instead of the original data source. This is only valid for Terraform v1.10+.||
7171
|[aws_iam_policy_attachment_exclusive_attachment](aws_iam_policy_attachment_exclusive_attachment.md)|Consider alternative resources to `aws_iam_policy_attachment`||
7272
|[aws_iam_policy_document_gov_friendly_arns](aws_iam_policy_document_gov_friendly_arns.md)|Ensure `iam_policy_document` data sources do not contain `arn:aws:` ARN's||
7373
|[aws_iam_policy_gov_friendly_arns](aws_iam_policy_gov_friendly_arns.md)|Ensure `iam_policy` resources do not contain `arn:aws:` ARN's||
7474
|[aws_iam_role_deprecated_policy_attributes](aws_iam_role_deprecated_policy_attributes.md)|Disallow using deprecated policy attributes of `aws_iam_role`||
7575
|[aws_iam_role_policy_gov_friendly_arns](aws_iam_role_policy_gov_friendly_arns.md)|Ensure `iam_role_policy` resources do not contain `arn:aws:` ARN's||
76+
|[aws_instance_previous_type](aws_instance_previous_type.md)|Disallow using previous generation instance types|✔|
7677
|[aws_lambda_function_deprecated_runtime](aws_lambda_function_deprecated_runtime.md)|Disallow deprecated runtimes for Lambda Function|✔|
78+
|[aws_provider_missing_default_tags](aws_provider_missing_default_tags.md)|Require specific tags for all AWS providers default tags||
7779
|[aws_resource_missing_tags](aws_resource_missing_tags.md)|Require specific tags for all AWS resource types that support them||
7880
|[aws_s3_bucket_name](aws_s3_bucket_name.md)|Ensures all S3 bucket names match the naming rules|✔|
7981
|[aws_security_group_inline_rules](aws_security_group_inline_rules.md)|Disallow `ingress` and `egress` arguments of the `aws_security_group` resource||
8082
|[aws_security_group_rule_deprecated](aws_security_group_rule_deprecated.md)|Disallow using `aws_security_group_rule` resource||
81-
|[aws_provider_missing_default_tags](aws_provider_missing_default_tags.md)|Require specific tags for all AWS providers default tags||
83+
|[aws_write_only_arguments](aws_write_only_arguments.md)|Recommends using available write-only arguments instead of the original sensitive attribute. This is only valid for Terraform v1.11+.||
8284

8385
### SDK-based Validations
8486

docs/rules/aws_ephemeral_resources.md

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# aws_ephemeral_resources
2+
3+
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+.
4+
5+
## Example
6+
7+
This example uses `aws_secretsmanager_random_password`, but the rule applies to all data sources with an ephemeral equivalent:
8+
9+
```hcl
10+
data "aws_secretsmanager_random_password" "test" {
11+
password_length = 50
12+
exclude_numbers = true
13+
}
14+
```
15+
16+
```
17+
$ tflint
18+
1 issue(s) found:
19+
20+
Warning: "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)
21+
22+
on test.tf line 2:
23+
2: data "aws_secretsmanager_random_password" "test"
24+
25+
```
26+
27+
## Why
28+
29+
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.
30+
31+
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.
32+
33+
## How To Fix
34+
35+
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.
36+
37+
In case of the previously shown `aws_secretsmanager_random_password` data source, replace `data` by `ephemeral`:
38+
39+
```hcl
40+
ephemeral "aws_secretsmanager_random_password" "test" {
41+
password_length = 50
42+
exclude_numbers = true
43+
}
44+
```
+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package ephemeral
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/terraform-linters/tflint-plugin-sdk/hclext"
7+
"github.com/terraform-linters/tflint-plugin-sdk/tflint"
8+
"github.com/terraform-linters/tflint-ruleset-aws/project"
9+
)
10+
11+
// AwsEphemeralResourcesRule checks for data sources which can be replaced by ephemeral resources
12+
type AwsEphemeralResourcesRule struct {
13+
tflint.DefaultRule
14+
15+
replacingEphemeralResources []string
16+
}
17+
18+
// NewAwsEphemeralResourcesRule returns new rule with default attributes
19+
func NewAwsEphemeralResourcesRule() *AwsEphemeralResourcesRule {
20+
return &AwsEphemeralResourcesRule{
21+
replacingEphemeralResources: replacingEphemeralResources,
22+
}
23+
}
24+
25+
// Name returns the rule name
26+
func (r *AwsEphemeralResourcesRule) Name() string {
27+
return "aws_ephemeral_resources"
28+
}
29+
30+
// Enabled returns whether the rule is enabled by default
31+
func (r *AwsEphemeralResourcesRule) Enabled() bool {
32+
return false
33+
}
34+
35+
// Severity returns the rule severity
36+
func (r *AwsEphemeralResourcesRule) Severity() tflint.Severity {
37+
return tflint.WARNING
38+
}
39+
40+
// Link returns the rule reference link
41+
func (r *AwsEphemeralResourcesRule) Link() string {
42+
return project.ReferenceLink(r.Name())
43+
}
44+
45+
// Check checks if there is an ephemeral resource which can replace an data source
46+
func (r *AwsEphemeralResourcesRule) Check(runner tflint.Runner) error {
47+
for _, resourceType := range r.replacingEphemeralResources {
48+
resources, err := GetDataSourceContent(runner, resourceType, &hclext.BodySchema{}, nil)
49+
if err != nil {
50+
return err
51+
}
52+
53+
for _, resource := range resources.Blocks {
54+
if err := runner.EmitIssue(
55+
r,
56+
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),
57+
resource.TypeRange,
58+
); err != nil {
59+
return fmt.Errorf("failed to call EmitIssue(): %w", err)
60+
}
61+
}
62+
}
63+
64+
return nil
65+
}
66+
67+
func GetDataSourceContent(r tflint.Runner, name string, schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, error) {
68+
body, err := r.GetModuleContent(&hclext.BodySchema{
69+
Blocks: []hclext.BlockSchema{
70+
{Type: "data", LabelNames: []string{"type", "name"}, Body: schema},
71+
},
72+
}, opts)
73+
if err != nil {
74+
return nil, err
75+
}
76+
77+
content := &hclext.BodyContent{Blocks: []*hclext.Block{}}
78+
for _, resource := range body.Blocks {
79+
if resource.Labels[0] != name {
80+
continue
81+
}
82+
83+
content.Blocks = append(content.Blocks, resource)
84+
}
85+
86+
return content, nil
87+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package ephemeral
2+
3+
import (
4+
"testing"
5+
6+
"github.com/hashicorp/hcl/v2"
7+
"github.com/terraform-linters/tflint-plugin-sdk/helper"
8+
)
9+
10+
func Test_AwsEphemeralResources(t *testing.T) {
11+
cases := []struct {
12+
Name string
13+
Content string
14+
Expected helper.Issues
15+
Fixed string
16+
}{
17+
{
18+
Name: "basic aws_eks_cluster_auth",
19+
Content: `
20+
data "aws_eks_cluster_auth" "test" {
21+
}
22+
`,
23+
Expected: helper.Issues{
24+
{
25+
Rule: NewAwsEphemeralResourcesRule(),
26+
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.`,
27+
Range: hcl.Range{
28+
Filename: "resource.tf",
29+
Start: hcl.Pos{Line: 2, Column: 1},
30+
End: hcl.Pos{Line: 2, Column: 5},
31+
},
32+
},
33+
},
34+
},
35+
}
36+
37+
rule := NewAwsEphemeralResourcesRule()
38+
39+
for _, tc := range cases {
40+
filename := "resource.tf"
41+
runner := helper.TestRunner(t, map[string]string{filename: tc.Content})
42+
43+
if err := rule.Check(runner); err != nil {
44+
t.Fatalf("Unexpected error occurred: %s", err)
45+
}
46+
helper.AssertIssues(t, tc.Expected, runner.Issues)
47+
}
48+
}
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// This file generated by `generator/main.go`. DO NOT EDIT
2+
3+
package ephemeral
4+
5+
var replacingEphemeralResources = []string{
6+
"aws_eks_cluster_auth",
7+
"aws_kms_secrets",
8+
"aws_lambda_invocation",
9+
"aws_secretsmanager_random_password",
10+
"aws_secretsmanager_secret_version",
11+
"aws_ssm_parameter",
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// This file generated by `generator/main.go`. DO NOT EDIT
2+
3+
package ephemeral
4+
5+
var replacingEphemeralResources = []string{
6+
{{- range $value := . }}
7+
"{{ $value}}",
8+
{{- end }}
9+
}

rules/ephemeral/generator/main.go

+15-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"slices"
45
"strings"
56

67
tfjson "github.com/hashicorp/terraform-json"
@@ -25,8 +26,21 @@ func main() {
2526
}
2627
}
2728

28-
// Generate the write-only arguments variable to file
29+
// Generate the write-only arguments variable
2930
utils.GenerateFile("../../rules/ephemeral/write_only_arguments_gen.go", "../../rules/ephemeral/write_only_arguments_gen.go.tmpl", resourcesWithWriteOnly)
31+
32+
ephemeralResourcesAsDataAlternative := []string{}
33+
// Iterate over all ephemeral resources in the AWS provider schema
34+
for resourceName, _ := range awsProvider.EphemeralResourceSchemas {
35+
if awsProvider.DataSourceSchemas[resourceName] != nil {
36+
ephemeralResourcesAsDataAlternative = append(ephemeralResourcesAsDataAlternative, resourceName)
37+
}
38+
}
39+
40+
slices.Sort(ephemeralResourcesAsDataAlternative)
41+
42+
// Generate the ephemeral resources variable
43+
utils.GenerateFile("../../rules/ephemeral/ephemeral_resources_gen.go", "../../rules/ephemeral/ephemeral_resources_gen.go.tmpl", ephemeralResourcesAsDataAlternative)
3044
}
3145

3246
func findReplaceableAttribute(arguments []string, resource *tfjson.Schema) []writeOnlyArgument {

rules/provider.go

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ var manualRules = []tflint.Rule{
4646
NewAwsSecurityGroupRuleDeprecatedRule(),
4747
NewAwsIAMRoleDeprecatedPolicyAttributesRule(),
4848
ephemeral.NewAwsWriteOnlyArgumentsRule(),
49+
ephemeral.NewAwsEphemeralResourcesRule(),
4950
}
5051

5152
// Rules is a list of all rules
+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.1.2
1+
1.11.2

0 commit comments

Comments
 (0)