sched: SECURITY — закрыть /mcp и / (morda) без токена; auth-гвард на входе (кроме /api/health + static UI)
This commit is contained in:
@@ -1,9 +1,10 @@
|
|||||||
# sched custom single-binary — embedded engine + admin API + UI morda + MCP + storage (MariaDB).
|
# sched custom single-binary — embedded engine + admin API + UI morda + MCP + storage (MariaDB).
|
||||||
# Built on published @schedjs/* npm packages (artifacts that passed the gates).
|
# Built on published @schedjs/* npm packages (artifacts that passed the gates).
|
||||||
# Runs apps/daemon/src/custom-entry.mjs — one HTTP server :8080 routing /api, /mcp, / (morda).
|
# Runs custom-entry.mjs — one HTTP server :8080 routing /api, /mcp, / (morda).
|
||||||
|
# Auth at the boundary: everything EXCEPT /api/health + static UI assets requires Bearer SCHED_ADMIN_KEY.
|
||||||
#
|
#
|
||||||
# Build:
|
# Build (context = this dir):
|
||||||
# docker build -f apps/daemon/Dockerfile.custom -t registry.kzntsv.site/sched-custom:0.12.1-mysql apps/daemon
|
# docker build -f host-stacks/vds-kzntsv/sched-custom/Dockerfile.custom -t registry.kzntsv.site/sched-custom:0.12.1-mysql host-stacks/vds-kzntsv/sched-custom
|
||||||
# Run:
|
# Run:
|
||||||
# docker run -e MYSQL_URL=mysql://sched:pw@mariadb:3306/sched?ssl={"rejectUnauthorized":false} \
|
# docker run -e MYSQL_URL=mysql://sched:pw@mariadb:3306/sched?ssl={"rejectUnauthorized":false} \
|
||||||
# -e SCHED_ADMIN_KEY=... -e SCHED_TASKS=/app/config/tasks.json \
|
# -e SCHED_ADMIN_KEY=... -e SCHED_TASKS=/app/config/tasks.json \
|
||||||
@@ -21,7 +22,7 @@ RUN echo '{"name":"schedd","private":true,"packageManager":"yarn@4.18.0"}' > pac
|
|||||||
&& yarn cache clean
|
&& yarn cache clean
|
||||||
|
|
||||||
ENV PATH="/app/node_modules/.bin:${PATH}"
|
ENV PATH="/app/node_modules/.bin:${PATH}"
|
||||||
COPY src/custom-entry.mjs /app/custom-entry.mjs
|
COPY custom-entry.mjs /app/custom-entry.mjs
|
||||||
# Default tasks.json baked into the image (overridden on prod by a bind mount / SCHED_TASKS).
|
# Default tasks.json baked into the image (overridden on prod by a bind mount / SCHED_TASKS).
|
||||||
RUN echo '{"tasks":[]}' > /app/tasks.json
|
RUN echo '{"tasks":[]}' > /app/tasks.json
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|||||||
@@ -51,10 +51,28 @@ const mcpHandler = createMcpHttpHandler({ client: mcpClient, readonly: process.e
|
|||||||
let uiBundle = null;
|
let uiBundle = null;
|
||||||
try { uiBundle = readFileSync(DEFAULT_UI_BUNDLE); } catch { /* bundle missing → morda 404s its script */ }
|
try { uiBundle = readFileSync(DEFAULT_UI_BUNDLE); } catch { /* bundle missing → morda 404s its script */ }
|
||||||
|
|
||||||
|
// One HTTP server, auth at the boundary: everything EXCEPT /api/health (probe, open) and
|
||||||
|
// /sched-ui.bundle.js (static UI assets) requires `Authorization: Bearer SCHED_ADMIN_KEY`.
|
||||||
|
// This closes the MCP + admin API + morda exposed WITHOUT a token (vitya found /mcp open).
|
||||||
|
const AUTH_OPEN_PATHS = ['/api/health'];
|
||||||
|
function isOpenPath(p) {
|
||||||
|
return AUTH_OPEN_PATHS.includes(p) || p === '/sched-ui.bundle.js';
|
||||||
|
}
|
||||||
|
function isAuthorized(req) {
|
||||||
|
if (!ADMIN_KEY) return true; // dev mode, open
|
||||||
|
const h = req.headers.authorization;
|
||||||
|
return h === `Bearer ${ADMIN_KEY}`;
|
||||||
|
}
|
||||||
|
|
||||||
const server = createServer(async (req, res) => {
|
const server = createServer(async (req, res) => {
|
||||||
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
|
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
|
||||||
const p = url.pathname;
|
const p = url.pathname;
|
||||||
try {
|
try {
|
||||||
|
if (!isOpenPath(p) && !isAuthorized(req)) {
|
||||||
|
res.writeHead(401, { 'content-type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: 'unauthorized' }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (p === '/api' || p.startsWith('/api/')) {
|
if (p === '/api' || p.startsWith('/api/')) {
|
||||||
req.url = p.replace(/^\/api/, '') + url.search; // strip /api mount prefix
|
req.url = p.replace(/^\/api/, '') + url.search; // strip /api mount prefix
|
||||||
await adminApi.handleRequest(req, res);
|
await adminApi.handleRequest(req, res);
|
||||||
|
|||||||
Reference in New Issue
Block a user