> ## Documentation Index
> Fetch the complete documentation index at: https://actelos.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Security

> Trust boundaries, secrets, modules, and known limitations

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.

<Warning>
  For These reasons, we **strongly** recommend using **only** trusted
  modules. **DO NOT** enable any untrusted modules.
</Warning>

#### 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](/cyrnel/docs/adapters-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](/cyrnel/docs/environments-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

| 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\`.
