fix(connectors): runtime config API returns plugin credentials with auth disabled by default
- Dominant language
- Rust
- Stars
- 4.9k
- Forks
- 432
- Avg merge
- 2d 10h
- Merged PRs (30d)
- 173
Description
## Summary
The connectors runtime control API returns plugin configuration verbatim, credentials included, and its authentication is disabled by default. The default bind is loopback, so this is a hardening gap rather than a remote disclosure — but nothing warns an operator who moves the listener off loopback, which is a normal thing to do when running the runtime in a container.
Filing publicly rather than to the security list because of the loopback default. Happy to move it if maintainers judge otherwise.
## Configuration returns secrets
`core/connectors/runtime/src/api/source.rs::get_source_plugin_config` serves `source.config.plugin_config` as-is:
```rust
let (content_type, config) = map_connector_config(config, format)?;
```
`plugin_config` is the raw `serde_json::Value` parsed from TOML, so every credential an operator configured comes back in the clear. `GET /sources/{key}/configs` and `/configs/{version}` and `/configs/active` return the enclosing `SourceConfig`, which carries the same field. The sink routes mirror all of it. A repo-wide grep finds no redaction anywhere in `core/connectors/runtime/src` — the only `redact` hits are log-line truncation in `stream.rs`.
For most connectors that means a database connection string. For `iggy_connector_http_source` (#3798) it also means a `management_token`, which mints webhook endpoints, and each endpoint's HMAC secret.
## Authentication is off by default
`core/connectors/runtime/src/api/auth.rs::resolve_api_key`:
```rust
if context.api_key.expose_secret().is_empty() {
return Ok(next.run(request).await);
}
```
`core/connectors/runtime/config.toml` ships:
```toml
address = "127.0.0.1:8081"
api_key = "" # Optional API key for authentication to be passed as `api-key` header
```
So out of the box the API is unauthenticated. Loopback confines that to local processes, which is a defensible posture for an admin API — the gap is that the two defaults compose into "any local process can read every connector credential", and an operator who changes `address` to `0.0.0.0` to reach the API from outside a container gets an unauthenticated endpoint serving credentials with no warning at any layer.
## Suggested fixes, roughly in order of value
1. Redact credential-bearing fields in the config responses, or gate those specific routes behind a configured `api_key` regardless of the global default.
2. Log a warning at startup when `api_key` is empty and `address` is not loopback.
3. Document in the runtime README that the control API returns credentials in plaintext and should be treated as privileged.
Related: #3801, which is why the plugin-side `SecretString` annotations do not help here.
Found while preparing #3798, whose README now documents this exposure for its own users.
Contributor guide
Research direction
Start with core/connectors/runtime/src/api/source.rs::get_source_plugin_config and trace the GET /sources/{key}/configs routes, then inspect api/auth.rs::resolve_api_key and core/connectors/runtime/config.toml. Review the runtime README and related issues #3801 and #3798 before choosing the security boundary. Done means credential exposure and the unauthenticated non-loopback configuration are addressed, with operator guidance where needed.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- api, backend, security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100