Skip to content
32 changes: 32 additions & 0 deletions changelog/fragments/1764638389-secrets-fleet-ssl.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Kind can be one of:
# - breaking-change: a change to previously-documented behavior
# - deprecation: functionality that is being removed in a later release
# - bug-fix: fixes a problem in a previous version
# - enhancement: extends functionality but does not break or fix existing behavior
# - feature: new functionality
# - known-issue: problems that we are aware of in a given version
# - security: impacts on the security of a product or a user’s deployment.
# - upgrade: important information for someone upgrading from a prior version
# - other: does not fit into any of the other categories
kind: feature

# Change summary; a 80ish characters long description of the change.
summary: Support secrets in fleet section of policy

# Long description; in case the summary is not enough to describe the change
# this field accommodate a description without length limits.
# NOTE: This field will be rendered only for breaking-change and known-issue kinds at the moment.
#description:

# Affected component; usually one of "elastic-agent", "fleet-server", "filebeat", "metricbeat", "auditbeat", "all", etc.
component: fleet-server

# PR URL; optional; the PR number that added the changeset.
# If not present is automatically filled by the tooling finding the PR where this changelog fragment has been added.
# NOTE: the tooling supports backports, so it's able to fill the original PR number instead of the backport PR number.
# Please provide it if you are adding a fragment for a different PR.
#pr: https://github.com/owner/repo/1234

