Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ paths:
description: Filter credentials by provider
schema:
type: string
enum: [github, gitlab, jira, google]
enum: [github, gitlab, jira, google, kubeconfig]
post:
summary: Create a new credential
security:
Expand Down Expand Up @@ -276,7 +276,7 @@ components:
type: string
provider:
type: string
enum: [github, gitlab, jira, google]
enum: [github, gitlab, jira, google, kubeconfig]
token:
type: string
writeOnly: true
Expand Down Expand Up @@ -311,7 +311,7 @@ components:
type: string
provider:
type: string
enum: [github, gitlab, jira, google]
enum: [github, gitlab, jira, google, kubeconfig]
token:
type: string
writeOnly: true
Expand All @@ -338,7 +338,7 @@ components:
description: ID of the credential
provider:
type: string
enum: [github, gitlab, jira, google]
enum: [github, gitlab, jira, google, kubeconfig]
description: Provider type for this credential
token:
type: string
Expand Down
4 changes: 2 additions & 2 deletions components/ambient-cli/cmd/acpctl/credential/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -356,12 +356,12 @@ func init() {

listCmd.Flags().StringVarP(&listArgs.outputFormat, "output", "o", "", "Output format: json")
listCmd.Flags().IntVar(&listArgs.limit, "limit", 100, "Maximum number of items to return")
listCmd.Flags().StringVar(&listArgs.provider, "provider", "", "Filter by provider (github|gitlab|jira|google)")
listCmd.Flags().StringVar(&listArgs.provider, "provider", "", "Filter by provider (github|gitlab|jira|google|kubeconfig)")

getCmd.Flags().StringVarP(&getArgs.outputFormat, "output", "o", "", "Output format: json")

createCmd.Flags().StringVar(&createArgs.name, "name", "", "Credential name (required)")
createCmd.Flags().StringVar(&createArgs.provider, "provider", "", "Provider (github|gitlab|jira|google) (required)")
createCmd.Flags().StringVar(&createArgs.provider, "provider", "", "Provider (github|gitlab|jira|google|kubeconfig) (required)")
createCmd.Flags().StringVar(&createArgs.token, "token", "", "Secret token or API key")
createCmd.Flags().StringVar(&createArgs.description, "description", "", "Description")
createCmd.Flags().StringVar(&createArgs.url, "url", "", "Service URL")
Expand Down
8 changes: 8 additions & 0 deletions components/runners/ambient-runner/.mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@
"READ_ONLY_MODE": "${JIRA_READ_ONLY_MODE:-true}"
}
},
"openshift": {
"command": "uvx",
"args": [
"kubernetes-mcp-server@latest",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Verify floating MCP package versions in runner MCP configs
fd -i '.mcp.json$' --exec rg -n '"[^"]+@latest"' {}

Repository: ambient-code/platform

Length of output: 47


🏁 Script executed:

# Check if the file exists and read its contents
if [ -f "components/runners/ambient-runner/.mcp.json" ]; then
  echo "=== File exists, reading contents ==="
  cat -n "components/runners/ambient-runner/.mcp.json"
else
  echo "=== File does not exist at specified path ==="
  # Search for any .mcp.json files
  echo "=== Searching for .mcp.json files ==="
  find . -name ".mcp.json" -o -name "*.mcp.json" 2>/dev/null | head -20
fi

Repository: ambient-code/platform

Length of output: 1776


Pin kubernetes-mcp-server to an immutable version.

Line 29 uses @latest, which pulls unreviewed upstream changes at runtime. This introduces non-deterministic behavior and supply-chain exposure. Compare with line 37's pinned workspace-mcp@1.6.1 approach.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/runners/ambient-runner/.mcp.json` at line 29, Replace the
non-deterministic dependency "kubernetes-mcp-server@latest" with a pinned
immutable version (e.g., a specific semver or exact commit/hash) to avoid
pulling unreviewed upstream changes at runtime; mirror the approach used for
"workspace-mcp@1.6.1" by specifying an exact version for "kubernetes-mcp-server"
in the list so the build is reproducible and auditable.

"--kubeconfig", "/tmp/.ambient_kubeconfig",
"--disable-multi-cluster"
]
},
"google-workspace": {
"command": "uvx",
"args": [
Expand Down
30 changes: 28 additions & 2 deletions components/runners/ambient-runner/ambient_runner/platform/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
# time), so updating os.environ mid-run would not reach it without these files.
_GITHUB_TOKEN_FILE = Path("/tmp/.ambient_github_token")
_GITLAB_TOKEN_FILE = Path("/tmp/.ambient_gitlab_token")
_KUBECONFIG_FILE = Path("/tmp/.ambient_kubeconfig")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Use symlink-safe file creation for kubeconfig secret material.

Lines 44 and 440-443 write sensitive kubeconfig content to a predictable /tmp path via write_text, which can be abused via symlink attacks in shared environments.

Suggested secure write pattern
-            _KUBECONFIG_FILE.write_text(kubeconfig_creds["token"])
-            _KUBECONFIG_FILE.chmod(0o600)
+            flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
+            if hasattr(os, "O_NOFOLLOW"):
+                flags |= os.O_NOFOLLOW
+            fd = os.open(_KUBECONFIG_FILE, flags, 0o600)
+            with os.fdopen(fd, "w", encoding="utf-8") as f:
+                f.write(kubeconfig_creds["token"])

As per coding guidelines, "Flag only errors, security risks, or functionality-breaking problems."

Also applies to: 440-443

🧰 Tools
🪛 Ruff (0.15.9)

[error] 44-44: Probable insecure usage of temporary file or directory: "/tmp/.ambient_kubeconfig"

(S108)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/runners/ambient-runner/ambient_runner/platform/auth.py` at line
44, The code currently writes kubeconfig material directly to _KUBECONFIG_FILE
via Path.write_text which is vulnerable to symlink attacks; change the write
sites (the code that writes the kubeconfig content — references to
_KUBECONFIG_FILE) to create a secure temporary file in the same directory (e.g.
tempfile.NamedTemporaryFile(dir=_KUBECONFIG_FILE.parent,
prefix=_KUBECONFIG_FILE.name, delete=False) or use os.open with O_CREAT|O_EXCL),
write the content to that temp file, fsync it, set strict permissions (chmod
0o600), then atomically move it into place using os.replace(_KUBECONFIG_FILE,
target) so the final path is never opened as a symlink; apply this pattern to
both the initial declaration uses and the later write sites that currently call
write_text.



# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -315,6 +316,10 @@ async def fetch_gitlab_token(context: RunnerContext) -> str:
return data.get("token", "")


async def fetch_kubeconfig_credential(context: RunnerContext) -> dict:
return await _fetch_credential(context, "kubeconfig")


async def fetch_token_for_url(context: RunnerContext, url: str) -> str:
"""Fetch appropriate token based on repository URL host."""
try:
Expand All @@ -337,11 +342,12 @@ async def populate_runtime_credentials(context: RunnerContext) -> None:
logger.info("Fetching fresh credentials from backend API...")

# Fetch all credentials concurrently
google_creds, jira_creds, gitlab_creds, github_creds = await asyncio.gather(
google_creds, jira_creds, gitlab_creds, github_creds, kubeconfig_creds = await asyncio.gather(
fetch_google_credentials(context),
fetch_jira_credentials(context),
fetch_gitlab_credentials(context),
fetch_github_credentials(context),
fetch_kubeconfig_credential(context),
return_exceptions=True,
)

Expand Down Expand Up @@ -425,6 +431,25 @@ async def populate_runtime_credentials(context: RunnerContext) -> None:
if github_creds.get("email"):
git_user_email = github_creds["email"]

if isinstance(kubeconfig_creds, Exception):
logger.warning(f"Failed to refresh kubeconfig credentials: {kubeconfig_creds}")
if isinstance(kubeconfig_creds, PermissionError):
auth_failures.append(str(kubeconfig_creds))
elif kubeconfig_creds.get("token"):
# Setting KUBECONFIG in os.environ is safe here: the runner's own platform
# communication (backend REST API, gRPC to API server) uses the mounted SA
# token and CA cert directly from /var/run/secrets/..., not via any kube
# client library or KUBECONFIG. This env var only affects child processes
# (Claude CLI, MCP servers, kubectl/oc) so they can reach the user's
# remote cluster without interfering with the pod's own identity.
try:
_KUBECONFIG_FILE.write_text(kubeconfig_creds["token"])
_KUBECONFIG_FILE.chmod(0o600)
os.environ["KUBECONFIG"] = str(_KUBECONFIG_FILE)
logger.info(f"Written kubeconfig to {_KUBECONFIG_FILE}")
except OSError as e:
logger.warning(f"Failed to write kubeconfig file: {e}")

# Configure git identity and credential helper
await configure_git_identity(git_user_name, git_user_email)
install_git_credential_helper()
Expand Down Expand Up @@ -452,6 +477,7 @@ def clear_runtime_credentials() -> None:
"JIRA_URL",
"JIRA_EMAIL",
"USER_GOOGLE_EMAIL",
"KUBECONFIG",
]:
if os.environ.pop(key, None) is not None:
cleared.append(key)
Expand All @@ -468,7 +494,7 @@ def clear_runtime_credentials() -> None:
cleared.append(key)

# Remove token files used by the git credential helper.
for token_file in (_GITHUB_TOKEN_FILE, _GITLAB_TOKEN_FILE):
for token_file in (_GITHUB_TOKEN_FILE, _GITLAB_TOKEN_FILE, _KUBECONFIG_FILE):
try:
token_file.unlink(missing_ok=True)
cleared.append(token_file.name)
Expand Down
Loading