|
| 1 | +# Authentication & Client Configuration |
| 2 | + |
| 3 | +This document covers how `kubernetesjs` handles authentication, endpoint detection, and client configuration. It also compares the approach with Go's `client-go` and outlines future work. |
| 4 | + |
| 5 | +## Quick Reference |
| 6 | + |
| 7 | +```typescript |
| 8 | +import { KubernetesClient } from 'kubernetesjs'; |
| 9 | + |
| 10 | +// Auto-detect: in-cluster if available, else kubectl proxy at localhost:8001 |
| 11 | +const client = new KubernetesClient(KubernetesClient.getDefaultConfig()); |
| 12 | + |
| 13 | +// Explicit in-cluster (throws if not in a pod) |
| 14 | +const client = new KubernetesClient(KubernetesClient.getInClusterConfig()); |
| 15 | + |
| 16 | +// Explicit token |
| 17 | +const client = new KubernetesClient({ |
| 18 | + restEndpoint: 'https://my-cluster:6443', |
| 19 | + token: myBearerToken, |
| 20 | +}); |
| 21 | + |
| 22 | +// kubectl proxy (backward-compatible, no auth needed) |
| 23 | +const client = new KubernetesClient({ restEndpoint: 'http://127.0.0.1:8001' }); |
| 24 | +``` |
| 25 | + |
| 26 | +## Constructor Options |
| 27 | + |
| 28 | +```typescript |
| 29 | +interface APIClientOptions { |
| 30 | + restEndpoint: string; // Required: Kubernetes API URL |
| 31 | + token?: string; // Bearer token (SA, OIDC, etc.) |
| 32 | + headers?: Record<string, string>; // Default headers for every request |
| 33 | + timeout?: number; // Default timeout in ms (default: 10000) |
| 34 | +} |
| 35 | +``` |
| 36 | + |
| 37 | +All fields except `restEndpoint` are optional and backward-compatible. |
| 38 | + |
| 39 | +### Header Merge Order |
| 40 | + |
| 41 | +When a request is made, headers are merged with this priority (highest wins): |
| 42 | + |
| 43 | +1. **Per-request headers** (passed via `opts.headers` on individual API calls) |
| 44 | +2. **Auth header** (auto-injected `Authorization: Bearer <token>` if `token` is set) |
| 45 | +3. **Default headers** (from `options.headers` in constructor) |
| 46 | + |
| 47 | +This means per-request headers can override the default token if needed. |
| 48 | + |
| 49 | +## Static Helpers |
| 50 | + |
| 51 | +These are defined on `APIClient` and inherited by `KubernetesClient`, so you can call them as `KubernetesClient.methodName()`. |
| 52 | + |
| 53 | +### `detectEndpoint(fallback?)` |
| 54 | + |
| 55 | +Detects the Kubernetes API endpoint from environment variables. |
| 56 | + |
| 57 | +**Priority:** |
| 58 | +1. `KUBERNETES_API_URL` — explicit override (custom env var for flexibility) |
| 59 | +2. `KUBERNETES_SERVICE_HOST` + `KUBERNETES_SERVICE_PORT` — standard in-cluster env vars set by Kubernetes |
| 60 | +3. `fallback` — defaults to `http://127.0.0.1:8001` (kubectl proxy) |
| 61 | + |
| 62 | +```typescript |
| 63 | +const endpoint = KubernetesClient.detectEndpoint(); |
| 64 | +// In-cluster: "https://10.96.0.1:443" |
| 65 | +// Local dev: "http://127.0.0.1:8001" |
| 66 | +``` |
| 67 | + |
| 68 | +### `readServiceAccountToken(path?)` |
| 69 | + |
| 70 | +Reads the mounted service account bearer token from the filesystem. Returns `undefined` if the file doesn't exist or isn't readable. |
| 71 | + |
| 72 | +**Default path:** `/var/run/secrets/kubernetes.io/serviceaccount/token` |
| 73 | + |
| 74 | +```typescript |
| 75 | +const token = KubernetesClient.readServiceAccountToken(); |
| 76 | +if (token) { |
| 77 | + console.log('Running in-cluster with SA token'); |
| 78 | +} |
| 79 | +``` |
| 80 | + |
| 81 | +> **Node.js only** — uses `require('node:fs')`. Returns `undefined` in browser environments. |
| 82 | +
|
| 83 | +### `getServiceAccountCAPath()` |
| 84 | + |
| 85 | +Returns the path to the service account CA certificate if it exists at the standard mount point (`/var/run/secrets/kubernetes.io/serviceaccount/ca.crt`), or `undefined` if not found. |
| 86 | + |
| 87 | +Use this to set `NODE_EXTRA_CA_CERTS` before starting your application so that `fetch()` trusts the cluster's API server certificate: |
| 88 | + |
| 89 | +```bash |
| 90 | +NODE_EXTRA_CA_CERTS=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt node app.js |
| 91 | +``` |
| 92 | + |
| 93 | +### `getInClusterConfig()` |
| 94 | + |
| 95 | +Builds a complete `APIClientOptions` for in-cluster usage. Reads the SA token and detects the endpoint from environment variables. |
| 96 | + |
| 97 | +**Equivalent to client-go's `rest.InClusterConfig()`.** |
| 98 | + |
| 99 | +Throws if: |
| 100 | +- The SA token file is not found |
| 101 | +- `KUBERNETES_SERVICE_HOST` is not set |
| 102 | + |
| 103 | +```typescript |
| 104 | +try { |
| 105 | + const config = KubernetesClient.getInClusterConfig(); |
| 106 | + const client = new KubernetesClient(config); |
| 107 | +} catch (err) { |
| 108 | + console.error('Not running in-cluster:', err.message); |
| 109 | +} |
| 110 | +``` |
| 111 | + |
| 112 | +### `getDefaultConfig(fallbackEndpoint?)` |
| 113 | + |
| 114 | +Tries in-cluster config first, falls back to kubectl proxy. |
| 115 | + |
| 116 | +**Equivalent to client-go's `BuildConfigFromFlags("", "")`.** |
| 117 | + |
| 118 | +```typescript |
| 119 | +// Always works — in-cluster or local dev |
| 120 | +const client = new KubernetesClient(KubernetesClient.getDefaultConfig()); |
| 121 | +``` |
| 122 | + |
| 123 | +## Environment Variables |
| 124 | + |
| 125 | +| Variable | Used By | Description | |
| 126 | +|---|---|---| |
| 127 | +| `KUBERNETES_API_URL` | `detectEndpoint()` | Explicit API server URL override | |
| 128 | +| `KUBERNETES_SERVICE_HOST` | `detectEndpoint()` | In-cluster API server host (set by K8s) | |
| 129 | +| `KUBERNETES_SERVICE_PORT` | `detectEndpoint()` | In-cluster API server port (set by K8s, default: 443) | |
| 130 | +| `NODE_EXTRA_CA_CERTS` | Node.js TLS | Path to CA cert for HTTPS verification | |
| 131 | + |
| 132 | +## In-Cluster Usage |
| 133 | + |
| 134 | +When running inside a Kubernetes pod, the kubelet automatically mounts: |
| 135 | + |
| 136 | +| File | Path | Purpose | |
| 137 | +|---|---|---| |
| 138 | +| SA Token | `/var/run/secrets/kubernetes.io/serviceaccount/token` | Bearer token for API auth | |
| 139 | +| CA Cert | `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt` | Cluster CA for TLS verification | |
| 140 | +| Namespace | `/var/run/secrets/kubernetes.io/serviceaccount/namespace` | Pod's namespace | |
| 141 | + |
| 142 | +And sets environment variables: |
| 143 | +- `KUBERNETES_SERVICE_HOST` — API server IP |
| 144 | +- `KUBERNETES_SERVICE_PORT` — API server port (usually 443) |
| 145 | + |
| 146 | +### Minimal In-Cluster Example |
| 147 | + |
| 148 | +```dockerfile |
| 149 | +FROM node:20-alpine |
| 150 | +ENV NODE_EXTRA_CA_CERTS=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt |
| 151 | +COPY . /app |
| 152 | +CMD ["node", "/app/index.js"] |
| 153 | +``` |
| 154 | + |
| 155 | +```typescript |
| 156 | +import { KubernetesClient } from 'kubernetesjs'; |
| 157 | + |
| 158 | +const client = new KubernetesClient(KubernetesClient.getInClusterConfig()); |
| 159 | +const pods = await client.listCoreV1NamespacedPod({ path: { namespace: 'default' } }); |
| 160 | +``` |
| 161 | + |
| 162 | +### RBAC Requirements |
| 163 | + |
| 164 | +The pod's service account needs appropriate RBAC permissions: |
| 165 | + |
| 166 | +```yaml |
| 167 | +apiVersion: rbac.authorization.k8s.io/v1 |
| 168 | +kind: ClusterRole |
| 169 | +metadata: |
| 170 | + name: my-app-role |
| 171 | +rules: |
| 172 | + - apiGroups: [""] |
| 173 | + resources: ["pods", "services", "namespaces"] |
| 174 | + verbs: ["get", "list", "watch", "create", "update", "delete"] |
| 175 | +--- |
| 176 | +apiVersion: rbac.authorization.k8s.io/v1 |
| 177 | +kind: ClusterRoleBinding |
| 178 | +metadata: |
| 179 | + name: my-app-binding |
| 180 | +subjects: |
| 181 | + - kind: ServiceAccount |
| 182 | + name: default |
| 183 | + namespace: my-namespace |
| 184 | +roleRef: |
| 185 | + kind: ClusterRole |
| 186 | + name: my-app-role |
| 187 | + apiGroup: rbac.authorization.k8s.io |
| 188 | +``` |
| 189 | +
|
| 190 | +## Comparison with client-go |
| 191 | +
|
| 192 | +| Feature | client-go | kubernetesjs | Status | |
| 193 | +|---|---|---|---| |
| 194 | +| In-cluster config (SA token + env detection) | `rest.InClusterConfig()` | `getInClusterConfig()` | Supported | |
| 195 | +| Auto-detect (in-cluster → fallback) | `BuildConfigFromFlags("","")` | `getDefaultConfig()` | Supported | |
| 196 | +| Bearer token auth | `BearerToken` field | `token` option | Supported | |
| 197 | +| Custom headers | Transport wrappers | `headers` option | Supported | |
| 198 | +| SA token file path | `BearerTokenFile` | `readServiceAccountToken(path)` | Supported | |
| 199 | +| CA certificate | `TLSClientConfig.CAFile` | `NODE_EXTRA_CA_CERTS` env var | Partial | |
| 200 | +| Kubeconfig parsing (`~/.kube/config`) | `clientcmd` package | — | Planned | |
| 201 | +| Multiple kubeconfig merge (`KUBECONFIG`) | `ClientConfigLoadingRules` | — | Planned | |
| 202 | +| Context switching | `--context` flag | — | Planned | |
| 203 | +| Client certificate auth (mTLS) | `TLSClientConfig.CertFile/KeyFile` | — | Planned | |
| 204 | +| Exec-based credential plugins | `ExecProvider` | — | Planned | |
| 205 | +| OIDC / auth provider plugins | `AuthProvider` | — | Planned | |
| 206 | +| Token file watching (auto-refresh) | `BearerTokenFile` + transport | — | Planned | |
| 207 | +| Basic auth (username/password) | `Username`/`Password` | — | Not planned | |
| 208 | +| Impersonation headers | `Impersonate` config | — | Planned | |
| 209 | +| HTTP proxy support | `ProxyURL` / env vars | — | Planned | |
| 210 | +| Connection backoff | `KUBE_CLIENT_BACKOFF_*` env vars | — | Planned | |
| 211 | + |
| 212 | +### How client-go Handles Auth Internally |
| 213 | + |
| 214 | +client-go uses a layered transport system with `http.RoundTripper` wrappers: |
| 215 | + |
| 216 | +1. **Config loading**: `rest.Config` is built from kubeconfig or in-cluster detection |
| 217 | +2. **Transport wrapping**: `HTTPWrappersForConfig` wraps a base transport with: |
| 218 | + - `BearerAuthWithRefreshRoundTripper` — reads token, re-reads `BearerTokenFile` on each request |
| 219 | + - `BasicAuthRoundTripper` — adds `Authorization: Basic` header |
| 220 | + - `ImpersonatingRoundTripper` — adds `Impersonate-*` headers |
| 221 | + - `UserAgentRoundTripper` — adds `User-Agent` header |
| 222 | +3. **TLS**: Custom `tls.Config` with cluster CA, client certs, SNI, minimum TLS 1.2 |
| 223 | + |
| 224 | +The `InClusterConfig()` flow specifically: |
| 225 | +1. Check `KUBERNETES_SERVICE_HOST` and `KUBERNETES_SERVICE_PORT` — if missing, return `ErrNotInCluster` |
| 226 | +2. Read token from `/var/run/secrets/kubernetes.io/serviceaccount/token` |
| 227 | +3. Read CA cert from `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt` (non-fatal if missing) |
| 228 | +4. Return `rest.Config` with `Host`, `BearerToken`, `BearerTokenFile`, and `TLSClientConfig.CAFile` |
| 229 | + |
| 230 | +## Future Work |
| 231 | + |
| 232 | +### Kubeconfig Support |
| 233 | + |
| 234 | +Parsing `~/.kube/config` (or `$KUBECONFIG`) to extract cluster endpoints, contexts, and user credentials. This would enable: |
| 235 | + |
| 236 | +```typescript |
| 237 | +// Future API |
| 238 | +const client = new KubernetesClient( |
| 239 | + KubernetesClient.fromKubeConfig({ context: 'my-cluster' }) |
| 240 | +); |
| 241 | +``` |
| 242 | + |
| 243 | +**Challenge**: Requires a YAML parser. Options include adding `js-yaml` as an optional peer dependency, or implementing minimal YAML parsing for the kubeconfig subset. |
| 244 | + |
| 245 | +### TLS / CA Certificate Handling |
| 246 | + |
| 247 | +Native support for custom CA certificates without relying on `NODE_EXTRA_CA_CERTS`. This would require creating a custom `fetch` agent with TLS options: |
| 248 | + |
| 249 | +```typescript |
| 250 | +// Future API |
| 251 | +const client = new KubernetesClient({ |
| 252 | + restEndpoint: 'https://my-cluster:6443', |
| 253 | + token: myToken, |
| 254 | + tls: { |
| 255 | + caFile: '/path/to/ca.crt', |
| 256 | + // or caData: Buffer.from('...') |
| 257 | + insecureSkipVerify: false, // for dev only |
| 258 | + } |
| 259 | +}); |
| 260 | +``` |
| 261 | + |
| 262 | +### Client Certificate Auth (mTLS) |
| 263 | + |
| 264 | +```typescript |
| 265 | +// Future API |
| 266 | +const client = new KubernetesClient({ |
| 267 | + restEndpoint: 'https://my-cluster:6443', |
| 268 | + tls: { |
| 269 | + certFile: '/path/to/client.crt', |
| 270 | + keyFile: '/path/to/client.key', |
| 271 | + caFile: '/path/to/ca.crt', |
| 272 | + } |
| 273 | +}); |
| 274 | +``` |
| 275 | + |
| 276 | +### Token File Watching |
| 277 | + |
| 278 | +Automatic re-reading of the SA token file on each request (or periodically), supporting Kubernetes bound service account token rotation: |
| 279 | + |
| 280 | +```typescript |
| 281 | +// Future API |
| 282 | +const client = new KubernetesClient({ |
| 283 | + restEndpoint: 'https://...', |
| 284 | + tokenFile: '/var/run/secrets/kubernetes.io/serviceaccount/token', |
| 285 | + // Token is re-read from file before each request |
| 286 | +}); |
| 287 | +``` |
| 288 | + |
| 289 | +### Exec-Based Credential Plugins |
| 290 | + |
| 291 | +Support for external credential commands (used by GKE, EKS, AKS): |
| 292 | + |
| 293 | +```typescript |
| 294 | +// Future API |
| 295 | +const client = new KubernetesClient({ |
| 296 | + restEndpoint: 'https://...', |
| 297 | + exec: { |
| 298 | + command: 'gke-gcloud-auth-plugin', |
| 299 | + apiVersion: 'client.authentication.k8s.io/v1beta1', |
| 300 | + } |
| 301 | +}); |
| 302 | +``` |
| 303 | + |
| 304 | +### OIDC / Auth Provider Plugins |
| 305 | + |
| 306 | +OpenID Connect token authentication for identity-based access. |
| 307 | + |
| 308 | +### Impersonation |
| 309 | + |
| 310 | +```typescript |
| 311 | +// Future API |
| 312 | +const client = new KubernetesClient({ |
| 313 | + restEndpoint: 'https://...', |
| 314 | + token: adminToken, |
| 315 | + impersonate: { |
| 316 | + user: 'jane@example.com', |
| 317 | + groups: ['developers'], |
| 318 | + } |
| 319 | +}); |
| 320 | +``` |
0 commit comments