Security audit identified 8 endpoint groups that were publicly accessible
without authentication. Added authMiddleware and requireRole where appropriate.
Protected endpoints:
- /api/payloads/* - authMiddleware (trusted origins or API token)
- /api/job-queue/* - authMiddleware + requireRole('admin')
- /api/workers/* - authMiddleware
- /api/worker-registry/* - authMiddleware (pods access via trusted IPs)
- /api/k8s/* - authMiddleware + requireRole('admin')
- /api/pipeline/* - authMiddleware + requireRole('admin')
- /api/tasks/* - authMiddleware + requireRole('admin')
- /api/admin/orchestrator/* - authMiddleware + requireRole('admin')
Also:
- Added API_SECURITY.md documentation
- Filter AI settings from /settings page (managed in /ai-settings)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
146 lines
3.6 KiB
TypeScript
146 lines
3.6 KiB
TypeScript
/**
|
|
* Kubernetes Control Routes
|
|
*
|
|
* Provides admin UI control over k8s resources like worker scaling.
|
|
* Uses in-cluster config when running in k8s, or kubeconfig locally.
|
|
*/
|
|
|
|
import { Router, Request, Response } from 'express';
|
|
import * as k8s from '@kubernetes/client-node';
|
|
import { authMiddleware, requireRole } from '../auth/middleware';
|
|
|
|
const router = Router();
|
|
|
|
// K8s control routes require authentication and admin role
|
|
router.use(authMiddleware);
|
|
router.use(requireRole('admin', 'superadmin'));
|
|
|
|
// K8s client setup - lazy initialization
|
|
let appsApi: k8s.AppsV1Api | null = null;
|
|
let k8sError: string | null = null;
|
|
|
|
function getK8sClient(): k8s.AppsV1Api | null {
|
|
if (appsApi) return appsApi;
|
|
if (k8sError) return null;
|
|
|
|
try {
|
|
const kc = new k8s.KubeConfig();
|
|
|
|
// Try in-cluster config first (when running in k8s)
|
|
try {
|
|
kc.loadFromCluster();
|
|
console.log('[K8s] Loaded in-cluster config');
|
|
} catch {
|
|
// Fall back to default kubeconfig (local dev)
|
|
try {
|
|
kc.loadFromDefault();
|
|
console.log('[K8s] Loaded default kubeconfig');
|
|
} catch (e) {
|
|
k8sError = 'No k8s config available';
|
|
console.log('[K8s] No config available - k8s routes disabled');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
appsApi = kc.makeApiClient(k8s.AppsV1Api);
|
|
return appsApi;
|
|
} catch (e: any) {
|
|
k8sError = e.message;
|
|
console.error('[K8s] Failed to initialize client:', e.message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
const NAMESPACE = process.env.K8S_NAMESPACE || 'dispensary-scraper';
|
|
const WORKER_DEPLOYMENT = 'scraper-worker';
|
|
|
|
/**
|
|
* GET /api/k8s/workers
|
|
* Get current worker deployment status
|
|
*/
|
|
router.get('/workers', async (_req: Request, res: Response) => {
|
|
const client = getK8sClient();
|
|
|
|
if (!client) {
|
|
return res.json({
|
|
success: true,
|
|
available: false,
|
|
error: k8sError || 'K8s not available',
|
|
replicas: 0,
|
|
readyReplicas: 0,
|
|
});
|
|
}
|
|
|
|
try {
|
|
const deployment = await client.readNamespacedDeployment({
|
|
name: WORKER_DEPLOYMENT,
|
|
namespace: NAMESPACE,
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
available: true,
|
|
replicas: deployment.spec?.replicas || 0,
|
|
readyReplicas: deployment.status?.readyReplicas || 0,
|
|
availableReplicas: deployment.status?.availableReplicas || 0,
|
|
updatedReplicas: deployment.status?.updatedReplicas || 0,
|
|
});
|
|
} catch (e: any) {
|
|
console.error('[K8s] Error getting deployment:', e.message);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: e.message,
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /api/k8s/workers/scale
|
|
* Scale worker deployment
|
|
* Body: { replicas: number }
|
|
*/
|
|
router.post('/workers/scale', async (req: Request, res: Response) => {
|
|
const client = getK8sClient();
|
|
|
|
if (!client) {
|
|
return res.status(503).json({
|
|
success: false,
|
|
error: k8sError || 'K8s not available',
|
|
});
|
|
}
|
|
|
|
const { replicas } = req.body;
|
|
|
|
if (typeof replicas !== 'number' || replicas < 0 || replicas > 50) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'replicas must be a number between 0 and 50',
|
|
});
|
|
}
|
|
|
|
try {
|
|
// Patch the deployment to set replicas
|
|
await client.patchNamespacedDeploymentScale({
|
|
name: WORKER_DEPLOYMENT,
|
|
namespace: NAMESPACE,
|
|
body: { spec: { replicas } },
|
|
});
|
|
|
|
console.log(`[K8s] Scaled ${WORKER_DEPLOYMENT} to ${replicas} replicas`);
|
|
|
|
res.json({
|
|
success: true,
|
|
replicas,
|
|
message: `Scaled to ${replicas} workers`,
|
|
});
|
|
} catch (e: any) {
|
|
console.error('[K8s] Error scaling deployment:', e.message);
|
|
res.status(500).json({
|
|
success: false,
|
|
error: e.message,
|
|
});
|
|
}
|
|
});
|
|
|
|
export default router;
|