Skip to main content

Deploying emisar in production

The quickstart put one runner on one host. Production is a different question: dozens of hosts, real blast radius, an agent you won't hand a shell — and a security review that will ask exactly how a wrong call gets stopped. emisar's control plane is hosted, so you don't stand anything up; "deploying" here means two things — rolling the runner across your fleet, and setting the envelope around it. This page is the envelope.

None of it is much configuration. The defaults are already conservative — reads run, mutations wait for a human, critical is denied outright. Going to production is mostly deciding where to tighten, and doing it once.

The shape of a production fleet

Two decisions carry most of the safety, and you make both up front: how runners are grouped, and who — which person, which agent key — is scoped to reach them. Get these right and the rest is defaults.

  • One runner per host. A runner's identity is its account plus a stable id; names are unique within an account. Don't share one across hosts, and don't run two on the same box.
  • Group by tier, not by host. A group is the unit that policy, runbooks, member scopes, and agent fan-out all target. Name it for the fleet role — web, edge, db-prod, cache — not the machine. The installer defaults the group to the hostname; change it on your second host. web-01 is a label, not a group.
  • Labels for the cross-cutting facts. Free-form key=value pairs — region=us-east-1, environment=prod, service=checkout — for filtering the fleet and targeting policy without redrawing your groups.
  • Keep policy and access aligned. Policy decides what a runner will run; access decides who can reach it. A locked-down database tier should have both — a strict ruleset and only the DBAs (and their agent) in scope.

A three-tier layout, concretely — the same fleet three ways, each with the packs it needs and the posture it warrants:

web
10 hosts · packs nginx, linux-core, debugging. Reads run on policy; a restart waits for one approval. The everyday tier.
db-prod
3 hosts · packs postgres, linux-core. Locked: a targeted ruleset denies critical, four-eyes on high, and signed dispatch is on. Only the DBA team in scope.
staging
2 hosts · the full catalog, including the shell break-glass pack. Looser so an agent can iterate on a fix — never a path to a prod host.

Roll out in phases

Each phase is safe to stop at, and each is reversible. You never have a half-configured fleet that's more open than the phase before it.

  1. Pilot on one non-prod host. Install, watch it connect, run a read, open the audit row it wrote. Prove the loop end to end before it touches anything that matters.
  2. Set the default policy. Confirm the tier floor for the whole fleet — low/medium allow, high require approval, critical deny. Higher tiers can't be made more permissive than lower ones; the editor enforces it.
  3. Add approvals where they matter. For anything that can drop a service or lose data, turn on four-eyes — two distinct approvers, self-approval off. Approvers get an email with the request; the on-call decides.
  4. Scope people and agents. Give each member and each MCP key only the runners or groups they own. Out-of-scope hosts don't render, 404 on their detail pages, and vanish from the dispatch surface — it hides the host, it doesn't just gray out a button.
  5. Lock the sensitive groups. Add a targeted ruleset for your production tiers. A ruleset replaces the default for its target (most specific wins: runner over group over default), so the db-prod group can deny critical and four-eyes high while the rest of the fleet stays on the default.
  6. Turn on signed dispatch where it must be provable. On the hosts where "a real human asked for this" has to be cryptographically true — not just policy-checked — enforce signed dispatch. Those runners execute only a call carrying a valid Ed25519 signature the operator produced in their own client; the portal Run button, runbooks, and API keys stop dispatching there.
  7. Wire identity. Require MFA account-wide. Add SSO so people sign in through your IdP; on Enterprise, add SCIM so offboarding someone in the directory suspends them here — sessions killed, keys revoked — with no checklist. Only available on Team & Enterprise.
  8. Ship the audit to your SIEM. Mint an audit:read key, point your collector at the NDJSON feed, and alert on the handful of events that actually mean something (below).

Best practices, by layer

