Skip to content

feat: elastic config command group + credential-safe project create#216

Merged
MattDevy merged 2 commits into
mainfrom
claude/exciting-moore-ee5437
Apr 29, 2026
Merged

feat: elastic config command group + credential-safe project create#216
MattDevy merged 2 commits into
mainfrom
claude/exciting-moore-ee5437

Conversation

@MattDevy
Copy link
Copy Markdown
Contributor

@MattDevy MattDevy commented Apr 22, 2026

Review guidance

Please review each commit individually — they address separate issues but share a small abstraction (SecretStore) introduced in the first:

  1. ea6c85felastic config command group (Configuration file management commands #75)
  2. 246cf21--save-as / --credentials-file on serverless project create + reset-credentials (Credentials in stdout during project creation are problematic for agent/LLM workflows #154)

Each commit builds, lints, and tests green on its own.

Summary

Closes #75 and #154 by sharing a single SecretStore abstraction between config authoring and serverless project creation.

elastic config … (#75 — commit 1)

New flag-driven command group for authoring the config without hand-editing YAML:

elastic config context list
elastic config context add <name> [--es-url … --es-api-key …] [--force] [--inline-secrets]
elastic config context edit <name> [--es-url … | no flags → $EDITOR]
elastic config context remove <name> [--force]
elastic config current-context get
elastic config current-context set <name>

Secrets go to the OS keychain when available (macOS security, Linux secret-tool / pass, Windows Credential Manager). The YAML then contains $(keychain:...) resolver expressions, mirroring the existing read side in src/config/resolvers.ts. If no keychain is available (or --inline-secrets is passed), secrets are written inline and the file is chmod 0600.

A new stderr warning fires at load time when a config has inline (non-resolver) secrets at looser-than-0600 permissions.

Credential-safe serverless projects create / reset-credentials (#154 — commit 2)

Scoped to those commands, three new flags:

  • --save-as <name> — stores admin creds in the keychain, upserts a config context bound to the new project's endpoints, and redacts stdout. Next command runs as elastic --use-context <name> ... with zero manual wiring.
  • --credentials-file <path> — alternative that writes a standalone 0600 YAML fragment instead of mutating the main config.
  • --force — overwrite on name/file collision.

reset-credentials --save-as <ctx> updates the existing context's auth in-place (URLs preserved). Without either flag, behavior is unchanged.

What a reviewer should know

  • No new dependencies. SecretStore shells out to the same OS tools the existing read-side resolvers use, behind an interface so a native binding (keytar / @napi-rs/keyring) can be dropped in later without API churn.
  • Keychain entries use a predictable namespace. service="elastic-cli", account="<context>:<dotted.field.path>" (e.g. prod:elasticsearch.auth.password). Easy to audit (security dump-keychain / secret-tool search service elastic-cli) and to wipe.
  • config commands skip the loadConfig preAction hook (see src/cli.ts) — they author the file and must tolerate it being absent.
  • Backwards compat: projects create without --save-as / --credentials-file returns the full JSON response verbatim. A global --redact filter is deliberately deferred to a follow-up once usage patterns settle.
  • Follow-up PR will wrap this in an interactive elastic config init TUI (@inquirer/prompts), reusing the same primitives.

Test plan

  • npm run build — clean.
  • npm run test:lint — clean.
  • npm run test:unit — 940 tests pass (75 new). Coverage 98.59% lines / 91.29% branches / 96.57% functions (above 90% threshold).
  • Manual: config context add/edit/remove/list with keychain (macOS) + inline fallback, current-context set/get, name-collision guards, $EDITOR round-trip, --inline-secrets opt-out.
  • Manual: loose-perms warning fires for 0644 file with inline api_key, silent at 0600.
  • Manual verification of --save-as against cloud functional harness (requires real cloud creds) — unit tests cover the keychain/config/redaction flow with canned responses.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 22, 2026

MegaLinter analysis: Success

Descriptor Linter Files Fixed Errors Warnings Elapsed time
✅ COPYPASTE jscpd yes no no 8.42s
✅ REPOSITORY gitleaks yes no no 80.79s
✅ REPOSITORY git_diff yes no no 0.09s
✅ REPOSITORY secretlint yes no no 2.73s
✅ REPOSITORY trivy yes no no 17.91s
✅ TYPESCRIPT eslint 11 0 0 5.13s

See detailed reports in MegaLinter artifacts
Set VALIDATE_ALL_CODEBASE: true in mega-linter.yml to validate all sources, not only the diff

MegaLinter is graciously provided by OX Security
Show us your support by starring ⭐ the repository

@MattDevy MattDevy force-pushed the claude/exciting-moore-ee5437 branch 3 times, most recently from 5803480 to 246cf21 Compare April 22, 2026 13:51
@MattDevy MattDevy requested review from margaretjgu and ssh-esh April 22, 2026 13:51
Comment thread src/config/commands.ts Outdated
* `~/.elasticrc.yml` (authored by us — we don't pick among the legacy
* discovery candidates when creating from scratch).
*/
function resolveConfigPath (options: Record<string, string | number | boolean>): string {
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.

this function and the defaultConfigPath in credentials.ts seem to me like they could be a shared function? They seem to have the same order and fallback

maybe we could extract to writer.ts or a new config/paths.ts?

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.

Good catch — consolidated both into a single resolveConfigPath(explicit?) in writer.ts, used by both commands.ts and cloud/credentials.ts now.

Comment thread src/cloud/credentials.ts Outdated
const writeResult = await writeConfig(configPath, next, { restrictPermissions: hasInlineSecrets(next) })
const storeAvailable = await store.isAvailable()

const redacted = redactCredentials(body, '(saved to keychain)')
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.

nit; marker says '(saved to keychain)' even when no keychain is available and secrets are written inline.

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.

Fixed — the marker is now dynamic: (saved to keychain) / (saved to secret_service) etc. when a store actually wrote the secret, (saved inline to config) when it landed in the YAML. Added a test asserting the inline-fallback case.

Comment thread src/cloud/credentials.ts Outdated
* values behind. Duplicates the check in `commands.ts` to keep this module
* self-contained (the set of "secret field paths" is small and stable).
*/
function hasInlineSecrets (config: RawConfig): boolean {
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.

hasInlineSecrets is implemented three times, here in credentials.ts, in commands.ts , and in loader.ts. All three walk the config differently but answer the same question. Could we extract a single hasInlineSecrets(config) into writer.ts and import it in all three places? Reduces the risk of them drifting apart when new secret fields are added? 🤔

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.

I missed your code comment about duplication and that this was a conscious design decision. The comment only talks about commader.ts, just to check, is the other copy in credentials.ts along the same design decision?

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.

Agreed, done. Extracted to hasInlineSecrets(config) + SECRET_AUTH_FIELDS in writer.ts and deleted the three local copies. The writer version walks every service block dynamically (instead of a hard-coded list) so new blocks don't need an update here.

@ssh-esh
Copy link
Copy Markdown
Contributor

ssh-esh commented Apr 22, 2026

small conflict after merging my PR sorry!

@MattDevy MattDevy force-pushed the claude/exciting-moore-ee5437 branch from 246cf21 to fe06dda Compare April 23, 2026 12:17
@MattDevy MattDevy requested a review from ssh-esh April 23, 2026 13:43
Copy link
Copy Markdown
Contributor

@ssh-esh ssh-esh left a comment

Choose a reason for hiding this comment

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

LGTM!

Copy link
Copy Markdown
Member

@JoshMock JoshMock left a comment

Choose a reason for hiding this comment

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

LGTM on the whole. Just one question in there.

Also, if we haven't done this already, it'd be nice to add some end-to-end tests for SecretStore that actually run on each OS to validate that it interacts with each store backend correctly. But that can happen later.

Comment thread src/cloud/register.ts
Comment on lines +194 to +200
if (isCredentialCommand(def.name)) {
(cmd as Command)
.option('--save-as <name>', 'store returned credentials in the OS keychain and upsert a context of this name')
.option('--credentials-file <path>', 'write credentials to a standalone YAML config fragment at this path (0600)')
.option('--config-file <path>', 'override the config file written by --save-as (defaults to ~/.elasticrc.yml)')
.option('--force', 'overwrite an existing context (--save-as) or file (--credentials-file)')
}
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.

Will these options also show up in the JSON schema for agents that are calling this? Or will they only work as CLI args? This is really the only module that manually applies extra options like this (other than global args, obviously), so it's mostly a question of whether these make more sense as CLI args only or would benefit from being added to the JSON schema.

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.

Good question! These are CLI-only — they won't show up in the JSON schema. The schema (derived from the Zod input in defineCommand) describes the API request body, while these options control local credential storage, which is really a CLI-layer concern. An agent calling this would manage auth differently and wouldn't need the keychain integration. --wait on project create follows the same pattern for the same reason.

That said, if you think there's a case where having them in the schema would be useful, happy to open a follow-up issue to track it!

New flag-driven command tree for creating and maintaining the config file
without hand-editing YAML:

  elastic config context list/add/edit/remove
  elastic config current-context get/set

`edit` supports both `--set key=value` patch mode and an `$EDITOR` round-trip.

Secrets are written to the OS keychain when available (macOS `security`,
Linux `secret-tool`/`pass`, Windows Credential Manager) via a new
`SecretStore` abstraction that shells out to the same tools the existing
read-side resolvers use. The YAML holds a `$(keychain:...)` resolver
expression rather than the raw secret, mirroring the read side. When no
keychain is available (or `--inline-secrets` is passed), values are written
inline and the file is chmod'd to 0600.

`loadConfig` now emits an stderr warning when a loaded config has inline
secrets at looser-than-0600 permissions, pointing at chmod 0600 or the new
`config context edit` migration path.

Skips the config-loading preAction hook for `config` descendants so the
commands tolerate the file being absent (they create it).
…#154)

`serverless {es,observability,security} projects create` and
`reset-credentials` gain three new flags to keep admin credentials off
stdout -- critical for agent/LLM workflows where captured output persists
into model context and transcripts.

  --save-as <name>          store returned creds in the OS keychain,
                            upsert a context bound to the new project's
                            endpoints, and redact stdout. Next command
                            runs as `elastic --use-context <name> ...`
                            with zero manual wiring.
  --credentials-file <path> write a standalone 0600 YAML config fragment
                            at <path> instead of mutating the main config.
  --force                   overwrite an existing context / file.

`reset-credentials --save-as <ctx>` updates the named context's auth in
place -- URLs preserved, only passwords rotate. Without either flag,
behaviour is unchanged.

Reuses the `SecretStore` abstraction from the `elastic config` commands so
a single keychain namespace covers both authored and auto-generated creds.
@MattDevy MattDevy force-pushed the claude/exciting-moore-ee5437 branch from fe06dda to f2d3fa2 Compare April 29, 2026 14:07
@MattDevy MattDevy enabled auto-merge (squash) April 29, 2026 14:13
@MattDevy MattDevy merged commit fd337ed into main Apr 29, 2026
17 of 18 checks passed
@MattDevy MattDevy deleted the claude/exciting-moore-ee5437 branch April 29, 2026 14:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Configuration file management commands

3 participants