You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Linux secret-store probe runs secret-tool --version, which libsecret does not support, so the keyring is never detected #664
On Linux, @elastic/cli never uses the Secret Service keyring even when libsecret's secret-tool is installed and a working D-Bus session with an unlocked keyring is present. Every command that stores a credential falls back to inline storage and prints:
Warning: No OS secret store is available; credentials will be written inline to the config file (chmod 0600).
Cause
LinuxSecretServiceStore.probe() in dist/config/secret-store.js runs:
secret-tool has no --version option. It prints its usage text and exits 2, so the probe throws and the store is marked unavailable. The CLI then tries PassStore (pass version), which is usually absent, and ends on NoopStore.
$ secret-tool --version; echo "exit=$?"
usage: secret-tool store --label='label' attribute value ...
secret-tool lookup attribute value ...
secret-tool clear attribute value ...
secret-tool search [--all] [--unlock] attribute value ...
secret-tool lock --collection='collection'
exit=2
The put, lookup, and clear calls themselves work fine. Only the availability probe fails, so reads of an existing $(secret_service:...) reference succeed while writes never produce one.
Environment
@elastic/cli
0.5.0
OS
Ubuntu 24.04.5 LTS, GNOME keyring unlocked, DBUS_SESSION_BUS_ADDRESS set
libsecret-tools
0.21.4-1build3
Node
22.22.3
Reproduction
sudo apt install libsecret-tools
printf x | secret-tool store --label=probe service probe account probe &&echo"keyring works"
elastic config context add demo --cloud-url https://api.elastic-cloud.com --cloud-api-key "$KEY" --json
Expected secrets[0].storage to be secret_service. Actual: inline, plus the warning above.
Confirming the diagnosis
Placing a wrapper named secret-tool earlier on PATH that exits 0 for --version and otherwise execs the real binary makes the same command report "storage":"secret_service", writes $(secret_service:elastic-cli/demo:cloud.auth.api_key) into the config, and creates the keyring entry. Nothing else changes.
Replace the --version probe with something secret-tool actually supports. Options, in order of preference:
Check that the binary exists (command -v secret-tool or which) and let a failed put trigger the inline fallback. This matches how reads already behave.
Do a round trip: secret-tool store a throwaway entry, then secret-tool clear it. This also verifies the D-Bus session and unlocked keyring, which a binary check does not.
Note that secret-tool lookup on a missing item exits 1 and secret-tool search exits 0 even with no D-Bus session, so neither is a reliable availability signal on its own.
Impact
Every Linux user of the CLI is silently downgraded to plaintext credentials in ~/.elasticrc.yml, and the warning text tells them to install a store they may already have.
Some notes for whoever picks this up, checked against src/config/secret-store.ts. These cover things the issue body doesn't already say.
1. Switch to argv-based exec while touching this. Every probe, put and delete calls _execSync with an interpolated shell string. They live inside async methods but block the event loop, and they rely on the hand-rolled POSIX shellEscape (L88). Examples: L213-214, L238-239, L251-252, L283, L294. Using execFile/spawn with an argv array, and input for the secret, removes the need for shellEscape and closes off shell injection entirely.
2. Probe choice. The issue already notes that search exits 0 without D-Bus, so don't use it as the replacement probe. If option 1 from the issue (binary-exists check) is chosen, a failed put should fall through to the next candidate (PassStore, then inline) inside getSecretStore / the callers. Otherwise a headless box reports secret_service, and the first put fails after the user has already supplied the secret.
3. The availability memo does nothing today.ShellSecretStore.isAvailable caches per instance (L156-167). getSecretStore() (L383-388) calls candidatesForPlatform() (L394-400), which builds fresh new …Store() instances on every call, so every caller probes again. That's 4 callers: src/config/commands.ts:207,266,356 and src/cloud/credentials.ts:285. Fixing this means a module-level memo of the resolved store.
4. Tests.test/config/secret-store.test.ts matches on stringified shell commands (L30 cmd.includes(h.match), L266 c.cmd.includes('secret-tool store'), L293 'pass insert'). These should move to argv-level assertions as part of the exec migration. Also add a regression test where the probe binary prints usage and exits 2.
PassStore's probe pass version (L270) is fine: version is a real pass subcommand. It has the same "binary exists ≠ store usable" gap, though (no initialised store or GPG key).
Activity
github-actions commentedon Oct 2, 2026
This issue is stale because it has been open 7 days with no activity. Remove stale label or comment or this will be closed in 2 days.
JoshMock commentedon Oct 9, 2026
Some notes for whoever picks this up, checked against
src/config/secret-store.ts. These cover things the issue body doesn't already say.1. Switch to argv-based exec while touching this. Every probe,
putanddeletecalls_execSyncwith an interpolated shell string. They live insideasyncmethods but block the event loop, and they rely on the hand-rolled POSIXshellEscape(L88). Examples: L213-214, L238-239, L251-252, L283, L294. UsingexecFile/spawnwith an argv array, andinputfor the secret, removes the need forshellEscapeand closes off shell injection entirely.2. Probe choice. The issue already notes that
searchexits 0 without D-Bus, so don't use it as the replacement probe. If option 1 from the issue (binary-exists check) is chosen, a failedputshould fall through to the next candidate (PassStore, then inline) insidegetSecretStore/ the callers. Otherwise a headless box reportssecret_service, and the firstputfails after the user has already supplied the secret.3. The availability memo does nothing today.
ShellSecretStore.isAvailablecaches per instance (L156-167).getSecretStore()(L383-388) callscandidatesForPlatform()(L394-400), which builds freshnew …Store()instances on every call, so every caller probes again. That's 4 callers:src/config/commands.ts:207,266,356andsrc/cloud/credentials.ts:285. Fixing this means a module-level memo of the resolved store.4. Tests.
test/config/secret-store.test.tsmatches on stringified shell commands (L30cmd.includes(h.match), L266c.cmd.includes('secret-tool store'), L293'pass insert'). These should move to argv-level assertions as part of the exec migration. Also add a regression test where the probe binary prints usage and exits 2.PassStore's probepass version(L270) is fine:versionis a realpasssubcommand. It has the same "binary exists ≠ store usable" gap, though (no initialised store or GPG key).authored by Pi Coding Agent (Claude Opus 5.5)