ModernCIPush. Green. Merged.

Security: tokens, secrets, isolation and what leaves your network

How ModernCI handles tokens, secrets, runner isolation, approvals and the optional cloud link. What is built, and what isn't.

Checked against the code on 2026-10-13. ModernCI is not released (Release status); this describes the pilot build.

Network

  • One daemon, one listening port. It serves the UI, the API and MCP on that port. Plain HTTP is answered only for callers on the same machine; everyone else is redirected to HTTPS.
  • Agent hosts connect out to the daemon over MCP (Streamable HTTP, bearer token). They need no open port.
  • Backends: the daemon connects out to SSH hosts, and asks a runner provider for a fresh runner per job.
  • Forge webhooks are signed (HMAC-SHA256) and checked with their delivery id and timestamp. Without webhooks, the daemon polls.

Tokens and sign-in

  • The daemon mints its own tokens. Each is shown once and stored only as a SHA-256 hash.
  • Scopes: read, operate, agent and admin. admin is for the host only; a tenant's tokens never carry it.
  • A token can expire, and can be revoked. Rotation is by hand today.
  • People sign in with the forge's OAuth, or a local account (Argon2id password, at least 12 characters, sessions of 12 hours, throttled after 5 failed tries).
  • The forge token: for GitHub OAuth, ModernCI asks for the repo scope. A documented minimum set of fine-grained permissions is not written yet.

Secrets

  • Encrypted at rest: one data key per tenant, ChaCha20-Poly1305, under a master key kept in a file only the daemon can read.
  • Scoped by repo, branch and check. A secret's value never goes to the UI or MCP, only to a run that matches its scope.
  • Passed to a check through stdin or the environment, never on the command line.
  • Redacted from logs: each secret value, and its base64, URL-encoded and JSON-escaped forms, is masked.

Isolation

  • On this machine, checks run as a separate OS user per tenant, never as the daemon's user. A job that would expose the daemon's home or another tenant's work is refused.
  • With a runner provider, each job gets a fresh runner.
  • Not built: container or VM sandboxing on this machine (namespaces, seccomp and the like).
  • Tenants: every record, config, secret and stored key carries its tenant, and nothing crosses tenants. A tenant can't see or configure the install's backends.

What merges, and who approves it

  • ModernCI merges exactly what it checked. Where the target allows it, the target moves to the checked commit. Where the target requires pull requests, reviews or signed commits, the forge merges each one in checked order, and ModernCI verifies that the result matches the checked tree, or stops.
  • Your forge's rules still hold: required reviews, code owners and signed commits are waited for, not bypassed.
  • ModernCI writes one kind of commit itself: a conflict confined to files a rule settles (a lockfile, say) is resolved in a labelled commit, as the repo's configured identity. Any other conflict goes to an agent.
  • Protected paths, where weakening a test would need a person's order, are planned, not built.

modernci cloud connect opens one outbound connection to modernci.dev. Nothing is sent unless you run it.

  • The cloud can only ask, and only read: health, alarms, PRs, repos, spend and usage. Everything else, including admin, is refused.
  • Logs, diffs, excerpts and reproduce commands are dropped before anything is sent. Strings are cut to their first line and 400 bytes, and secret values are redacted.
  • Usage records and state (PRs, results, alarms) go up as metadata. Code, logs and secrets stay home.

Not yet

  • A published threat model and a security contact.
  • Automatic token rotation.
  • A documented minimum forge permission set.
  • Sandboxed checks on a shared machine.