Skip to content

keystore: add --from-dir to import secrets from a directory - #22305

Open
aelnahas wants to merge 7 commits into
elastic:mainfrom
aelnahas:keystore/add-entries-from-directory
Open

aelnahas wants to merge 7 commits into
elastic:mainfrom
aelnahas:keystore/add-entries-from-directory

Conversation

@aelnahas

@aelnahas aelnahas commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Motivation/summary

This commit will allow the user to pass in a directory to the apm-server keystore add command to store files as values with their filenames as keys by passing a --from-dir flag. The command will simply scan the directory and not walk it recursively. Additionally, it will skip hidden files.

e.g

apm-server keystore add --from-dir /path/to/dir

Checklist

For functional changes, consider:

  • Is it observable through the addition of either logging or metrics?
  • Is its use being published in telemetry to enable product improvement?
  • Have system tests been added to avoid regression?

How to test these changes

  • Added System test and unit tests for coverage but for manual you can refer to the steps below
# 1. compile the changes in some. Optionally place conduct the test in a temp directory
cd /path/to/apm-server
mkdir -p /tmp/test && go build -o /tmp/ks-test/apm-server ./x-pack/apm-server

# 2. make sure to add an apm-server.yaml file in the same folder


# 3. Create some secrets
mkdir -p /tmp/test/secrets
echo 'token-123'   > /tmp/test/secrets/secret_token
echo 'some-other-secret' > /tmp/test/secrets/some.other.secret

# 4. Optional reduce typing with alias
apm() { /tmp/test/apm-server --path.home /tmp/test/home "$@"; }```

# 5. test with --from-dir
apm keystore add --from-dir /tmp/test/secrets
apm keystore list  # should show the keys secret_token, some.other.secret

# 6. test with --stdin
apm keystore add stdkey --stdin
apm keystore list  # should show 3 keys now

I found it tricky to try and pull the secret content to verify it if is written correctly. So one workaround is to setup the secrets so that they are used for request and then attempt to use a correct and incorrect secret token to authenticate the request.

Steps:

  1. setup your apm-server.yml as following:
apm-server:
  host: "127.0.0.1:8200"
  auth:
    secret_token: "${SECRET_TOKEN}"
output.elasticsearch:
  hosts: ["http://localhost:9200"]
  username: admin
  password: changeme
  1. Add the SECRET_TOKEN file something like
    `echo "some-token" >> /tmp/test/secrets/SECRET_TOKEN

  2. add the token via --from-dir apm keystore add --from-dir /tmp/test/secrets
    4 start the apm server

  3. send a request to intake with the correct token

# request with invalid token should respond back with 401
curl -H 'Authorization: Bearer wrong'  -H 'Content-Type: application/x-ndjson' --data-binary @/path/to/event.ndjson http://127.0.0.1:8200/intake/v2/events

# request with valid token should respond back with 202
curl -H 'Authorization: Bearersome-token'  -H 'Content-Type: application/x-ndjson' --data-binary @/path/to/event.ndjson http://127.0.0.1:8200/intake/v2/events

Related issues

Fixes: #21863

This commit will allow the user to pass in a directory to the
`apm-server keystore add` command to store files as values with
their filenames as keys. The command will simply scan the directory
and not walk it recursively. Additionally, it will skip hidden files.
Copilot AI balanced review requested due to automatic review settings October 28, 2026 18:28
@aelnahas
aelnahas requested a review from a team as a code owner October 28, 2026 18:28
@github-actions

Copy link
Copy Markdown
Contributor

🤖 GitHub comments

Just comment with:

  • run docs-build : Re-trigger the docs validation. (use unformatted text in the comment!)

@mergify

mergify Bot commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

This pull request does not have a backport label. Could you fix it @aelnahas? 🙏
To fixup this pull request, you need to add the backport labels for the needed
branches, such as:

  • backport-8.19 is the label to automatically backport to the 8.19 branch.
  • backport-9./d is the label to automatically backport to the 9./d branch. /d is the digit.
  • backport-active-all is the label that automatically backports to all active branches.
  • backport-active-9 is the label that automatically backports to all active minor branches for the 9 major.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The keystore system tests run against the FIPS binary, which omits the keystore command, causing both tests to fail.

3 open issues
What changed in this PR

Adds directory-based secret imports to apm-server keystore add, supporting bulk setup in shell-less containers such as those used by ECK.

Changes:

  • Adds --from-dir to import filenames as keys and file contents as values, skipping hidden entries and subdirectories.
  • Refactors input handling and supports noninteractive keystore creation for directory and stdin imports.
  • Adds unit and system tests for imports, validation, and overwrite behavior.
File Description
systemtest/​keystore_test.go Adds end-to-end directory and stdin import tests.
internal/​beatcmd/​keystore_nofips.go Implements directory imports and shared secret-input handling.
internal/​beatcmd/​keystore_nofips_test.go Tests directory filtering, validation, persistence, and conflicts.

