bunker-admin 721b4df6c3 Add remote instance management with mTLS agent and phone-home registration
Enables the CCP to manage CML instances on remote servers via a lightweight
HTTP agent. Key components:

- ExecutionDriver abstraction (local-driver.ts / remote-driver.ts) routes
  operations to local Docker or remote agent transparently
- Remote agent package (agent/) with mTLS authentication, Docker Compose
  operations, file management, backup/upgrade delegation
- Certificate service using openssl CLI for CA management and cert issuance
- Phone-home registration: remote agents register via invite code, CCP admin
  approves, agent receives mTLS cert bundle automatically
- config.sh integration with configure_control_panel() section
- ccp-agent Docker Compose service (profile-gated)
- Frontend: AgentRegistrationsPage, InviteCodesPage, Remote Agents sidebar menu
- Security hardened: cert bundle wiped after delivery, shell injection prevention
  via execFile, command allowlist with metachar rejection, rate-limited public
  endpoints, auto-populated fingerprint pinning

Also wires ENABLE_SOCIAL/PEOPLE/ANALYTICS through env.ts, seed.ts, and
docker-compose env passthrough (from previous session).

Bunker Admin
2026-04-07 15:24:33 -06:00

86 lines
3.0 KiB
TypeScript

import 'express-async-errors';
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import compression from 'compression';
import rateLimit from 'express-rate-limit';
import { env } from './config/env';
import { logger } from './utils/logger';
import { errorHandler } from './middleware/error-handler';
// Route imports
import authRoutes from './modules/auth/auth.routes';
import instanceRoutes from './modules/instances/instances.routes';
import settingsRoutes from './modules/settings/settings.routes';
import healthRoutes from './modules/health/health.routes';
import auditRoutes from './modules/audit/audit.routes';
import backupRoutes from './modules/backups/backup.routes';
import eventsRoutes, { instanceEventsRouter } from './modules/events/events.routes';
import agentRoutes from './modules/agents/agents.routes';
import certificateRoutes from './modules/certificates/certificates.routes';
import inviteCodeRoutes from './modules/invite-codes/invite-codes.routes';
import { startHealthScheduler } from './services/health.service';
import { autoDiscoverOnStartup } from './services/discovery.service';
const app = express();
// Global middleware
app.use(helmet());
app.use(compression());
app.use(express.json({ limit: '10mb' }));
app.use(
cors({
origin: env.CORS_ORIGINS.split(',').map((s) => s.trim()),
credentials: true,
})
);
// Rate limiters
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 15, // 15 attempts per window
standardHeaders: true,
legacyHeaders: false,
message: { error: { message: 'Too many attempts, please try again later', code: 'RATE_LIMITED' } },
});
// Global API rate limiter — safety net against resource exhaustion
const apiLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 300, // 300 req/min per IP (generous for a control panel)
standardHeaders: true,
legacyHeaders: false,
message: { error: { message: 'Too many requests, please try again later', code: 'RATE_LIMITED' } },
});
// Routes
app.use('/api', apiLimiter);
app.use('/api/auth', authLimiter, authRoutes);
app.use('/api/instances', instanceRoutes);
app.use('/api/settings', settingsRoutes);
app.use('/api/health', healthRoutes);
app.use('/api/audit', auditRoutes);
app.use('/api/backups', backupRoutes);
app.use('/api/events', eventsRoutes);
app.use('/api/instances/:id/events', instanceEventsRouter);
app.use('/api/agents', agentRoutes);
app.use('/api/certificates', certificateRoutes);
app.use('/api/invite-codes', inviteCodeRoutes);
// Error handler (must be last)
app.use(errorHandler);
app.listen(env.PORT, () => {
logger.info(`CCP API listening on port ${env.PORT} (${env.NODE_ENV})`);
startHealthScheduler(env.HEALTH_CHECK_INTERVAL_MS);
// Auto-discover parent CML instance on first boot (5s delay for DB readiness)
setTimeout(() => {
autoDiscoverOnStartup().catch((err) =>
logger.error(`[discovery] Auto-discovery failed: ${(err as Error).message}`)
);
}, 5_000);
});
export default app;