ColonyOS uses zero-trust, signature-based authentication: every authenticated
RPC is signed with the caller's secp256k1 private key and the server recovers
the caller's identity from the signature. The only exceptions are the version
and server-info messages (versionmsg, getserverinfomsg), which are answered
before signature verification. There are no cookies or sessions, so CSRF is not a
concern; authorization is checked per handler against the recovered identity.
This document covers the transport- and protocol-level hardening added in v2.0
and the settings that control it. All settings live in the Security config
section and can be set with COLONIES_SECURITY_* environment variables (or the
config file). Defaults are chosen to be safe without breaking existing
executors and SDKs.
Three kinds of identity exist, and the handlers check the recovered identity against one of three predicates:
| Predicate | Satisfied by | Guards |
|---|---|---|
| Server owner | The server private key | Adding and removing colonies, server statistics |
| Colony owner | The colony private key | Adding, approving, rejecting and removing executors and users, adding blueprint definitions, removing all processes of a colony |
| Membership | Any approved executor or any user of that colony | Everything else: assigning and closing processes, files, snapshots, logs, function registration, blueprint reconciles, and reading colony state |
Membership is colony-scoped. An executor or user of one colony cannot act on another colony, and an executor cannot change its own executor type (the type used for process assignment is read from the database record, never from the request), so it cannot take over another executor's queue.
Membership on its own is broad: it lets a caller inject new work into the colony, which is then executed by other executors. Since executors are designed to run anywhere on the Internet, often on hardware that is less trusted than the server, a leaked executor key would otherwise be enough to run arbitrary workloads anywhere in the colony.
Executors therefore carry a cansubmit flag, controlled by the colony owner,
which is required in addition to membership by every endpoint that injects work:
submitfuncspecmsg(submit a function spec)submitworkflowspecmsgandaddchildmsg(submit or extend a workflow)addcronmsgandruncronmsg(add or trigger a cron)addgeneratormsgandpackgeneratormsg(add or drive a generator)
Users are unaffected: a colony user may always submit.
The flag defaults to true, so upgrading changes no behaviour. Executors registered before the flag existed, and clients that do not send the field, keep their previous ability to submit. Denying it is an explicit opt-in:
# an executor that may only execute, never submit
colonies executor add --name my-worker --type my-type --executorid <id> \
--nosubmit --approve
# same for the key-generating variant
colonies executor create --name my-worker --type my-type \
--keypath worker.prv --idpath worker.id --nosubmit --approveThe current value is shown by colonies executor get --name <name> as
CanSubmit, and appears as cansubmit in the JSON representation of an
executor. A spec file passed with --spec may set cansubmit directly;
--nosubmit overrides it.
The permission is set when the executor is registered. To change it for an existing executor, remove and re-add it:
colonies executor remove --name my-worker
colonies executor add --name my-worker --type my-type --executorid <id> --nosubmit --approveRe-adding reuses the existing record, so the executor keeps its identity and history. Note that this is a permission change only; if the executor's key is believed to be compromised, rotate the key rather than only clearing the flag.
Which executors should keep the permission is a per-deployment decision. A
meta-orchestrating executor that decomposes work into sub-workflows needs it; a
leaf executor that only pulls and runs processes does not, and is the case where
--nosubmit reduces the blast radius of a compromised key the most.
| Setting | Env | Default | Notes |
|---|---|---|---|
| Read-header timeout | COLONIES_SECURITY_READ_HEADER_TIMEOUT |
20 s |
Slowloris defense. Does not affect the response side, so the executor long-poll assign is unaffected. 0 disables. |
| Idle timeout | COLONIES_SECURITY_IDLE_TIMEOUT |
120 s |
Keep-alive idle bound. 0 disables. |
| Max body bytes | COLONIES_SECURITY_MAX_BODY_BYTES |
104857600 (100 MiB) |
Caps request bodies so an unauthenticated caller cannot exhaust memory. Also caps a single websocket message. 0 disables. |
| CORS allowlist | COLONIES_SECURITY_CORS_ALLOW_ORIGINS |
empty | Comma-separated browser origin allowlist. Empty installs no CORS middleware, so cross-origin browser requests get no allow header and are blocked by the browser; non-browser clients (executors, SDKs, CLI) send no Origin header and are always allowed. A non-* allowlist permits only GET/POST methods and the Origin, Content-Type, Accept headers. * restores allow-all. |
| WebSocket origin allowlist | COLONIES_SECURITY_WS_ALLOW_ORIGINS |
empty | Same semantics for /pubsub. Empty allows no-Origin (non-browser) and same-origin requests only. * allows any. |
| Rate limit | COLONIES_SECURITY_RATE_LIMIT |
0 (off) |
Per-client-IP requests/second on all routes; only /health is exempt. The assign long-poll and /pubsub subscribe count against the limit, so size it for the number of executors sharing a source IP. |
| Rate-limit burst | COLONIES_SECURITY_RATE_LIMIT_BURST |
0 (= rate) |
Allowed burst above the rate. 0 or negative falls back to the rate value. |
The server always runs gin in release mode. When TLS is configured it is served with a minimum version of TLS 1.2.
Deliberately, no ReadTimeout/WriteTimeout is set on the HTTP server: the
executor long-poll assign holds the response open, and /pubsub websockets are
long-lived, so a blanket read/write deadline would break legitimate connections.
Signed RPC messages carry an optional nonce and timestamp. When the nonce is
non-empty, the signed bytes are payload + "|" + timestamp + "|" + nonce,
binding them against tampering; when the nonce is empty, only the payload is
signed (the legacy scheme). The server rejects a message whose timestamp is
outside the allowed skew window or whose nonce has already been seen.
| Setting | Env | Default | Notes |
|---|---|---|---|
| Mode | COLONIES_SECURITY_REPLAY_PROTECTION |
advisory |
off (aliases disabled, none): ignore replay fields. advisory: check messages that carry replay fields, still accept legacy messages that omit them. enforce (aliases enforced, strict): additionally reject messages that lack replay fields. Any unrecognized value silently falls back to advisory, so a typo will not raise an error. |
| Window | COLONIES_SECURITY_REPLAY_WINDOW |
300 s |
Allowed timestamp skew. Nonces are retained for twice the window, since a message timestamped up to one window in the future stays valid until now + 2x the window. |
Rollout is staged: v2.0 defaults to advisory so v2.0 clients are protected
immediately while pre-v2.0 clients keep working. A later release is expected to
default to enforce. Old clients that sign the payload only remain valid in
off/advisory modes.
Single-node deployments run no relay or etcd. Multi-node clusters do, and those ports must be reachable only from trusted peers.
COLONIES_CLUSTER_SECRETauthenticates the cluster relay: peers HMAC-sign each broadcast (sent in theX-Colonies-Relay-Authheader) and the receiver rejects unsigned or mismatched posts. Set the same value on every node. When unset, the relay logs a warning at startup.- The embedded etcd peer/client ports have no TLS or authentication. Restrict them to a private/firewalled network. The server logs a warning when etcd binds to all interfaces in a multi-node configuration. (Full etcd mTLS is planned; see the modernization backlog.)
S3 credentials are read from server/executor configuration (AWS_S3_*), never
from per-file database records. The server does not persist or return S3 access
keys, secret keys, or encryption keys; a migration blanks any that older
versions stored. See modernization/DB-MIGRATION-NOTES.md.