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:
- 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.
- 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.
- 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:
- Blocked CIDRs
- Allowed CIDRs
- Block-all mode
- Default unicast SSRF guard
- 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
| What | Where | Protection |
|---|
| Service metadata, tools, schemas | services, tools tables | None (plaintext). |
| Service configuration (JSON-Patch-edited) | service_configurations.payload | None (plaintext). |
| Service secrets | service_secrets.payload | AES-256-GCM with CYRNEL_SECRETS_KEY. |
| Module enable/missing state | modules | None (plaintext, but boolean flags only). |
| Process records, stdout, stderr, output | In-memory only | Lost 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`.