[RFC] Add `token_command` authentication option to YTsaurus CLI and SDKs
- Dominant language
- C++
- Stars
- 2.2k
- Forks
- 220
- PR merge metrics
- No merged PRs in 30d
Description
## Summary
Add a new client configuration option, `token_command`, that allows the YTsaurus CLI and SDKs to obtain an access token by executing an external local command.
This enables integration with external credential stores such as OS keychains, password managers, Vault, cloud CLIs, SSO helpers, and enterprise secret brokers without requiring users to store YTsaurus tokens in plaintext files or environment variables.
Example:
```yson
{
proxy = { url = "cluster-name" };
token_command = "yt-token-helper get --cluster cluster-name";
}
```
or profile-based:
```yson
{
config_version = 2;
default_profile = "prod";
profiles = {
prod = {
proxy = { url = "prod-cluster" };
token_command = "secret-tool lookup service ytsaurus cluster prod-cluster";
};
};
}
```
## Motivation
Today, the documented token lookup order for the Python client is:
1. `config["token"]`, commonly populated from `YT_TOKEN`.
2. `config["token_path"]`, defaulting to `~/.yt/token`, optionally overridden by `YT_TOKEN_PATH`.
The CLI documentation similarly tells users to place the token in `~/.yt/token` or `YT_TOKEN`.
This is simple, but it has drawbacks:
* `YT_TOKEN` leaks too easily through process environments, shell history, CI logs, debug dumps, and `/proc//environ`-style inspection.
* `~/.yt/token` is a static plaintext secret on disk unless users build custom wrappers around the CLI.
* Token rotation usually requires rewriting files or restarting long-lived tools.
* Organizations often already have approved credential mechanisms: macOS Keychain, GNOME Keyring, KWallet, 1Password, Bitwarden, HashiCorp Vault, cloud IAM helpers, Kerberos/OIDC wrappers, or internal SSO tools.
* Python currently has `auth_class`, but that requires Python importable code and is not a universal CLI/SDK contract.
The proposed `token_command` is the minimal cross-language primitive: “run this command, read token from stdout.”
Similar patterns are already widely used:
* AWS has `credential_process`, where a configured external process returns credentials and optional expiration metadata.
* Kubernetes has exec credential plugins that return credentials to clients through a structured API.
* Git supports credential helpers for delegating storage to local mechanisms such as keychains or in-memory caches.
## Goals
* Allow CLI and SDK clients to obtain a token from an external command.
* Avoid storing long-lived tokens in plaintext YTsaurus config files.
* Keep the feature simple enough to implement in every SDK.
* Preserve the existing behavior for `token`, `YT_TOKEN`, `token_path`, and `YT_TOKEN_PATH`.
* Support token rotation without changing YTsaurus config.
* Support local OS keychains and enterprise vaults through user-provided helpers.
* Make failure modes explicit and debuggable without leaking token contents.
## Non-goals
* Implement a built-in keychain, Vault, OIDC, Kerberos, or cloud IAM client inside YTsaurus.
* Define a full cross-vendor credential exchange protocol in the first version.
* Replace Python `auth_class`.
* Change server-side authentication.
* Store or cache returned tokens server-side.
* Automatically refresh a token in the middle of a single HTTP/RPC request.
## Proposed configuration
Add a new optional client config key:
```yson
token_command = "";
```
Example:
```yson
{
proxy = { url = "hahn" };
token_command = "pass show ytsaurus/hahn/token";
}
```
For JSON config:
```json
{
"proxy": { "url": "hahn" },
"token_command": "op read op://infra/ytsaurus-hahn/token"
}
```
Optional environment override:
```bash
export YT_TOKEN_COMMAND='secret-tool lookup service ytsaurus cluster hahn'
```
The env var should apply to the global config in the same spirit as `YT_TOKEN_PATH`.
## Token lookup order
Recommended final lookup order:
1. Explicit `config["token"]` / `YT_TOKEN`.
2. Explicit `config["token_command"]` / `YT_TOKEN_COMMAND`.
3. `config["token_path"]` / `YT_TOKEN_PATH`.
4. Existing default `~/.yt/token`.
Rationale: `token` remains the strongest explicit override. `token_command` should precede `token_path` because it is normally chosen to avoid static token files. Existing users with only `token_path` are unaffected.
A stricter alternative is to make `token_command` mutually exclusive with `token_path`; however, that makes migration annoying. Prefer priority order plus a debug log saying which source was selected.
## Command execution semantics
`token_command` is executed locally by the client process.
Rules:
* Command stdout contains the token.
* The token is the first line of stdout, with one trailing `\r?\n` stripped.
* Additional stdout after the first line is an error, unless explicitly allowed later.
* Stderr is captured for diagnostics, but must not be printed at normal log levels unless the command fails.
* Exit code `0` means success.
* Non-zero exit code means auth-token retrieval failed.
* Empty stdout means auth-token retrieval failed.
* The command has a timeout, default `10s`.
* The command is executed with stdin closed by default.
* The command inherits the environment by default, except sensitive YTsaurus token variables should be removed or masked.
Example valid helper:
```bash
#!/usr/bin/env bash
set -euo pipefail
secret-tool lookup service ytsaurus cluster "${YT_PROXY:?}"
```
## Command parsing
Two possible designs:
### Option A: string command, shell-style parsing
```yson
token_command = "op read op://infra/ytsaurus-hahn/token";
```
Pros:
* Easy to write.
* Similar to AWS `credential_process`.
* Good for config files edited by humans.
Cons:
* Cross-platform quoting is subtle.
* If implemented through a shell, it creates injection risks.
### Option B: argv list
```yson
token_command = ["op"; "read"; "op://infra/ytsaurus-hahn/token"];
```
Pros:
* No shell required.
* Safer and unambiguous.
* Easier to implement consistently.
Cons:
* More verbose.
* Existing config format examples become less copy-paste-friendly.
### Recommendation
Support both, but internally normalize to argv.
```yson
token_command = "op read op://infra/ytsaurus-hahn/token";
```
and:
```yson
token_command = ["op"; "read"; "op://infra/ytsaurus-hahn/token"];
```
The string form must not be executed via `/bin/sh -c`. It should be parsed using a small shell-like splitter, or initially restricted to “executable plus arguments with quotes.” If reliable cross-platform parsing is too much for v1, accept only list form and add a convenience `token_command_string` later.
## Structured output: defer to v2
For v1, stdout is just a raw bearer token:
```text
ytct-...
```
A future v2 may support JSON:
```json
{
"token": "ytct-...",
"expires_at": "2026-05-09T12:00:00Z"
}
```
This would enable SDK-side caching and refresh. But v1 should remain line-oriented because it is enough for keychains and password managers.
## Caching
Initial behavior:
* CLI: run `token_command` once per CLI invocation.
* Short-lived SDK clients: run once per client construction.
* Long-lived SDK clients: run lazily and cache in memory until auth failure or configured TTL.
Add optional config:
```yson
token_command_cache_ttl = 300000; // milliseconds
```
or:
```yson
token_command = {
command = ["op"; "read"; "op://infra/ytsaurus-hahn/token"];
cache_ttl = 300000;
timeout = 10000;
}
```
Recommended v1 shape:
```yson
token_command = "op read op://infra/ytsaurus-hahn/token";
token_command_timeout = 10000;
token_command_cache_ttl = 300000;
```
Reason: minimal changes to existing config schema.
Default cache policy:
* CLI: no persistent cache; process lifetime only.
* SDK: in-memory cache for `300s`.
* No disk cache in YTsaurus itself.
## Security considerations
`token_command` reduces the need to store tokens in plaintext files, but it introduces new risks.
### Config file becomes security-sensitive
Anyone who can modify `~/.yt/config` can make the client execute arbitrary commands. This is already partly true for Python `auth_class`, but `token_command` makes the behavior more general.
Mitigation:
* Document that config files must be user-writable only.
* Warn if config file is group/world-writable.
* Optionally refuse `token_command` from unsafe config files unless `allow_unsafe_token_command_config = true`.
### Avoid shell execution
Do not implement string form as:
```bash
/bin/sh -c "$token_command"
```
This would make config injection and quoting bugs worse.
Prefer argv execution.
### Avoid token leaks in logs
Never log:
* token stdout,
* full Authorization header,
* command output,
* expanded command if it embeds secrets.
Safe debug log:
```text
Using token_command from profile "prod": executable "op"
```
Unsafe debug log:
```text
Running token command: op read op://... --password hunter2
```
### Environment hygiene
The helper should not receive stale `YT_TOKEN` by default, because then accidental helper scripts may simply echo it or make precedence confusing.
Recommended child environment:
Inherit normal environment (without prefix `YT_`).
Remove `YT_TOKEN`.
Keep `YT_SECURE_VAULT_*`.
Keep and add when loaded from config:
- `YT_PROXY`
- `YT_CONFIG_PROFILE`
- `YT_TOKEN_COMMAND`
### Interactive commands
By default, stdin should be closed and interactive prompting should be disabled.
This avoids hanging scripts in CI.
## Failure behavior
If `token_command` fails, the client should fail authentication setup before making the YTsaurus request.
Example error:
```text
Failed to obtain YTsaurus token using token_command from profile "prod":
command exited with status 1 after 124 ms.
stderr:
secret not found: service=ytsaurus cluster=hahn
```
Do not fall back to `token_path` after a configured `token_command` fails.
Silent fallback could mask broken vault integration and accidentally use a stale token.
Fallback should happen only when the option is absent, not when it is present but failing.
## Examples
### macOS Keychain
```yson
{
proxy = { url = "hahn" };
token_command = ["security"; "find-generic-password"; "-s"; "ytsaurus:hahn"; "-w"];
}
```
### GNOME Secret Service
```yson
{
proxy = { url = "hahn" };
token_command = ["secret-tool"; "lookup"; "service"; "ytsaurus"; "cluster"; "hahn"];
}
```
### `pass`
```yson
{
proxy = { url = "hahn" };
token_command = ["pass"; "show"; "ytsaurus/hahn/token"];
}
```
### 1Password CLI
```yson
{
proxy = { url = "hahn" };
token_command = ["op"; "read"; "op://infra/ytsaurus-hahn/token"];
}
```
### HashiCorp Vault
```yson
{
proxy = { url = "hahn" };
token_command = ["sh"; "-c"; "vault kv get -field=token secret/ytsaurus/hahn"];
}
```
For this case, the safer recommended style is a wrapper script:
```yson
{
proxy = { url = "hahn" };
token_command = ["/usr/local/bin/get-ytsaurus-token"; "hahn"];
}
```
### Yandex Cloud IAM helper
Managed YTsaurus docs currently show Python-specific IAM auth through `auth_class` and Go-specific credential provider code. A command-based bridge could make CLI and non-Python SDKs consume the same helper:
```yson
{
proxy = {
url = "https://proxy..ytsaurus.yandexcloud.net";
enable_proxy_discovery = %false;
};
token_command = ["yc"; "iam"; "create-token"];
}
```
## SDK API design
### Python
Existing Python config already supports `token` and `token_path`. Add:
```python
client = yt.YtClient(
proxy="hahn",
config={
"token_command": ["op", "read", "op://infra/ytsaurus-hahn/token"],
},
)
```
Implementation sketch:
```python
def get_token_from_config(config):
if config.get("token"):
return config["token"]
if config.get("token_command"):
return run_token_command(config)
return read_token_from_path(config.get("token_path", "~/.yt/token"))
```
Interaction with `auth_class`:
* If `auth_class` is set, it remains a separate authentication provider.
* `auth_class` and `token_command` should be mutually exclusive unless a clear precedence already exists.
* If both are set, fail with a configuration error.
### Go SDK
The Go SDK already has a `CredentialsProviderFn` style in documented Yandex Cloud examples. Add a helper constructor:
```go
provider := yt.TokenCommandCredentialsProvider(yt.TokenCommandOptions{
Command: []string{"op", "read", "op://infra/ytsaurus-hahn/token"},
Timeout: 10 * time.Second,
CacheTTL: 5 * time.Minute,
})
```
or config-level support:
```go
client, err := ythttp.NewClient(&yt.Config{
Proxy: "hahn",
TokenCommand: []string{"op", "read", "op://infra/ytsaurus-hahn/token"},
})
```
Eventually Go SDK should support same config file as Python SDK `~/.yt/config`.
### C++ SDK
Add equivalent config fields and a reusable token provider implementation.
Conceptually:
```cpp
TClientConfig config;
config.Proxy = "hahn";
config.TokenCommand = std::vector{
"op", "read", "op://infra/ytsaurus-hahn/token"
};
```
## CLI UX
Config patch support:
```bash
yt --config '{token_command=["op"; "read"; "op://infra/ytsaurus-hahn/token"]}' list /
```
## Compatibility
Existing configs keep working.
No behavior changes unless `token_command` or `YT_TOKEN_COMMAND` is set.
Recommended precedence:
```text
token > token_command > token_path > ~/.yt/token
```
No fallback from a failing `token_command`.
## Open questions
1. Should `token_command` be a string, an argv list, or both?
2. Should v1 support JSON output with expiration, or keep that for v2?
3. Should SDK clients cache command output by default?
4. Should CLI cache command output between invocations? Probably no.
5. Should unsafe config permissions be a warning or hard error?
6. Should child process inherit full environment, or a restricted allowlist?
7. Should `token_command` be allowed in cluster-provided config, if such config is ever loaded from Cypress? Strong recommendation: no, only local trusted config.
## Minimal v1 proposal
Implement only:
```yson
token_command = ["program"; "arg1"; "arg2"];
token_command_timeout = 10000;
```
Behavior:
* Run once per process/client.
* Redirect stdin to `/dev/null`.
* Forward stderr.
* Read first stdout line as token.
* Strip one trailing newline.
* Fail on non-zero exit, timeout, or empty output.
* Do not use shell.
* Do not log token.
* Precedence: `token`, then `token_command`, then `token_path`.
This gives immediate integration with keychains and vaults while keeping the surface small.
## Suggested documentation text
> Instead of storing a token in `~/.yt/token` or `YT_TOKEN`, you can configure `token_command`. The command is executed locally, and its first stdout line is used as the YTsaurus token. This is useful for integrating with OS keychains, password managers, Vault, or SSO helpers.
>
> The command must be trusted. Anyone who can modify your YTsaurus config file may be able to execute arbitrary code as your user. Keep `~/.yt/config` readable and writable only by your user.
## Suggested first implementation steps
1. Add config fields: `token_command`, `token_command_timeout`.
2. Add unit tests for precedence, stdout parsing, timeout, non-zero exit, empty output, and no fallback after failure.
3. Add docs near the existing token setup section.
4. Add Go/C++ SDK equivalent helpers after the Python CLI semantics are settled.
Contributor guide
Assessment
This issue has not been assessed yet.