-
Notifications
You must be signed in to change notification settings - Fork 218
sec: Support Laminar API key injection to prevent credential leaks #2814
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,8 +32,13 @@ def _get_int_env(key: str) -> int | None: | |
| return None | ||
|
|
||
|
|
||
| def maybe_init_laminar(): | ||
| """Initialize Laminar if the environment variables are set. | ||
| def maybe_init_laminar(api_key: str | None = None): | ||
| """Initialize Laminar if the environment variables are set or api_key is provided. | ||
|
|
||
| Args: | ||
| api_key: Optional Laminar API key. If provided, used instead of reading from | ||
| environment variables. This allows secure credential injection without | ||
| exposing secrets in environment variables. | ||
|
|
||
| Example configuration: | ||
|
|
||
|
|
@@ -53,8 +58,13 @@ def maybe_init_laminar(): | |
| LMNR_HTTP_PORT=8000 | ||
| LMNR_GRPC_PORT=8001 | ||
| """ | ||
| if should_enable_observability(): | ||
| if _is_otel_backend_laminar(): | ||
| if should_enable_observability(api_key=api_key): | ||
| if _is_otel_backend_laminar(api_key=api_key): | ||
| # If api_key is provided, set it in environment for Laminar initialization | ||
| if api_key: | ||
| import os | ||
| os.environ["LMNR_PROJECT_API_KEY"] = api_key | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Critical - Design Flaw: You're still setting The problem: PR description says credentials leak because "Laminar SDK reads credentials from os.environ[], which gets serialized into logged data structures." Your solution: Pass the key as a parameter... then set it in os.environ anyway. Question: How does this prevent the environment from being logged? If any code takes a snapshot of What you've actually done: Changed WHEN the key enters the environment, not WHETHER it enters the environment. That's not a solution to environment serialization. Better approach: Either:
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Critical: This defeats the entire purpose of the PR. The code still sets Root cause: Laminar SDK provides a Laminar.initialize(
project_api_key=api_key,
http_port=_get_int_env("LMNR_HTTP_PORT"),
grpc_port=_get_int_env("LMNR_GRPC_PORT"),
)This eliminates the need to touch |
||
|
|
||
| Laminar.initialize( | ||
| http_port=_get_int_env("LMNR_HTTP_PORT"), | ||
| grpc_port=_get_int_env("LMNR_GRPC_PORT"), | ||
|
|
@@ -112,7 +122,18 @@ def decorator(func: Callable[P, R]) -> Callable[P, R]: | |
| return decorator | ||
|
|
||
|
|
||
| def should_enable_observability(): | ||
| def should_enable_observability(api_key: str | None = None): | ||
| """Check if observability/tracing should be enabled. | ||
|
|
||
| Args: | ||
| api_key: Optional Laminar API key. If provided, enables observability | ||
| even if no environment variables are set. | ||
|
|
||
| Returns: | ||
| True if observability should be enabled, False otherwise. | ||
| """ | ||
| if api_key: | ||
| return True | ||
| keys = [ | ||
| "LMNR_PROJECT_API_KEY", | ||
| "OTEL_ENDPOINT", | ||
|
|
@@ -126,12 +147,18 @@ def should_enable_observability(): | |
| return False | ||
|
|
||
|
|
||
| def _is_otel_backend_laminar(): | ||
| def _is_otel_backend_laminar(api_key: str | None = None): | ||
| """Simple heuristic to check if the OTEL backend is Laminar. | ||
|
|
||
| Caveat: This will still be True if another backend uses the same | ||
| authentication scheme, and the user uses LMNR_PROJECT_API_KEY | ||
| instead of OTEL_HEADERS to authenticate. | ||
|
|
||
| Args: | ||
| api_key: Optional Laminar API key. If provided, backend is Laminar. | ||
| """ | ||
| if api_key: | ||
| return True | ||
| key = get_env("LMNR_PROJECT_API_KEY") | ||
| return key is not None and key != "" | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -75,6 +75,12 @@ class ApptainerWorkspace(RemoteWorkspace): | |
| default_factory=lambda: ["DEBUG"], | ||
| description="Environment variables to forward to the container.", | ||
| ) | ||
| laminar_api_key: str | None = Field( | ||
| default=None, | ||
| description="Laminar API key for observability tracing. " | ||
| "When provided, injected as LMNR_PROJECT_API_KEY in the container. " | ||
| "Should NOT be included in forward_env to avoid logging leaks.", | ||
| ) | ||
| mount_dir: str | None = Field( | ||
| default=None, | ||
| description="Optional host directory to mount into the container.", | ||
|
|
@@ -243,8 +249,22 @@ def _start_container(self) -> None: | |
| env_args: list[str] = [] | ||
| for key in self.forward_env: | ||
| if key in os.environ: | ||
| # Skip LMNR_PROJECT_API_KEY if it's in forward_env to avoid log leaks | ||
| # Use laminar_api_key field instead | ||
| if key == "LMNR_PROJECT_API_KEY": | ||
| logger.warning( | ||
| "LMNR_PROJECT_API_KEY is in forward_env list. " | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Suggestion - Same as Docker: Same log spam concern as docker/workspace.py. Consider rate-limiting or making this a migration-period warning. |
||
| "This may cause credential leaks in logs. " | ||
| "Use the 'laminar_api_key' field instead." | ||
| ) | ||
| continue | ||
| env_args += ["--env", f"{key}={os.environ[key]}"] | ||
|
|
||
| # Inject Laminar API key directly (not via environment variable) | ||
| # This ensures credentials don't appear in logged payloads | ||
| if self.laminar_api_key: | ||
| env_args += ["--env", f"LMNR_PROJECT_API_KEY={self.laminar_api_key}"] | ||
|
|
||
| # Prepare bind mounts | ||
| bind_args: list[str] = [] | ||
| if self.mount_dir: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -91,6 +91,12 @@ class DockerWorkspace(RemoteWorkspace): | |
| default_factory=lambda: ["DEBUG"], | ||
| description="Environment variables to forward to the container.", | ||
| ) | ||
| laminar_api_key: str | None = Field( | ||
| default=None, | ||
| description="Laminar API key for observability tracing. " | ||
| "When provided, injected as LMNR_PROJECT_API_KEY in the container. " | ||
| "Should NOT be included in forward_env to avoid logging leaks.", | ||
| ) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Critical: The Tested with Fix: Add Pydantic field attributes to prevent serialization: laminar_api_key: str | None = Field(
default=None,
exclude=True, # Prevents model_dump() serialization
repr=False, # Prevents __repr__ exposure
description="Laminar API key for observability tracing. "
"When provided, injected as LMNR_PROJECT_API_KEY in the container. "
"Should NOT be included in forward_env to avoid logging leaks.",
)Apply the same fix to |
||
| mount_dir: str | None = Field( | ||
| default=None, | ||
| description="Optional host directory to mount into the container.", | ||
|
|
@@ -211,8 +217,22 @@ def _start_container(self, image: str, context: Any) -> None: | |
| flags: list[str] = [] | ||
| for key in self.forward_env: | ||
| if key in os.environ: | ||
| # Skip LMNR_PROJECT_API_KEY if it's in forward_env to avoid log leaks | ||
| # Use laminar_api_key field instead | ||
| if key == "LMNR_PROJECT_API_KEY": | ||
| logger.warning( | ||
| "LMNR_PROJECT_API_KEY is in forward_env list. " | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Suggestion - Log Spam: This warning will fire every time someone has Consider: Either make this a one-time warning, or only warn in specific contexts (e.g., when Datadog logging is enabled), or document that users should migrate and remove the warning after a deprecation period. Continuous log spam isn't helpful - it trains users to ignore warnings. |
||
| "This may cause credential leaks in logs. " | ||
| "Use the 'laminar_api_key' field instead." | ||
| ) | ||
| continue | ||
| flags += ["-e", f"{key}={os.environ[key]}"] | ||
|
|
||
| # Inject Laminar API key directly (not via environment variable) | ||
| # This ensures credentials don't appear in logged payloads | ||
| if self.laminar_api_key: | ||
| flags += ["-e", f"LMNR_PROJECT_API_KEY={self.laminar_api_key}"] | ||
|
|
||
| for volume in self.volumes: | ||
| flags += ["-v", volume] | ||
| logger.info(f"Adding volume mount: {volume}") | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -84,6 +84,12 @@ class APIRemoteWorkspace(RemoteWorkspace): | |
| default_factory=list, | ||
| description="Environment variable names to forward from host to runtime.", | ||
| ) | ||
| laminar_api_key: str | None = Field( | ||
| default=None, | ||
| description="Laminar API key for observability tracing. " | ||
| "When provided, injected as LMNR_PROJECT_API_KEY in the runtime environment. " | ||
| "Should NOT be included in forward_env to avoid logging leaks.", | ||
| ) | ||
|
|
||
| _runtime_id: str | None = PrivateAttr(default=None) | ||
| _runtime_url: str | None = PrivateAttr(default=None) | ||
|
|
@@ -192,8 +198,22 @@ def _start_runtime(self) -> None: | |
| environment: dict[str, str] = {} | ||
| for key in self.forward_env: | ||
| if key in os.environ: | ||
| # Skip LMNR_PROJECT_API_KEY if it's in forward_env to avoid log leaks | ||
| # Use laminar_api_key field instead | ||
| if key == "LMNR_PROJECT_API_KEY": | ||
| logger.warning( | ||
| "LMNR_PROJECT_API_KEY is in forward_env list. " | ||
| "This may cause credential leaks in logs. " | ||
| "Use the 'laminar_api_key' field instead." | ||
| ) | ||
| continue | ||
| environment[key] = os.environ[key] | ||
|
|
||
| # Inject Laminar API key directly (not via environment variable) | ||
| # This ensures credentials don't appear in logged payloads | ||
| if self.laminar_api_key: | ||
| environment["LMNR_PROJECT_API_KEY"] = self.laminar_api_key | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 Important - Missing Evidence: The PR claims this prevents credential leaks in "workspace initialization payloads (logged to Datadog)" and "runtime API request logs (/start endpoint)". Where's the proof? Show me:
Security claims without evidence are just wishes. Did you actually verify this prevents the logging you mentioned in OpenHands/evaluation#390?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 Important: The comment claims "credentials don't appear in logged payloads," but this is misleading. The credential is added to the
Additionally, line 243 logs The credential is still being passed as an environment variable — just injected at a different point. The proper fix is to use Laminar's |
||
|
|
||
| # For binary target, use the standalone binary | ||
| payload: dict[str, Any] = { | ||
| "image": self.server_image, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟠 Important - Missing Tests: This is a security-critical feature with zero test coverage.
Required tests:
api_keyparameter is provided, Laminar initializes correctlyDon't just test the happy path - test the actual security property you're claiming to provide.