37 KiB
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
endpoint. Control commands connect to a per-home Unix socket on Linux/macOS or
a per-home named pipe on Windows and send typed JSONL requests.
Within geth-node, daemon lifecycle code is separated from feature handlers:
daemon.rs owns geth daemon run startup, local endpoint 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. peer_control.rs
centralizes inbound Iroh ALPN classification, peer endpoint-address parsing,
signed peer-card endpoint binding validation, and authenticated caller context
creation for protected peer-control, pipe, SSH-proxy, and overlay paths.
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.
Daemon shutdown is an owned lifecycle transition, not process-exit cleanup.
Before starting Iroh or background modules, the daemon binds the selected
home's local control endpoint. A live endpoint makes a second daemon fail with
daemon_already_running; only an endpoint that cannot be connected is treated
as stale and replaced. This keeps one daemon authoritative for each geth home
and prevents a second process from unlinking the first daemon's control path.
After local control stops accepting requests, daemon.rs cancels the Iroh
accept and live-sync tasks, releases native docs/gossip/blob handles, calls
Endpoint::close().await, and releases the local control endpoint. On Unix this
also removes the socket path; Windows named pipes disappear with their server
handles. The same cleanup path runs when the serving loop returns an error,
preventing stale endpoints and unclosed endpoint clones from becoming restart
behavior.
Foreground daemons handle Ctrl-C, and Unix daemons also handle the SIGTERM used
by user service managers, through this same graceful shutdown path.
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.
The common service lifecycle is available directly as geth daemon install|start|stop|status|logs|uninstall; the nested service commands remain
the advanced and compatibility surface. Direct installation waits for local
control readiness. A service executable resolved under a Cargo target or OS
temporary directory is copied atomically into the selected geth home's bin/
directory before the definition is written, unless the operator explicitly
allows the transient reference. Service log inspection reads the systemd user
journal on Linux and user-owned daemon log files on macOS and Windows.
geth daemon run --ephemeral creates a
temporary home, reports how another CLI process can select it with --home, and
removes it after a normal foreground shutdown. Ephemeral mode still starts the
same daemon-owned Iroh endpoint and local control stack; it is not a second
runtime or transport path.
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.
The same degraded-local behavior applies when the endpoint starts but a native
blob, document, gossip, or initial mirroring step fails. Partially initialized
native handles are released, the endpoint is closed, and geth status reports
the runtime backends as unavailable with the concrete startup blocker. Local
metadata, diagnosis, backup, and other non-network control remain available.
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 synchronizationkv: Iroh Documents backed key-value datapipe: authorized byte streams and forwardingdocument: Automerge CRDT documentspubsub: lossy notifications and presencecas: content-addressed blobsssh-proxy: SSH/admin proxying over Irohoverlay: 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.
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 join explicitly imports an owner admin public key as
the new node's trust anchor, imports the signed peer card only as untrusted
candidate routing metadata, creates the request, and submits it over Iroh. It
does not grant trust to the peer card or approve the requesting node. The lower
level 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.