SSyncropel Docs

Security model — secrets at rest, threat model, operator discipline

What spl serve protects against (default-secure auth + filesystem permissions) and what it does NOT (host compromise, cloud-sync of identity dirs). Read before deploying to a multi-tenant or shared-storage environment.

Your identity keypair and provider secrets live as files on disk. Filesystem permissions (0600, owner-only) protect against casual file enumeration. They do NOT protect against host compromise, cloud-sync to Dropbox / Time Machine / rsync, forensic disk imaging, or accidental commit to a dotfiles repo. Operators must maintain the discipline below.

TL;DR

  • ~/.syncro/keys/ and ~/.syncro/secrets/ hold your identity and provider keys. Keep the directory private (permissions are 0600, owner-only) and back it up encrypted.
  • The record store carries every record. Bearer-token secrets are not stored in it; only a hash is kept for validation.
  • Excluded from auto-backup: the startup backup at ~/.local/share/syncropel/backups/instance-<did-tail>/hub.db.bak does NOT include the keys/ or secrets/ dirs.
  • What you must do: exclude ~/.syncro/keys/ and ~/.syncro/secrets/ from cloud-sync (Dropbox, iCloud, OneDrive, rsync, dotfiles repos).

What's protected

Filesystem permissions

All sensitive files land at 0600 (owner-only). spl doctor audits permissions on every run and warns if a file is world-readable:

spl doctor

The audit runs against ~/.syncro/secrets/ and ~/.syncro/keys/. A failed audit returns exit code 1 and prints the offending paths.

Token-at-rest format

Bearer tokens are never stored in the record store as issued. Only a hash lands; the token is shown ONCE at mint time and never persisted in the instance. If you misplace a token, mint a new one.

Per-instance keypair scope

The identity keypair is per-instance, content-addressed via did:key. Rotating the keypair (regenerating the DID) invalidates all federation pairs — peers cache the DID and reject pair traffic from a different keypair. Recovery: re-run spl federation pair after rotation.

Bootstrap discipline

spl serve writes a copy of the record store to ~/.local/share/syncropel/backups/instance-<did-tail>/hub.db.bak automatically on every startup (the directory is keyed by the instance DID — see Operator runbook → Backup discipline). This backup deliberately does NOT include ~/.syncro/keys/ or ~/.syncro/secrets/ so a cp of the backup (e.g., to a different host for inspection) doesn't accidentally include identity material.

What's NOT protected (operator must mitigate)

Host compromise

Root or the instance's user account can read every file in ~/.syncro/. The identity seed lets an attacker:

  • Forge records under your DID
  • Decrypt federation pair traffic addressed to you
  • Issue bearer tokens that pass the instance's auth gate (after replacing the record store)

Mitigation: standard host hardening (disk encryption at the filesystem layer, restrict shell access, monitor for unauthorized SSH). For high-stakes tenants, run spl serve inside a dedicated VM or container with the keys/secrets dir on a separate filesystem volume.

Cloud-sync of ~/.syncro/

Dropbox, iCloud, OneDrive, Time Machine, rsync-to-NAS — all happily pick up ~/.syncro/keys/ and ~/.syncro/secrets/ by default. Once the contents are on a sync provider's servers:

  • The provider's logs may retain them indefinitely.
  • A subpoena to the provider yields your keys.
  • A breach of the provider's storage exposes them.

Mitigation: explicitly exclude these paths from any sync tool you use. Examples:

# rsync — exclude pattern
rsync --exclude='.syncro/keys/' --exclude='.syncro/secrets/' ...

# Time Machine (macOS)
sudo tmutil addexclusion ~/.syncro/keys
sudo tmutil addexclusion ~/.syncro/secrets

# Dropbox / iCloud / OneDrive — install spl OUTSIDE the sync root,
# or use a non-default data dir:
SYNCROPEL_HOME=/var/lib/syncropel spl serve

Accidental commit to a dotfiles repo

Some operators commit ~/.syncro/config.toml to a dotfiles repo. The default .gitignore may NOT exclude keys/ and secrets/ — re-check yours.

Mitigation: explicitly add to .gitignore:

.syncro/keys/
.syncro/secrets/
.syncro/token

Forensic disk imaging

A stolen laptop without full-disk encryption yields your keys. A discarded SSD without secure-erase yields your keys.

Mitigation: full-disk encryption (FileVault on macOS, LUKS on Linux, BitLocker on Windows). Secure-erase before disposal.

Hosted instances

On a hosted instance the operator of the host can access the instance's keys, as with any hosted service; run locally if that is unacceptable. Self-hosting users on shared compute (a VM where another tenant has root) should treat the deployment as compromised by default.

Audit-emission completeness for portability

Since v0.31, every export/import operation emits a core.portability.event.v1 LEARN record on th_audit_portability. Operators auditing "who exported actor X when" or "who initiated instance import" fold this thread:

spl thread records th_audit_portability -o json | jq '.[] | .body'

This audit thread does NOT prevent the export from happening — auth scope (Admin for instance-level export, RecordsRead for GET /v1/actors/*/export) is what gates access. The audit makes consequential ops forensically traceable.

Threat model summary

ThreatProtected?By
Casual file enumeration on multi-user host✓0600 file permissions
Instance-user-only attacker (no root)✓0600 file permissions
Network-only attacker (over HTTP)✓Bearer-token auth (default-secure since v0.16)
Replay of leaked bearer token✓Server-side validation against the stored hash
Host root / instance-user compromise✗(mitigation: host hardening, full-disk encryption)
Cloud-sync of identity dir✗(mitigation: explicit exclusion)
Forensic disk imaging✗(mitigation: full-disk encryption)
Accidental git commit✗(mitigation: .gitignore)
Cross-tenant on shared compute✗(mitigation: platform-level tenant isolation)
Audit gap on portability ops✓th_audit_portability (since v0.31)
A work loop reading the provider credential or the operator's bearer✓ (sandboxed transport, the default for every non-operator tier; opt-in for the operator's own runs)separate isolated process, no network, no view of the instance home, one scoped credential handed to it at start
A same-user process reading the daemon's environment or memory✓the daemon's process is protected and credentials are moved out of its environment at boot
A run declaring its own success or its own permission to act✓the kernel authors the tool verdict and the outcome; the run credential cannot write either kind

Protection domains and the work loop

Every tool a work loop can reach is classified by protection domain before it is offered, and the domains order: sandboxed < mediated < kernel-trust-domain. A sandboxed worker executes only what its domain allows and asks the kernel for everything else through five doors on a local socket. The daemon checks each isolation layer before it hands the run its credential. The order is the guarantee: a worker that could not be isolated is stopped having never held a credential.

Three things a run cannot say about itself:

  • Whether it was allowed to act. Every tool call is judged by the kernel over the live permission rules and recorded as core.work.tool_verdict.v1 before it runs. The run credential cannot write that kind.
  • Whether it succeeded. The kernel writes the outcome, counts the turns itself, requires a clean exit, and checks the claim against the thread's own tool tally: a run whose every tool call failed is not a success whatever its final answer says. The outcome carries tool_calls_ok and tool_calls_failed.
  • What tools it has. A defined actor's declared tool list bounds its runs on both transports; a declaration can narrow a tier and never widen one.

Host tools (bash and the filesystem set) are wired for the operator's own machine only. The rollback for the whole transport is one config record; see The sandboxed transport.

See also

On this page