Runners & the network

  • The installer already runs the binary as a dedicated non-root emisar user under systemd. Keep it that way. Grant elevated rights per action through sudoers or polkit, not by running the runner as root.
  • Runners dial out over :443. Nothing listens on the host — there's no inbound port and no bastion to stand up. Don't open one; there's nothing to open.
  • Give packs their secrets through the runner's environment, never through call arguments. Put them in /etc/emisar/runner.env (mode 0600) and allowlist the names in execution.inherit_env. Only those names reach an action's process; the runner's own token and the rest of its environment never do.
  • Set a local admission ceiling as a backstop. admission.max_risk: medium on a read-only box drops every high/critical action before it can run — even one a compromised control plane tried to push. It's the host operator's veto over the cloud, baked in at boot.

Policy

  • Leave critical denied. It's default-closed on purpose. Opt a single action out with a per-action override if you must — not the whole tier.
  • Gate high, don't allow it. A restart, a kill, a drain — one approval is cheap insurance against a bad call, human or model.
  • Reach for per-action overrides sparingly and specifically (nomad.job_stoprequire_approval), most specific first. The tier defaults should carry the bulk of the fleet, not a wall of exceptions.
  • Use a targeted ruleset to lock a group, not to loosen one broadly. A ruleset replaces the default for its target; delete it to fall back. See policies & approvals.

Approvals

  • Four-eyes for anything destructive: min_approvals of 2, self-approval off. The person who asked can't wave through their own change.
  • Standing grants are a scalpel, not a blanket. When you approve a repeat action, scope it tight — exact arguments, a use cap, the shortest window that fits the job. A 90-day any-arguments grant on a mutating action is a standing shell; don't mint one.
  • Let expiry do the cleanup. An undecided request cancels after 24 hours; a grant lapses on schedule. You don't have to remember to revoke — but you can, in one click, from the Approvals page.

Access & identity

  • Least-privilege roles: viewer to watch, operator to drive and decide approvals, admin to manage policy and the fleet, owner for billing. Most people are operators.
  • Scope every member and every key to the hosts they own. "The DBA agent only reaches db-*" is one setting on the key — and out-of-scope hosts never appear.
  • Require MFA account-wide — TOTP with one-time recovery codes; a replayed code is rejected. On Team and up, an SSO session can satisfy it when your IdP enforces MFA.
  • Wire your directory. SSO binds people to your IdP by issuer and subject, never by email. On Enterprise, SCIM lets the IdP drive the whole lifecycle: deactivate someone there and emisar suspends them here, kills their sessions, and revokes their keys — offboarding stops being a thing you can forget. See single sign-on & directory sync. Only available on Team & Enterprise.
  • Scope MCP keys per action and rotate them. A key carries actions:read / actions:execute / audit:read and an optional runner restriction — so the log-shipping key never executes, and the agent key never sees a host it doesn't own.

Packs

  • Trust is deliberate, and that's the point. A pack runs only when its on-disk SHA-256 matches the hash you trusted on the Packs page; edit it and it goes pending until you re-trust. Leave that drift guard on — it's how a changed pack never runs silently.
  • Ship a host only the packs it needs. A web box has no use for the postgres pack; a smaller catalog is a smaller surface and a shorter list for an agent to reason over.
  • Keep the shell break-glass pack off production. It's the one action where the operator supplies the whole command — critical-risk, default-denied — for verifying a fix on staging before you encode it as a real action. Install it on staging; never on a prod runner.

Audit

  • Stream it to your SIEM. GET /api/audit is forward-only NDJSON with cursor paging; an audit:read key reads events and can't run anything, so it's safe to hand to a Splunk or Datadog collector.
  • Keep the host journal too. /var/log/emisar/events.jsonl is hash-chained — each line sealed by the one before it — so a cut or an edit is detectable with emisar audit verify. The cloud audit is the searchable system of record; the journal is on-host forensics that doesn't lean on the cloud to exist.
  • Alert on signal, not noise: policy.updated (someone changed the rules), pack-trust transitions (new bytes approved), and the most interesting one — a runner logging repeated validation_failed or action_blocked_by_admission: an agent asking, over and over, for things it can't have.

