Skip to content

Commit 5391e85

Browse files
committed
fix(e2e): secure pulumi host passwords
1 parent f93514b commit 5391e85

5 files changed

Lines changed: 106 additions & 8 deletions

File tree

test/e2e-framework/resources/azure/compute/vm.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,10 @@ func NewWindowsInstance(e azure.Environment, name, imageUrn, instanceType string
6666
pwdOpts := make([]pulumi.ResourceOption, 0, len(opts)+1)
6767
copy(pwdOpts, opts)
6868
pwdOpts = append(pwdOpts, e.WithProviders(config.ProviderRandom))
69-
windowsAdminPassword, err := random.NewRandomString(e.Ctx(), e.Namer.ResourceName(name, "admin-password"), &random.RandomStringArgs{
69+
windowsAdminPassword, err := random.NewRandomPassword(e.Ctx(), e.Namer.ResourceName(name, "admin-password"), &random.RandomPasswordArgs{
7070
Length: pulumi.Int(20),
7171
Special: pulumi.Bool(true),
72-
// Disallow "<", ">" and "&" as they get encoded by json.Marshall in the CI log output, making the password hard to read
72+
// Avoid characters that are awkward in generated PowerShell and Pulumi JSON.
7373
OverrideSpecial: pulumi.String("!@#$%*()-_=+[]{}:?"),
7474
}, pwdOpts...)
7575
if err != nil {
@@ -132,7 +132,7 @@ func NewWindowsInstance(e azure.Environment, name, imageUrn, instanceType string
132132
return args[0].(string)
133133
}).(pulumi.StringOutput)
134134

135-
return vm, privateIP, windowsAdminPassword.Result, nil
135+
return vm, privateIP, pulumi.ToSecret(windowsAdminPassword.Result).(pulumi.StringOutput), nil
136136
}
137137

138138
func newVMInstance(e azure.Environment, name, imageUrn, instanceType string, enableAcceleratedNetworking bool, osProfile compute.OSProfilePtrInput, opts ...pulumi.ResourceOption) (*compute.VirtualMachine, *network.NetworkInterface, error) {

test/e2e-framework/scenarios/aws/ec2/vm.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,10 @@ func NewVM(e aws.Environment, name string, params ...VMOption) (*remote.Host, er
108108
// * Base 10 digits (0 through 9).
109109
// * Non-alphanumeric characters (special characters): '-!"#$%&()*,./:;?@[]^_`{|}~+<=>
110110
// Source: https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/security-policy-settings/password-must-meet-complexity-requirements
111-
randomPassword, err := random.NewRandomString(e.Ctx(), e.Namer.ResourceName(name, "win-admin-password"), &random.RandomStringArgs{
111+
randomPassword, err := random.NewRandomPassword(e.Ctx(), e.Namer.ResourceName(name, "win-admin-password"), &random.RandomPasswordArgs{
112112
Length: pulumi.Int(20),
113113
Special: pulumi.Bool(true),
114-
// Disallow "<", ">" and "&" as they get encoded by json.Marshall in the CI log output, making the password hard to read
114+
// Avoid characters that are awkward in the generated PowerShell command and Pulumi JSON.
115115
OverrideSpecial: pulumi.String("!@#$%*()-_=+[]{}:?"),
116116
MinLower: pulumi.Int(1),
117117
MinUpper: pulumi.Int(1),
@@ -129,7 +129,7 @@ func NewVM(e aws.Environment, name string, params ...VMOption) (*remote.Host, er
129129
return err
130130
}
131131

132-
c.Password = randomPassword.Result
132+
c.Password = pulumi.ToSecret(randomPassword.Result).(pulumi.StringOutput)
133133
}
134134

135135
return nil

test/e2e-framework/testing/provisioners/BUILD.bazel

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
load("@rules_go//go:def.bzl", "go_library")
1+
load("@rules_go//go:def.bzl", "go_library", "go_test")
22

33
go_library(
44
name = "provisioners",
@@ -15,3 +15,10 @@ go_library(
1515
"@com_github_pulumi_pulumi_sdk_v3//go/pulumi",
1616
],
1717
)
18+
19+
go_test(
20+
name = "provisioners_test",
21+
srcs = ["pulumi_provisioner_test.go"],
22+
embed = [":provisioners"],
23+
gotags = ["test"],
24+
)

test/e2e-framework/testing/provisioners/pulumi_provisioner.go

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package provisioners
77

88
import (
9+
"bytes"
910
"context"
1011
"encoding/json"
1112
"fmt"
@@ -113,11 +114,56 @@ func (pp *PulumiProvisioner[Env]) ProvisionEnv(ctx context.Context, stackName st
113114
func dumpRawResources(resources RawResources) string {
114115
var builder strings.Builder
115116
for key, value := range resources {
116-
fmt.Fprintf(&builder, "%s: %s\n", key, value)
117+
fmt.Fprintf(&builder, "%s: %s\n", key, redactRawResource(value))
117118
}
118119
return builder.String()
119120
}
120121

122+
func redactRawResource(rawResource []byte) []byte {
123+
var resource any
124+
if err := json.Unmarshal(rawResource, &resource); err != nil {
125+
return []byte("<invalid resource output>")
126+
}
127+
128+
redactedResource := redactSensitiveFields(resource)
129+
var buffer bytes.Buffer
130+
encoder := json.NewEncoder(&buffer)
131+
encoder.SetEscapeHTML(false)
132+
encoder.SetIndent("", "\t")
133+
if err := encoder.Encode(redactedResource); err != nil {
134+
return []byte("<invalid resource output>")
135+
}
136+
137+
return bytes.TrimSuffix(buffer.Bytes(), []byte("\n"))
138+
}
139+
140+
func redactSensitiveFields(value any) any {
141+
switch typedValue := value.(type) {
142+
case map[string]any:
143+
redacted := make(map[string]any, len(typedValue))
144+
for key, nestedValue := range typedValue {
145+
if isSensitiveField(key) {
146+
redacted[key] = "<redacted>"
147+
continue
148+
}
149+
redacted[key] = redactSensitiveFields(nestedValue)
150+
}
151+
return redacted
152+
case []any:
153+
redacted := make([]any, len(typedValue))
154+
for index, nestedValue := range typedValue {
155+
redacted[index] = redactSensitiveFields(nestedValue)
156+
}
157+
return redacted
158+
default:
159+
return value
160+
}
161+
}
162+
163+
func isSensitiveField(key string) bool {
164+
return strings.Contains(strings.ToLower(key), "password")
165+
}
166+
121167
// Diagnose runs the diagnose function if it is set diagnoseFunc
122168
func (pp *PulumiProvisioner[Env]) Diagnose(ctx context.Context, stackName string) (string, error) {
123169
if pp.diagnoseFunc != nil {
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed
2+
// under the Apache License Version 2.0.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
// Copyright 2016-present Datadog, Inc.
5+
6+
package provisioners
7+
8+
import (
9+
"strings"
10+
"testing"
11+
)
12+
13+
func TestDumpRawResourcesRedactsPasswords(t *testing.T) {
14+
resources := RawResources{
15+
"dd-Host-aws-vm": []byte(`{
16+
"address": "10.0.0.1",
17+
"password": "top-secret-password",
18+
"nested": {
19+
"adminPassword": "nested-secret-password",
20+
"username": "Administrator"
21+
}
22+
}`),
23+
}
24+
25+
output := dumpRawResources(resources)
26+
27+
for _, secret := range []string{"top-secret-password", "nested-secret-password"} {
28+
if strings.Contains(output, secret) {
29+
t.Fatalf("dumpRawResources leaked secret %q in output:\n%s", secret, output)
30+
}
31+
}
32+
for _, expected := range []string{
33+
`"password": "<redacted>"`,
34+
`"adminPassword": "<redacted>"`,
35+
`"username": "Administrator"`,
36+
} {
37+
if !strings.Contains(output, expected) {
38+
t.Fatalf("dumpRawResources output does not contain %q:\n%s", expected, output)
39+
}
40+
}
41+
42+
if !strings.Contains(string(resources["dd-Host-aws-vm"]), "top-secret-password") {
43+
t.Fatal("dumpRawResources modified the raw resources used for environment imports")
44+
}
45+
}

0 commit comments

Comments
 (0)