464 lines
24 KiB
Markdown
464 lines
24 KiB
Markdown
# geth
|
|
|
|
`geth` is a personal, local-first mesh runtime for scripts, devices, databases,
|
|
documents, blobs, pipes, and future multi-user collaboration.
|
|
|
|
This project is not the Ethereum `geth` client. The project and executable are
|
|
still named `geth`.
|
|
|
|
## One Binary
|
|
|
|
There is one executable: `geth`.
|
|
|
|
It has daemon mode and control mode:
|
|
|
|
```sh
|
|
geth init
|
|
geth daemon run
|
|
geth daemon service install
|
|
geth status
|
|
geth node id
|
|
geth resource list
|
|
geth cas add ./file
|
|
```
|
|
|
|
The daemon owns local identity, the Iroh endpoint, trust state, resource
|
|
registry, module router, local metadata store, and synchronized data structures.
|
|
Most non-daemon commands talk to the daemon through a local Unix socket at
|
|
`$GETH_HOME/run/geth.sock`.
|
|
|
|
`geth init --admin-key <public-key> --signing-key <private-key> --node-name
|
|
<name>` records an owner/admin keychain, the local user/device/node binding, and
|
|
signs canonical keychain payloads through `ssh-keygen -Y sign` using the
|
|
`geth.keychain.v1@geth.local` namespace. This is the bootstrap path for
|
|
admin/YubiKey-rooted trust. `geth keychain sync <node>` pulls the signed
|
|
keychain operation log from an imported peer and imports only operations with
|
|
valid OpenSSH signatures from currently trusted admin keys. `geth node list`
|
|
shows the active reduced node view, and `geth node rename/revoke` require
|
|
`--signing-key` so device-management changes can replicate as verified admin
|
|
statements. `geth node grant/revoke-grant` and `geth auth grant/revoke` also
|
|
require `--signing-key` in the CLI and store signed auth operations for
|
|
replication.
|
|
SSH certificate-flow and revocation records carry agent-key signed provenance
|
|
over canonical payloads, and sync import rejects new unsigned or invalidly
|
|
signed records.
|
|
|
|
The daemon can also install itself as a user service:
|
|
|
|
```sh
|
|
geth daemon service install
|
|
geth daemon service status
|
|
geth daemon service uninstall
|
|
```
|
|
|
|
The bootstrap service managers are systemd user units on Linux, launchd user
|
|
agents on macOS, and per-user scheduled tasks on Windows. These are user-level
|
|
services, not system services.
|
|
|
|
## Transport And SSH
|
|
|
|
All remote node-to-node geth communication is designed to happen over Iroh only.
|
|
SSH is not a geth transport backend, and there is no SSH fallback transport.
|
|
The default node config uses Iroh's default relay policy for practical
|
|
connectivity; set `[iroh].relay_mode = "disabled"` for local-only/offline
|
|
development. Named custom relay maps can be selected with
|
|
`relay_mode = "custom"` and `relay_map = "<name>"`. Iroh local-network
|
|
discovery is enabled by default with `[iroh].local_discovery = true`.
|
|
|
|
SSH keys are used as admin trust anchors and ecosystem integration points.
|
|
OpenSSH, FIDO, and YubiKey-backed keys can sign geth trust objects through
|
|
canonical geth envelopes with explicit namespaces such as
|
|
`geth.keychain.v1@geth.local`. SSH proxying carries SSH protocol bytes over an
|
|
authorized Iroh stream, but SSH is still not a geth transport backend.
|
|
|
|
SSH certificate request and renewal flows are managed as geth metadata. A node
|
|
can create a certificate request, another machine can approve it and receive an
|
|
explicit `ssh-keygen -s ...` command suitable for a CA key or YubiKey-backed CA,
|
|
or pass `--sign` to run `ssh-keygen` immediately and import the resulting
|
|
`-cert.pub` for distribution. Certificate and key revocation entries are tracked
|
|
locally and can be exported as JSONL or as an OpenSSH KRL specification file or
|
|
a binary OpenSSH KRL generated through `ssh-keygen -k`. `geth ssh cert sync
|
|
<node-id>` and
|
|
`geth ssh revocation sync <node-id>` pull certificate-flow and revocation
|
|
metadata from an authorized peer over Iroh.
|
|
|
|
## MVP Features
|
|
|
|
The bootstrap implementation provides:
|
|
|
|
- `geth init`
|
|
- `geth init --admin-key <public-key> --signing-key <private-key> --node-name <name>`
|
|
- `geth daemon run`
|
|
- `geth daemon service install|uninstall|start|stop|status|print`
|
|
- `geth status`
|
|
- `geth node id`
|
|
- `geth node list`
|
|
- `geth node enroll request --node-name <name> --capability <resource=capability> [--out <path>]`
|
|
- `geth node enroll submit <owner-node> [--request-id <id>|--path <path>]`
|
|
- `geth node enroll import <path>`
|
|
- `geth node enroll list [--status pending|approved|rejected]`
|
|
- `geth node enroll approve <request-id> --signing-key <private-key>`
|
|
- `geth node enroll sync <owner-node>`
|
|
- `geth node rename <node-or-name> <name> --signing-key <private-key>`
|
|
- `geth node revoke <node-or-name> --signing-key <private-key>`
|
|
- `geth node endpoint-add <node-or-name> <endpoint-id> --signing-key <private-key>`
|
|
- `geth node endpoint-revoke <node-or-name> <endpoint-id> --signing-key <private-key>`
|
|
- `geth node grant <node-or-name> <resource> <capability> --signing-key <private-key> [--grant-id <id>]`
|
|
- `geth node revoke-grant <resource> <grant-id> --signing-key <private-key>`
|
|
- `geth peer export [--out <path>]`
|
|
- `geth peer import <path>`
|
|
- `geth peer list`
|
|
- `geth peer ping <node-id>`
|
|
- `geth peer auth-check <node-id> <resource> <capability>`
|
|
- `geth resource list`
|
|
- `geth resource create <kind> <name>`
|
|
- `geth keychain init [--admin-key <path>] [--signing-key <path>]`
|
|
- `geth keychain status`
|
|
- `geth keychain sync <node-id-or-name>`
|
|
- `geth auth sync <node-id-or-name>`
|
|
- `geth sync status`
|
|
- `geth sync now [node-id-or-name]`
|
|
- `geth secret status`
|
|
- `geth secret create <resource>`
|
|
- `geth secret rotate <resource>`
|
|
- `geth secret bearer create <resource> --capability <capability>`
|
|
- `geth secret bearer list`
|
|
- `geth secret bearer challenge <resource> --capability <capability>`
|
|
- `geth secret bearer prove <token> <resource> --nonce <nonce> --capability <capability>`
|
|
- `geth secret bearer verify <token> <resource> --nonce <nonce> --response <response> --capability <capability>`
|
|
- `geth secret bearer revoke <resource> <bearer-id>`
|
|
- `geth auth explain <subject> <resource> <capability>`
|
|
- `geth auth grant <subject> <resource> <capability> --signing-key <private-key> [--grant-id <id>]`
|
|
- `geth auth revoke <resource> <grant-id> --signing-key <private-key>`
|
|
- local filesystem CAS commands: `add`, `get`, `fetch`, `hash`, `has`, `pin`,
|
|
`unpin`, `cleanup`, `providers`, `list`; remote fetch accepts
|
|
`--bearer-secret <secret>`
|
|
- private CAS envelope commands:
|
|
`geth cas add-private <resource> <path>` and
|
|
`geth cas get-private <resource> <hash> --out <path>`. These use local
|
|
resource secret epochs and are a prototype envelope, not audited AEAD.
|
|
- local CAS tree objects describe file trees and are stored as CAS blobs
|
|
- local file-root commands: `geth cas root add/list/scan/sync/apply`; root sync
|
|
pulls authorized remote tree metadata and CAS tree bytes into a peer-qualified
|
|
remote root, and apply materializes a tree without deleting files or
|
|
overwriting local edits. Repeated syncs keep the previous imported remote
|
|
tree as the base and record durable conflicts when local and remote roots
|
|
both changed.
|
|
- local file conflict metadata commands:
|
|
`geth cas conflict record/list/resolve`
|
|
- local DB resource registration: `geth db add <name> <path>` and
|
|
`geth db status <name>` with schema and `crsql_changes` metadata; the DB
|
|
crate and daemon can extract typed local `crsql_changes` batches through
|
|
`geth db changes <name>` and exchange authorized remote batches with
|
|
`geth db sync <node-id> <name>`
|
|
- local SQLite-backed KV commands: `geth kv create/set/get`; `kv set` accepts
|
|
`--subject <principal>` to exercise local capability checks for non-local
|
|
callers; `geth kv sync <node-id> <name> [--bearer-secret <secret>]` pulls
|
|
authorized remote updates
|
|
- local Automerge document commands: `geth document create/status/set/get`;
|
|
CLI input and output are JSON views, while the store keeps durable Automerge
|
|
save bytes. `geth document sync <node-id> <name> [--bearer-secret <secret>]`
|
|
pulls authorized remote Automerge state.
|
|
- local daemon-lifetime pubsub snapshots: `geth pubsub pub/sub`; `geth pubsub
|
|
pub <topic> <message> --node <node-id>` publishes to an authorized peer;
|
|
`geth pubsub sub <topic> --node <node-id>` reads an authorized peer snapshot
|
|
- SSH certificate flow metadata:
|
|
- `geth ssh cert request --public-key <path> --principal <name> [--subject <principal>]`
|
|
- `geth ssh cert requests [--subject <principal>]`
|
|
- `geth ssh cert approve <request-id> --ca-key <path> [--sign] [--subject <principal>]`
|
|
- `geth ssh cert import <request-id> --cert <path> [--subject <principal>]`
|
|
- `geth ssh cert list [--subject <principal>]`
|
|
- `geth ssh cert sync <node-id> [--bearer-secret <secret>]`
|
|
- `geth ssh revocation add <kind> <target> [--subject <principal>]`
|
|
- `geth ssh revocation list [--subject <principal>]`
|
|
- `geth ssh revocation export --out <path> [--format jsonl|openssh-krl-spec|openssh-krl] [--subject <principal>]`
|
|
- `geth ssh revocation import <path> [--format jsonl|openssh-krl-spec] [--subject <principal>]`
|
|
- `geth ssh revocation sync <node-id> [--bearer-secret <secret>]`
|
|
- SSH proxy/admin over Iroh:
|
|
`geth ssh proxy <node-id> [--bearer-secret <secret>]` and
|
|
`geth ssh admin-shell <node-id> <help|status|node-id> [--bearer-secret <secret>]`
|
|
- pipe registry/message commands:
|
|
`geth pipe listen <name> [--node <node-id>] [--bearer-secret <secret>]`,
|
|
`geth pipe connect <name> [--node <node-id>] [--bearer-secret <secret>]`,
|
|
`geth pipe send <name> [message|--in <path>|--in -] [--node <node-id>] [--bearer-secret <secret>]`,
|
|
`geth pipe recv <name> [--peek]`, and
|
|
`geth pipe forward-tcp --listen 127.0.0.1:<port> --node <node-id> --target 127.0.0.1:<port>`
|
|
or `geth pipe forward-unix --listen /tmp/local.sock --node <node-id> --target /tmp/remote.sock`
|
|
|
|
`geth peer export/import/list` is for untrusted peer-card exchange. Peer cards
|
|
include the Iroh EndpointID plus currently known relay/direct addresses.
|
|
`geth peer ping <node-id>` uses the local daemon's Iroh endpoint to dial an
|
|
imported peer card and exchange a signed candidate-only peer-card ping.
|
|
`geth peer auth-check <node-id> <resource> <capability>` sends a protected
|
|
Iroh control request: the remote daemon verifies that the caller's signed peer
|
|
card binds the actual Iroh EndpointID before reducing resource-local auth ops.
|
|
`geth cas fetch <node-id> <hash>` uses the same protected Iroh control path to
|
|
request a blob from a peer. The remote daemon only returns bytes when the caller
|
|
has `cas.fetch` on `resource:cas:local`, and the caller verifies that the bytes
|
|
hash to the requested BLAKE3 CAS hash before storing them locally. Successful
|
|
fetches record the serving peer as a local provider, visible with
|
|
`geth cas providers <hash>`. This is the bootstrap transfer path; future work
|
|
will move provider/fetch behavior to `iroh-blobs`. `geth status` currently
|
|
reports the native backend blocker for CAS, KV, and pubsub: the daemon endpoint
|
|
is pinned to `iroh 0.90.0`, while the Rust-1.85-compatible backend crates
|
|
resolved from crates.io are `iroh-blobs 0.97.0`, `iroh-docs 0.95.0`, and
|
|
`iroh-gossip 0.95.0`, all of which require `iroh 0.95`. These cannot be wired
|
|
to the daemon-owned endpoint until the endpoint wrapper is upgraded in one
|
|
coordinated step.
|
|
Remote resource commands that accept `--bearer-secret` can also authorize with a
|
|
resource-scoped bearer proof generated from the private bearer token returned at
|
|
creation time. The persisted auth log stores a public bearer id and token
|
|
verifier, not the private token. This does not enroll the caller as a trusted
|
|
node; it only unlocks the requested capability on that one resource.
|
|
`geth ssh cert sync <node-id>` requires `ssh_cert.sync` on `resource:ssh:certs`
|
|
at the peer. `geth ssh revocation sync <node-id>` requires
|
|
`ssh_revocation.sync` on `resource:ssh:revocations`. Both commands merge
|
|
authorized peer metadata into the local store for offline listing and later
|
|
approval/signing workflows. While the daemon is running, it also performs a
|
|
background live-sync tick for known peers. The default interval is 30 seconds
|
|
and can be changed in `config.toml` with `[sync] live_sync_enabled` and
|
|
`live_sync_interval_ms`. Live-sync stores per-peer high-water cursors in local
|
|
metadata so repeated ticks request only newer SSH certificate-flow and
|
|
revocation records. Sync import preserves local metadata by rejecting
|
|
conflicting records with ids that already exist locally.
|
|
Before probing individual modules, the daemon asks the peer for an authorized
|
|
sync-status summary over Iroh. The peer only returns stream watermarks for
|
|
resources where the caller already has the matching capability, letting the
|
|
local daemon skip unchanged or unauthorized streams.
|
|
File roots advertise `cas-tree:<name>` watermarks when the caller has
|
|
`cas.fetch`; background live-sync imports updated tree metadata and CAS tree
|
|
bytes into peer-qualified remote roots without writing files.
|
|
When a previous imported remote tree is available, sync compares that base
|
|
against the current local same-named root and the newly imported remote tree.
|
|
Concurrent edit, delete/edit, and divergent rename conflicts are recorded in
|
|
the local conflict table for later resolution.
|
|
`geth cas root apply <root> --to <path>` can then materialize that tree locally:
|
|
without a registered local root base it creates missing directories/files, never
|
|
deletes extra files, never overwrites differing local files, and records
|
|
conflicts for manual resolution. When the target path is a registered local file
|
|
root with a previous scan, apply uses a three-way base/local/remote check and
|
|
can safely apply non-conflicting remote creates, updates, deletes, and renames
|
|
only where local state still matches the recorded base.
|
|
Named KV stores participate in the same live-sync loop once they exist locally:
|
|
manual `geth kv sync <node-id> <name>` and background ticks require `kv.read`
|
|
on the remote `resource:kv:<name>` and import only remote entries that are not
|
|
older than the local value.
|
|
Remote pubsub publish uses the protected Iroh control path too. The remote peer
|
|
requires `pubsub.publish` on `resource:pubsub:<topic>` before recording the
|
|
message in its local daemon-lifetime ring buffer. Pubsub remains lossy and is
|
|
not durable storage; facts that must survive restart or reconcile offline
|
|
belong in CAS, KV, document, or DB resources. Remote pubsub subscribe uses the
|
|
same protected path and requires `pubsub.subscribe` on
|
|
`resource:pubsub:<topic>` before returning the peer's current daemon-lifetime
|
|
snapshot for that topic.
|
|
Remote pipe connect uses the same protected Iroh control path and requires
|
|
`pipe.connect` on `resource:pipe:<name>`. The current prototype records a remote
|
|
connection attempt and whether a listener exists. `geth pipe send <name>
|
|
[message|--in <path>|--in -] --node <node-id>` uses the dedicated `/geth/pipe/1`
|
|
Iroh ALPN to write a byte message to an authorized peer listener, and `geth pipe
|
|
recv <name>` drains local daemon-lifetime messages.
|
|
Remote pipe listen uses the same protected path:
|
|
`geth pipe listen <name> --node <node-id>` requires `pipe.listen` on
|
|
`resource:pipe:<name>` before registering a daemon-lifetime listener on the
|
|
peer.
|
|
`geth pipe forward-tcp --listen 127.0.0.1:<local-port> --node <node-id> --target
|
|
127.0.0.1:<remote-port>` starts a local loopback TCP listener. Each accepted
|
|
connection asks the local daemon to open an authorized `/geth/pipe/1` byte
|
|
stream to the peer. The remote daemon validates the signed endpoint/card binding
|
|
and requires `pipe.forward` on `resource:pipe-tcp:<target>` before connecting to
|
|
the remote loopback TCP target. This is loopback-only in the prototype to avoid
|
|
turning geth into an accidental open proxy.
|
|
`geth pipe forward-unix --listen <local-socket> --node <node-id> --target
|
|
<remote-socket>` uses the same `/geth/pipe/1` byte stream and requires
|
|
`pipe.forward` on `resource:pipe-unix:<target>` before connecting to the remote
|
|
Unix socket. Unix socket paths must be absolute.
|
|
`geth ssh proxy <node-id>` is usable as an OpenSSH `ProxyCommand`: the CLI opens
|
|
a local daemon stream, the daemon opens the dedicated `/geth/ssh-proxy/1` Iroh
|
|
ALPN, the remote daemon validates the caller's endpoint/card binding and
|
|
requires `ssh_proxy.connect` on `resource:ssh-proxy:local`, and only then
|
|
connects the stream to `127.0.0.1:22`. SSH remains normal OpenSSH on top of that
|
|
byte stream; SSH is not a geth transport backend.
|
|
`geth ssh admin-shell <node-id> <command>` is a restricted geth admin workflow
|
|
over the protected Iroh control path. It requires `ssh_proxy.admin_shell` on the
|
|
same resource and supports only built-in commands (`help`, `status`, `node-id`);
|
|
it does not execute host shell commands.
|
|
Document sync exchanges durable Automerge state with a JSON view for CLI output:
|
|
manual `geth document sync <node-id> <name>` and background live-sync require
|
|
`document.read` on `resource:document:<name>` and merge authorized remote state
|
|
when the peer advertises a state timestamp at or after the local document.
|
|
DB sync is a staged cr-sqlite path: manual `geth db sync <node-id> <name>` and
|
|
background live-sync require `db.sync` on `resource:db:<name>`, exchange typed
|
|
`crsql_changes` batches over the protected Iroh control path, and check remote
|
|
schema metadata against the local DB before applying and advancing the per-peer
|
|
cursor. Compatible batches are inserted into the local `crsql_changes` table or
|
|
view; for real cr-sqlite databases, loading/configuring cr-sqlite remains the
|
|
database owner's responsibility. The prototype does not use CAS-backed DB
|
|
snapshots or change-batch blobs; that is deferred until large initial catch-up
|
|
needs it. The automated tests use deterministic `crsql_changes` fixtures because
|
|
this dev environment does not provide a `sqlite3` CLI or cr-sqlite extension
|
|
artifact for a real extension-backed integration test.
|
|
Importing or pinging a peer card never grants capabilities by itself.
|
|
When `[iroh].local_discovery = true`, the daemon also advertises and discovers
|
|
signed peer cards on LAN using a geth-specific mDNS TXT payload. That payload is
|
|
candidate metadata only; all geth node-to-node requests still run over Iroh.
|
|
|
|
## Resource Modules
|
|
|
|
Everything meaningful is modeled as a resource. Planned resource kinds are:
|
|
|
|
- `db`: SQLite/cr-sqlite synchronization
|
|
- `kv`: Iroh Documents backed key-value stores
|
|
- `pipe`: dumbpipe-like byte streams over Iroh; the bootstrap has a local
|
|
daemon registry only
|
|
- `document`: Automerge documents over Iroh streams
|
|
- `pubsub`: lossy notifications, not authoritative storage; the bootstrap
|
|
keeps only an in-memory daemon-lifetime ring buffer
|
|
- `cas`: content-addressed blob storage and distribution
|
|
- `ssh-proxy`: authorized SSH proxy/admin access over Iroh
|
|
|
|
Authorization is resource-scoped and capability-based. Bearer secrets may grant
|
|
specific resource capabilities but do not create trusted node identity. The auth
|
|
evaluator supports scoped KV write grants such as `kv.write_prefix:apps/foo/`
|
|
for `kv.write_key:apps/foo/config` explain checks. `geth kv set --subject
|
|
<principal>` enforces those local grants for test callers; the local node/agent
|
|
still has owner access for local administration. SSH certificate and revocation
|
|
commands also accept `--subject <principal>` on local metadata operations to
|
|
exercise the same capability checks: certificate requests/read/approval/import
|
|
use `ssh_cert.request`, `ssh_cert.read`, `ssh_cert.approve`, and
|
|
`ssh_cert.import` on `resource:ssh:certs`, while revocation publish/read/import
|
|
use `ssh_revocation.publish`, `ssh_revocation.read`, and
|
|
`ssh_revocation.import` on `resource:ssh:revocations`.
|
|
|
|
`geth auth explain <subject> <resource> <capability>` is the operator-facing
|
|
debug path for those decisions. Human output includes the allow/deny result,
|
|
the reason, evaluated auth-op count, and compact diagnostics. JSON output
|
|
includes the same diagnostics so scripts can distinguish discovered-only peers,
|
|
unknown subjects, missing or matched endpoint bindings, missing grants, revoked
|
|
grants, and bearer-secret access without scraping prose.
|
|
|
|
## Local State
|
|
|
|
If `GETH_HOME` is set, geth uses it. Otherwise it uses an OS-specific data
|
|
directory. The bootstrap layout is:
|
|
|
|
```text
|
|
$GETH_HOME/
|
|
geth.sqlite
|
|
config.toml
|
|
identity/agent.ed25519
|
|
identity/iroh.ed25519
|
|
cas/blobs/
|
|
run/geth.sock
|
|
```
|
|
|
|
## Quick Start
|
|
|
|
In one shell:
|
|
|
|
```sh
|
|
export GETH_HOME="$(mktemp -d)"
|
|
cargo run -p geth -- init
|
|
cargo run -p geth -- daemon run
|
|
```
|
|
|
|
In another shell:
|
|
|
|
```sh
|
|
export GETH_HOME="<same dir>"
|
|
cargo run -p geth -- status
|
|
cargo run -p geth -- node id
|
|
echo "hello geth" > /tmp/hello-geth.txt
|
|
cargo run -p geth -- cas add /tmp/hello-geth.txt
|
|
cargo run -p geth -- cas list
|
|
```
|
|
|
|
## Owner And Node Management
|
|
|
|
The intended owner setup is SSH-admin-rooted:
|
|
|
|
```sh
|
|
geth init \
|
|
--admin-key ~/.ssh/id_ed25519_sk.pub \
|
|
--signing-key ~/.ssh/id_ed25519_sk \
|
|
--node-name laptop \
|
|
--capability resource:ssh-proxy:local=ssh_proxy.admin_shell
|
|
```
|
|
|
|
When any owner setup option is used, both `--admin-key` and `--signing-key` are
|
|
required. This prevents accidentally creating an unsigned owner/device/node
|
|
statement that cannot be accepted by another node during keychain sync.
|
|
|
|
This records signed keychain operations for `KeychainInit`, `AdminKeyAdd`,
|
|
`UserAdd`, `DeviceAdd`, `NodeAdd`, and `AgentBind`. The current node identity is
|
|
stable above endpoint rotation: future endpoint bindings should attach to the
|
|
node, not replace it. Node management is done through the reduced keychain view:
|
|
|
|
```sh
|
|
geth node list
|
|
geth node rename laptop work-laptop --signing-key ~/.ssh/id_ed25519_sk
|
|
geth node endpoint-add work-laptop <iroh-endpoint-id> --signing-key ~/.ssh/id_ed25519_sk
|
|
geth node grant work-laptop resource:ssh-proxy:local ssh_proxy.connect \
|
|
--signing-key ~/.ssh/id_ed25519_sk
|
|
geth node revoke-grant resource:ssh-proxy:local <grant-id> \
|
|
--signing-key ~/.ssh/id_ed25519_sk
|
|
geth node revoke work-laptop --signing-key ~/.ssh/id_ed25519_sk
|
|
```
|
|
|
|
The enrollment flow for a new node is:
|
|
|
|
```sh
|
|
# On the new node, initialize local state and trust the owner's admin public key:
|
|
geth init
|
|
geth daemon run
|
|
geth keychain init --admin-key ~/.ssh/id_ed25519_sk.pub
|
|
|
|
# Then create a signed enrollment request:
|
|
geth node enroll request \
|
|
--node-name workstation \
|
|
--capability resource:ssh-proxy:local=ssh_proxy.connect \
|
|
--out /tmp/workstation-enrollment.json
|
|
|
|
# Either submit over Iroh to an imported owner peer:
|
|
geth node enroll submit owner-laptop --path /tmp/workstation-enrollment.json
|
|
|
|
# Or import the JSON on the owner/YubiKey machine:
|
|
geth node enroll import /tmp/workstation-enrollment.json
|
|
geth node enroll list --status pending
|
|
geth node enroll approve <request-id> --signing-key ~/.ssh/id_ed25519_sk
|
|
|
|
# Back on the new node, pull signed identity and authorization state:
|
|
geth sync now owner-laptop
|
|
geth sync status
|
|
```
|
|
|
|
Enrollment requests are signed by the requesting agent key. Approval records
|
|
signed keychain operations for the new device/node/agent binding and signed auth
|
|
operations for requested resource capabilities. The requesting node must already
|
|
know the owner's admin public key so it can verify the signed operation logs
|
|
before importing them. `geth sync now` pulls both signed logs from the owner
|
|
node through the same path used by background live sync.
|
|
|
|
`geth keychain sync <node>` pulls signed keychain operations from an imported
|
|
peer over Iroh and rejects operations that do not have a valid OpenSSH signature
|
|
from a currently trusted admin key over the canonical keychain payload. This is
|
|
the current replicated device-management substrate. It is still a pull-based
|
|
operation log, not yet a CRDT or Keyhive-style convergent authority.
|
|
|
|
The daemon also runs best-effort live sync for imported peers. `geth sync now
|
|
[node]` triggers the same sync pass immediately, and `geth sync status` reports
|
|
the last local attempt, success, cursor, import count, rejection count, and
|
|
error per peer stream. Keychain and auth sync now use per-peer high-water
|
|
cursors, while receivers still verify every imported signed operation before it
|
|
can affect the reduced keychain or authorization views.
|
|
|
|
## Authorization Direction
|
|
|
|
The MVP defines the split between:
|
|
|
|
- keychain: SSH-rooted users, devices, nodes, agents, and endpoint bindings
|
|
- auth: resource-local signed authorization operations and capability grants
|
|
- secrets: resource master secrets, epochs, envelopes, and bearer access
|
|
|
|
The current code does not implement Keyhive, BeeKEM, strong forward secrecy, or
|
|
post-compromise security. It leaves room for future local-first, replicated auth
|
|
logs and BeeKEM/CGKA-style group key evolution.
|