feat: encrypted secrets with passphrase prompt at gtc setup (v0.6.0)#60
Draft
BimaPangestu28 wants to merge 4 commits into
Draft
feat: encrypted secrets with passphrase prompt at gtc setup (v0.6.0)#60BimaPangestu28 wants to merge 4 commits into
BimaPangestu28 wants to merge 4 commits into
Conversation
Wires the upstream greentic-secrets v0.6.0 encrypted backend into the
gtc setup CLI:
- New flags --passphrase-stdin, --passphrase-file, --reconfigure,
--allow-downgrade on bundle setup/update commands
- init_global_passphrase_provider resolves passphrase via
greentic-secrets-cli (priority: file -> stdin -> TTY) and installs
a process-global PassphraseKeyProvider before any dev-store open
- crate::secrets::open_dev_store + SecretsSetup::new transparently
use the global provider when present (encrypted), or fall back to
legacy plaintext when not (back-compat for tests/non-CLI consumers)
- First-run shows 'no recovery' warning; subsequent runs prompt only
for unlock
- --reconfigure wipes existing store + .encrypted-marker
i18n: cli.setup.passphrase.{failed,kdf_failed,first_setup_complete,
reconfigured} added to en.json.
Version bump 0.5.1 -> 0.6.0 for the new dep on greentic-secrets-* 0.6.
[patch.crates-io] in Cargo.toml temporarily points at local clone
while greenticai/greentic-secrets#51 awaits publish; remove the patch
block before merging this PR.
7 tasks
Adds the missing pieces from the spec that were deferred in the initial PR: * SecretsSetup gains missing_pack_secrets() returning only required secret keys absent from BOTH the dev store and seeds.yaml. Existing values are never re-prompted; seed.yaml entries are auto-applied transparently. * New src/secrets_prompt.rs with rpassword (no-echo) for secret-shaped keys (token / password / api_key / private_key / credential / passphrase in the name) and dialoguer (echoed) for plain configuration values. Empty optional values are skipped; required values cannot be empty. * setup_or_update calls prompt_missing_pack_secrets_blocking after the encryption layer is wired and after dry-run/emit-answers short-circuits, but before engine.execute. --non-interactive skips the prompt step entirely (CI/answers-file flow is unchanged). * tests/clap_help_check.rs — security regression: asserts no --passphrase=<value> flag exists on either bundle setup or bundle update. Pure compile-time/help-output check. * tests/passphrase_setup.rs — end-to-end: invokes the binary with --passphrase-stdin + --emit-answers --non-interactive, pipes a known-string passphrase, verifies (a) the encrypted v1 file format on disk when present, and (b) the known passphrase string never appears anywhere in stdout/stderr even at RUST_LOG=trace. Closes the diff-only and security-test items the original PR #60 deferred.
Until now, run_ui_mode (the web UI launcher) bypassed init_global_passphrase_provider, so a UI launch on an encrypted bundle would crash with InvalidPassphrase as soon as the first UI handler tried to open_dev_store. Changes: - Top-level Cli struct gains four global flags so they apply uniformly across the CLI subcommands and the --ui flow: --passphrase-stdin, --passphrase-file, --reconfigure, --allow-downgrade - The duplicate per-subcommand definitions in BundleSetupArgs are removed; the values are bridged from the global Cli into the per-subcommand args inside the dispatcher. - init_global_passphrase_provider is moved from cli_commands/setup.rs to crate::secrets so both run_ui_mode and setup_or_update share one implementation. - run_ui_mode now resolves the passphrase + installs the global key provider before launching the web server. All UI handlers that open the dev store via crate::secrets::open_dev_store transparently get the AES-256-GCM-backed store. - The friendlier no-TTY error message from greentic-secrets-cli is picked up via the cargo workspace patch. - clap_help_check.rs updated to assert global passphrase flags appear on top-level help and that no --passphrase=<value> flag exists at any subcommand level.
Two security fixes for init_global_passphrase_provider that were exposed by end-to-end smoke testing with a real encrypted store: * Fail-fast on wrong passphrase. When an existing encrypted store is detected (Unlock mode), immediately try to open + decrypt it via open_dev_store(). Without this verify, command paths that short-circuit early (e.g. --emit-answers --non-interactive) would install a wrong-passphrase provider, complete successfully, and only fail much later when something actually read a secret. AES-GCM auth failure is mapped to a clean 'passphrase incorrect' error. * Downgrade-attack guard at the init layer. If peek_header returns None (no v1 header) but the .encrypted-marker sidecar exists, refuse to proceed with: 'refusing to load legacy plaintext store after encryption was previously enabled (downgrade-attack guard); pass --allow-downgrade to override'. Previously the guard only fired inside Persistence::load_with_provider, which meant CLI paths that never opened the store missed it entirely. Plus: add examples/seed_encrypted_store.rs as a test helper that seeds a real v1 encrypted store from a passphrase on stdin. Used by the end-to-end shell smoke tests to validate behavior without needing a TTY.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Wires the upstream encrypted-secrets-passphrase backend (greentic-secrets v0.6.0) into the `gtc setup` CLI. First-run prompts user for a passphrase + min-length validation, subsequent runs prompt only for unlock. Secrets at rest are then AES-256-GCM encrypted with an Argon2id-derived master key.
What changes
CLI surface (`bundle setup` and `bundle update`)
When none of the non-interactive flags are set, defaults to TTY prompt via `rpassword`. Passphrase resolution priority: `file` → `stdin` → `tty`.
Wiring
Why no diff-only seed prompting in this PR
The original spec called for skipping prompts on keys already present and prompting only for newly-required keys. The current change reuses the existing `SecretsSetup::ensure_pack_secrets` flow which already seeds placeholders for missing keys — the encryption layer is transparent to that flow. Diff-only interactive prompting is a UX polish that can land in a follow-up without touching the security layer.
Version
`0.5.1` → `0.6.0` (matches upstream secrets workspace bump).
Security guarantees inherited from upstream
Test plan
Spec & plan