diff --git a/README.md b/README.md
index 4bb7f5861f..6d215bb037 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,34 @@ See the wiki page and documentation [here](https://github.com/kubernetes-client/
- [Client Versioning and Compatibility](https://github.com/kubernetes-client/java/wiki/2.-Versioning-and-Compatibility)
- [Code Examples](https://github.com/kubernetes-client/java/wiki/3.-Code-Examples)
+### Exec credential plugin policy
+
+Kubeconfig `exec` credential plugins start local processes. Only load trusted kubeconfigs with the
+default `AllowAll` policy. Applications can restrict which plugins may run using
+`ExecCredentialPluginPolicy`, which follows the `credentialPluginPolicy` /
+`credentialPluginAllowlist` semantics of [kuberc](https://kubernetes.io/docs/reference/kubectl/kuberc/):
+
+```java
+// Deny all exec plugins
+ApiClient client = ClientBuilder.standard(ExecCredentialPluginPolicy.DENY_ALL).build();
+
+// Only allow specific plugins (basenames resolved via PATH, or full paths)
+ApiClient client = ClientBuilder.standard(
+ ExecCredentialPluginPolicy.allowlist("kubelogin", "/usr/local/bin/my-trusted-plugin")).build();
+
+// Use the same policy as kubectl ($KUBERC or ~/.kube/kuberc)
+ApiClient client = ClientBuilder.standard(ExecCredentialPluginPolicy.fromDefaultKubeRc()).build();
+
+// Or on a KubeConfig loaded manually
+kubeConfig.setExecCredentialPluginPolicy(ExecCredentialPluginPolicy.DENY_ALL);
+```
+
+An allowlist entry matches when it is exactly equal to the plugin command, or when both resolve to the
+same executable via `PATH` (symlinks and globs are not resolved). Denied plugins are never started and
+an `ExecCredentialPluginDeniedException` is thrown. The policy applies to both initial credential
+acquisition and token refresh. It does not restrict plugin arguments or environment, and is a
+defense-in-depth measure rather than a sandbox.
+
## Release
Starting from `20.0.0` (Kubernetes 1.28), `client-java-api` has introduced non-backward-compatible changes. Optional
diff --git a/util/src/main/java/io/kubernetes/client/util/ClientBuilder.java b/util/src/main/java/io/kubernetes/client/util/ClientBuilder.java
index ae0bfb4011..d3326a058c 100644
--- a/util/src/main/java/io/kubernetes/client/util/ClientBuilder.java
+++ b/util/src/main/java/io/kubernetes/client/util/ClientBuilder.java
@@ -107,11 +107,46 @@ public static ClientBuilder standard() throws IOException {
}
public static ClientBuilder standard(boolean persistConfig) throws IOException {
+ return standard(persistConfig, ExecCredentialPluginPolicy.ALLOW_ALL);
+ }
+
+ /**
+ * Creates a builder which is pre-configured as described in {@link #standard()}, applying the
+ * given policy to any exec credential plugin found in the kubeconfig.
+ *
+ * @param execCredentialPluginPolicy the exec credential plugin policy, for example {@link
+ * ExecCredentialPluginPolicy#fromDefaultKubeRc()}; null allows all plugins
+ * @return ClientBuilder pre-configured using the precedence of {@link #standard()}
+ * @throws IOException if the configuration file or a file specified in a configuration file
+ * cannot be read.
+ * @throws io.kubernetes.client.util.exception.ExecCredentialPluginDeniedException if the
+ * kubeconfig requires an exec credential plugin which the policy does not allow
+ */
+ public static ClientBuilder standard(ExecCredentialPluginPolicy execCredentialPluginPolicy)
+ throws IOException {
+ return standard(true, execCredentialPluginPolicy);
+ }
+
+ /**
+ * Same as {@link #standard(ExecCredentialPluginPolicy)}, optionally persisting refreshed
+ * credentials to the kubeconfig file.
+ *
+ * @param persistConfig whether to persist refreshed credentials to the kubeconfig file
+ * @param execCredentialPluginPolicy the exec credential plugin policy; null allows all plugins
+ * @return ClientBuilder pre-configured using the precedence of {@link #standard()}
+ * @throws IOException if the configuration file or a file specified in a configuration file
+ * cannot be read.
+ */
+ public static ClientBuilder standard(
+ boolean persistConfig, ExecCredentialPluginPolicy execCredentialPluginPolicy)
+ throws IOException {
final File kubeConfig = findConfigFromEnv();
- ClientBuilder clientBuilderEnv = getClientBuilder(persistConfig, kubeConfig);
+ ClientBuilder clientBuilderEnv =
+ getClientBuilder(persistConfig, kubeConfig, execCredentialPluginPolicy);
if (clientBuilderEnv != null) return clientBuilderEnv;
final File config = findConfigInHomeDir();
- ClientBuilder clientBuilderHomeDir = getClientBuilder(persistConfig, config);
+ ClientBuilder clientBuilderHomeDir =
+ getClientBuilder(persistConfig, config, execCredentialPluginPolicy);
if (clientBuilderHomeDir != null) return clientBuilderHomeDir;
final File clusterCa = new File(SERVICEACCOUNT_CA_PATH);
if (clusterCa.exists()) {
@@ -120,7 +155,10 @@ public static ClientBuilder standard(boolean persistConfig) throws IOException {
return new ClientBuilder();
}
- private static ClientBuilder getClientBuilder(boolean persistConfig, File kubeConfig)
+ private static ClientBuilder getClientBuilder(
+ boolean persistConfig,
+ File kubeConfig,
+ ExecCredentialPluginPolicy execCredentialPluginPolicy)
throws IOException {
if (kubeConfig != null) {
try (BufferedReader kubeConfigReader =
@@ -132,6 +170,7 @@ private static ClientBuilder getClientBuilder(boolean persistConfig, File kubeCo
kc.setPersistConfig(new FilePersister(kubeConfig));
}
kc.setFile(kubeConfig);
+ kc.setExecCredentialPluginPolicy(execCredentialPluginPolicy);
return kubeconfig(kc);
}
}
diff --git a/util/src/main/java/io/kubernetes/client/util/ExecCredentialPluginPolicy.java b/util/src/main/java/io/kubernetes/client/util/ExecCredentialPluginPolicy.java
new file mode 100644
index 0000000000..4071c54a12
--- /dev/null
+++ b/util/src/main/java/io/kubernetes/client/util/ExecCredentialPluginPolicy.java
@@ -0,0 +1,523 @@
+/*
+Copyright 2026 The Kubernetes Authors.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+package io.kubernetes.client.util;
+
+import io.kubernetes.client.util.exception.ExecCredentialPluginDeniedException;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.yaml.snakeyaml.LoaderOptions;
+import org.yaml.snakeyaml.Yaml;
+import org.yaml.snakeyaml.constructor.SafeConstructor;
+import org.yaml.snakeyaml.error.YAMLException;
+
+/**
+ * Policy controlling which kubeconfig exec credential plugins may be run.
+ *
+ *
The semantics mirror the {@code credentialPluginPolicy} and {@code credentialPluginAllowlist}
+ * fields of a kubectl kuberc
+ * file:
+ *
+ *
+ *
{@link #ALLOW_ALL} ({@code AllowAll}): any plugin may run. This is the default, for
+ * backwards compatibility.
+ *
{@link #DENY_ALL} ({@code DenyAll}): no plugin may run.
+ *
{@link #allowlist(String...)} ({@code Allowlist}): a plugin may run only if its command
+ * matches at least one allowlist entry. An entry matches if it is exactly equal to the
+ * plugin's command, or if both the entry and the plugin's command resolve (using {@code
+ * PATH}) to the same executable path. Symlinks and shell globs are not resolved.
+ *
+ *
+ *
A policy can be supplied directly by the application, or loaded from a kuberc file using
+ * {@link #fromKubeRc(Reader)} / {@link #fromDefaultKubeRc()}. When a plugin is denied, an {@link
+ * ExecCredentialPluginDeniedException} is thrown before any process is started.
+ *
+ *
This policy is a defense-in-depth measure, not a sandbox: it does not restrict plugin
+ * arguments, environment variables or executable contents.
+ */
+public final class ExecCredentialPluginPolicy {
+ private static final Logger log = LoggerFactory.getLogger(ExecCredentialPluginPolicy.class);
+
+ public static final String ENV_KUBERC = "KUBERC";
+ public static final String ENV_KUBECTL_KUBERC = "KUBECTL_KUBERC";
+ public static final String KUBERC_OFF = "off";
+ public static final String KUBERC = "kuberc";
+ public static final String KUBERC_API_GROUP = "kubectl.config.k8s.io";
+ public static final String KUBERC_KIND = "Preference";
+
+ /** The type of a credential plugin policy, as spelled in a kuberc file. */
+ public enum PolicyType {
+ ALLOW_ALL("AllowAll"),
+ DENY_ALL("DenyAll"),
+ ALLOWLIST("Allowlist");
+
+ private final String value;
+
+ PolicyType(String value) {
+ this.value = value;
+ }
+
+ /** @return the kuberc spelling of this policy type. */
+ public String getValue() {
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ return value;
+ }
+ }
+
+ /** Allows any exec credential plugin to run. */
+ public static final ExecCredentialPluginPolicy ALLOW_ALL =
+ new ExecCredentialPluginPolicy(PolicyType.ALLOW_ALL, Collections.emptyList());
+
+ /** Prevents all exec credential plugins from running. */
+ public static final ExecCredentialPluginPolicy DENY_ALL =
+ new ExecCredentialPluginPolicy(PolicyType.DENY_ALL, Collections.emptyList());
+
+ private final PolicyType type;
+ private final List allowlist;
+
+ private ExecCredentialPluginPolicy(PolicyType type, List allowlist) {
+ this.type = type;
+ this.allowlist = allowlist;
+ }
+
+ /**
+ * Creates an {@code Allowlist} policy.
+ *
+ * @param commands plugin commands which may run; either basenames (resolved using {@code PATH})
+ * or paths. Must not be empty, and each entry must be non-empty and normalized.
+ * @return an immutable allowlist policy
+ * @throws IllegalArgumentException if the allowlist or one of its entries is invalid
+ */
+ public static ExecCredentialPluginPolicy allowlist(String... commands) {
+ if (commands == null) {
+ throw new IllegalArgumentException(
+ "credential plugin policy set to \"Allowlist\", but allowlist is unspecified");
+ }
+ return allowlist(Arrays.asList(commands));
+ }
+
+ /**
+ * Creates an {@code Allowlist} policy.
+ *
+ * @param commands plugin commands which may run; see {@link #allowlist(String...)}
+ * @return an immutable allowlist policy
+ * @throws IllegalArgumentException if the allowlist or one of its entries is invalid
+ */
+ public static ExecCredentialPluginPolicy allowlist(List commands) {
+ if (commands == null) {
+ throw new IllegalArgumentException(
+ "credential plugin policy set to \"Allowlist\", but allowlist is unspecified");
+ }
+ if (commands.isEmpty()) {
+ throw new IllegalArgumentException(
+ "credential plugin policy set to \"Allowlist\", but allowlist is empty; use \"DenyAll\" policy instead");
+ }
+ List copy = new ArrayList<>(commands.size());
+ for (int i = 0; i < commands.size(); i++) {
+ String command = commands.get(i);
+ if (command == null || command.isEmpty()) {
+ throw new IllegalArgumentException(
+ "misconfigured credential plugin allowlist: empty allowlist entry #" + (i + 1));
+ }
+ String cleaned = clean(command);
+ if (cleaned == null) {
+ throw new IllegalArgumentException(
+ "misconfigured credential plugin allowlist: invalid file path: \"" + command + "\"");
+ }
+ if (!command.equals(cleaned)) {
+ throw new IllegalArgumentException(
+ "misconfigured credential plugin allowlist: non-normalized file path: \""
+ + command
+ + "\" vs \""
+ + cleaned
+ + "\"");
+ }
+ copy.add(command);
+ }
+ return new ExecCredentialPluginPolicy(PolicyType.ALLOWLIST, Collections.unmodifiableList(copy));
+ }
+
+ /**
+ * Loads the credential plugin policy from a kuberc document.
+ *
+ * @param input reader for a kuberc ({@code kind: Preference}) YAML document
+ * @return the policy; {@link #ALLOW_ALL} if {@code credentialPluginPolicy} is unset or empty
+ * @throws IllegalArgumentException if the document or the policy is invalid
+ */
+ @SuppressWarnings("unchecked")
+ public static ExecCredentialPluginPolicy fromKubeRc(Reader input) {
+ Object document;
+ try {
+ document = new Yaml(new SafeConstructor(new LoaderOptions())).load(input);
+ } catch (YAMLException ex) {
+ throw new IllegalArgumentException("kuberc: failed to parse: " + ex.getMessage(), ex);
+ }
+ return fromKubeRcDocument(document);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static ExecCredentialPluginPolicy fromKubeRcDocument(Object document) {
+ if (document == null) {
+ return ALLOW_ALL;
+ }
+ if (!(document instanceof Map)) {
+ throw new IllegalArgumentException("kuberc: expected a YAML mapping");
+ }
+ Map preference = (Map) document;
+
+ Object apiVersion = preference.get("apiVersion");
+ if (!(apiVersion instanceof String)
+ || !((String) apiVersion).startsWith(KUBERC_API_GROUP + "/")) {
+ throw new IllegalArgumentException("kuberc: unsupported apiVersion: " + apiVersion);
+ }
+ Object kind = preference.get("kind");
+ if (!KUBERC_KIND.equals(kind)) {
+ throw new IllegalArgumentException("kuberc: unsupported kind: " + kind);
+ }
+
+ Object policy = preference.get("credentialPluginPolicy");
+ Object allowlist = preference.get("credentialPluginAllowlist");
+ if (policy != null && !(policy instanceof String)) {
+ throw new IllegalArgumentException("kuberc: credentialPluginPolicy must be a string");
+ }
+ String policyName = policy == null ? "" : (String) policy;
+ switch (policyName) {
+ case "":
+ case "AllowAll":
+ case "DenyAll":
+ if (allowlist != null) {
+ throw new IllegalArgumentException(
+ "misconfigured credential plugin allowlist: plugin policy is \""
+ + policyName
+ + "\" but allowlist is non-nil");
+ }
+ return "DenyAll".equals(policyName) ? DENY_ALL : ALLOW_ALL;
+ case "Allowlist":
+ return allowlist(parseAllowlist(allowlist));
+ default:
+ throw new IllegalArgumentException("unknown plugin policy: \"" + policyName + "\"");
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private static List parseAllowlist(Object allowlist) {
+ if (allowlist == null) {
+ return null;
+ }
+ if (!(allowlist instanceof List)) {
+ throw new IllegalArgumentException("kuberc: credentialPluginAllowlist must be a list");
+ }
+ List