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 clones | One per member | One, shared by all writers |
| How writes reach others | Push 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 control | Each member commits in its own clone; the rebase-before-push loop reconciles | A single exclusive lock serializes writers on the one clone |
| OS users | May be distinct; each member can run as its own operating-system user | Must be a single operating-system user |
| Isolates | Credentials and compute between members, plus the transport history each member holds locally | Nothing 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-mailinstall error: SECURITY GUARD REFUSAL: 'topology: shared-clone' is incompatiblewith distinct operating-system users ['alice', 'balogh', 'bob']. Shared localclone directory permissions dissolve credential isolation and allow direct inboxmodification across users. Use 'topology: per-member-clone' for multi-userisolation.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 whostore: /path/to/store-sharedupstream: origin/maintopology: shared-cloneidentity: (unresolved)roster: planner active builder activeThreat 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
| Property | Guaranteed? | Notes |
|---|---|---|
| Addressing | Yes | Closed roster allowlist; mail to a non-member is refused. |
| Read-tracking | Yes | Per-member pointer refs record read and archived state. |
| Delivery durability | Yes | Every 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 resistance | Yes | Newlines and control characters rejected in headers; duplicate or unknown header keys refused at parse time. |
| Message confidentiality between members | No | Every clone holds every message and its full history. Git has no path-level access control. |
| Credential and compute isolation | Only under per-member-clone with distinct OS users | Isolates members’ machines and tokens; does not isolate messages. |
| Authenticated sender identity | No | The from: role is self-asserted and checked only against the roster. |
Confidentiality under shared-clone | No | All writers share one group-writable clone; each can read and write the others’ mailboxes. |
See also
- Configuration — the
topology,os_user,remote, andbranchkeys. - 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.