CMLabs · Psyclone AIOS

5. Security & SSL

Secure by default: certificate verification with SSL_VERIFY_PEER, the allowselfsigned escape hatch at four scopes, hostname verification, and custom CAs via cafile/capath.

Secure by default Shipped

SSL-enabled Psyclone builds now verify the remote certificate on every client connection: connections use SSL_VERIFY_PEER against the OS/CA trust store, and the presented certificate's identity is checked against the hostname being connected to (via SSL_set1_host, with SNI sent on connect). No configuration means secure: a spec that says nothing about SSL policy gets full verification.

Note. This replaces the historical behaviour (encrypted but unverified TLS: SSLv23_method() with VERIFY_NONE). Any older documentation implying “SSL just works with self-signed certificates” is obsolete — self-signed certs now fail unless you explicitly opt out of verification or trust your own CA.

The verification decision

Server presents certificate allowselfsigned = yes? (connection > manager > interface > global) Accept (encrypted, NOT verified) ⚠ cafile / capath configured? custom CA vs OS trust store Verify chain against custom CA (load_verify_locations) Verify chain against OS trust store (VERIFY_PEER) Hostname matches cert? SSL_set1_host Accept ✔ Reject ✘ yes no (default) yes no chain ok chain ok (fail → reject) yes no
The SSL client verification flow. Only an explicit allowselfsigned="yes" bypasses verification; otherwise the chain is verified against the custom CA (if configured) or the OS trust store, then the hostname is checked.

allowselfsigned: the explicit escape hatch

The allowselfsigned flag (default FALSE) loosens verification so self-signed certificates are accepted. Loosening is always a deliberate, visible spec entry — it can never happen by accident. The flag exists at four scopes with clear precedence:

ScopeWhereEffect
Global<psySpec allowselfsigned="yes">Blanket policy for all SSL client connections (dev rigs, loopback testing)
Per interface<interface ... allowselfsigned="yes">Overrides the global setting for that interface's connections — either way (an interface can also say no to re-tighten under a loose global)
Per managerconnection-manager level (API)Programmatic override for all connections of a manager
Per connectionindividual connection (API)Finest-grained override

Precedence is most-specific-wins: connection > manager > interface > global. Accepted true values are yes/true/1; anything else is false.

<!-- dev rig: accept self-signed everywhere -->
<psySpec allowselfsigned="yes">
  ...
  <!-- but this externally-reachable interface stays strict -->
  <interface name="Public" port="8443" protocol="HTTP"
             service="MyWebService" allowselfsigned="no" />
</psySpec>
Warning. allowselfsigned="yes" gives you encryption without authentication: traffic is unreadable to passive eavesdroppers, but an active man-in-the-middle can present its own certificate. Use it for development and closed lab networks only; production setups with self-minted certs should use a custom CA instead (below).

Custom CA: cafile and capath

To get real verification with your own self-minted certificates — no certificate purchase needed — mint a private CA, sign your node/server certificates with it, and point Psyclone at the CA instead of the OS trust store. The attributes map to OpenSSL's SSL_CTX_load_verify_locations:

<!-- global: trust our private CA for all SSL client connections -->
<psySpec cafile="/etc/psyclone/ca/rootCA.pem">

<!-- or a directory of CA certs (c_rehash format) -->
<psySpec capath="/etc/psyclone/ca/">

<!-- per-interface override -->
<interface name="GridLink" port="10443" protocol="Message"
           service="GridService" cafile="/etc/psyclone/ca/gridCA.pem" />

cafile is a PEM bundle; capath is a hashed directory of CA certificates. Both are available globally on <psySpec> and per <interface> (interface values inherit from global when unset). With a custom CA configured, full chain and hostname verification still apply — issue your certificates with correct hostnames/SANs.

Trust models compared

ModelConfigEncryptionAuthenticationUse for
Public CAnone (default)✔ OS trust store + hostnameInternet-facing links, certs from a public CA
Private CAcafile/capath✔ your CA + hostnameProduction grids, robot fleets, closed networks
Self-signed acceptedallowselfsigned="yes"✘ noneDevelopment, loopback, throwaway rigs
No SSL buildnon-SSL binaryTrusted single-host / air-gapped setups only

Operating LLM Connections Shipped

A CMSDK <llm> named connection (see the User Guide chapter on LLM Connections) makes outbound HTTPS calls to a vendor endpoint using an API key. Two things therefore land on an administrator’s desk: where the credential lives, and whether the outbound certificate chain verifies.

Credential references only — never inline secrets

A spec never contains the key. The <llm> element carries an indirect reference in its apikeyref attribute, and CMSDK resolves it at configure() time:

apikeyref formResolved from
env:NAMEEnvironment variable NAME, which must be set and non-empty. Preferred form.
file:/pathThe whole file contents, with trailing whitespace and newlines trimmed — i.e. a single-line key file.
NAME (bare)Legacy form: environment variable NAME, and failing that a key=value entry in the optional secretsfile named on the same element.
<!-- preferred: the key lives only in the process environment -->
<llm name="main" apikeyref="env:MY_VENDOR_API_KEY">
  <llmvendor type="openai" endpoint="https://api.example-vendor.invalid/v1/chat/completions">
    <header name="Authorization" value="Bearer %apikey%"/>
  </llmvendor>
