Security
The runtime verifies what it loads and makes no network calls when it evaluates. Access control, network enforcement, secret storage and tamper-evident audit are up to you. This page shows how to add each of them.
Trust boundaries#
What the runtime protects#
| Control | Available now | Planned |
|---|---|---|
| License authenticity | Ed25519 signature verified against the Helixor authority key before any pack is unsealed. Validity window checked with a grace period. | Host-bound licenses; revocation Planned |
| Pack confidentiality | AES-256-GCM encryption. The pack is bound to its pack_id and to the license it was compiled for. It is decrypted into process memory only and never written to disk by the runtime. | Key wrapping with a key you control Planned |
| Pack integrity | The GCM authentication tag and a digest of the decrypted content are both checked. Any modification fails the load. | Publisher-signed packs Planned |
| Entitlement | Packs and solvers the license is not entitled to are refused at compile time and at load time. | |
| Evaluation isolation | Evaluation makes no network calls. It runs in your process with your process's privileges. | Native runtime library; hardware-isolated execution in a SmolVM microVM Planned. Until then, isolate with your own container or VM boundary. |
| Service access | Optional bearer token on every endpoint except GET /v1/health. Binds loopback by default and refuses other addresses without a token. No cross-origin access unless origins are listed. | TLS termination in the service; per-caller credentials |
| Audit fingerprint | receipt_hash on every result. | Keyed and chained receipts Planned |
Licenses are secrets#
A license file does two jobs. It proves your entitlement, and it carries the key that decrypts the packs compiled for it. Anyone who holds the license and a pack can read the pack's rules. Anyone who holds the license alone can load catalog packs issued for it. Handle it like a database password.
- Store it in a secret manager. Mount it into the container as a read-only file. On hosts, keep it at mode
600, owned by the service account that runs the runtime. - Pass it by path. Use
--licenseorHELIXOR_LICENSE_FILE. Never put license contents in environment variables that other tools print, and never in a URL or query string, where proxies and access logs record them. - Keep it out of images and repositories. Add
*.hxlicandhelixor.licto.gitignoreand.dockerignore. - Limit where it goes. Remote compilation (
compile --remote),cataloganddownload-packsend the license to the Helixor compiler service. Run those on build agents, not on production hosts. - Separate environments. Use a different license for development and production when your agreement allows it. A leaked development license then cannot open production packs.
A license cannot be revoked or bound to a host yet
A license works on any machine until it expires. Revocation and host binding are Planned. If a license is exposed, contact Helixor for a replacement. Then recompile and redeploy every pack for the new license, and remove the old files everywhere.
The CLI also reads .env files from the working directory, its parent and ~/.helixor/. Run it from a directory that holds no unrelated secrets.
Secure the decision service#
The decision service binds 127.0.0.1 by default. It can require a bearer token (HELIXOR_SERVICE_TOKEN or --token) on every endpoint except GET /v1/health, and it refuses to bind any other address without one. Cross-origin requests are refused unless you list origins in HELIXOR_CORS_ORIGINS; a wildcard is rejected. The token is one shared secret and the service speaks plain HTTP, so reach it only through a path you control:
- Sidecar: keep the default loopback bind. Only containers in the same pod or processes on the same host can reach it. Set a token as well if other processes share the host.
- Shared service: set a token, and put a gateway or service mesh in front of it for TLS and per-caller authentication. Use mutual TLS or a token check, and allow ingress only from that gateway. See Deployment patterns.
- Browsers: never expose the service to a browser directly. If a web front end needs decisions, call your own back end. Set CORS on the gateway to the exact origins you serve.
- Request size: set a maximum body size and a timeout on the gateway. The service does not limit payload size.
Sensitive values in results#
Every entry in triggers carries matched_items: the raw values that matched, such as the Social Security number itself. The remedy's clean_text is safe to pass on. The result as a whole is not safe to log.
def loggable(result, request_id):
"""Fields that are safe to log. Never log the raw result or matched_items."""
return {
"request_id": request_id,
"pack_id": result.pack_id,
"action": result.action,
"invariants_passed": result.invariants_passed,
"rule_ids": [t.rule_id for t in result.triggers],
"severities": [t.severity for t in result.triggers],
"redactions": result.remedy.redactions_count,
"receipt_hash": result.receipt_hash,
"latency_us": result.latency_us,
}
- Responses from
/v1/evaluate,/v1/decisionand blocked streaming events contain the same fields. Do not let gateways, tracing or error reporters capture those response bodies. - A streaming session keeps the raw text it has received in memory until the session is discarded. Create one session per stream and drop it when the stream ends.
- License and pack errors name the license ID, organization, pack ID or solver ID. They do not contain payload text.
Egress control#
Evaluation opens no sockets. Every result reports egress_bytes as 0. That is a property of the code, not an enforced boundary: the runtime runs with whatever network access its process has. To make zero egress a guarantee:
- Deny all egress for dedicated decision-service workloads with a network policy or firewall rule. The service needs no DNS and no outbound access once the pack and license are mounted.
- For the in-process and sidecar patterns, egress is controlled per workload. Allow only the destinations your application needs.
- In air-gapped sites, compile locally and use no CLI command that contacts Helixor. See Deployment patterns.
Receipts you can rely on#
receipt_hash is an unkeyed, truncated SHA-256 fingerprint of the decision. It lets you match a log line to a decision. It does not prove that the log line was not altered, because anyone can recompute it. If you need tamper evidence:
- Sign
Compute an HMAC or signature over the request ID, the pack version, the action, the receipt hash and a timestamp. Use a key held in your own key management service.
- Chain or anchor
Append the signed records to write-once storage. Or chain them, with each record including the previous record's digest, and periodically anchor the chain head somewhere independent.
- Retain
Keep the records for your regulatory retention period, together with the pack versions they refer to.
The audit pipeline tutorial builds this end to end. Keyed and chained receipts in the runtime are Planned.
Clock and expiry#
License validity is checked against the local system clock when a pack is loaded. Keep hosts on a trusted time source. A clock that jumps forward can make a valid license look expired, which stops start-up. Trusted time for license checks is Planned.
Supply chain#
- Install the runtime from the wheel that comes with your evaluation access. Record its checksum and pin that exact file in your builds. A package on the public index is Planned.
- Build your own container image from a minimal base. Run as a non-root user with a read-only root file system. A reference Dockerfile is in Deployment patterns.
- Review playbooks like code. A playbook's regex rules run against every payload. Test each pattern for correctness and for pathological backtracking before you promote it.
Hosted reasoning#
A call to the hosted reasoning API sends data outside your perimeter. Decide in advance which fields may leave. Send the remedy's clean_text, never the original payload. Authenticate with an API key in the X-Helixor-API-Key header, stored in your secret manager. See Hosted escalation.