Two worked examples

An on-call agent that reads the web tier and restarts behind approval

Goal: your incident agent should investigate nginx freely, and be able to restart it — but a restart should need a human nod.

  • Fleet: the ten web hosts share group web, packs nginx + linux-core + debugging.
  • Policy: the defaults already fit — log tails and status reads are low/medium (allow); nginx.restart is high (require approval). No override to write.
  • Access: one MCP key, actions:read + actions:execute, restricted to the web group. That's the whole grant.

Result: the agent calls nginx.error_log_tail and it runs; it calls nginx.restart and the run holds for the on-call, who sees the host, the exact arguments, and the agent's reason before approving. Every call lands in the audit with its client, key, and reason.

A destructive action on prod-db, done safely

Goal: a Postgres failover on db-prod should be possible — but only when two people agree and a real human signed the request.

  • Fleet: group db-prod, three hosts, packs postgres + linux-core. Signed dispatch enforced on all three.
  • Policy + approvals: a targeted ruleset for the group — critical denied, high require approval — with four-eyes on: two approvers, self-approval off.
  • Signing: each DBA holds a 24-hour leaf certificate from your offline CA in their MCP client; the CA's public key sits in every db-prod runner's config, shipped through normal config management.

The flow: a DBA's agent calls postgres.promote_replica with a reason; the call carries the DBA's Ed25519 signature. The control plane relays it — it can't forge or alter it, and holds no key to try. Policy holds the run for approval; two other operators approve. The signed, twice-approved run dispatches; the runner verifies the signature against its CA, checks the nonce hasn't been seen and the timestamp is fresh, then runs it. The audit shows the requester, both approvers, the signature, and the redacted command. If the cloud were compromised it could refuse to relay the call — but it could never have originated that failover.

Go-live checklist

The list to walk before you point production at emisar — the same one a security reviewer will:

  • Every runner runs as the non-root emisar user — the installer default; confirm nobody changed it.
  • Runners are grouped by tier; every production group carries its own locked ruleset.
  • critical is denied fleet-wide; high requires approval.
  • Four-eyes is on for any group that can lose data or drop a service.
  • Members and MCP keys are scoped to the hosts they own — nothing is account-wide by accident.
  • MFA is required; SSO / SCIM is wired if you're on Team or Enterprise.
  • Pack secrets live in runner.env + inherit_env, never in call arguments.
  • The shell break-glass pack is on no production runner.
  • Signed dispatch is enforced wherever a human's signature must be provable.
  • Audit is streaming to your SIEM through an audit:read key, with alerts on policy changes and admission blocks.

What emisar is not

Worth saying plainly, because a good reviewer will ask. emisar bounds what can be run and keeps the record; it doesn't replace the layers underneath it.

  • Not a sandbox. A restart still restarts the service. emisar bounds what can be called and records it; process isolation is the host's job — systemd hardening, namespaces, SELinux.
  • Not an EDR. It won't spot a malicious binary or lateral movement. It will refuse a pack whose bytes drifted from what you trusted, and reject an argument its schema doesn't allow.
  • Signed dispatch is integrity, not availability. A compromised control plane can refuse to relay a signed call; it can't forge one. And there's no revocation list — short certificate TTLs (24h) are how a leaked key stops mattering.
  • Redaction is pattern-bound. Built-in rules mask tokens and keys before output leaves the host, but a secret in a shape no rule matches will slip through — so design actions not to print secrets, and pass them through the environment.
  • The cloud audit is searchable and append-only, not tamper-evident. The hash-chained journal on the host is the tamper-evident record — and even that a root attacker can delete. Ship it off-box if you need a copy beyond their reach.
On Enterprise, you don't do this alone. A security and procurement review, a rollout plan mapped to your fleet, and design-partner support come with the plan. Tell us what you're running and we'll help you land it — see plans or email sales@emisar.dev.