Skip to content
Merged
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
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