Skip to content

provider refresh configure: accept secret material off-argv (file/stdin) so private keys aren't exposed in process listings #2104

Description

@hunglp6d

Problem Statement

openshell provider refresh configure accepts all refresh material — including secrets like a service-account private_key or an OAuth2 client_secret — only through --material KEY=VALUE command-line arguments. Secret values therefore appear in the host process table (ps aux), /proc/<pid>/cmdline, shell history, process accounting, and any audit/observability tooling that records command lines.

This affects every strategy carrying secret material — google_service_account_jwt (private_key), oauth2_client_credentials (client_secret), etc. — including OpenShell's own built-in GOOGLE_VERTEX_AI_SERVICE_ACCOUNT_TOKEN provider when configured via the CLI. OpenShell already treats this material as sensitive: --secret-material-key marks which keys are secret so they're redacted in storage and diagnostics — yet the value is still forced through argv, where redaction does not apply. The CLI knows the value is secret but has no way to receive it without exposing it. On the multi-admin / audited hosts where a gateway typically runs, this is a real credential exposure and contradicts OpenShell's own "secrets stay gateway-side / never exposed" posture.

Proposed Design

Add an off-argv ingestion path for secret material to provider refresh configure. The value still flows through the existing gRPC ConfigureProviderRefreshRequest.material map — only the CLI ingestion changes. Two complementary flags:

  1. --secret-material-file KEY=PATH — read the value for KEY from a file or from stdin:

    • KEY=/path/to/file: read the file at that path (e.g. a 0600 temp file the caller writes then deletes).
    • KEY=-: read from stdin instead — - is the conventional Unix "filename" for standard input (as in kubectl apply -f -). OpenShell already has a piped-stdin reader (run.rs ~2761, 4 MiB cap), so this is consistent.
    • Contents are preserved byte-for-byte as UTF-8 (no trimming or normalization). Symmetric with the existing --secret-material-key.
  2. --secret-material-env KEY[=ENVVAR] — read the value for KEY from the named environment variable of the CLI's own process (ENVVAR defaults to KEY). Mirrors how provider create --credential KEY already ingests credential values from the CLI environment today, so this is the smallest consistent implementation. Serves callers whose secrets already live in env (Kubernetes secretKeyRef, CI-injected secrets) without an intermediate file. Missing or blank env fails closed, naming the variable and never echoing values.

Shared rules for both flags:

  • Duplicate keys are an error, not a precedence order. The same KEY may be supplied only once across --material, --secret-material-file, and --secret-material-env.
  • Auto-marked secret. Any key supplied through either new flag is automatically added to secret_material_keys.
  • Non-secret metadata stays on --material (client_email, scope, tenant_id) — those values are not sensitive, and argv is fine for them.

Examples (the secret never appears on argv in any of these):

# value read from a file
openshell provider refresh configure my-bridge \
  --credential-key GOOGLE_CHAT_ACCESS_TOKEN \
  --strategy google-service-account-jwt \
  --material client_email=bot@proj.iam.gserviceaccount.com \
  --material scope=https://www.googleapis.com/auth/chat.bot \
  --secret-material-file private_key=/run/secrets/sa.pem

# value piped through stdin ("-" = read standard input)
cat /run/secrets/sa.pem | openshell provider refresh configure my-bridge \
  ... --secret-material-file private_key=-

# value taken from an environment variable of the CLI process
openshell provider refresh configure my-bridge \
  ... --secret-material-env private_key=GC_SA_PRIVATE_KEY

(Design updated 2026-07-08: folded in maintainer feedback below — single file flag with KEY=- for stdin, byte-exact UTF-8 preservation, duplicate-key rejection instead of precedence — and added the env-flag option for consideration.)

Alternatives Considered

  • Separate --material-stdin flag (this proposal's initial draft): folded into KEY=- on --secret-material-file per review — one flag, same capability.
  • --material KEY=@PATH value convention: smaller, but ambiguous with values legitimately starting with @ and silently changes existing --material semantics. Rejected (surprise/compat).
  • Env-var indirection via value grammar (--material KEY=env:VAR): overloads the --material value grammar and is ambiguous with literal values. Rejected in that form. A dedicated flag (option 2 above) avoids the grammar problem; the remaining trade-off is the environ window (/proc/<pid>/environ, owner-only) — the same exposure class as the shipped --credential KEY ingestion, and a programmatic caller can pass a minimal child environment so nothing else inherits it.
  • Do nothing / document the risk: leaves a secret-in-argv anti-pattern in the product's own security-sensitive path. Rejected as the long-term answer.
  • Callers use the gRPC API directly (bypass CLI): forces every integrator to build an mTLS gRPC client; the CLI is the supported interface. Rejected.

Agent Investigation

Explored openshell-cli + openshell-server:

  • crates/openshell-cli/src/main.rs:898 — --material is Vec<String>; :902 --secret-material-key is names only. No file/stdin/env flag on the Configure subcommand.
  • crates/openshell-cli/src/run.rs:3974 parse_key_value_pairs — splits on =, stores value verbatim (no @file/env:).
  • crates/openshell-cli/src/run.rs:5298 — CLI forwards material into the gRPC request; the transport already carries it off-argv.
  • crates/openshell-server/src/provider_refresh.rs:506 / :487google_service_account_jwt / oauth2_client_credentials read secrets from the material map only.
  • crates/openshell-cli/src/run.rs:4335 read_gcloud_adc reads a file but is authorized_user-only and rejects service_account keys (:4376).
  • Stdin precedent: run.rs ~2761 (4 MiB reader).
  • Env-ingestion precedent: provider create --credential KEY already resolves the credential value from the CLI process environment (run.rs ~4039), so option 2 reuses an established pattern.
    Fix is localized to the CLI ingestion boundary (main.rs add flags + run.rs provider_refresh_config merge file/stdin/env material before the gRPC call); server/gRPC/strategy unchanged.

Checklist

  • I've reviewed existing issues and the architecture docs
  • This is a design proposal, not a "please build this" request

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:cliCLI-related work

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions