Skip to main content
There are three actors that matter:
  • The operator. Whoever runs the cyrnel process. They choose the env vars, the modules on disk, the key material, and what to expose.
  • The API caller. Anyone with a valid CYRNEL_API_KEY (or any caller if the API runs anonymously). They can install services, write secrets, toggle modules, and run code.
  • The end services. Whatever the adapters reach. They are downstream of cyrnel and largely outside its control.
Everything else (user code in the sandbox, adapters loaded from disk, the web UI) is a tool the operator chooses to trust.

API Surface

  • Authentication is one static bearer token (CYRNEL_API_KEY). No scopes, no expiry. Anonymous mode is intended for 127.0.0.1 only.
  • No rate limiting is enforced by the API. A noisy or hostile caller will be felt directly by adapters and end services.
  • Error envelopes are intentionally terse ({ "error": "..." }). They do not leak stack traces, but 5xx messages occasionally include exception text. Don’t proxy them to untrusted users verbatim.

Modules

Adapter modules

Adapter modules are the most security-sensitive component cyrnel loads, for three reasons:
  1. They run with full host privileges. Custom adapters loaded via execute() run in the API’s Node.js process. They can read environment variables, open files, make arbitrary network requests, and call native code. There is no sandbox between an adapter and the host. Adapter modules also have access to service secrets and keys as they are required to function.
  2. They receive decrypted secrets. ServiceState.secrets is the plaintext secret document the operator stored for a given service. Every enabled adapter sees every secret of every service it owns.
  3. They can choose what to persist. Adapters are expected to keep per-service state in memory, a malicious or careless adapter can write that state anywhere it pleases.
For These reasons, we strongly recommend using only trusted modules. DO NOT enable any untrusted modules.

Operator guidance

  • Treat modules as part of the program. Review additions with the same scrutiny you give to a node_modules or any program installed your device.
  • Disable adapters you aren’t actively using. A disabled adapter still has its row in modules, but its setup() is never called.
  • Don’t share secrets across deployments. A leaked key plus a stolen db is equivalent to leaking every credential the deployment has stored.
  • If you’re integrating a third-party adapter, verify what it does with secrets, at minimum, that it does not log them, persist them to disk, or forward them anywhere outside the target service.
See Adapter Modules for the interface and expectations.

Environment modules

The active environment owns the sandbox user code runs inside. The bundled typescript-ivm:
  • Must run each execution in an isolate.
  • Should provide no module loader, no filesystem, no network, and no Node built-ins. The only outbound channel is the small set of references from cyrnel.
  • Terminates the isolate on timeout and recreates worker slots.
caveat:
  • Tool invocations bypass the sandbox. When client code calls an invoke, the request leaves the isolate and runs in the host through the adapter. The sandbox limits what code can do directly, not what tools it can ask cyrnel to call.
See Environment Modules.

Secrets

Secrets live in service_secrets.payload as ciphertext with a fresh 12-byte IV per write. The auth tag is stored alongside.
  • The key is CYRNEL_SECRETS_KEY, base64-encoded, decoded to 32 bytes. Anything else (missing, short, long, non-base64) fails the request with 500 Secrets key is not configured.
  • The example key in .example.env (AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=) decodes to 32 NUL bytes. It is not a secret. Anything encrypted with it is effectively in plaintext as soon as the database leaves the machine. Replace it before storing anything.
  • There is no key rotation. Rotating means: re-PATCH every service’s secrets through the API (which decrypts with the old key, re-encrypts with the new). Plan for a re-run during cutover.
  • Secrets never leave the API in plaintext. There is no GET /services/.../secrets endpoint. Patches operate against the decrypted document in memory.

Service Installation (Definition Fetching)

POST /services (direct install), POST /services/install (registry install), PATCH /services/:serviceId (direct update), and POST /services/:serviceId/update (registry update) all fetch a URL supplied by or resolved from caller input, hash the content, and pass it to the adapter’s generateDefinition. The fetcher has a few guards:
  • Hard timeout of 10 seconds per request.
  • Maximum response body of 30 MiB.
  • IP-literal hostnames whose range is not unicast are rejected (loopback, link-local, multicast, private-v4 ranges).
DNS hostnames are resolved and each address is checked. This includes redirect targets, every hop is validated before the request proceeds.
  • DNS names are resolved. If the hostname is not an IP literal, the guard resolves it via dns.lookup and checks every returned address against the same range filter. http://internal.example.com/ is blocked if it resolves to 10.0.0.5. http://localhost/ is blocked because it resolves to 127.0.0.1.
  • There is no domain allowlist. The guard only checks IP ranges, not the hostname itself. A hostname that resolves to public unicast addresses will pass.
  • Configurable IP controls. Registry downloads support CIDR-based allow and block lists:
    • CYRNEL_BLOCKED_IPS denies matching addresses and takes highest priority.
    • CYRNEL_ALLOWED_IPS allows matching addresses and bypasses the default SSRF guard.
    • CYRNEL_BLOCK_ALL_REGISTRIES=true denies all registry downloads unless an address matches CYRNEL_ALLOWED_IPS.
    Evaluation order is:
    1. Blocked CIDRs
    2. Allowed CIDRs
    3. Block-all mode
    4. Default unicast SSRF guard
    5. Allow
    Both IPv4 and IPv6 CIDR notation are supported.

Operator guidance

  • Treat the API key as the security boundary. Do not expose /services/install (or the API at all) to clients you wouldn’t trust to make outbound network requests from the host.
  • Use CYRNEL_ALLOWED_IPS, CYRNEL_BLOCKED_IPS, and CYRNEL_BLOCK_ALL_REGISTRIES to restrict which registry addresses may be contacted.
  • For stricter controls (e.g. domain allowlists or network-level restrictions), run cyrnel inside a network namespace or egress firewall that restricts which hosts it can reach.

Data at Rest

WhatWhereProtection
Service metadata, tools, schemasservices, tools tablesNone (plaintext).
Service configuration (JSON-Patch-edited)service_configurations.payloadNone (plaintext).
Service secretsservice_secrets.payloadAES-256-GCM with CYRNEL_SECRETS_KEY.
Module enable/missing statemodulesNone (plaintext, but boolean flags only).
Process records, stdout, stderr, outputIn-memory onlyLost on restart.
If the db leaks, the encrypted secrets are protected by the key only. Configuration is not, operators must avoid storing credentials in config blocks. The secretsSchema exists for that exact reason; use it.

Defaults Worth Hardening

  • Replace CYRNEL_SECRETS_KEY before first use.
  • Set CYRNEL_API_KEY whenever the API listens on anything other than 127.0.0.1.
  • Restrict the host’s outbound network if /services/install is reachable by untrusted callers.
  • Don’t deploy the web UI publicly with credentials baked into the bundle.
  • Review adapters before installing them`.
Last modified on July 1, 2026