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.
  • Timeouts are enforced at two layers: the API hard-kills via setTimeout using timeoutMs (API-level), and the sandbox enforces its own limit via envConfig.timeoutMs (environment-level). The worker slot is recreated after either fires.
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.
  • Key rotation is supported via the key ring mechanism: CYRNEL_SECRETS_PREVIOUS_KEYS holds old keys (comma-separated, base64). Decryption falls back through the previous keys if the primary key doesn’t match. Each encrypted payload records its key ID (kid) so the system can look up the correct key. When a record is read that was encrypted with a previous key, it is silently re-encrypted with the primary key and persisted. See the key rotation procedure below.
  • Secrets never leave the API in plaintext. There is no GET /services/.../secrets endpoint. Patches operate against the decrypted document in memory.

Service & Module Installation (Definition/Archive Fetching)

POST /services, POST /modules (direct install), POST /services/install, POST /modules/install (registry install), PATCH /services/:serviceId, PATCH /modules/:moduleId (direct update), and POST /services/:serviceId/update, POST /modules/:moduleId/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 (or extract and register the module archive). 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_REGISTRY_BLOCKED_IPS denies matching addresses and takes highest priority.
    • CYRNEL_REGISTRY_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_REGISTRY_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 /modules/install (or the API at all) to clients you wouldn’t trust to make outbound network requests from the host.
  • Use CYRNEL_REGISTRY_ALLOWED_IPS, CYRNEL_REGISTRY_BLOCKED_IPS, and CYRNEL_BLOCK_ALL_REGISTRIES to restrict which registry addresses may be contacted.
  • Use CYRNEL_ALLOWED_IPS and CYRNEL_BLOCKED_IPS to control inbound API access.
  • 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

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.

Key Rotation Procedure

  1. Record the current key and generate a new one:
  2. Set the old key as a fallback and replace the primary:
  3. Restart the server: Existing secrets continue to decrypt using the old key in the fallback list.
  4. Verify: All existing endpoints work, secrets are readable.
  5. Optional: Accelerate the transition by touching all records: Iterate over services and modules and call their secrets presence endpoint to trigger re-encryption. Otherwise records are re-encrypted naturally on first read.
  6. After all records have been re-encrypted (confirmed by checking kid in the JSON payload), remove the fallback:
    Then restart again. Old ciphertext without a kid field (from before the upgrade) are automatically stamped on first read.

Multi-instance rollout

During a rolling update, both old and new instances must be able to decrypt all records. This works because:
  • Old instances have CYRNEL_SECRETS_KEY = old key, they decrypt everything natively.
  • New instances have CYRNEL_SECRETS_KEY = new key and CYRNEL_SECRETS_PREVIOUS_KEYS = old key, they decrypt with fallback.
  • After full rollout, old instances can be retired.
Compromise recovery: if a key is suspected compromised, repeat the rotation. As long as at least one known key can decrypt, old ciphertext is never lost.

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 or /modules/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 4, 2026