Skip to content

Commit 1861658

Browse files
committed
Add exec credential plugin policy matching kubectl kuberc
1 parent b39cffd commit 1861658

6 files changed

Lines changed: 858 additions & 4 deletions

File tree

‎README.md‎

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,46 @@ const k8sApi = kc.makeApiClient(k8s.CoreV1Api);
9595
...
9696
```
9797

98+
## Exec credential plugin policy
99+
100+
Kubeconfig `exec` credential plugins start local processes. By default all plugins are allowed,
101+
so only load trusted kubeconfigs. Applications can restrict which plugins may run with the same
102+
policy that kubectl reads from its [kuberc](https://kubernetes.io/docs/reference/kubectl/kuberc/#credential-plugin-policy)
103+
file:
104+
105+
```javascript
106+
const kc = new k8s.KubeConfig();
107+
kc.loadFromDefault();
108+
109+
// Never run exec plugins
110+
kc.setExecCredentialPluginPolicy({ credentialPluginPolicy: 'DenyAll' });
111+
112+
// Only run approved plugins, by basename or full path
113+
kc.setExecCredentialPluginPolicy({
114+
credentialPluginPolicy: 'Allowlist',
115+
credentialPluginAllowlist: [{ command: 'kubelogin' }, { command: '/usr/local/bin/my-trusted-plugin' }],
116+
});
117+
118+
// Or use the policy from the user's kuberc file ($KUBERC or ~/.kube/kuberc)
119+
kc.loadExecCredentialPluginPolicyFromKuberc();
120+
```
121+
122+
The policy semantics match kubectl:
123+
124+
- An unspecified or empty `credentialPluginPolicy` is the same as `AllowAll`.
125+
- `credentialPluginAllowlist` is required (and must be non-empty) for `Allowlist`, and is an error for any other
126+
policy. Entries must specify a non-empty `command` (the deprecated `name` alias is accepted, but not both).
127+
- A plugin is allowed if its command exactly matches an allowlist entry, or if both resolve to the same executable
128+
using `PATH`. Neither symlinks nor shell globs are resolved.
129+
- In `Allowlist` mode the resolved full path of the plugin is executed, so the plugin `env` from the kubeconfig cannot
130+
change which executable is started. Plugin arguments and environment are not otherwise restricted.
131+
- Denied plugins throw an `ExecCredentialPluginDeniedError` before any process is started; the policy is checked
132+
for the initial execution and every token refresh.
133+
- `KUBERC=off` or `KUBECTL_KUBERC=false` disables loading the kuberc file.
134+
135+
This policy is a defense-in-depth measure, not a sandbox. Allowing an interpreter or other general purpose launcher
136+
can permit arbitrary code execution.
137+
98138
# Documentation
99139

100140
📖 **[View Documentation](https://kubernetes-client.github.io/javascript/)**
@@ -120,7 +160,7 @@ npm start # opens http://localhost:3000 with hot-reload
120160

121161
`npm start` automatically runs the `prestart` hook which generates the API
122162
reference, SDK docs, and model pages from source before launching the dev
123-
server. Changes to hand-written docs (e.g. `docs/docs/examples/`) are
163+
server. Changes to hand-written docs (e.g. `docs/docs/examples/`) are
124164
reflected instantly; changes to the generated sources require restarting the
125165
server.
126166

‎src/config.ts‎

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ import {
2828
User,
2929
} from './config_types.js';
3030
import { ExecAuth } from './exec_auth.js';
31+
import {
32+
ExecCredentialPluginPolicy,
33+
parseKubercExecCredentialPluginPolicy,
34+
validateExecCredentialPluginPolicy,
35+
} from './exec_credential_policy.js';
3136
import { FileAuth } from './file_auth.js';
3237
import { GoogleCloudPlatformAuth } from './gcp_auth.js';
3338
import {
@@ -135,10 +140,12 @@ function createSocksConnector(proxyUrl: string, tlsOptions: tls.ConnectionOption
135140
export interface ApiType {}
136141

137142
export class KubeConfig implements SecurityAuthentication {
143+
private execAuth: ExecAuth = new ExecAuth();
144+
138145
private authenticators: Authenticator[] = [
139146
new AzureAuth(),
140147
new GoogleCloudPlatformAuth(),
141-
new ExecAuth(),
148+
this.execAuth,
142149
new FileAuth(),
143150
new OpenIDConnectAuth(),
144151
];
@@ -158,6 +165,59 @@ export class KubeConfig implements SecurityAuthentication {
158165
this.custom_authenticators.push(authenticator);
159166
}
160167

168+
/**
169+
* Sets the application policy for kubeconfig `exec` credential plugins.
170+
* The semantics match the `credentialPluginPolicy` and `credentialPluginAllowlist`
171+
* fields of a kubectl kuberc file, see https://kubernetes.io/docs/reference/kubectl/kuberc/
172+
* The policy is checked before every plugin execution, including token refreshes.
173+
* @param policy the policy, undefined restores the default `AllowAll` behavior.
174+
* @throws Error if the policy is misconfigured.
175+
*/
176+
public setExecCredentialPluginPolicy(policy?: ExecCredentialPluginPolicy): void {
177+
validateExecCredentialPluginPolicy(policy);
178+
this.execAuth.setPolicy(policy);
179+
}
180+
181+
/**
182+
* Loads the exec credential plugin policy from a kubectl kuberc file and applies it
183+
* with `setExecCredentialPluginPolicy`.
184+
*
185+
* If `file` is not given, the file is located the same way kubectl does: the `KUBERC`
186+
* environment variable if set, otherwise `$HOME/.kube/kuberc`. Setting `KUBERC=off` or
187+
* `KUBECTL_KUBERC=false` disables loading, in which case the current policy is unchanged.
188+
* A missing default kuberc file is not an error, a missing explicitly specified file is.
189+
* @param file optional path to the kuberc file.
190+
* @returns the loaded policy, or undefined if no kuberc file was loaded.
191+
*/
192+
public loadExecCredentialPluginPolicyFromKuberc(
193+
file?: string,
194+
platform: string = process.platform,
195+
): ExecCredentialPluginPolicy | undefined {
196+
let kubercFile = file;
197+
let required = true;
198+
if (!kubercFile) {
199+
if (process.env.KUBECTL_KUBERC === 'false' || process.env.KUBERC === 'off') {
200+
return undefined;
201+
}
202+
if (process.env.KUBERC) {
203+
kubercFile = process.env.KUBERC;
204+
} else {
205+
const home = findHomeDir(platform);
206+
if (!home) {
207+
return undefined;
208+
}
209+
kubercFile = path.join(home, '.kube', 'kuberc');
210+
required = false;
211+
}
212+
}
213+
if (!required && !fileExists(kubercFile)) {
214+
return undefined;
215+
}
216+
const policy = parseKubercExecCredentialPluginPolicy(fs.readFileSync(kubercFile).toString('utf-8'));
217+
this.setExecCredentialPluginPolicy(policy);
218+
return policy;
219+
}
220+
161221
/**
162222
* The list of all known clusters
163223
*/

‎src/exec_auth.ts‎

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ import https from 'node:https';
33

44
import { Authenticator } from './auth.js';
55
import { User } from './config_types.js';
6+
import {
7+
checkExecCredentialPluginPolicy,
8+
ExecCredentialPluginPolicy,
9+
validateExecCredentialPluginPolicy,
10+
} from './exec_credential_policy.js';
611

712
import child_process from 'node:child_process';
813

@@ -18,13 +23,34 @@ export interface Credential {
1823
}
1924

2025
export class ExecAuth implements Authenticator {
21-
private readonly tokenCache: { [key: string]: Credential | null } = {};
26+
private tokenCache: { [key: string]: Credential | null } = {};
27+
private policy?: ExecCredentialPluginPolicy;
2228
private execFn: (
2329
command: string,
2430
args?: readonly string[],
2531
options?: child_process.SpawnOptionsWithoutStdio,
2632
) => child_process.ChildProcessWithoutNullStreams = child_process.spawn;
2733

34+
/**
35+
* @param policy optional application policy restricting which exec credential plugins may run.
36+
* Defaults to allowing all plugins.
37+
*/
38+
constructor(policy?: ExecCredentialPluginPolicy) {
39+
this.setPolicy(policy);
40+
}
41+
42+
/**
43+
* Sets the policy restricting which exec credential plugins may run.
44+
* The policy applies to every subsequent plugin execution, including token refreshes.
45+
* Cached credentials are discarded when the policy changes.
46+
* @param policy the policy, undefined allows all plugins.
47+
*/
48+
public setPolicy(policy?: ExecCredentialPluginPolicy): void {
49+
validateExecCredentialPluginPolicy(policy);
50+
this.policy = policy;
51+
this.tokenCache = {};
52+
}
53+
2854
public isAuthProvider(user: User): boolean {
2955
if (!user) {
3056
return false;
@@ -96,6 +122,7 @@ export class ExecAuth implements Authenticator {
96122
if (!exec.command) {
97123
throw new Error('No command was specified for exec authProvider!');
98124
}
125+
const command = checkExecCredentialPluginPolicy(this.policy, exec.command);
99126
let opts = {};
100127
if (exec.env) {
101128
const env = { ...process.env };
@@ -108,7 +135,7 @@ export class ExecAuth implements Authenticator {
108135
let stderrData: string = '';
109136
let savedError: Error | undefined = undefined;
110137

111-
const subprocess = this.execFn(exec.command, exec.args, opts);
138+
const subprocess = this.execFn(command, exec.args, opts);
112139
subprocess.stdout.setEncoding('utf8');
113140
subprocess.stderr.setEncoding('utf8');
114141

0 commit comments

Comments
 (0)