Skip to content
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
23 changes: 20 additions & 3 deletions internal/pkg/api/handleCheckin.go
Original file line number Diff line number Diff line change
Expand Up @@ -903,13 +903,15 @@ func processPolicy(ctx context.Context, zlog zerolog.Logger, bulker bulk.Bulk, a
}

data := model.ClonePolicyData(pp.Policy.Data)
for _, policyOutput := range data.Outputs {
for name, policyOutput := range data.Outputs {
// NOTE: Not sure if output secret keys collected here include new entries, but they are collected for completeness
ks, err := secret.ProcessOutputSecret(policyOutput, secretValues)
if err != nil {
return nil, fmt.Errorf("failed to process output secret for output %q: %w", policyOutput["name"], err)
return nil, fmt.Errorf("failed to process output secret for output %q: %w", name, err)
}
for _, key := range ks {
pp.SecretKeys = append(pp.SecretKeys, "outputs."+name+"."+key)
}
pp.SecretKeys = append(pp.SecretKeys, ks...)
}
// Iterate through the policy outputs and prepare them
for _, policyOutput := range pp.Outputs {
Expand All @@ -918,6 +920,21 @@ func processPolicy(ctx context.Context, zlog zerolog.Logger, bulker bulk.Bulk, a
policyOutput.Name, err)
}
}

// Do not advertise remote ES service_token in secret_paths once Prepare(...) has
// deleted it from the policy sent to agents. Use pp.Outputs for type because
// Prepare rewrites data.Outputs type to elasticsearch.
for name, out := range pp.Outputs {
if out.Type != policy.OutputTypeRemoteElasticsearch {
continue
}
if _, ok := data.Outputs[name][policy.FieldOutputServiceToken]; !ok {
prefixed := "outputs." + name + "." + policy.FieldOutputServiceToken
pp.SecretKeys = slices.DeleteFunc(pp.SecretKeys, func(key string) bool {
return key == prefixed
})
}
}
// Prepare OTel exporters from the information in outputs.
if err := prepareOTelExporters(data.Outputs, data.Exporters); err != nil {
return nil, fmt.Errorf("failed to prepare OTel exporters: %w", err)
Expand Down
113 changes: 113 additions & 0 deletions internal/pkg/api/handleCheckin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1506,3 +1506,116 @@ func TestProcessPolicyDetails(t *testing.T) {
assert.Empty(t, opts)
})
}

// TestProcessPolicyRemoteESServiceTokenSecretPaths ensures secret_path does not
// contain service_token for remote ES output as they are stripped before sending the policy to the agents

func TestProcessPolicyRemoteESServiceTokenSecretPaths(t *testing.T) {
logger := testlog.SetLogger(t)

const policyPayload = `{
"id": "remote-with-secrets",
"revision": 1,
"outputs": {
"default": {
"type": "elasticsearch",
"hosts": ["https://local.es.example:443"]
},
"OUTPUT_ID": {
"type": "remote_elasticsearch",
"hosts": ["https://remote.es.example:443"],
"service_token": null,
"secrets": {
"service_token": {"id": "SERVICE_TOKEN_ID"},
"ssl": {"key": {"id": "SSL_KEY_ID"}}
}
}
},
"output_permissions": {
"default": {
"_fallback": {
"cluster": ["monitor"],
"indices": [{"names": ["logs-*", "metrics-*"], "privileges": ["auto_configure", "create_doc"]}]
}
},
"OUTPUT_ID": {
"_fallback": {
"cluster": ["monitor"],
"indices": [{"names": ["logs-*", "metrics-*"], "privileges": ["auto_configure", "create_doc"]}]
}
}
},
"inputs": [],
"secret_references": [
{"id": "SERVICE_TOKEN_ID"},
{"id": "SSL_KEY_ID"}
],
"agent": {
"monitoring": {
"enabled": true,
"use_output": "OUTPUT_ID",
"logs": true,
"metrics": true
}
},
"fleet": {
"hosts": ["http://localhost:8220"]
}
}`

var d model.PolicyData
err := json.Unmarshal([]byte(policyPayload), &d)
require.NoError(t, err)

bulker := ftesting.NewMockBulk()
pp, err := policy.NewParsedPolicy(t.Context(), bulker, model.Policy{
PolicyID: "policy1",
RevisionIdx: 1,
Data: &d,
})
require.NoError(t, err)

defaultOut := pp.Outputs["default"]
remoteOut := pp.Outputs["OUTPUT_ID"]
require.NotNil(t, defaultOut.Role)
require.NotNil(t, remoteOut.Role)

outputBulker := ftesting.NewMockBulk()
bulker.On("CreateAndGetBulker", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
Return(outputBulker, false, nil)

defaultKey := bulk.APIKey{ID: "default-id", Key: "default-key"}
remoteKey := bulk.APIKey{ID: "remote-id", Key: "remote-key"}
agent := &model.Agent{
ESDocument: model.ESDocument{Id: "agent1"},
Outputs: map[string]*model.PolicyOutput{
"default": {
APIKey: defaultKey.Agent(),
APIKeyID: defaultKey.ID,
PermissionsHash: defaultOut.Role.Sha2,
Type: policy.OutputTypeElasticsearch,
},
"OUTPUT_ID": {
APIKey: remoteKey.Agent(),
APIKeyID: remoteKey.ID,
PermissionsHash: remoteOut.Role.Sha2,
Type: policy.OutputTypeRemoteElasticsearch,
},
},
}

action, err := processPolicy(t.Context(), logger, bulker, agent, pp)
require.NoError(t, err)

pc, err := action.Data.AsActionPolicyChange()
require.NoError(t, err)

assert.NotContains(t, pc.Policy.SecretPaths, "outputs.OUTPUT_ID.service_token")
assert.Contains(t, pc.Policy.SecretPaths, "outputs.OUTPUT_ID.ssl.key")

remotePolicy, ok := pc.Policy.Outputs["OUTPUT_ID"].(map[string]any)
require.True(t, ok)
_, hasServiceToken := remotePolicy["service_token"]
assert.False(t, hasServiceToken, "service_token should be deleted by Prepare before delivery to agents")
assert.Equal(t, policy.OutputTypeElasticsearch, remotePolicy["type"])
}
Loading