Repository navigation
provider refresh configure: accept secret material off-argv (file/stdin) so private keys aren't exposed in process listings #2104
Description
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:
-
--secret-material-file KEY=PATH— read the value forKEYfrom 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 inkubectl 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.
-
--secret-material-env KEY[=ENVVAR]— read the value forKEYfrom the named environment variable of the CLI's own process (ENVVARdefaults toKEY). Mirrors howprovider create --credential KEYalready ingests credential values from the CLI environment today, so this is the smallest consistent implementation. Serves callers whose secrets already live in env (KubernetessecretKeyRef, 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
KEYmay 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-stdinflag (this proposal's initial draft): folded intoKEY=-on--secret-material-fileper review — one flag, same capability. --material KEY=@PATHvalue 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--materialvalue 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 KEYingestion, 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 —
--materialisVec<String>;:902--secret-material-keyis names only. No file/stdin/env flag on theConfiguresubcommand. - 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 /
:487—google_service_account_jwt/oauth2_client_credentialsread secrets from the material map only. - crates/openshell-cli/src/run.rs:4335
read_gcloud_adcreads a file but isauthorized_user-only and rejectsservice_accountkeys (:4376). - Stdin precedent: run.rs ~2761 (4 MiB reader).
- Env-ingestion precedent:
provider create --credential KEYalready 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.rsadd flags +run.rs provider_refresh_configmerge 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
Activity
johntmyers commentedon Jul 7, 2026
Thanks @hunglp6d. I think going with
--secret-material-file KEY=PATHis the right approach. Additionally we could doKEY=-to support reading from stdin. I would also preserve all bytes exactly as utf-8 text.I would prefer to reject duplicate keys rather than define precedence.
hunglp6d commentedon Jul 8, 2026
@johntmyers
Folded your feedback into the body:
--secret-material-file KEY=PATHwithKEY=-for stdin, byte-exact UTF-8, and duplicate-key rejection instead of precedence.I also added a second complementary flag to the proposal:
--secret-material-env KEY[=ENVVAR]— a dedicated flag, not the--material KEY=env:VARvalue-grammar variant rejected under Alternatives. It mirrors howprovider create --credential KEYalready ingests credential values from the CLI environment, so it's the smallest consistent implementation (client-side only), and it serves callers whose secrets already live in env (k8ssecretKeyRef, CI) with no file on disk at all. #2178 implements it, manually verified against a stock v0.0.78 gateway.My suggestion: land the env flag first since it's already implemented and verified, and follow up with
--secret-material-file/KEY=-on the same seam. If you'd rather have the file flag first, I'll gladly pivot #2178 — either way.hunglp6d commentedon Jul 10, 2026
Resolved by #2178 (merged):
provider refresh configure --secret-material-env KEY[=ENVVAR]addresses the secret-in-argv exposure raised here. Only the variable name reaches argv — the value no longer appears inps,/proc/<pid>/cmdline, or shell history — and duplicate keys across--material/--secret-material-envare rejected.Closing as completed. The
--secret-material-file KEY=PATH/KEY=-(stdin) variant remains the natural follow-up for disk-backed secrets — will reopen if that becomes actionable.Thanks @johntmyers for the review.