Technical Research & Implementation Report: Nebius CLI & Kubernetes Exec Plugin OAuth Browser Loop Issue
Document Version: 2.1 (Fully Generalized & Portable Edition)
Target File Path: nebius_auth_browser_loop_research.md
Topics Covered: Nebius Managed Kubernetes (mk8s), OAuth 2.0 PKCE Flow, client-go Exec Plugin Spec, Kubeconfig Hardening (--no-browser, interactiveMode: Never), Dynamic CLI & User Retrieval (which nebius, nebius profile current, jq / grep), Service Account Authorized Keys vs. NEBIUS_IAM_TOKEN Environment Variables.
When using Nebius AI Cloud Managed Kubernetes (mk8s), executing kubectl commands or running background Kubernetes developer tools (e.g., k9s, Lens, VSCode / JetBrains Kubernetes plugins, ArgoCD, custom daemon scripts) can trigger an aggressive infinite loop of opening web browser tabs (Google Chrome/Firefox). These tabs continuously redirect between https://auth.nebius.com/oauth2/authorize?client_id=nebius-cli... and local loopback callback URLs like http://127.0.0.1:<port>.
This issue stems from an architectural conflict between Kubernetes client-go exec credential plugin mechanisms and interactive OAuth 2.0 PKCE authentication fallback in the Nebius CLI (nebius).
When a developer's local profile credentials (IAM access token and OAuth refresh token) expire or are missing, nebius mk8s v1 cluster get-token falls back to an interactive browser-based OAuth PKCE flow by invoking xdg-open. Because background Kubernetes tools poll cluster endpoints repeatedly in parallel or on tight timers, each polling attempt spawns a separate nebius process. Each isolated process launches its own ephemeral HTTP server on a random local port and opens a new browser tab, flooding Chrome within seconds.
This report details the root cause, explains the behavior of all configuration parameters in ~/.kube/config, compares non-interactive authentication methods, and provides safe, fully dynamic commands to resolve the issue permanently across any developer workstation environment.
Kubernetes CLI tools and IDE extensions communicate with cluster API servers via client-go. To support cloud provider-specific authentication, Kubernetes standardizes external credential fetching via the Exec Credential Plugin API (client.authentication.k8s.io/v1 or v1beta1).
When kubectl or an IDE plugin sends an HTTP request to a Kubernetes cluster API server:
client-goinspects the user's~/.kube/configfile under the activeuserentry.- If an
execblock is present,client-goexecutes the specified binary (e.g.,command: ~/.nebius/bin/nebiusor$(which nebius)withargs: ["mk8s", "v1", "cluster", "get-token", "--profile", "<profile-name>", "--format", "json"]). client-goreadsstdoutexpecting a structured JSON object (ExecCredential) containing an IAM Bearer token and expiration timestamp:{ "apiVersion": "client.authentication.k8s.io/v1beta1", "kind": "ExecCredential", "status": { "token": "NET...<IAM_ACCESS_TOKEN>", "expirationTimestamp": "2026-07-23T21:00:00Z" } }
When nebius mk8s v1 cluster get-token is executed by client-go:
- Profile State Inspection: Nebius CLI checks the specified profile (e.g., as returned dynamically by
nebius profile current). - Token Cache Check: It checks for an unexpired local IAM access token (valid up to 12 hours).
- Silent OAuth Refresh: If the IAM access token is expired, the CLI attempts to silently exchange the cached OAuth refresh token for a fresh IAM access token.
- Interactive PKCE Fallback: If the refresh token is missing, expired, or invalid—and no
--no-browserflag or Service Account key was provided—nebiusassumes an interactive terminal session and initiates an OAuth 2.0 PKCE flow.
During the PKCE flow:
- Nebius CLI binds to a random unprivileged local TCP port (e.g.,
43125) and starts a temporary HTTP web server athttp://127.0.0.1:43125/. - It constructs an OAuth authorization URL:
https://auth.nebius.com/oauth2/authorize?client_id=nebius-cli&response_type=code&redirect_uri=http://127.0.0.1:43125&code_challenge=<CHALLENGE>&code_challenge_method=S256 - Default Behavior (Without
--no-browser): The CLI executesxdg-open(Linux),open(macOS), orstart(Windows) to launch the default browser (Google Chrome) pointing to the authorization URL. - Headless Behavior (With
--no-browser): The CLI does not callxdg-openand does not launch Chrome. Instead, it prints a text prompt containing the URL to standard error (stderr) and waits or exits immediately ifinteractiveMode: Neveris set.
Modern Kubernetes UI and CLI tools do not issue single queries; they continuously monitor cluster state:
- k9s: Refreshes active resource lists (pods, nodes, services) every 2 to 5 seconds.
- Lens / IDE Plugins (VSCode, JetBrains): Run background goroutines polling metrics, logs, and CRDs concurrently across multiple namespaces.
- ArgoCD / Helm / Custom Scripts: Run automated background reconciliation loops.
[Background Tool (k9s / Lens / Extension)]
│
├── (1) Polls K8s API every 2s
│
├── (2) Invokes client-go -> runs `nebius mk8s v1 cluster get-token` (Process #1)
│ │
│ ├── Token expired -> Starts HTTP Server on 127.0.0.1:41001
│ └── Calls `xdg-open` -> Opens Chrome Tab #1 (Port 41001)
│
├── (3) 2s later: Poll #2 runs `nebius mk8s v1 cluster get-token` (Process #2)
│ │
│ ├── Isolated Process -> Starts HTTP Server on 127.0.0.1:41002
│ └── Calls `xdg-open` -> Opens Chrome Tab #2 (Port 41002)
│
├── (4) 2s later: Poll #3 runs (Process #3) -> Opens Chrome Tab #3 (Port 41003)
│
└── ... Repetitive loop spawns 10-50+ tabs until system resource exhaustion!
- Expired Session State: The developer's profile has an expired IAM token and invalid refresh token.
- Process #1 Executed: Lens/k9s triggers
nebius get-token. Process #1 starts an HTTP listener on127.0.0.1:41001, callsxdg-open, and opens Tab #1 in Chrome. - Parallel Polling (Processes #2, #3...): Background tools do not wait for Process #1 to complete browser login. Subsequent polling cycles spawn additional instances of
nebius get-token. - Port Divergence: Each spawned process is isolated. Process #2 cannot bind to port
41001, so it binds to41002and executesxdg-openagain, opening Tab #2. - Tab Explosion: Within 30 seconds, 15 to 30 separate CLI processes are active, each opening new tabs pointing to different
http://127.0.0.1:<port>callbacks.
When configuring ~/.kube/config, the exec: block contains four specific fields. Here is why each exists and whether it is required:
users:
- name: <nebius-cluster-user>
user:
exec:
apiVersion: client.authentication.k8s.io/v1beta1
command: ~/.nebius/bin/nebius # or resolved via $(which nebius)
args:
- mk8s
- v1
- cluster
- get-token
- --profile
- <profile-name>
- --no-browser
- --format
- json
interactiveMode: Never
provideClusterInfo: false
env: null| Parameter | Belongs To | Purpose | Required for Tab Suppression? |
|---|---|---|---|
--no-browser |
Nebius CLI Flag | Instructs Nebius CLI to never call xdg-open or launch browser windows automatically when authentication fails/expires. |
ESSENTIAL |
interactiveMode: Never |
Kubernetes Spec | Instructs client-go that the credential plugin runs in headless mode and MUST NOT attempt interactive TTY prompts. |
ESSENTIAL |
provideClusterInfo: false |
Kubernetes Spec | Toggles passing cluster API server metadata via KUBERNETES_EXEC_INFO env var. Nebius CLI ignores this variable. |
OPTIONAL (Boilerplate) |
env: null |
YAML Syntax | Explicitly indicates an empty list of custom environment variables. | OPTIONAL (Boilerplate) |
Important
Crucial Distinction for --no-browser:
--no-browser prevents nebius from executing xdg-open (launching Chrome tabs). However, if the token is expired, nebius will still print a text authorization URL to stderr ("To complete the authentication process, open the following link..."). Seeing this text URL in terminal logs does NOT mean Chrome was launched; it is simply a text prompt written to stderr.
To ensure scripts and commands are completely portable across any system, username, binary path, or profile name, retrieve all variables dynamically:
# 1. Retrieve the full absolute path of the Nebius CLI binary dynamically:
NEBIUS_BIN=$(which nebius || echo "$HOME/.nebius/bin/nebius")
# 2. Retrieve the active Nebius profile name dynamically:
NEBIUS_PROFILE=$(nebius profile current)
# 3. Retrieve the Nebius cluster user entry from kubeconfig dynamically (no positional indexing):
KUBE_USER=$(kubectl config view -o json | jq -r '.users[].name | select(contains("nebius"))')Backup ~/.kube/config and execute the update using nested subshell command substitution:
# Step 1: Backup kubeconfig
cp ~/.kube/config ~/.kube/config.bak
# Step 2: Apply updated exec configuration with dynamic parameters
kubectl config set-credentials "$(kubectl config view -o json | jq -r '.users[].name | select(contains("nebius"))')" \
--exec-api-version=client.authentication.k8s.io/v1beta1 \
--exec-command="$(which nebius || echo "$HOME/.nebius/bin/nebius")" \
--exec-arg=mk8s \
--exec-arg=v1 \
--exec-arg=cluster \
--exec-arg=get-token \
--exec-arg=--profile \
--exec-arg="$(nebius profile current)" \
--exec-arg=--no-browser \
--exec-arg=--format \
--exec-arg=jsonFor environments where interactive login is undesirable, Nebius supports two distinct non-interactive authentication methods. While both avoid browser tabs, they operate at completely different layers of the identity stack:
┌─────────────────────────────────────────────────────────────────────────────┐
│ IDENTITY STACK COMPARISON │
├──────────────────────────────┬──────────────────────────────────────────────┤
│ Solution 3 (Service Account) │ Long-lived PRIVATE KEY FILE on disk │
│ │ -> CLI auto-generates short-lived tokens │
├──────────────────────────────┼──────────────────────────────────────────────┤
│ Solution 4 (Env Var) │ Ephemeral TOKEN STRING in shell memory │
│ │ -> Hard lifetime limit of 12 hours │
└──────────────────────────────┴──────────────────────────────────────────────┘
| Feature | Solution 3: Service Account Key File (sa-key.json) |
Solution 4: Environment Variable (NEBIUS_IAM_TOKEN) |
|---|---|---|
| Credential Type | Authorized RSA/ECDSA Private Key JSON file stored on disk (~/.nebius/sa-key.json). |
Raw IAM Access Token string stored in shell memory (export NEBIUS_IAM_TOKEN=...). |
| Execution Flow | nebius reads sa-key.json, signs JWT requests locally, and fetches new IAM access tokens automatically. |
nebius detects NEBIUS_IAM_TOKEN, skips profile/key lookups entirely, and uses the raw token directly. |
| Token Lifespan | Permanent access (Key file never expires until revoked in Nebius IAM Console). | Max 12 hours (Hard lifetime limit for raw Nebius IAM access tokens). |
| User Interaction | Zero interaction. Set up once and forget permanently. | Must manually re-export export NEBIUS_IAM_TOKEN=$(nebius iam get-access-token) every 12 hours. |
| Primary Use Case | Personal Developer Laptops & IDEs: Permanent hands-off background K8s access without browser logins. | CI/CD Pipelines & Transient Runners: Short-lived automated build/test jobs where saving persistent key files on disk is insecure. |
With Solution 2 applied (--no-browser and interactiveMode: Never in ~/.kube/config), your day-to-day developer workflow changes as follows:
- You run
kubectl,k9s, Lens, or VSCode extensions normally. - Background tools poll the cluster silently.
- Zero browser tabs pop open.
kubectlork9sreportsUnauthorizedorAuthentication token expiredinside its own UI.- No browser tabs open automatically.
- To renew your session at your convenience, run one single command in your terminal using dynamic profile resolution:
nebius auth login --profile "$(nebius profile current)" - A single browser tab opens once. You complete login, and a fresh OAuth refresh token is stored in your active profile.
- All
kubectl,k9s, and IDE background tasks immediately resume working silently.
- Kubernetes Client Authentication Exec Plugin Specification:
- Standard:
client.authentication.k8s.io/v1beta1/v1(ExecCredential) - Documentation: Kubernetes Authentication Exec Plugins
- Standard:
- Nebius Official Documentation & CLI Reference:
- Managed Kubernetes (
mk8s):nebius mk8s cluster get-credentials,nebius mk8s v1 cluster get-token - Identity & Access Management:
nebius auth login,nebius profile current,nebius iam key create,nebius config profile set - Portal: docs.nebius.com / nebius.ai
- Managed Kubernetes (
- OAuth 2.0 PKCE & Client-Go Execution Behavior:
- RFC 7636: Proof Key for Code Exchange by OAuth Public Clients
client-goExec Credential Provider implementation (stdout JSON parsing, stderr logging, TTY detection).