fix(CubeAPI): propagate create-time env vars to envd-backed commands#566
fix(CubeAPI): propagate create-time env vars to envd-backed commands#566zyl1121 wants to merge 1 commit into
Conversation
273018b to
2779e07
Compare
| return Ok(()); | ||
| }; | ||
| let url = build_envd_init_url(base_url, sandbox_id); | ||
| let resp = self |
There was a problem hiding this comment.
Missing HTTP timeout on envd init call – The shared reqwest::Client in state.rs (line 46-50) is built without .timeout(), which means reqwest::Client defaults to no timeout. If the envd proxy is unresponsive, this POST will hang indefinitely, blocking the sandbox creation endpoint for that handler. Since this call is best-effort (failure is logged and swallowed), it should have a tight timeout so it fails fast.
| let resp = self | |
| let resp = self | |
| .http_client | |
| .post(url) | |
| .json(&EnvdInitRequest { env_vars }) | |
| .timeout(std::time::Duration::from_secs(10)) | |
| .send() | |
| .await | |
| .map_err(|e| AppError::Internal(anyhow::anyhow!("envd init request failed: {}", e)))?; |
| let url = build_envd_init_url(base_url, sandbox_id); | ||
| let resp = self | ||
| .http_client | ||
| .post(url) |
There was a problem hiding this comment.
Missing auth header on envd init request – Every other call to the sandbox proxy (e.g. run_envd_command in agenthub.rs:2162) sends Authorization: Basic cm9vdDo=. This new init_sandbox_env_vars method sends no auth header. If the downstream /init endpoint enforces the same proxy-level auth, this will silently fail on every request (logged at warn, caller sees success). If it does not enforce auth, this is an inconsistency that may widen the attack surface.
| .post(url) | |
| .http_client | |
| .post(url) | |
| .header("Authorization", "Basic cm9vdDo=") | |
| .json(&EnvdInitRequest { env_vars }) |
|
|
||
| async fn init_sandbox_env_vars( | ||
| &self, | ||
| sandbox_id: &str, |
There was a problem hiding this comment.
Silent skip when env vars are provided but no proxy URL is configured – When sandbox_proxy_base_url is None and env_vars is non-empty, the init call is skipped silently. The caller (create_sandbox) only logs a warning when the HTTP call itself fails, not when it's skipped. This makes it hard for operators to debug why client-provided env vars are being silently ignored. Consider adding a tracing::warn! here when env_vars is non-empty but the proxy URL is unset.
| env_vars: &'a HashMap<String, String>, | ||
| } | ||
|
|
||
| pub(crate) fn default_sandbox_proxy_base_url() -> Option<String> { |
There was a problem hiding this comment.
pub(crate) function should have a doc comment – default_sandbox_proxy_base_url is visible outside this module (called from services/mod.rs:104) but has no doc comment. It reads three environment variables with a fallback chain. A doc comment explaining the env vars (AGENTHUB_SANDBOX_PROXY_URL, CUBE_SANDBOX_NODE_IP, CUBE_PROXY_HTTP_PORT), their meanings, and the fallback priority would help future maintainers.
| Path((sandbox_id, port)): Path<(String, u16)>, | ||
| Json(body): Json<Value>, | ||
| ) -> StatusCode { | ||
| assert_eq!(sandbox_id, "sb-123"); |
There was a problem hiding this comment.
Assertions inside spawned test handler are invisible on failure – These assert_eq! calls run inside a tokio::spawn-ed axum handler. If they fail (say from a code change that alters the URL path), the panic is caught by tokio and logged to stderr, but does not cause the test to fail. The test would silently pass because the handler returning an error response triggers the best-effort fallback. Consider returning the received values in the response and asserting from the caller side, or using a tokio::sync::oneshot channel to propagate assertion failures.
|
Are you doing the same thing as PR #554? |
Thanks for pointing this out. It is related to #554, but the fix is applied at a different layer. #554 calls envd The main reason is compatibility with clients that do not use the CubeSandbox SDK, especially E2B SDK users, since the E2B SDK sends |
| const RET_CODE_HTTP_OK: i32 = 200; | ||
| const RET_CODE_NOT_FOUND: i32 = 130404; | ||
| const RET_CODE_CONFLICT: i32 = 130409; | ||
| const ENVD_PORT: u16 = 49983; |
There was a problem hiding this comment.
Security: This hardcoded credential (Basic cm9vdDo= → root with empty password) is shared across all deployments and transmitted over plain HTTP when the proxy URL is derived from CUBE_SANDBOX_NODE_IP. Consider making this configurable via an environment variable (with the current value as a fallback default), and document that production deployments should use per-deployment secrets over HTTPS.
There was a problem hiding this comment.
This PR intentionally reuses the existing sandbox-proxy auth behavior rather than introducing a new auth contract.
The same Authorization: Basic cm9vdDo= value is already used by the existing envd proxy path in run_envd_command (CubeAPI/src/handlers/agenthub.rs). In this PR I only aligned the new create-time /init path with that existing server-side behavior.
I agree that making the proxy auth configurable would be a worthwhile follow-up, but I would prefer to keep that as a separate change because it affects the broader CubeAPI -> sandbox proxy contract rather than this narrow env propagation fix.
| return Ok(()); | ||
| }; | ||
| let url = build_envd_init_url(base_url, sandbox_id); | ||
| let resp = self |
There was a problem hiding this comment.
Code quality: Wrapping the error with format!("...{}", e) calls Display on the underlying error, which often produces a shorter message than the full chain. Consider using .context("envd init request failed") from the anyhow crate (if AppError::Internal accepts anyhow::Error) to preserve the full error chain for debugging.
| @@ -469,6 +497,98 @@ | |||
| limit, | |||
There was a problem hiding this comment.
Security (input validation): The env_vars map accepts arbitrary key/value pairs with no size limits. An API client could send thousands of entries or large values, potentially causing memory exhaustion. Consider capping the number of entries and maximum key/value length, either via #[validate] attributes or manual validation in create_sandbox.
There was a problem hiding this comment.
Thanks. I agree that request-size limits are worth considering.
For this PR, I kept the scope limited to restoring the verified CubeAPI -> envd propagation path for create-time envs. Adding entry-count / key-length / value-length limits would introduce a broader API validation contract for NewSandbox, which likely deserves separate discussion so the limits can be applied consistently across callers and documented clearly.
I would prefer to keep this PR focused on the propagation fix and handle request-size validation as a follow-up if maintainers think that should be enforced at the API layer.
| derive_sandbox_proxy_base_url( | ||
| agenthub_proxy.as_deref(), | ||
| node_ip.as_deref(), | ||
| proxy_port.as_deref(), |
There was a problem hiding this comment.
Security / Robustness: CUBE_SANDBOX_NODE_IP is used directly in URL construction without validating it's a valid IP address or hostname. Consider validating the value (e.g., parse as IPv4) before using it in URL construction, and reject malformed values instead of silently constructing a potentially unsafe URL.
34e765f to
c466139
Compare
| const RET_CODE_CONFLICT: i32 = 130409; | ||
| const ENVD_PORT: u16 = 49983; | ||
| const ENVD_INIT_TIMEOUT_SECS: u64 = 10; | ||
| const SANDBOX_PROXY_BASIC_AUTH: &str = "Basic cm9vdDo="; |
There was a problem hiding this comment.
Hardcoded weak Basic auth credential — This decodes to root: (username root, empty password). The credential is identical across all deployments, non-rotatable without a code change, and trivially recoverable from source/binary. Anyone with network access to the CubeProxy endpoint can replay it to inject arbitrary env vars into any sandbox.
Suggestion: Promote this to a runtime configuration value (e.g., env var CUBE_PROXY_AUTH_TOKEN) loaded at service startup instead of hardcoding it. At minimum, use a non-empty password and document that operators must override it.
Ref: CWE-798
| if let Some(env_vars) = env_vars.as_ref() { | ||
| if let Err(err) = self.init_sandbox_env_vars(&resp.sandbox_id, env_vars).await { | ||
| tracing::warn!( | ||
| sandbox_id = %resp.sandbox_id, | ||
| error = %err, | ||
| "envd init after sandbox create failed" | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
Sequential envd init delays sandbox creation response — The init_sandbox_env_vars call is awaited synchronously before returning the sandbox response to the caller. Since this is best-effort (errors are swallowed), the client still waits for envd init latency — up to the 10-second timeout on failure. This inflates tail latency for every create_sandbox even though the sandbox is already usable.
Suggestion: Spawn envd init as a detached background task so the response returns immediately after CubeMaster confirms creation. SandboxService already derives Clone and reqwest::Client is Arc-backed, so cloning is cheap.
| let proxy_port = proxy_port | ||
| .map(str::trim) | ||
| .filter(|value| !value.is_empty()) | ||
| .unwrap_or("80"); |
There was a problem hiding this comment.
Silent default port 80 masks misconfiguration — When CUBE_PROXY_HTTP_PORT is unset, the code silently defaults to port 80. If the proxy listens on 8080 (a common alternative), envd init silently fails and — because it's best-effort — the sandbox still creates without env vars being applied. The failure surfaces only as a generic HTTP error.
Suggestion: Return None from derive_sandbox_proxy_base_url when CUBE_SANDBOX_NODE_IP is set but CUBE_PROXY_HTTP_PORT is absent, letting the missing configuration be logged as a warning rather than silently assuming port 80.
| fn build_envd_init_url(base_url: &str, sandbox_id: &str) -> String { | ||
| format!( | ||
| "{}/sandbox/{}/{}/init", | ||
| base_url.trim_end_matches('/'), | ||
| sandbox_id, | ||
| ENVD_PORT | ||
| ) | ||
| } |
There was a problem hiding this comment.
Missing URL path-segment encoding — sandbox_id is interpolated directly into the URL path without validation or encoding. The codebase has a validate_path_segment helper (in cubemaster/mod.rs:567) used elsewhere that is not applied here. While the current sandbox_id is server-generated with a constrained character set, inconsistent validation creates a maintenance hazard.
Suggestion: Call validate_path_segment before constructing the URL, or add a comment documenting why it's unnecessary (e.g., the ID is guaranteed UUID-like).
|
|
||
| #[serde(rename = "envVars", skip_serializing_if = "Option::is_none")] | ||
| #[serde( | ||
| alias = "envs", |
There was a problem hiding this comment.
envs alias not documented on the struct — The #[serde(alias = "envs")] attribute is tested but the NewSandbox struct-level doc comment only mentions envVars. API consumers reading the source won't know that envs (used by the E2B SDK) is also accepted.
Suggestion: Update the struct doc comment to mention that envs is an accepted alias for envVars. Also consider documenting the behavior when both envs and envVars are present (serde_json last-writer-wins for aliased fields).
OverviewThis PR adds envd-based environment variable injection during sandbox creation, with input validation, E2B SDK compatibility ( RED Medium Severity
YELLOW Low-Medium Severity
GREEN Minor
Overall: Clean, well-tested PR with good security. The information disclosure fix and value validation are the most actionable items. |
851ec0d to
c7f0071
Compare
c7f0071 to
7afe5f1
Compare
| let url = build_envd_init_url(base_url, sandbox_id); | ||
|
|
||
| let mut last_err: Option<anyhow::Error> = None; | ||
| for attempt in 0..ENVD_INIT_MAX_ATTEMPTS { |
There was a problem hiding this comment.
I think we should avoid retry here.
There was a problem hiding this comment.
I think we should avoid retry here.
I agree this is worth reconsidering.
For context, my original design was best-effort and did not retry. The retry was added later in a follow-up commit by @fslongjin while consolidating parts of #554, mainly to avoid false failures when envd is not ready immediately.
I think the trade-off is reliability vs. create-path efficiency. It may be better to discuss the expected behavior and overall design first.
There was a problem hiding this comment.
I think we should avoid retry here.
I agree this is worth reconsidering.
For context, my original design was best-effort and did not retry. The retry was added later in a follow-up commit by @fslongjin while consolidating parts of #554, mainly to avoid false failures when envd is not ready immediately.
I think the trade-off is reliability vs. create-path efficiency. It may be better to discuss the expected behavior and overall design first.
I thought about it, maybe it's more reasonable to change this to fail-fast — errors shouldn't be silently swallowed.
|
Can you squash it into one commit? |
7afe5f1 to
2b485fa
Compare
@kinwin-ustc Done. I rebased the branch onto the latest
I updated the envd init behavior to fail fast with cleanup based on the discussion with @fslongjin. The PR description has also been updated to match the final behavior. The current behavior is: when create-time env vars are provided for envd-backed commands, envd init is treated as part of successful sandbox creation. If envd init fails after CubeMaster creates the sandbox, CubeAPI cleans up the created sandbox and returns an error instead of retrying or ignoring the failure. |
2b485fa to
68c848a
Compare
68c848a to
629828b
Compare
Accept the E2B envs alias on sandbox creation requests and validate create-time env var names before creating the sandbox. After CubeMaster creates the sandbox, initialize envd through CubeProxy using the configured sandbox proxy base URL. If envd init fails, clean up the created sandbox and return an error instead of retrying or silently swallowing the failure. Signed-off-by: zhengyilei <zheng_yilei@qq.com> Co-authored-by: jinlong <jinlong@tencent.com>
629828b to
bd9f786
Compare
Motivation
For envd-backed command execution, create-time environment variables should be propagated from CubeAPI into envd.
On the current CubeAPI sandbox creation path, this propagation is incomplete:
envs, while CubeAPI only acceptedenvVarsAs a result, envd-backed execution such as
commands.runcannot see environment variables passed at sandbox creation time.This PR does not introduce a generic sandbox startup-time environment refresh model across all runtime paths.
What this PR changes
accept
envsas an alias ofenvVarsinNewSandbox, so both SDK entrypoints are supportedreject invalid or dangerous create-time env var names before sandbox creation
after CubeMaster successfully creates a sandbox, initialize envd with the create-time environment variable map through CubeProxy path-based routing:
derive the CubeProxy base URL from existing runtime configuration instead of introducing a new configuration item
fail sandbox creation if envd initialization fails after CubeMaster creates the sandbox, and clean up the created sandbox to avoid leaving a partial result
add focused tests for alias deserialization, CubeProxy URL derivation, path-based envd
/initrouting, fail-fast failure handling, and post-create cleanupScope
This PR restores create-time environment variables for envd-backed execution, such as
commands.run.It does not change
run_code.run_codegoes through the bundled lightweight code interpreter instead of envd-backed process execution, and that interpreter does not currently read envd/envs.It also does not claim generic startup-time env refresh support for every sandbox runtime path.
Testing
Unit tests:
Cluster validation:
Before this PR, on current
origin/master:After this PR:
envspathenvVarspathcommands.runsees create-time env vars after sandbox creationcommands.runfalls back to the sandbox-level env varspause/resumepreserves the samecommands.runenv visibility in the current lifecycle modelrun_coderemains<NOT SET>on the current runtime imageCompatibility
This change is backward compatible:
envVarsrequests continue to workenvsrequests now workFor envd-backed command execution, precedence remains:
Documentation
No repository documentation was updated in this PR.
The behavior change is intentionally narrow: CubeAPI now propagates create-time environment variables to envd for envd-backed command execution and treats failed envd initialization as a create failure.
The remaining
run_codebehavior is tracked in the linked issue and requires runtime/image-side support in the bundled lightweight code interpreter.Related issue
Refs #565