Skip to content
Draft
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
42 changes: 41 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,46 @@ const k8sApi = kc.makeApiClient(k8s.CoreV1Api);
...
```

## Exec credential plugin policy

Kubeconfig `exec` credential plugins start local processes. By default all plugins are allowed,
so only load trusted kubeconfigs. Applications can restrict which plugins may run with the same
policy that kubectl reads from its [kuberc](https://kubernetes.io/docs/reference/kubectl/kuberc/#credential-plugin-policy)
file:

```javascript
const kc = new k8s.KubeConfig();
kc.loadFromDefault();

// Never run exec plugins
kc.setExecCredentialPluginPolicy({ credentialPluginPolicy: 'DenyAll' });

// Only run approved plugins, by basename or full path
kc.setExecCredentialPluginPolicy({
credentialPluginPolicy: 'Allowlist',
credentialPluginAllowlist: [{ command: 'kubelogin' }, { command: '/usr/local/bin/my-trusted-plugin' }],
});

// Or use the policy from the user's kuberc file ($KUBERC or ~/.kube/kuberc)
kc.loadExecCredentialPluginPolicyFromKuberc();
```

The policy semantics match kubectl:

- An unspecified or empty `credentialPluginPolicy` is the same as `AllowAll`.
- `credentialPluginAllowlist` is required (and must be non-empty) for `Allowlist`, and is an error for any other
policy. Entries must specify a non-empty `command` (the deprecated `name` alias is accepted, but not both).
- A plugin is allowed if its command exactly matches an allowlist entry, or if both resolve to the same executable
using `PATH`. Neither symlinks nor shell globs are resolved.
- In `Allowlist` mode the resolved full path of the plugin is executed, so the plugin `env` from the kubeconfig cannot
change which executable is started. Plugin arguments and environment are not otherwise restricted.
- Denied plugins throw an `ExecCredentialPluginDeniedError` before any process is started; the policy is checked
for the initial execution and every token refresh.
- `KUBERC=off` or `KUBECTL_KUBERC=false` disables loading the kuberc file.

This policy is a defense-in-depth measure, not a sandbox. Allowing an interpreter or other general purpose launcher
can permit arbitrary code execution.

# Documentation

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

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

Expand Down
62 changes: 61 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ import {
User,
} from './config_types.js';
import { ExecAuth } from './exec_auth.js';
import {
ExecCredentialPluginPolicy,
parseKubercExecCredentialPluginPolicy,
validateExecCredentialPluginPolicy,
} from './exec_credential_policy.js';
import { FileAuth } from './file_auth.js';
import { GoogleCloudPlatformAuth } from './gcp_auth.js';
import {
Expand Down Expand Up @@ -135,10 +140,12 @@ function createSocksConnector(proxyUrl: string, tlsOptions: tls.ConnectionOption
export interface ApiType {}

export class KubeConfig implements SecurityAuthentication {
private execAuth: ExecAuth = new ExecAuth();

private authenticators: Authenticator[] = [
new AzureAuth(),
new GoogleCloudPlatformAuth(),
new ExecAuth(),
this.execAuth,
new FileAuth(),
new OpenIDConnectAuth(),
];
Expand All @@ -158,6 +165,59 @@ export class KubeConfig implements SecurityAuthentication {
this.custom_authenticators.push(authenticator);
}

/**
* Sets the application policy for kubeconfig `exec` credential plugins.
* The semantics match the `credentialPluginPolicy` and `credentialPluginAllowlist`
* fields of a kubectl kuberc file, see https://kubernetes.io/docs/reference/kubectl/kuberc/
* The policy is checked before every plugin execution, including token refreshes.
* @param policy the policy, undefined restores the default `AllowAll` behavior.
* @throws Error if the policy is misconfigured.
*/
public setExecCredentialPluginPolicy(policy?: ExecCredentialPluginPolicy): void {
validateExecCredentialPluginPolicy(policy);
this.execAuth.setPolicy(policy);
}

/**
* Loads the exec credential plugin policy from a kubectl kuberc file and applies it
* with `setExecCredentialPluginPolicy`.
*
* If `file` is not given, the file is located the same way kubectl does: the `KUBERC`
* environment variable if set, otherwise `$HOME/.kube/kuberc`. Setting `KUBERC=off` or
* `KUBECTL_KUBERC=false` disables loading, in which case the current policy is unchanged.
* A missing default kuberc file is not an error, a missing explicitly specified file is.
* @param file optional path to the kuberc file.
* @returns the loaded policy, or undefined if no kuberc file was loaded.
*/
public loadExecCredentialPluginPolicyFromKuberc(
file?: string,
platform: string = process.platform,
): ExecCredentialPluginPolicy | undefined {
let kubercFile = file;
let required = true;
if (!kubercFile) {
if (process.env.KUBECTL_KUBERC === 'false' || process.env.KUBERC === 'off') {
return undefined;
}
if (process.env.KUBERC) {
kubercFile = process.env.KUBERC;
} else {
const home = findHomeDir(platform);
if (!home) {
return undefined;
}
kubercFile = path.join(home, '.kube', 'kuberc');
required = false;
}
}
if (!required && !fileExists(kubercFile)) {
return undefined;
}
const policy = parseKubercExecCredentialPluginPolicy(fs.readFileSync(kubercFile).toString('utf-8'));
this.setExecCredentialPluginPolicy(policy);
return policy;
}

/**
* The list of all known clusters
*/
Expand Down
31 changes: 29 additions & 2 deletions src/exec_auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import https from 'node:https';

import { Authenticator } from './auth.js';
import { User } from './config_types.js';
import {
checkExecCredentialPluginPolicy,
ExecCredentialPluginPolicy,
validateExecCredentialPluginPolicy,
} from './exec_credential_policy.js';

import child_process from 'node:child_process';

Expand All @@ -18,13 +23,34 @@ export interface Credential {
}

export class ExecAuth implements Authenticator {
private readonly tokenCache: { [key: string]: Credential | null } = {};
private tokenCache: { [key: string]: Credential | null } = {};
private policy?: ExecCredentialPluginPolicy;
private execFn: (
command: string,
args?: readonly string[],
options?: child_process.SpawnOptionsWithoutStdio,
) => child_process.ChildProcessWithoutNullStreams = child_process.spawn;

/**
* @param policy optional application policy restricting which exec credential plugins may run.
* Defaults to allowing all plugins.
*/
constructor(policy?: ExecCredentialPluginPolicy) {
this.setPolicy(policy);
}

/**
* Sets the policy restricting which exec credential plugins may run.
* The policy applies to every subsequent plugin execution, including token refreshes.
* Cached credentials are discarded when the policy changes.
* @param policy the policy, undefined allows all plugins.
*/
public setPolicy(policy?: ExecCredentialPluginPolicy): void {
validateExecCredentialPluginPolicy(policy);
this.policy = policy;
this.tokenCache = {};
}

public isAuthProvider(user: User): boolean {
if (!user) {
return false;
Expand Down Expand Up @@ -96,6 +122,7 @@ export class ExecAuth implements Authenticator {
if (!exec.command) {
throw new Error('No command was specified for exec authProvider!');
}
const command = checkExecCredentialPluginPolicy(this.policy, exec.command);
let opts = {};
if (exec.env) {
const env = { ...process.env };
Expand All @@ -108,7 +135,7 @@ export class ExecAuth implements Authenticator {
let stderrData: string = '';
let savedError: Error | undefined = undefined;

const subprocess = this.execFn(exec.command, exec.args, opts);
const subprocess = this.execFn(command, exec.args, opts);
subprocess.stdout.setEncoding('utf8');
subprocess.stderr.setEncoding('utf8');

Expand Down
Loading
Loading