Skip to content

Commit 34886a0

Browse files
committed
fix(secretsmanager): prevent binary secret plaintext at rest and strengthen enrichment test
1 parent 317361a commit 34886a0

2 files changed

Lines changed: 53 additions & 6 deletions

File tree

pkg/cfres/secretsmanager/secret.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,12 @@ func (s *Secret) readWithClients(ctx context.Context, ccxClient ccxReader, smCli
9898
if secret.SecretString != nil {
9999
props["SecretString"] = *secret.SecretString
100100
}
101-
if secret.SecretBinary != nil {
102-
props["SecretBinary"] = secret.SecretBinary
103-
}
101+
// SecretBinary is intentionally not enriched here. The agent's opaque-hashing
102+
// table (persist_value_transformer.knownOpaqueFields) only covers SecretString
103+
// for this resource type; returning SecretBinary as a plain []byte would store
104+
// the raw binary secret as base64 plaintext at rest with no hashing. Binary
105+
// secrets remain unresolvable via formae until the agent core and this plugin's
106+
// schema are updated in concert to add SecretBinary opaque coverage.
104107

105108
completeProps, err := json.Marshal(props)
106109
if err != nil {

pkg/cfres/secretsmanager/secret_test.go

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ func (m *mockSMClient) GetSecretValue(ctx context.Context, input *secretsmanager
5050

5151
func TestSecret_Read_AlwaysEnrichesSecretValue(t *testing.T) {
5252
// Read unconditionally enriches: the secret value is always fetched and
53-
// merged into the result properties.
53+
// merged into the result properties regardless of the RedactSensitive flag.
54+
// Setting RedactSensitive=true proves the plugin no longer short-circuits on
55+
// that flag (the whole point of this branch's removal).
5456
ctx := context.Background()
5557
ccxMock := &mockCCXReader{}
5658
smMock := &mockSMClient{}
@@ -68,9 +70,12 @@ func TestSecret_Read_AlwaysEnrichesSecretValue(t *testing.T) {
6870
}, nil)
6971

7072
s := &Secret{cfg: &config.Config{}}
73+
// RedactSensitive=true is the flag the old code used to skip enrichment.
74+
// With the flag removed, enrichment must still fire.
7175
result, err := s.readWithClients(ctx, ccxMock, smMock, &resource.ReadRequest{
72-
NativeID: "my-secret-id",
73-
ResourceType: "AWS::SecretsManager::Secret",
76+
NativeID: "my-secret-id",
77+
ResourceType: "AWS::SecretsManager::Secret",
78+
RedactSensitive: true,
7479
})
7580

7681
require.NoError(t, err)
@@ -84,6 +89,45 @@ func TestSecret_Read_AlwaysEnrichesSecretValue(t *testing.T) {
8489
smMock.AssertExpectations(t)
8590
}
8691

92+
func TestSecret_Read_SecretBinaryNotEnriched(t *testing.T) {
93+
// SecretBinary is intentionally excluded from enrichment: the agent's
94+
// opaque-hashing table only covers SecretString for this resource type.
95+
// Returning raw binary data would persist it as base64 plaintext at rest.
96+
// This test asserts the absence of SecretBinary in the result properties
97+
// when the AWS API returns a binary secret.
98+
ctx := context.Background()
99+
ccxMock := &mockCCXReader{}
100+
smMock := &mockSMClient{}
101+
102+
ccxMock.On("ReadResource", ctx, mock.Anything).Return(&resource.ReadResult{
103+
ResourceType: "AWS::SecretsManager::Secret",
104+
Properties: `{"Name":"binary-secret"}`,
105+
}, nil)
106+
107+
smMock.On("GetSecretValue", ctx, mock.Anything).Return(&secretsmanager.GetSecretValueOutput{
108+
SecretBinary: []byte("binary-payload"),
109+
}, nil)
110+
111+
s := &Secret{cfg: &config.Config{}}
112+
result, err := s.readWithClients(ctx, ccxMock, smMock, &resource.ReadRequest{
113+
NativeID: "binary-secret-id",
114+
ResourceType: "AWS::SecretsManager::Secret",
115+
})
116+
117+
require.NoError(t, err)
118+
require.NotNil(t, result)
119+
120+
var props map[string]any
121+
require.NoError(t, json.Unmarshal([]byte(result.Properties), &props))
122+
_, hasSecretBinary := props["SecretBinary"]
123+
assert.False(t, hasSecretBinary, "SecretBinary must not be present in result (no opaque coverage in agent)")
124+
_, hasSecretString := props["SecretString"]
125+
assert.False(t, hasSecretString, "SecretString must not be present when the secret has no string value")
126+
127+
ccxMock.AssertExpectations(t)
128+
smMock.AssertExpectations(t)
129+
}
130+
87131
func TestSecret_Read_EnrichesEvenWhenCCXReturnsEmptyProperties(t *testing.T) {
88132
// Enrichment works even when Cloud Control returns an empty properties blob.
89133
ctx := context.Background()

0 commit comments

Comments
 (0)