</llm>

<!-- alternative: a root-owned, mode 0400 key file -->
<llm name="main" apikeyref="file:/etc/psyclone/secrets/vendor.key"> ... </llm>
Warning. An inline apikey attribute or an <apikey> child element is rejected as a typed configuration error (LLM_ERR_CONFIG_INLINE_SECRET). This is deliberate and non-negotiable: it means a PsySpec can be checked into version control, shipped to a customer or attached to a bug report without leaking a credential. Do not work around it.

Resolution is attempted during configure(), but a missing environment variable or unreadable key file surfaces as the typed error LLM_ERR_SECRET_UNRESOLVED at connect(). The secret value and the failure detail never appear in logs, in error text, or in getSystemStatus() output — the API exposes only getAPIKeyRef() (the reference string) and hasResolvedSecret() (a boolean). So a “secret unresolved” report will not tell you why; diagnose it from the environment, not from the log.

Diagnosing LLM_ERR_SECRET_UNRESOLVED. Check, in order: the variable is exported in the environment of the process that actually hosts the component (a space is a separate OS process — it inherits the node’s environment, not your interactive shell’s); the variable is non-empty; for file:, the path exists and is readable by the account the node runs as; and the reference itself is spelled with its prefix (env:/file:), since an unprefixed value silently falls through to the legacy bare form.

Key-file permissions

  • Keep key files outside the spec tree and outside any directory served by an interface or PsyProbe. /etc/psyclone/secrets/ is a reasonable convention.
  • Owner-read-only, owned by the account the node runs as: chmod 0400 on the file and chmod 0700 on the containing directory. On Windows, remove inherited permissions and grant read to the service account only.
  • One key per file, no trailing content — the whole file is the secret (trailing whitespace is trimmed, nothing else is parsed). A stray comment line becomes part of the key.
  • A secretsfile used with the legacy bare form holds key=value lines and must be protected identically.
  • Exclude both from backups that leave the machine, from container images, and from crash/core dumps where you can. Rotate by replacing the file or the environment value and restarting the affected node — resolution happens at configure time, so a running connection keeps the old value.
  • Prefer env: where your process supervisor can inject the value (systemd EnvironmentFile= with the same permissions, or an equivalent secret store) so the key never sits on disk in the node’s own tree.

Outbound HTTPS verification to LLM endpoints

Calls to a vendor endpoint use the CMSDK HTTP client and therefore the same client-side SSL policy documented above: SSL_VERIFY_PEER plus hostname verification by default, a custom CA via cafile/capath, and allowselfsigned as the explicit opt-out. Public vendor endpoints are signed by public CAs, so the correct configuration is the default one — do not set allowselfsigned="yes" to make an LLM call succeed; that would leave an API key travelling over an unauthenticated channel.

When no cafile/capath is configured, verification uses the operating system trust store. On Linux and macOS that is OpenSSL’s default verify paths. On Windows, 2.2.0 fixed a real failure here: OpenSSL’s SSL_CTX_set_default_verify_paths() points at the build-time OPENSSLDIR, which does not exist on an end-user machine, so verification against public endpoints failed. The client now imports the Windows ROOT system store (via CryptoAPI) into the SSL context’s X509 store instead, so certificate verification works on end-user Windows machines with no extra configuration.

Note. If the ROOT store cannot be read, the log line is SSL: could not load Windows ROOT certificate store and verification will subsequently fail (duplicate certificates during the import are normal and are not failures). Practical checks on Windows: the machine’s ROOT store is populated and current, the service account can read it, and any TLS-inspecting proxy’s CA is installed there — or pointed at explicitly with cafile. On all platforms, an outbound HTTPS problem shows up as LLM_ERR_HTTP_UNREACHABLE rather than as an SSL-specific LLM error, so read the network log alongside it.

Building the SSL variant

SSL support is a build variant of both Psyclone and CMSDK:

# Linux (requires OpenSSL development libraries: -lssl -lcrypto)
make ssl        # release
make ssldebug   # debug

On Windows, select the Release SSL / Debug SSL configurations in the Visual Studio solution; the projects expect the OpenSSL headers and libraries under the _libs/OpenSSL tree referenced by the project files. SSL-enabled CMSDK link libraries carry SSL in their names (e.g. CMSDKSSLDebug2015.lib).

Note. The reference manual's OpenSSL 1.0.2g paths reflect its era; build against a current OpenSSL and update the project/Makefile paths accordingly.

Hardening checklist

  • Run the SSL build in any deployment that crosses a network you do not fully control.
  • Leave allowselfsigned unset (secure default); use a private CA via cafile/capath for self-minted certificates.
  • Issue certificates with correct hostnames/SANs — hostname verification is enforced.
  • Disable the Telnet Console and, if unused, the PsyProbe HTTP interface on exposed ports (chapter 4).
  • Remember that interface <authentication> and encryption attributes are parsed-but-stubbed Stub — do not count them as controls.
  • Firewall the main port; PsyProbe, the Console and node traffic all ride on it by default.
  • Keep LLM credentials out of specs (apikeyref="env:NAME" or file:/path only), restrict key-file permissions to the node’s account, and never relax allowselfsigned for an outbound LLM endpoint.
Psyclone AIOS · CMLabs