529 lines
34 KiB
Markdown
529 lines
34 KiB
Markdown
# Architecture
|
|
|
|
`geth` is a single-binary local-first mesh runtime. One executable provides both
|
|
daemon mode and control mode. The daemon owns local identity, metadata storage,
|
|
the shared Iroh endpoint, resource registry, module routing, and local control
|
|
socket. Control commands connect to the Unix socket and send typed JSONL
|
|
requests.
|
|
|
|
Within `geth-node`, daemon lifecycle code is separated from feature handlers:
|
|
`daemon.rs` owns `geth daemon run` startup, local socket binding, shutdown
|
|
signal handling, Iroh endpoint startup, the Iroh accept loop, and background
|
|
live-sync task spawning. `local_control.rs` owns async local `ControlRequest`
|
|
routing, safe trace-field classification, and named peer/resource/local handler
|
|
families before delegating to feature implementations. `resource_contracts.rs`
|
|
records the review boundary for each resource family: resource ID patterns,
|
|
capabilities, and mutation or host-access points. Runtime registries for
|
|
pubsub, pipes, and overlays live behind narrow mutex-protected structs in
|
|
`runtime.rs`. Protected peer-control ALPN dispatch remains a separate refactor
|
|
target.
|
|
|
|
The local metadata store is SQLite product state. `geth-store` tracks a numeric
|
|
`schema_version` in the `meta` table and applies ordered migrations up to the
|
|
crate's current schema version when the store opens. Fresh database creation and
|
|
repeated opens are idempotent; migrations that change existing schemas run in a
|
|
SQLite transaction where SQLite supports it. File-backed stores deliberately use
|
|
SQLite WAL mode with `synchronous=NORMAL`: committed transactions remain
|
|
consistent after process crashes, while the most recent transaction can be lost
|
|
on an OS crash or power loss before the WAL is durable. `geth status` reports
|
|
daemon uptime, the observed schema version, journal mode, synchronous mode, and
|
|
whether those values match the expected store policy.
|
|
|
|
Service management is also exposed through the single binary. `geth daemon
|
|
service ...` installs and controls a user-level service definition for the local
|
|
daemon. The initial backends are systemd user units on Linux, launchd user agents
|
|
on macOS, and per-user scheduled tasks on Windows. Geth does not install itself
|
|
as a privileged system service.
|
|
|
|
## Iroh-Only Remote Communication
|
|
|
|
Remote geth node-to-node communication is Iroh-only. The daemon will own one
|
|
shared Iroh endpoint and register module protocols on ALPNs such as
|
|
`/geth/cas/1`, `/geth/kv/1`, `/geth/pipe/1`, `/geth/ssh-proxy/1`, and
|
|
`/geth/overlay/1`.
|
|
|
|
The pinned Iroh integration uses `iroh = 1.0.0`. `geth-iroh` wraps
|
|
`iroh::Endpoint::builder(iroh::endpoint::presets::N0)`, configures geth
|
|
ALPNs with `Builder::alpns`, uses `Builder::relay_mode`, adds
|
|
`iroh-mdns-address-lookup` when local discovery is enabled, persists an
|
|
`iroh::SecretKey` as hex-encoded 32-byte key
|
|
material, and shuts down through `Endpoint::close().await`. The default config
|
|
uses Iroh's default relay policy; local-only/offline development can set
|
|
`[iroh].relay_mode = "disabled"`. Named custom relay maps are configured under
|
|
`[iroh.relay_maps.<name>]`, selected with `relay_mode = "custom"` plus
|
|
`relay_map = "<name>"`, validated at config load, and reported in status as
|
|
`custom:<name>` without exposing relay URLs.
|
|
|
|
The native module-backend crates for the intended CAS, KV, and pubsub
|
|
replacements now compile against the same endpoint generation:
|
|
`iroh-blobs 0.103.0`, `iroh-docs 0.101.0`, and `iroh-gossip 0.101.0`. `geth-iroh`
|
|
exposes their native ALPNs without creating a second daemon endpoint. CAS now
|
|
registers an `iroh-blobs` provider handler on `/iroh-bytes/4`; local CAS writes
|
|
are mirrored into the native blob store, and remote `geth cas fetch` performs a
|
|
geth control-ALPN authorization preflight before transferring payload bytes over
|
|
`iroh-blobs`. KV now starts `iroh-docs` with `iroh-gossip` and the same native
|
|
blob store, mirrors named KV stores into read-shared Iroh Documents namespaces,
|
|
and sends read-only docs tickets only after geth control authorization succeeds.
|
|
Pubsub joins deterministic native `iroh-gossip` topics after the geth control
|
|
path authenticates the peer-card endpoint binding and authorizes the topic
|
|
capability. `geth status` reports CAS, KV, and pubsub as wired native backends.
|
|
|
|
Module ALPNs are registered through `geth-iroh`'s protocol router scaffold. The
|
|
router owns the default protocol descriptors, rejects duplicate ALPN
|
|
registrations, and returns explicit unknown-ALPN errors. The current daemon
|
|
accept loop dispatches geth control, pipe, SSH-proxy, and native CAS blob
|
|
streams directly, plus native docs and gossip streams used by KV and pubsub.
|
|
|
|
The target product should use Iroh relay support for practical internet
|
|
connectivity and mDNS/LAN discovery for local networks. These are connectivity
|
|
and candidate-discovery mechanisms only. They do not grant trust, mutate
|
|
authorization state, or make EndpointID knowledge sufficient for access.
|
|
The current daemon can enable Iroh's local-network discovery service through
|
|
`[iroh].local_discovery = true`, which is the default. This publishes and
|
|
discovers Iroh node addressing. `geth peer export/import/list` supports manual
|
|
exchange of signed peer cards as untrusted candidates. Peer cards include the
|
|
Iroh EndpointID plus relay/direct address candidates when the daemon can observe
|
|
them. `geth peer ping <node-id>` dials an imported peer card over Iroh and
|
|
exchanges signed peer-card metadata. `geth peer auth-check <node-id>
|
|
<resource> <capability>` sends a protected Iroh control request that validates
|
|
the caller's signed peer card against the actual Iroh EndpointID before
|
|
evaluating resource-local capabilities. When local discovery is enabled, the
|
|
daemon also advertises and discovers signed peer cards through a geth-specific
|
|
mDNS service. The LAN payload is TXT-encoded signed metadata only; remote geth
|
|
traffic still uses Iroh.
|
|
|
|
Peer cards are the discovery payload. A peer card carries node ID, agent ID,
|
|
endpoint candidates, timestamp, signing public key, and an Ed25519 signature
|
|
over a canonical payload. Imported and ping-discovered peer cards are stored as
|
|
untrusted metadata in `peer_cards`; trust reduction is future work. `auth
|
|
explain` reports when a subject is only a discovered peer candidate and denies
|
|
access. It also reports whether a trusted node has no endpoint binding, whether
|
|
the discovered peer card has no endpoint bound to that node, or whether a peer
|
|
card endpoint matches the reduced keychain view. The peer ping path
|
|
authenticates the Iroh endpoint and peer-card signature, but it does not
|
|
authorize any resource module. Protected peer control requests must also prove
|
|
that the signed peer card binds the observed Iroh EndpointID, then reduce
|
|
resource auth ops; an EndpointID alone is not accepted as a resource principal.
|
|
Inbound protected control, pipe-wire, SSH-proxy, and overlay-wire handlers use a
|
|
shared authenticated caller context that validates the peer card, checks the
|
|
observed endpoint binding, records the peer card as untrusted candidate
|
|
metadata, and then passes the verified caller identity plus store handle to the
|
|
resource-specific authorization step.
|
|
Remote geth JSONL messages over Iroh are bounded before decoding:
|
|
peer-control and module wire requests/responses are limited to 16 MiB,
|
|
streaming handshakes for SSH proxy and TCP/Unix pipe forwarding are limited to
|
|
64 KiB, and each remote line read has a 30 second timeout. Oversized remote
|
|
lines are rejected before the request is decoded or dispatched to a resource
|
|
handler.
|
|
|
|
The daemon starts this endpoint during `geth daemon run` and keeps it alive for
|
|
the daemon lifetime. When endpoint startup succeeds, the Iroh EndpointID is
|
|
recorded as a transport binding for the stable geth node identity. If local UDP
|
|
binding is unavailable, the daemon keeps local control running and reports the
|
|
Iroh startup error through status output.
|
|
|
|
SSH keys are not transport keys. They are admin trust anchors and signing
|
|
identities for keychain and authorization operations. The bootstrap `geth ssh
|
|
proxy <node-id>` command performs an authorized Iroh control-plane handshake:
|
|
the remote daemon validates the caller's signed peer card against the observed
|
|
Iroh EndpointID and requires `ssh_proxy.connect` on
|
|
`resource:ssh-proxy:local`. It returns connection metadata only. Carrying SSH
|
|
bytes over an Iroh stream is implemented on the dedicated `/geth/ssh-proxy/1`
|
|
ALPN and only connects to remote `127.0.0.1:22` after authorization.
|
|
`geth ssh admin-shell <node-id> <help|status|node-id>` is a separate restricted
|
|
geth admin workflow over the protected Iroh control path. It requires
|
|
`ssh_proxy.admin_shell` and executes only built-in geth commands, never host
|
|
shell commands. Neither path makes SSH a geth transport backend.
|
|
|
|
SSH certificate flows use the same split. Nodes can request new OpenSSH
|
|
certificates or renewals through geth metadata. A machine with the CA key or
|
|
YubiKey can approve the request and run an explicit `ssh-keygen -s ...` command,
|
|
or pass `--sign` to execute that command immediately and import the resulting
|
|
certificate into local metadata for distribution. This relies on the local
|
|
OpenSSH ecosystem, so hardware-backed keys remain mediated by `ssh-keygen` and
|
|
the host's agent/security-key flow. Certificate and key revocations are stored
|
|
as signed-list-ready records. Sync materializes those records as ordered SSH
|
|
distribution log entries: certificate requests, certificate imports, and
|
|
revocations each have stable log entry IDs and timestamps, and the receiver
|
|
reduces the entries into local state after provenance and conflict checks. The
|
|
bootstrap can pull certificate-flow log entries over Iroh with
|
|
`geth ssh cert sync <node-id>` when the peer grants `ssh_cert.sync` on
|
|
`resource:ssh:certs`, and revocation log entries with
|
|
`geth ssh revocation sync <node-id>` when the peer grants `ssh_revocation.sync`
|
|
on `resource:ssh:revocations`. The daemon also runs a configurable background
|
|
live-sync tick for known peers and records per-peer high-water cursors in
|
|
`module_state`, so repeated ticks request only entries at or beyond the last
|
|
remote cursor. The default interval is 30 seconds and can be changed under
|
|
`[sync]` in `config.toml`. Boundary duplicates are harmless because records are
|
|
keyed by stable IDs.
|
|
Before issuing per-module pulls, the daemon can request an authorized sync
|
|
status summary over the same protected Iroh control ALPN. The serving peer
|
|
validates endpoint/card binding and returns only watermarks for streams where
|
|
the caller already has the required resource capability, which reduces blind
|
|
polling without letting discovery reveal private resource names.
|
|
Keychain and auth operation logs are also advertised through this watermark
|
|
path. Pulls are delta-style by per-peer cursor, but every received operation is
|
|
still verified against trusted-admin OpenSSH signatures before import.
|
|
Operators can run `geth sync now [node]` to trigger the same best-effort pass
|
|
immediately and `geth sync status` to inspect locally recorded last-attempt,
|
|
last-success, cursor, import/rejection counts, and errors for each peer stream.
|
|
JSON status also includes `state`, `stale`, `stale_after_ms`,
|
|
`consecutive_failures`, `retry_after_ms`, `retry_in_ms`, and `next_action`
|
|
fields so scripts can fail on unhealthy streams. Background live-sync backs off
|
|
failed streams without blocking explicit `geth sync now [node]` retries. Common
|
|
daemon errors include a `next:` recovery line for missing peer cards, missing
|
|
grants, missing resources, unavailable endpoints, and missing DB/KV/document
|
|
registrations.
|
|
|
|
Daemon logs use structured `tracing` fields for local control requests:
|
|
`command`, `peer_node`, `resource`, `capability`, `stream`, and stable
|
|
`error_code` where applicable. The request tracer classifies requests instead of
|
|
formatting full payloads, so bearer tokens, private key paths, packet/message
|
|
payloads, and document JSON are not logged by that layer.
|
|
|
|
Host-opening paths are intentionally narrow. TCP pipe forwarding accepts only
|
|
explicit loopback socket addresses on both the local listener and remote target;
|
|
Unix pipe forwarding requires absolute paths without parent-directory
|
|
components; SSH proxying always connects the authorized remote stream to
|
|
`127.0.0.1:22`; and overlay interface creation happens only after the operator
|
|
runs `geth overlay up`. Host setup failures should be handled as local operator
|
|
or entitlement problems. For overlay platform recovery, see
|
|
`docs/overlay-platforms.md`; for missing peer cards, endpoint bindings, or
|
|
resource grants, use `geth peer import`, `geth node endpoint-add`, and
|
|
`geth auth grant`/`geth node grant` as indicated by `next:` error output.
|
|
|
|
## Resource Model
|
|
|
|
Everything meaningful is modeled as a resource. Resources have a kind, name,
|
|
authority reference, local role, replication policy, retention policy, and
|
|
status. Authorization is resource-scoped and capability-based.
|
|
|
|
Resource kinds:
|
|
|
|
- `db`: cr-sqlite-backed SQLite synchronization
|
|
- `kv`: Iroh Documents backed key-value data
|
|
- `pipe`: authorized byte streams and forwarding
|
|
- `document`: Automerge CRDT documents
|
|
- `pubsub`: lossy notifications and presence
|
|
- `cas`: content-addressed blobs
|
|
- `ssh-proxy`: SSH/admin proxying over Iroh
|
|
- `overlay`: optional Iroh-carried packet overlay planning
|
|
|
|
## Module Overview
|
|
|
|
The current per-resource sync and conflict behavior is a documented
|
|
automation-facing contract in [`conflict-semantics.md`](conflict-semantics.md).
|
|
|
|
`geth-cas` is implemented locally first using BLAKE3 hashes and filesystem blob
|
|
storage. Local pin/unpin metadata is tracked in SQLite and surfaced in
|
|
`cas list`. `cas cleanup` removes unpinned local blobs while retaining pinned
|
|
blobs. The CAS crate can build deterministic tree objects that describe
|
|
directories, files, executable bits, and file blob hashes; those tree objects
|
|
are stored as CAS blobs. The daemon can register local file roots and scan them
|
|
into CAS tree objects while reporting create/update/delete/rename changes. These
|
|
scans are local metadata only and never overwrite the working tree. A peer can
|
|
pull authorized remote file-root tree metadata with `geth cas root sync <node>
|
|
<name>` when it has `cas.fetch` on `resource:cas-tree:<name>`; sync imports the
|
|
remote CAS tree bytes and records a peer-qualified remote root whose path is
|
|
`remote:<node>:<name>` without applying files. File roots also participate in
|
|
the daemon background live-sync loop through authorized `cas-tree:<name>`
|
|
watermarks. Repeated remote file-root syncs retain the previous imported remote
|
|
tree as the base and compare base/local/remote tree state. When both local and
|
|
remote roots changed, geth records durable concurrent edit, delete/edit, or
|
|
divergent rename conflicts instead of applying remote content. `geth cas root
|
|
apply <root> --to <path>` is conservative when the target has no registered
|
|
base: it creates missing files and directories from the CAS tree, does not
|
|
delete extra local files, does not overwrite differing local files, and records
|
|
conflicts for manual resolution. When the target path matches a registered
|
|
local root with a previous scan, apply uses that scan as the base for a
|
|
three-way base/local/remote check. It can safely accept remote creates, updates,
|
|
deletes, and renames only when the current local filesystem still matches the
|
|
base; ambiguous paths remain durable conflicts. The daemon also has durable
|
|
local file-conflict records with explicit resolution choices.
|
|
|
|
As a bootstrap network path, `geth cas fetch <node-id> <hash>` dials an
|
|
imported signed peer card over the daemon-owned Iroh control ALPN. The serving
|
|
daemon validates the caller's peer-card signature and observed Iroh EndpointID,
|
|
then reduces local auth ops and requires `cas.fetch` on `resource:cas:local`
|
|
before returning blob bytes. The requester verifies that the returned bytes hash
|
|
to the requested BLAKE3 CAS hash before storing them. Successful fetches update
|
|
durable local provider metadata keyed by CAS hash and peer node, which can be
|
|
inspected through `geth cas providers <hash>`. Iroh-blobs provider/fetch
|
|
integration is future work.
|
|
|
|
`geth-db` currently registers local SQLite paths as DB resources and reports
|
|
local-only sync status plus a read-only SQLite schema summary/hash. It also
|
|
inspects `crsql_changes` metadata when that table or view exists, reporting
|
|
change count, columns, and max `db_version`. The crate and local daemon can
|
|
extract read-only typed change batches from `crsql_changes` with schema metadata
|
|
through `db changes`. As a staged network path, `geth db sync <node-id> <name>`
|
|
uses the protected Iroh control ALPN to request remote typed change batches when
|
|
the caller has `db.sync` on the remote `resource:db:<name>`. The requester
|
|
checks remote schema metadata against its local DB before applying changes and
|
|
advancing its per-peer/per-DB cursor. Compatible remote batches are inserted
|
|
into the local `crsql_changes` table or view before the cursor advances.
|
|
Loading/configuring the cr-sqlite extension for real application databases
|
|
remains the database owner's responsibility; the bootstrap tests use
|
|
deterministic fixture tables because this dev environment has no `sqlite3` CLI
|
|
or cr-sqlite extension artifact. DB sync intentionally does not use CAS-backed
|
|
snapshots or batch blobs in the prototype. Those become useful when initial
|
|
catch-up or large batches outgrow the protected control path.
|
|
|
|
`geth-kv` keeps SQLite as the durable local index for named KV stores through
|
|
`kv create/set/get`. `kv set --subject <principal>` evaluates local auth ops for
|
|
`kv.write_key:<key>` so prefix grants can be tested. The daemon mirrors local KV
|
|
entries and metadata into an Iroh Documents namespace per named store. `geth kv
|
|
sync <node-id> <name>` still starts with a protected geth control request that
|
|
requires `kv.read` on the remote `resource:kv:<name>`; if authorized, the remote
|
|
daemon returns a read-only docs ticket and the requester imports entries through
|
|
Iroh Documents. The control response still carries bootstrap entries for
|
|
compatibility. The daemon background live-sync loop runs the same KV sync for
|
|
local KV stores and known peers. Private value encryption should use resource
|
|
secret epochs before payloads are exposed to remote peers.
|
|
|
|
`geth-document` registers local document resources and stores durable Automerge
|
|
save bytes in the local SQLite metadata store. The CLI still accepts and returns
|
|
validated JSON views for `document create/status/set/get`, but the persisted
|
|
state is an Automerge envelope containing binary save data plus the current JSON
|
|
view for operator output. `geth document sync <node-id> <name>` pulls remote
|
|
Automerge state over the protected Iroh control ALPN when the peer grants
|
|
`document.read` on `resource:document:<name>`. The daemon background live-sync
|
|
loop runs the same sync for local documents and known peers using
|
|
per-peer/per-document cursors. Received Automerge documents are merged before
|
|
being stored.
|
|
|
|
`geth-pubsub` supports local publish/subscribe snapshots through the daemon
|
|
control protocol. Messages live in a bounded in-memory ring buffer and are lost
|
|
when the daemon stops. This is deliberate: pubsub is a lossy wakeup and presence
|
|
channel, not authoritative storage. Durable facts must be written to CAS, KV,
|
|
document, or DB resources before pubsub is used as a wakeup. `geth pubsub pub
|
|
<topic> <message> --node <node-id>` first uses the protected Iroh control ALPN
|
|
for authorization. The remote daemon validates endpoint/card binding and
|
|
requires `pubsub.publish` on `resource:pubsub:<topic>` before recording the
|
|
message and broadcasting it through a deterministic native `iroh-gossip` topic.
|
|
`geth pubsub sub <topic> --node <node-id>` uses the same protected path, joins
|
|
the gossip topic when the caller has `pubsub.subscribe`, and returns the peer's
|
|
current daemon-lifetime snapshot. Private topics remain future work.
|
|
|
|
`geth-overlay` defines an optional packet-overlay plan inspired by `iroh-lan`.
|
|
The target runtime is a private L3-style overlay where packets from an explicit
|
|
TUN/Wintun interface are carried over the daemon-owned Iroh endpoint on
|
|
`/geth/overlay/1`. The overlay is a geth resource (`resource:overlay:<name>`)
|
|
with `overlay.join`, `overlay.route`, and `overlay.admin` capabilities. The
|
|
prototype exposes `geth overlay status`, `geth overlay plan <name>`,
|
|
`geth overlay join <name> --secret <resource-secret>`, and `geth overlay leave
|
|
<name>`. Join creates or reuses `resource:overlay:<name>`, persists local
|
|
membership in `module_state`, assigns a deterministic virtual IP from the CIDR,
|
|
and stores only a BLAKE3 fingerprint of the supplied secret. If bearer access
|
|
already exists for that overlay resource, join requires the supplied secret to
|
|
be a bearer token with `overlay.join`; this lets operators create explicit
|
|
resource-scoped overlay invites through `geth secret bearer create`. The
|
|
`geth overlay interface-plan` generates Linux, macOS, or Windows host-interface
|
|
plans for review. `geth overlay up <name>` is the explicit opt-in that creates a
|
|
real L3 TUN/Wintun-style interface through `tun-rs`, assigns the local
|
|
deterministic overlay IP, reads IPv4 packets from the interface, maps
|
|
destination overlay IPs to imported peer cards, and carries those packets over
|
|
the dedicated `/geth/overlay/1` Iroh ALPN. `geth overlay down <name>` stops the
|
|
runtime and drops the device handle. The serving daemon validates the signed
|
|
peer card against the observed EndpointID and requires `overlay.route` on
|
|
`resource:overlay:<name>` before queuing a received packet into the active
|
|
interface; if no runtime is active, the packet is persisted for `geth overlay
|
|
recv`. Interface setup may require `CAP_NET_ADMIN`, sudo, a preconfigured
|
|
`/dev/net/tun`, Wintun availability, or platform-specific network entitlements.
|
|
Release artifacts include `docs/overlay-platforms.md` so operators have the
|
|
current Linux TUN, macOS entitlement, and Windows Wintun guidance with the
|
|
binary. Overlay discovery can use mDNS, peer exchange, and resource metadata, but
|
|
discovery remains untrusted and cannot grant overlay access.
|
|
|
|
`geth-pipe` currently supports `pipe listen/connect/send/recv` against a
|
|
daemon-lifetime runtime. `geth pipe connect <name> --node <node-id>` sends an
|
|
authorized remote connect request over the protected Iroh control ALPN. The
|
|
remote daemon validates endpoint/card binding and requires `pipe.connect` on
|
|
`resource:pipe:<name>` before recording the connection attempt and reporting
|
|
whether a listener exists. `geth pipe send <name> [message|--in <path>|--in -] --node <node-id>`
|
|
uses the dedicated `/geth/pipe/1` ALPN to write a byte message to a peer
|
|
listener after the same endpoint/card and capability checks. `geth pipe recv
|
|
<name>` drains local daemon-lifetime messages. `geth pipe listen <name> --node
|
|
<node-id>` can also ask a peer to register a daemon-lifetime listener after
|
|
checking `pipe.listen` on the same resource. `geth pipe forward-tcp --listen
|
|
127.0.0.1:<local-port> --node <node-id> --target 127.0.0.1:<remote-port>` runs a
|
|
local loopback listener and opens one authorized `/geth/pipe/1` byte stream per
|
|
accepted connection. The remote daemon validates endpoint/card binding and
|
|
requires `pipe.forward` on `resource:pipe-tcp:<target>` before connecting to the
|
|
remote loopback TCP target. TCP forwarding is loopback-only in the prototype;
|
|
`geth pipe forward-unix --listen <local-socket> --node <node-id> --target
|
|
<remote-socket>` uses the same authorized Iroh pipe stream and requires
|
|
`pipe.forward` on `resource:pipe-unix:<target>` before connecting to an absolute
|
|
remote Unix socket path.
|
|
|
|
`geth-ssh-proxy` defines proxy target and connection metadata. `geth ssh proxy
|
|
<node>` is a streaming command intended for OpenSSH `ProxyCommand`: the CLI
|
|
streams stdin/stdout through the local daemon, the local daemon dials the remote
|
|
daemon with `/geth/ssh-proxy/1`, the remote daemon validates the signed peer card
|
|
against the observed Iroh EndpointID, reduces `ssh_proxy.connect` on
|
|
`resource:ssh-proxy:local`, and only then connects the Iroh stream to
|
|
`127.0.0.1:22`. OpenSSH still performs its normal login authentication over the
|
|
resulting byte stream. SSH is not used as a geth transport backend.
|
|
|
|
`geth-ssh-identity` defines SSH trust namespaces plus certificate request,
|
|
approval, certificate import, and revocation-list data models. The bootstrap
|
|
persists these flows locally and exports revocations as JSONL or OpenSSH KRL
|
|
specification text. Certificate approval normally emits the exact
|
|
`ssh-keygen -s ...` command, and `approve --sign` can run that command, import
|
|
the resulting OpenSSH certificate, and mark the request signed. It can also
|
|
invoke `ssh-keygen -k` to produce a binary OpenSSH KRL; serial and key-ID KRL
|
|
entries require a CA public key via `--ca-public`, matching OpenSSH behavior.
|
|
It can import geth JSONL revocation exports and OpenSSH KRL specification source
|
|
files. Binary OpenSSH KRL files are not enumerable through OpenSSH tooling, so
|
|
geth treats binary import as unsupported and asks for JSONL or the spec source.
|
|
Revocation lists are not yet full CRDT-replicated resources, but the daemon now
|
|
syncs certificate-flow and revocation state as a small ordered resource log over
|
|
the protected Iroh control ALPN. Manual sync commands and the background
|
|
live-sync loop share the same capability checks and cursor state. Sync import
|
|
rejects conflicting records with ids that already exist locally instead of
|
|
replacing local metadata. Local SSH certificate and revocation metadata commands
|
|
also accept an optional subject principal for
|
|
authorization testing: non-owner subjects must hold `ssh_cert.*` capabilities on
|
|
`resource:ssh:certs` or `ssh_revocation.*` capabilities on
|
|
`resource:ssh:revocations` before requests, approval/import/read operations, or
|
|
revocation publish/read/import operations
|
|
are accepted. The live-sync loop first asks for authorized stream watermarks and
|
|
skips module pulls whose remote high-water value has not advanced. File-root
|
|
live-sync uses only sync-status-advertised `cas-tree:<name>` streams, because
|
|
the local node otherwise does not know which roots the peer is willing to
|
|
expose.
|
|
|
|
## Keychain, Auth, And Secrets
|
|
|
|
The identity plane is `geth-keychain`: admin keys, users, devices, nodes, agents,
|
|
and endpoint bindings. Endpoint rotation must not destroy higher-level node
|
|
identity. Keychain operations reduce into an active view containing current
|
|
admin keys, users, devices, node records, agent bindings, and endpoint-to-node
|
|
bindings. Revoked identity subtrees are excluded from that active view. `geth
|
|
init --admin-key <pub> --signing-key <key> --node-name <name>` records an
|
|
owner/admin key, user, device, node, and agent binding as keychain operations
|
|
and signs them with OpenSSH under `geth.keychain.v1@geth.local`. Both keys are
|
|
required when owner setup options are used, so the node does not create unsigned
|
|
owner statements by accident. `geth node list` shows the active reduced node
|
|
view. `geth node rename` and `geth node revoke` record signed keychain
|
|
operations and require `--signing-key`. Endpoint rotation is explicit:
|
|
`geth node endpoint-add` and `geth node endpoint-revoke` record signed
|
|
`NodeEndpointAdd` and `NodeEndpointRevoke` keychain operations. Admin SSH keys
|
|
are updated with signed `geth keychain admin-add` and `geth keychain
|
|
admin-revoke` operations; `AdminKeyAdd` carries the public key material needed
|
|
to reconstruct an OpenSSH `allowed_signers` view. `geth keychain verify` replays
|
|
the log against the previously accepted admin-key view, mirroring the `git-skm`
|
|
pattern of verifying key-registry changes from a prior trusted state. The
|
|
transport-neutral replay rules, application-specific signature namespaces,
|
|
allowed-signers projection, and JSONL sigchain helpers live in `geth-keychain`
|
|
so other applications can reuse the same identity-log model without depending
|
|
on the daemon, SQLite, Iroh, or local control. The CLI can export the same
|
|
reduced key registry as OpenSSH `allowed_signers` or as appendable JSONL
|
|
sigchain data for website publication. It can also sign and verify arbitrary
|
|
snapshots, such as externally managed `authorized_keys`, with an active
|
|
keychain signer under an explicit OpenSSH namespace. `geth keychain
|
|
publish-bundle` writes a website-ready bundle for
|
|
`https://example.com/.well-known/sshsigchain/`, including `allowed_signers`,
|
|
`geth.sigchain.jsonl`, a signed checkpoint, and optional signed snapshots.
|
|
`geth keychain fetch --import` verifies the checkpoint and records the last
|
|
accepted checkpoint per retrieval source URL to reject older bundles. The
|
|
retrieval source may be a local mirror; the checkpoint still carries the signed
|
|
advertised publication base URL, and explicit checkpoint verification can pin it. `geth keychain
|
|
explain` and `explain-signer` provide basic auditability for why a keychain
|
|
operation or signer is trusted. `geth keychain sync <node>` pulls keychain
|
|
operations and signatures from an imported peer over Iroh and imports only
|
|
operations with a valid OpenSSH signature from a currently trusted admin key
|
|
over the canonical payload. See `docs/sigchain-keychain.md` for the detailed
|
|
sigchain design. This is currently a pull-based signed operation log, not a
|
|
CRDT or Keyhive-style convergent authority.
|
|
|
|
New devices can use the node enrollment flow instead of hand-editing keychain
|
|
state. `geth node enroll request` creates a canonical, agent-key-signed request
|
|
containing the requesting node ID, agent ID, requested node name, optional Iroh
|
|
endpoint, and requested resource capabilities. The request can be submitted over
|
|
Iroh to an imported owner peer or moved as a JSON file to the owner machine.
|
|
`geth node enroll approve` runs on the owner/YubiKey machine and records signed
|
|
keychain operations for the device/node/agent/endpoint binding plus signed auth
|
|
operations for approved capabilities. `geth node enroll sync <owner-node>` pulls
|
|
both signed logs so the new node can see its approved identity and permissions.
|
|
|
|
The authorization plane is `geth-auth`: resource-local signed operation logs,
|
|
grants, revocations, groups, and `auth explain`. Auth operations reduce into a
|
|
current permission view for resources, grants, groups, and bearer access. The
|
|
library can explain direct grants, group grants, missing grants, revoked grants,
|
|
and bearer-secret access. The daemon persists local auth grant/revoke operations
|
|
and `geth auth explain` enriches the reducer result with keychain and discovery
|
|
diagnostics, including discovered-only peers and endpoint-binding state. Human
|
|
output prints those diagnostics and JSON output exposes them as structured
|
|
strings for scripts. `geth node grant`, `geth node revoke-grant`, `geth auth
|
|
grant`, and `geth auth revoke` require `--signing-key` in the CLI and store
|
|
OpenSSH-signed auth operations. Enrollment approval uses the same signed auth
|
|
operation path. Auth sync imports only auth operations signed by currently
|
|
trusted admin keys. Broader delegated authority is still future work.
|
|
|
|
Capability evaluation supports exact matches plus explicit scoped forms. For KV,
|
|
`kv.write_prefix:<prefix>` grants writes requested as `kv.write_key:<key>` only
|
|
when the key is under that prefix; `kv.write` remains the broad write
|
|
capability. Command-level KV enforcement is still future work.
|
|
|
|
Both keychain and auth operations use `geth-codec` canonical envelopes for
|
|
signature payloads. The envelope includes a version, an explicit signature
|
|
namespace, and the operation payload encoded with postcard. JSON remains useful
|
|
for CLI/control output, but it is not the signed representation.
|
|
|
|
The payload access plane is `geth-secrets`: resource master secrets, epochs,
|
|
key envelopes, bearer secrets, and rotation. Revocation for private data is
|
|
modeled initially as secret epoch rotation. The daemon persists resource secret
|
|
epoch metadata through `secret create/rotate/status`. Bearer access is recorded
|
|
as resource-scoped auth operations and rejects trust-mutation capabilities such
|
|
as `auth.delegate`, `auth.revoke`, and `node.enroll`. Bearer creation returns a
|
|
private bearer token once and stores a separate public bearer id plus token
|
|
verifier in the auth log. Bearer challenge/proof commands derive deterministic
|
|
BLAKE3 keyed responses from the private token, resource, nonce, and requested
|
|
capabilities, then verify them against active resource-scoped bearer grants.
|
|
Remote resource operations can carry optional bearer proofs over the protected
|
|
Iroh control path; a valid proof authorizes only the requested resource
|
|
capability and does not create node trust. The daemon does not yet store payload
|
|
key material, encrypt resource data, or distribute key envelopes.
|
|
|
|
## Multi-User Direction
|
|
|
|
The project is structured for future multi-user local-first authorization:
|
|
|
|
- authorization is replicated data, not one mutable ACL blob
|
|
- resources can carry or delegate to their own auth state
|
|
- users, devices, nodes, agents, and endpoints are separate principals
|
|
- capabilities are the underlying permission unit
|
|
- bearer access is resource-scoped and does not mutate the trust graph
|
|
- offline revocation is eventual
|
|
- encryption key distribution is part of authorization
|
|
|
|
## Keyhive/BeeKEM Roadmap
|
|
|
|
Resource secret epochs are the v0/v1 approximation for private payload access.
|
|
Private CAS writes use an AES-256-GCM envelope keyed from the resource, local
|
|
secret id, and epoch with resource-bound associated data and random nonces.
|
|
Prototype BLAKE3-XOR private blob envelopes from earlier pre-deployment builds
|
|
are rejected and should be recreated from plaintext.
|
|
Later designs can add Keyhive-like convergent capabilities and BeeKEM/CGKA-style
|
|
group key evolution. The bootstrap does not implement BeeKEM and does not claim
|
|
strong forward secrecy or post-compromise security.
|
|
|
|
## Security Invariants
|
|
|
|
- All remote node-to-node communication is over Iroh.
|
|
- SSH is not a geth transport.
|
|
- SSH keys are admin trust anchors and signing identities.
|
|
- Agent/node keys handle routine local identity.
|
|
- Discovery is untrusted.
|
|
- Knowing an EndpointID does not grant access.
|
|
- Bearer secrets are resource-scoped capabilities.
|
|
- Bearer access does not imply trust graph mutation rights.
|
|
- Authorization is capability-based and resource-scoped.
|
|
- SSH certificate issuance must be explicitly approved by an authorized
|
|
principal before signing.
|
|
- SSH certificate and key revocations are durable metadata that should be
|
|
distributed over Iroh, not fetched through unauthenticated discovery.
|
|
- Network and control decoders treat input as untrusted.
|
|
- Service installation targets user service managers, not system service
|
|
managers.
|