# Issue URL; optional; the GitHub issue related to this changeset (either closes or is part of).
# If not present is automatically filled by the tooling with the issue linked to the PR number.
#issue: https://github.com/owner/repo/1234
5 changes: 4 additions & 1 deletion internal/pkg/api/handleCheckin.go
Original file line number Diff line number Diff line change
Expand Up @@ -889,7 +889,10 @@ func processPolicy(ctx context.Context, zlog zerolog.Logger, bulker bulk.Bulk, a
data := model.ClonePolicyData(pp.Policy.Data)
for _, policyOutput := range data.Outputs {
// NOTE: Not sure if output secret keys collected here include new entries, but they are collected for completeness
ks := secret.ProcessOutputSecret(policyOutput, secretValues)
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)
}
pp.SecretKeys = append(pp.SecretKeys, ks...)
}
// Iterate through the policy outputs and prepare them
Expand Down
2 changes: 1 addition & 1 deletion internal/pkg/model/schema.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 19 additions & 2 deletions internal/pkg/policy/parsed_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ type ParsedPolicy struct {
Default ParsedPolicyDefaults
Inputs []map[string]interface{}
Agent map[string]interface{}
Fleet map[string]interface{}
SecretKeys []string
Links apm.SpanLink
}
Expand All @@ -76,7 +77,10 @@ func NewParsedPolicy(ctx context.Context, bulker bulk.Bulk, p model.Policy) (*Pa
return nil, err
}
for name, policyOutput := range p.Data.Outputs {
ks := secret.ProcessOutputSecret(policyOutput, secretValues)
ks, err := secret.ProcessOutputSecret(policyOutput, secretValues)
if err != nil {
return nil, fmt.Errorf("failed to replace secrets in output section of policy '%s': %w", name, err)
}
for _, key := range ks {
secretKeys = append(secretKeys, "outputs."+name+"."+key)
}
Expand All @@ -91,14 +95,26 @@ func NewParsedPolicy(ctx context.Context, bulker bulk.Bulk, p model.Policy) (*Pa
// Replace secrets in 'agent.download' section of policy
if agentDownload, exists := p.Data.Agent["download"]; exists {
if section, ok := agentDownload.(map[string]interface{}); ok {
agentDownloadSecretKeys := secret.ProcessMapSecrets(section, secretValues)
agentDownloadSecretKeys, err := secret.ProcessMapSecrets(section, secretValues)
if err != nil {
return nil, fmt.Errorf("failed to replace secrets in agent.download section of policy: %w", err)
}
for _, key := range agentDownloadSecretKeys {
secretKeys = append(secretKeys, "agent.download."+key)
}
p.Data.Agent["download"] = section
}
}

// Replace secrets in `fleet` section of policy
fleetSecretKeys, err := secret.ProcessMapSecrets(p.Data.Fleet, secretValues)
if err != nil {
return nil, fmt.Errorf("failed to replace secrets in fleet section of policy: %w", err)
}
for _, key := range fleetSecretKeys {
secretKeys = append(secretKeys, "fleet."+key)
}

// Done replacing secrets.
p.Data.SecretReferences = nil

Expand All @@ -112,6 +128,7 @@ func NewParsedPolicy(ctx context.Context, bulker bulk.Bulk, p model.Policy) (*Pa
},
Inputs: policyInputs,
Agent: p.Data.Agent,
Fleet: p.Data.Fleet,
SecretKeys: secretKeys,
}

Expand Down
6 changes: 5 additions & 1 deletion internal/pkg/policy/parsed_policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,15 @@ func TestParsedPolicyMixedSecretsReplacement(t *testing.T) {
require.NoError(t, err)

// Validate that secrets were identified
require.Len(t, pp.SecretKeys, 6)
require.Len(t, pp.SecretKeys, 8)
require.Contains(t, pp.SecretKeys, "outputs.fs-output.type")
require.Contains(t, pp.SecretKeys, "outputs.fs-output.ssl.key")
require.Contains(t, pp.SecretKeys, "inputs.0.streams.0.auth.basic.password")
require.Contains(t, pp.SecretKeys, "inputs.0.streams.1.auth.basic.password")
require.Contains(t, pp.SecretKeys, "agent.download.sourceURI")
require.Contains(t, pp.SecretKeys, "agent.download.ssl.key")
require.Contains(t, pp.SecretKeys, "fleet.hosts.0")
require.Contains(t, pp.SecretKeys, "fleet.ssl.key")

// Validate that secret references were replaced
firstInputStreams := pp.Inputs[0]["streams"].([]any)
Expand All @@ -143,4 +145,6 @@ func TestParsedPolicyMixedSecretsReplacement(t *testing.T) {
require.Equal(t, "w8yELZoBTAyw4gQK9KZ7_value", pp.Policy.Data.Outputs["fs-output"]["ssl"].(map[string]interface{})["key"])
require.Equal(t, "bcdefg234_value", pp.Policy.Data.Agent["download"].(map[string]interface{})["sourceURI"])
require.Equal(t, "rwXzUJoBxE9I-QCxFt9m_value", pp.Policy.Data.Agent["download"].(map[string]interface{})["ssl"].(map[string]interface{})["key"])
require.Equal(t, "abcdef123_value", pp.Policy.Data.Fleet["hosts"].([]interface{})[0])
require.Equal(t, "w8yELZoBTAyw4gQK9KZ7_value", pp.Policy.Data.Fleet["ssl"].(map[string]interface{})["key"])
}
11 changes: 9 additions & 2 deletions internal/pkg/policy/testdata/policy_with_secrets_mixed.json
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,14 @@
},
"fleet": {
"hosts": [
"https://c3564859758d4e41a2b2109ade35c1a2.fleet.us-west2.gcp.elastic-cloud.com:443"
]
"$co.elastic.secret{abcdef123}"
],
"secrets": {
"ssl": {
"key": {
"id": "w8yELZoBTAyw4gQK9KZ7"
}
}
}
}
}
28 changes: 19 additions & 9 deletions internal/pkg/secret/secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package secret
import (
"context"
"encoding/json"
"fmt"
"regexp"
"strconv"
"strings"
Expand Down Expand Up @@ -323,17 +324,20 @@ func setSecretPath(section smap.Map, secretValue string, secretPaths []string) {
}

// Read secret from output and mutate output with secret value
func ProcessOutputSecret(output smap.Map, secretValues map[string]string) []string {
func ProcessOutputSecret(output smap.Map, secretValues map[string]string) ([]string, error) {

// Unfortunately, there are two ways (formats) of specifying secret references in
// policies: inline and path (see https://github.com/elastic/fleet-server/pull/5852).
// So we try replacing secret references in both formats.

keys := processMapWithInlineSecrets(output, secretValues)
keys, err := processMapWithInlineSecrets(output, secretValues)
if err != nil {
return nil, fmt.Errorf("failed processing output secret with inline secrets: %w", err)
}
k := processMapWithPathSecrets(output, secretValues)

keys = append(keys, k...)
return keys
return keys, nil
}

// processMapWithPathSecrets reads secrets from the output and mutates the output with the secret values using
Expand Down Expand Up @@ -370,25 +374,31 @@ func processMapWithPathSecrets(m smap.Map, secretValues map[string]string) []str
return keys
}

func processMapWithInlineSecrets(m smap.Map, secretValues map[string]string) []string {
func processMapWithInlineSecrets(m smap.Map, secretValues map[string]string) ([]string, error) {
replacedM, keys := replaceInlineSecretRefsInMap(m, secretValues)
for _, key := range keys {
m[key] = replacedM[key]
rm := smap.Map(replacedM)
if err := m.Set(key, rm.Get(key)); err != nil {
return nil, fmt.Errorf("failed processing map with inline secrets: failed to set secret value for key %s: %w", key, err)
}
}
return keys
return keys, nil
}

// ProcessMapSecrets reads and replaces secrets in the agent.download section of the policy
func ProcessMapSecrets(m smap.Map, secretValues map[string]string) []string {
func ProcessMapSecrets(m smap.Map, secretValues map[string]string) ([]string, error) {
// Unfortunately, there are two ways (formats) of specifying secret references in
// policies: inline and path (see https://github.com/elastic/fleet-server/pull/5852).
// So we try replacing secret references in both formats.

keys := processMapWithInlineSecrets(m, secretValues)
keys, err := processMapWithInlineSecrets(m, secretValues)
if err != nil {
return nil, fmt.Errorf("failed processing map secrets with inline secrets: %w", err)
}
k := processMapWithPathSecrets(m, secretValues)

keys = append(keys, k...)
return keys
return keys, nil
}

// replaceStringRef replaces values matching a secret ref regex, e.g. $co.elastic.secret{<secret ref>} -> <secret value>
Expand Down
2 changes: 1 addition & 1 deletion internal/pkg/secret/secret_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ func TestProcessOutputSecret(t *testing.T) {
"sslother": "sslother_value",
"sslkey": "sslkey_value",
}
keys := ProcessOutputSecret(output, secretValues)
keys, err := ProcessOutputSecret(output, secretValues)
assert.NoError(t, err)

assert.Equal(t, expectOutput, output)
Expand Down
31 changes: 30 additions & 1 deletion internal/pkg/server/fleet_secrets_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,16 @@ func createAgentPolicyWithSecrets(t *testing.T, ctx context.Context, bulker bulk
},
},
},
Fleet: map[string]interface{}{
"hosts": []string{inlineSecretRef},
"secrets": map[string]interface{}{
"ssl": map[string]interface{}{
"key": map[string]interface{}{
"id": pathSecretID,
},
},
},
},
SecretReferences: []model.SecretReferencesItems{
{ID: inlineSecretID},
{ID: pathSecretID},
Expand Down Expand Up @@ -276,7 +286,26 @@ func Test_Agent_Policy_Secrets(t *testing.T) {
},
}, actionData.Policy.Agent["download"])

// expect fleet secrets to be replaced
assert.Equal(t, map[string]interface{}{
"hosts": []interface{}{"inline_secret_value"},
"ssl": map[string]interface{}{
"key": "path_secret_value",
},
}, actionData.Policy.Fleet)

assert.NotContains(t, output, "secrets")
// expect that secret_paths lists the key
assert.ElementsMatch(t, []string{"inputs.0.package_var_secret", "outputs.default.secret-key", "agent.download.sourceURI", "agent.download.ssl.key"}, actionData.Policy.SecretPaths)
assert.ElementsMatch(
t,
[]string{
"inputs.0.package_var_secret",
"outputs.default.secret-key",
"agent.download.sourceURI",
"agent.download.ssl.key",
"fleet.hosts.0",
"fleet.ssl.key",
},
actionData.Policy.SecretPaths,
)
}
Loading