Review effort: Balanced

Comment thread systemtest/keystore_nofips_test.go Resolved
Comment thread internal/beatcmd/keystore_nofips.go Resolved
Comment thread internal/beatcmd/keystore_nofips.go Resolved
Copilot AI balanced review requested due to automatic review settings October 28, 2026 21:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Removing whitespace normalization breaks existing positional-key removal: a key added as ' token ' is stored as 'token' but can no longer be removed using the original argument.

1 open issue
3 resolved since last review

Review effort: Balanced

Comment thread internal/beatcmd/keystore_nofips.go Resolved
@aelnahas aelnahas added backport-8.19 Automated backport to the 8.19 branch backport-9.4 Automated backport with mergify backport-9.5 Automated backport to the 9.5 branch backport-9.6 Automated backport with mergify labels Oct 28, 2026
Copilot AI balanced review requested due to automatic review settings October 29, 2026 14:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No active review issues remain for the directory-based keystore import change.

1 open issue

Review effort: Balanced

@carsonip carsonip left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we can drop 9.4 backport because there is no scheduled release. And for 9.6 please observe FF.

Comment thread internal/beatcmd/keystore_nofips.go Outdated
Comment on lines +114 to +117
c.keys = make([]string, len(args))
for i, arg := range args {
c.keys[i] = strings.TrimSpace(arg)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: there's no need to go through this if loading from directory

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true, I can guard this with the flag check.

Comment thread internal/beatcmd/keystore_nofips.go Outdated
if err != nil {
return fmt.Errorf("could not read value from the input, error: %w", err)

if err = writableKeystore.Store(key, keyValue); err != nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if err = writableKeystore.Store(key, keyValue); err != nil {
if err := writableKeystore.Store(key, keyValue); err != nil {

Comment thread internal/beatcmd/keystore_nofips.go Outdated
return fmt.Errorf("could not add the key in the keystore, error: %w", err)
}

if err = writableKeystore.Save(); err != nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if err = writableKeystore.Save(); err != nil {
if err := writableKeystore.Save(); err != nil {

Comment thread internal/beatcmd/keystore_nofips.go Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: will it be helpful for --from-dir to print the number of keys added / updated?

Comment thread internal/beatcmd/keystore_nofips.go Outdated

if !store.IsPersisted() {
if !force {
if !force && interactive {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q: why change the condition? --from-dir and --stdin can be in an interactive environment as well

@aelnahas aelnahas Nov 6, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More of an opinion than strong reason. When I was working on this, I took a look at some other cli commands such as kubectl unzip mv cp and the like. some ignore the interactive option while others would have additional options such as All/None or -n do not overwrite.

It is a bit split. I ended up following kubectl approach with kubectl create secret --from-file since that felt the closest to what we are doing and I assumed this is utility is mostly used in automated script.

I can changes this bit if it is undesired.

Comment thread internal/beatcmd/keystore_nofips.go Outdated
for _, key := range keys {
if _, err := store.Retrieve(key); err == nil {
if !force {
if !interactive {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto. I'm not sure deriving interactive from from-dir vs stdin vs input is correct.

Comment thread internal/beatcmd/keystore_nofips.go Outdated
Comment on lines +98 to +99
if len(c.keys) == 0 {
return fmt.Errorf("directory %q is empty", c.fromDir)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this is likely correct, but i wonder if there is any case if an empty directory is not an error case and should be no-op and exit cleanly.

@aelnahas aelnahas removed the backport-9.4 Automated backport with mergify label Nov 6, 2026
- empty directory is demoted to a warn message
- added updated/added count
- allow both --from-dir and --stdin to be used interactively
- add tests
Copilot AI balanced review requested due to automatic review settings November 9, 2026 22:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Creation tests omit required keystore confirmation, an overwrite test expects the wrong error, and empty-directory imports bypass mutually exclusive option validation.

4 open issues
1 resolved since last review

Review effort: Balanced

Comment on lines +258 to +259
files: map[string]string{"a": "1", "b": "new"},
wantErr: "the settings b already exist",
seedKeystore(t, ksPath, tt.seed)
}

c := &addKeystoreCmd{force: tt.force}
Comment on lines +39 to +40
require.NoError(tb, os.WriteFile(filepath.Join(homedir, "apm-server.yml"), nil, 0644))
return homedir
Comment on lines +111 to +112
if c.fromDir != "" {
keys, err := getKeysFromDir(c.fromDir)
@infra-vault-gh-plugin-prod

Copy link
Copy Markdown

💚 Build Succeeded

History

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport-8.19 Automated backport to the 8.19 branch backport-9.5 Automated backport to the 9.5 branch backport-9.6 Automated backport with mergify

Projects

None yet

Development

Successfully merging this pull request may close these issues.

keystore: Add support for importing entries from a directory

3 participants