Nest is a Node.js/Express HTTPS server that exposes Nagios-compatible checks and dynamically loads plugin-based checks at startup.
Unlike many traditional Nagios setups centered on Bash/Python/Ruby scripts, this project is built around TypeScript/JavaScript plugins so teams can use modern tooling, shared libraries, typing, and test workflows in one ecosystem.
Nest gives you a TypeScript-first monitoring runtime where checks are HTTP-addressable plugins with a strict Nagios output contract.
- Features
- Architecture at a Glance
- Quick Start
- Build Targets
- Configuration
- HTTP API
- Plugin Development
- Testing and Quality
- Nmap Honeypot E2E
- Troubleshooting
- Support
- Security
- License
- HTTPS-only server with automatic self-signed certificate generation when missing.
- Root overview page (
/) with built-in routes, plugin routes, and help links. - Nagios-compatible payloads:
message,code, optionalperformanceData. - Dynamic plugin loading from a filesystem directory.
- Runtime TypeScript plugin transpilation and cache reuse.
- Unified language stack for service logic and checks (TypeScript/JavaScript end to end).
- Built-in and plugin help pages via
?help. - External-link warning guard on help pages before leaving app origin.
- Shell-friendly checks via
script/check_nest.sh.
- Strong typing helps prevent fragile monitoring logic and parameter mistakes.
- Reuse existing Node.js libraries instead of reimplementing logic in shell scripts.
- Better testing ergonomics (Jest), linting, formatting, and CI integration.
- Keeps plugin implementation and application runtime in a single ecosystem.
npm install
npm run devnpm install
npm run build
npm startDefault listen address is https://localhost:5000 unless overridden by config.
npm run build:releaseOutput is produced in standalone/.
npm run build:debProduces two packages in build_deb/:
nest-deb.deb— main application + systemd servicenest-plugins-deb.deb— bundled plugins (requiresnestinstalled first)
Configuration is loaded in this order:
--configPath <file>CLI argumentNEST_CONFIG_FILEenvironment variable/etc/nest/nest.confwhenNODE_ENV=production.envin current working directory (development fallback)
Main variables:
NODE_ENV(default:development)HOST(default:localhost)PORT(default:5000)TLS_CERT_PATH(default:certs/nest-cert.pem)TLS_KEY_PATH(default:certs/nest-key.pem)TLS_CERT_COMMON_NAME(default:localhost)TLS_CERT_DAYS(default:365)PLUGINS_DIR(default:plugins)LOG_FILE_PATH(default:logs/nest.log)MAX_LOG_FILE_SIZE_BYTES(default:1048576)ENABLE_SECURITY_MIDDLEWARE(default:true)API_KEY_HEADER(default:x-api-key)API_KEY(default: empty, disabled)ALLOWED_IPS(default:127.0.0.1, ::1; comma-separated exact IPs)PLUGIN_WHITELIST_PATH(default: empty; when unset, Nest uses<PLUGINS_DIR>/plugin-whitelist.txt)RATE_LIMIT_WINDOW_MS(default:60000)RATE_LIMIT_MAX(default:120)
| Method | Path | Purpose |
|---|---|---|
GET |
/ |
Root overview page (routes + help links) |
GET |
/nagios |
Built-in app metrics check |
GET |
/nagios/honey-pot |
Honeypot/probe status in Nagios format |
GET |
/favicon.ico |
Returns 204 No Content to suppress default browser favicon noise |
GET |
/help/external-link-guard.js |
Script used by help pages to warn before opening external links |
GET |
/help/startup-warnings/<id> |
Dedicated help page for a specific startup warning topic |
| Method | Path | Purpose |
|---|---|---|
GET |
/plugins/<plugin-route> |
Executes a discovered plugin check |
- Root overview page:
GET /- Lists built-in routes and plugin routes.
- Includes direct
helplinks.
- Built-in help pages:
GET /nagios?helpGET /nagios/honey-pot?help
- Plugin help pages:
GET /plugins/<plugin-route>?help
All help pages include an external-link warning. When a user clicks a link to a different origin, the UI asks for confirmation before navigating away.
- Unknown routes return HTTP 404 with Nagios UNKNOWN payload (
code=3). - Unknown-route hits are recorded as honeypot signals.
At startup, the server scans PLUGINS_DIR and registers routes dynamically.
Supported plugin files:
.ts.js
Production safety checks:
- In
NODE_ENV=production, plugin files are loaded only when file owner uid matches the service process uid. - Plugin files must not be writable by group or others.
- Insecure plugin files are skipped and logged.
- In
NODE_ENV=production, the config file is validated with the same ownership and permission rules at startup. A bad config file causes a hard start failure. - On every startup, Nest hashes each effective plugin file and compares it with the trusted hash recorded in the plugin whitelist file. New or changed plugins are not registered until they are explicitly whitelisted.
Ignored plugin files:
*.test.ts*.spec.ts*.test.js*.spec.js*.d.ts
Route path generation:
- Plugin basename is converted to lowercase kebab-case.
- Dynamic routes are always namespaced under
/plugins. - Example:
check_debian_eol.ts->/plugins/check-debian-eol - Filenames must remain unique after normalization (for example,
check_test.tsandcheck-test.tswould conflict).
.tsplugins are transpiled at runtime.- Transpiled output is cached at
PLUGINS_DIR/plugin-cache/.
.jsplugins are loaded directly.- If both
foo.tsandfoo.jsexist,foo.tstakes precedence.
A plugin function should return:
{
message: string;
code: 0 | 1 | 2 | 3;
performanceData?: PerformanceData | PerformanceData[];
}Plugins may export meta.usage:
export const meta = {
usage: {
http: '/plugins/check-test?nagiosReturnMessage=<string>&nagiosReturnValue=<0|1|2|3>&performanceData=<true|false>',
shell:
'./check_nest.sh check-test nagiosReturnMessage=<string> nagiosReturnValue=<0|1|2|3> performanceData=<true|false>',
},
};Plugins may also export meta.help with extended HTML documentation:
export const meta = {
usage: {
http: '/plugins/check-custom?value=<number>',
shell: './check_nest.sh check-custom value=<number>',
},
help: `<h1>check-custom</h1><p>Extended setup guide...</p>`,
};Notes on meta.help:
- If
meta.helpis a full HTML document (<!DOCTYPE ...>/<html ...>), it is sandboxed inside an<iframe srcdoc>withsandbox="allow-popups"to isolate scripts and forms. - If it is an HTML fragment, it is sanitized with
sanitize-html(event-handler attributes and unsafe URI schemes removed) and rendered in a minimal help-page shell. - If
meta.helpis missing, Nest generates a fallback help page frommeta.usage. - All help pages set a strict Content Security Policy and include the external-link warning guard.
By default, Nest reads the plugin trust allowlist from <PLUGINS_DIR>/plugin-whitelist.txt.
Each non-comment line must contain a filename and a SHA-256 hash, in either of these forms:
check_test.ts 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef check_test.ts
Startup behavior:
- If the whitelist file is missing, Nest creates it automatically with restrictive permissions (
0600). - New plugins that are missing from the whitelist are skipped.
- Existing plugins whose contents changed since the last approved hash are skipped.
- The whitelist file itself is validated with the same Unix ownership/permission gate (
validateUnixFileSecurity): it must be owned by the service uid and must not be group/other writable. - Each skipped plugin produces a startup warning in the logs and on the route overview page.
- In the route overview page, each warning includes a direct link to a dedicated warning help page with handling instructions.
- After review, add or update the plugin's hash in the whitelist file and restart the server.
Create plugins/check_custom.ts:
export const meta = {
usage: {
http: '/plugins/check-custom?value=<number>',
shell: './check_nest.sh check-custom value=<number>',
},
};
export const checkCustom = async (params: {value?: string}) => {
const value = Number(params.value ?? '0');
if (Number.isNaN(value)) {
return {message: 'value must be a number', code: 3};
}
if (value > 90) {
return {message: `value=${value} is critical`, code: 2};
}
return {message: `value=${value} is ok`, code: 0};
};Call it with:
curl -k "https://localhost:5000/plugins/check-custom?value=42"npm run lint:check
npx tsc --noEmit
npm run test:coverageThe test suite includes security-focused tests that cover:
- XSS and event-handler injection in
meta.helpHTML fragments. - HTML injection via plugin metadata fields (
pluginName,usageHttp,usageShell). - Access control bypass attempts (key prefix/substring, case, forwarded-for spoofing, IPv4-mapped IPv6).
- File ownership and permission edge cases for plugin and config file validation.
- Startup plugin whitelist enforcement for new and changed plugin hashes.
- Adversarial IP normalization inputs.
| Script | Purpose |
|---|---|
npm run test:ci |
Jest in CI mode (--runInBand) |
npm run test:shell |
Shell script tests |
npm run test:e2e:nmap |
Honeypot nmap matrix E2E |
npm run validate |
Lint, format check, type check, build, test |
./script/check_nest.sh check-test nagiosReturnMessage=test nagiosReturnValue=0 performanceData=trueEnvironment variables for check_nest.sh:
NEST_SCHEME(default:https)NEST_HOST(default:localhost)NEST_PORT(default:5000)NEST_TLS_INSECURE(default:true)NEST_CA_CERT(optional path to CA cert)NEST_API_KEY(optional API key sent as request header)NEST_API_KEY_HEADER(default:x-api-key)
npm run test:e2e:nmapThe suite runs a matrix of 8 nmap scan types and reports DETECTED, UNDETECTED, and SKIPPED.
Expected detection model:
- Detected at app layer:
-sT,-sV - Typically not detected at app layer:
-sS,-sA,-sF,-sX,-sN,-sU
Reason: raw-socket and kernel-level scans can avoid full TCP accept/HTTP parsing, so Node.js TLS/client error hooks do not always fire.
Run with elevated privileges (for raw-socket scans):
sudo env PATH="$PATH" NEST_E2E_STRICT=false npm run test:e2e:nmapAlternative (one-time capability grant):
sudo setcap cap_net_raw+ep $(which nmap)
npm run test:e2e:nmap
sudo setcap -r $(which nmap)E2E environment variables:
NEST_E2E_PORT(default55443)NEST_E2E_STRICT(defaulttrue; setfalsefor report-only)
- Plugin not visible:
- Verify extension is
.tsor.js. - Ensure filename does not match ignored test/spec patterns.
- Restart service after adding plugin.
- Verify extension is
- TS plugin not loading:
- Check startup logs for transpilation warnings.
- Verify write access to plugin cache directory.
- Route mismatch:
- Confirm expected route is basename-to-kebab-case conversion.
- E2E permissions after running as root:
- If local artifacts become root-owned, fix with
sudo chown -R "$USER":"$USER" dist.
- If local artifacts become root-owned, fix with
- Open an issue in the repository issue tracker.
- For operational incidents, include:
- route called
- full Nagios payload returned
- relevant server log excerpt
- This service includes baseline application-layer protections, but public exposure still requires a reverse proxy, strong access control, trusted TLS, and network-level restrictions.
- Honeypot and protocol-error detection is application-layer telemetry. It does not replace host/network IDS.
Built-in application controls:
- Helmet security headers.
- Basic IP + API-key access control middleware.
- Request rate limiting.
- Production plugin file ownership/permission validation in the dynamic loader.
- Production config file ownership/permission validation at startup.
- Help-page Content Security Policy headers (
default-src 'none', lockedscript-src,frame-ancestors 'none'). - Help-page HTML sanitization via
sanitize-html(blocks XSS, event-handler attributes, unsafe URI schemes). - Full-doc plugin help pages (
<!DOCTYPE>/<html>) rendered in a sandboxed<iframe>withallow-popupsonly. - External-link warning guard on all help pages before leaving app origin.
By default, ALLOWED_IPS is restricted to loopback addresses 127.0.0.1 and ::1. Add your monitoring source addresses explicitly when the service must accept remote checks.
At startup in production, the app logs warnings when recommended security settings are missing or weak, including:
ENABLE_SECURITY_MIDDLEWARE=false- missing
API_KEY - empty
ALLOWED_IPS - non-positive rate-limit settings
MIT
