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 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.
-
--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 /
:487 — google_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
Problem Statement
openshell provider refresh configureaccepts all refresh material — including secrets like a service-accountprivate_keyor an OAuth2client_secret— only through--material KEY=VALUEcommand-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-inGOOGLE_VERTEX_AI_SERVICE_ACCOUNT_TOKENprovider 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 gRPCConfigureProviderRefreshRequest.materialmap — 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.--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:
KEYmay be supplied only once across--material,--secret-material-file, and--secret-material-env.secret_material_keys.--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):
(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
--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).--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.Agent Investigation
Explored
openshell-cli+openshell-server:--materialisVec<String>;:902--secret-material-keyis names only. No file/stdin/env flag on theConfiguresubcommand.parse_key_value_pairs— splits on =, stores value verbatim (no@file/env:).:487—google_service_account_jwt/oauth2_client_credentialsread secrets from the material map only.read_gcloud_adcreads a file but isauthorized_user-only and rejectsservice_accountkeys (:4376).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