Skip to content

Topologies and security

Relay runs in one of two topologies. Which one you choose is a security decision, not a filesystem-layout decision: the two shapes differ in what they isolate, not just in where files live. Get this choice right before you install, because the wrong one cannot be papered over with permissions.

The topology is declared in the transport store’s team.json under the topology key. It defaults to per-member-clone. See Configuration for the full schema and Concepts for what the store contains.

The two topologies

per-member-clone (default)shared-clone
Local clonesOne per memberOne, shared by all writers
How writes reach othersPush and rebase through a shared remote (or stay local on a single machine with no remote)Direct filesystem writes, serialized by a flock lock on .mail.lock
Concurrency controlEach member commits in its own clone; the rebase-before-push loop reconcilesA single exclusive lock serializes writers on the one clone
OS usersMay be distinct; each member can run as its own operating-system userMust be a single operating-system user
IsolatesCredentials and compute between members, plus the transport history each member holds locallyNothing between members — all writers share the clone and its permissions

per-member-clone is the older, longer-proven shape and the only one that survives operating-system user isolation. shared-clone is an opt-in for the single-user case, where several checkouts owned by one user share a clone and there is no isolation boundary to violate.

A per-member-clone team does not require a remote. A single machine with no remote configured is a fully supported first-run state: mail commits locally and simply never leaves the box. Add a remote later when a second machine joins. The remote and branch names are configurable and are not assumed to be origin or main; see Configuration.

Why this is a security choice, not a path choice

A shared clone works only if the clone directory is group-readable and group-writable so that every writer can commit into it. Those are exactly the permissions that let one user read another user’s inbox and write into it directly. The directory permissions that make shared-clone function at all are the permissions that dissolve isolation between members.

Under distinct operating-system users, credential and compute isolation is the whole point: each member’s tokens and processes are walled off by the operating system. A shared, group-writable clone punches a hole straight through that wall for mail. shared-clone and per-member operating-system isolation are therefore mutually exclusive. A configuration that appears to offer both is offering a false sense of security.

The topology guard

Relay makes the dangerous combination unrepresentable. The guard evaluates only when topology is shared-clone; per-member-clone is never checked. Under shared-clone, it collects the distinct os_user values declared by active and planned members, adds the operating-system user running the command, and refuses if more than one distinct user results. In practice: declaring shared-clone while any member carries an os_user naming a user other than the one running Relay fails closed. The guard keys on the os_user field — that is the signal that members are meant to be separate operating-system users.

The refusal fires at install time and again on every command, including read-only ones. A store that is misconfigured cannot be installed, sent from, or even inspected with who until the topology is corrected.

At install:

$ pibmo-relay-install --config team.json --store-dir ../repo-mail
install error: SECURITY GUARD REFUSAL: 'topology: shared-clone' is incompatible
with distinct operating-system users ['alice', 'balogh', 'bob']. Shared local
clone directory permissions dissolve credential isolation and allow direct inbox
modification across users. Use 'topology: per-member-clone' for multi-user
isolation.

The same refusal is raised by pibmo-relay who, send, list, and every other command against a store whose team.json declares that combination. The fix is to declare topology: per-member-clone, which is correct for any team whose members are distinct operating-system users.

A shared-clone store that declares no os_user keys (all checkouts owned by one user) installs and runs normally:

$ pibmo-relay --role planner who
store: /path/to/store-shared
upstream: origin/main
topology: shared-clone
identity: (unresolved)
roster:
planner active
builder active

Threat model

Be precise about what Relay guarantees, because a git-native transport gives every member a full clone, and a clone holds every message.

Relay provides addressing and read-tracking. It does not provide message confidentiality between members. Git has no path-level access control. Any member who can clone the transport holds every message in it, including messages addressed to other members and the full history of every message. The to: and cc: headers route mail and drive unread banners; they do not restrict who can read a file.

Under the isolated-user topology (per-member-clone with distinct operating-system users), the operating system additionally isolates members’ credentials and compute from each other. That isolates each member’s machine and tokens from the others. It does not isolate messages: every member’s clone still contains every message. Credential isolation and message confidentiality are different properties, and Relay delivers only the first.

Sender attribution is self-asserted and not authenticated. The from: role is a command-line argument checked only against the roster allowlist; any member can send as any other member. Relay does defend the message format itself: newlines and control characters are rejected in headers such as the subject, because a newline could inject a second from: line and forge attribution at the file level. That protects the on-disk format, not the identity of the sender. Treat message contents as untrusted input.

If a team needs per-member message confidentiality, a git-native transport is the wrong architecture. Per-member confidentiality is not achievable within this design; a request for it is a request to leave the architecture, and should be recognized as such rather than partially accommodated.

What is and is not guaranteed

PropertyGuaranteed?Notes
AddressingYesClosed roster allowlist; mail to a non-member is refused.
Read-trackingYesPer-member pointer refs record read and archived state.
Delivery durabilityYesEvery write commits locally; if a push cannot complete it retries with backoff, and the local commit survives for pibmo-relay flush to send later.
Header-injection resistanceYesNewlines and control characters rejected in headers; duplicate or unknown header keys refused at parse time.
Message confidentiality between membersNoEvery clone holds every message and its full history. Git has no path-level access control.
Credential and compute isolationOnly under per-member-clone with distinct OS usersIsolates members’ machines and tokens; does not isolate messages.
Authenticated sender identityNoThe from: role is self-asserted and checked only against the roster.
Confidentiality under shared-cloneNoAll writers share one group-writable clone; each can read and write the others’ mailboxes.

See also

  • Configuration — the topology, os_user, remote, and branch keys.
  • Concepts — what the transport store holds and how mail is addressed.
  • Getting started — installing an instance.
  • Onboarding — the guided setup that produces team.json.
  • Architecture — the transport engine and the invariants it enforces.
  • Troubleshooting — recovering from a guard refusal or an exhausted push.