Closes out Phase 1. A production operator can now cut a fleet CA,
sign per-node leaves, drop the resulting PEMs at
/etc/claw-store/tls/, point [cluster.tls] at them, and the daemon
loads real mTLS material on startup — no more ephemeral in-process
CA hack.
New public API in cluster::transport:
- FleetCa::generate(cn) — new self-signed root CA
- FleetCa::save(dir) / FleetCa::load(dir) — round-trip PEM
- FleetCa::sign_leaf(name) — mint an in-memory NodeIdentity
- FleetCa::sign_leaf_to_pem(name, out_dir) — write ca.crt + node.crt +
node.key (node.key at 0o600 on Unix)
- NodeIdentity::from_pem_files(ca, cert, key) — production load path
- NodeIdentity::from_pem_dir(dir) — canonical filename layout
- NodeIdentity::from_cluster_config(cfg) — pick up [cluster.tls] paths
Manual Debug for FleetCa redacts the private key.
Config extension:
- [cluster.tls] ca_cert / node_cert / node_key (all PathBuf).
- Optional at the top level; callers that need mTLS surface a clear
error when it's absent.
Deps:
- rcgen features += "x509-parser" (for FleetCa::load's from_ca_cert_pem).
- rustls-pemfile 2 (parse PEM back into DER for rustls).
CLI (new commands; short-circuit config load so they run on fresh
boxes without /etc/claw-store/config.toml):
- fleet-ca-init --dir <dir> [--cn <name>]
generates ca.crt + ca.key (both 0o600).
- fleet-ca-sign --ca-dir <dir> --node <name> --out-dir <dir>
writes ca.crt + node.crt + node.key (node.key at 0o600).
- cluster-ping now accepts --tls-dir <dir> to load persistent
NodeIdentity from disk (produced by fleet-ca-sign).
Tests (8 new, all real — no mocks, real filesystem, real TLS handshake):
- fleet_ca_rejects_empty_common_name
- fleet_ca_save_and_load_round_trip_preserves_signing (asserts 0o600
on ca.key)
- fleet_ca_load_errors_when_files_missing
- sign_leaf_to_pem_writes_all_three_files_with_correct_permissions
(asserts 0o600 on node.key)
- persistent_identity_round_trips_through_disk_and_pings — end-to-end:
cut CA on disk, reload it, sign two leaves via sign_leaf_to_pem,
reload them via from_pem_dir, run real QUIC ping/pong. This is the
operator flow.
- node_identity_from_cluster_config_errors_without_tls_section
- node_identity_from_cluster_config_loads_pem_paths
- from_pem_files_errors_on_missing_ca_file
80 tests pass. Pre-existing macOS-only hot test unchanged.
Also verified live CLI smoke test:
fleet-ca-init → ca.crt + ca.key at 0o600
fleet-ca-sign → ca.crt + node.crt + node.key at 0o600
Files parse as valid X.509.
File sizes (all under 1300-line ceiling):
- cluster/transport.rs: 916
- cluster/gossip.rs: 576
- cluster.rs: 275
- config.rs: 503
- main.rs: 662
Phase 1 complete. Next up:
- Phase 1e (daemon integration): gossip + QUIC RPC server wired into
claw-store daemon; hot-tier metrics periodically pushed; a real
PeerStatus RPC alongside ping.
- Phase 2: content-addressed blob store (BLAKE3 chunking, put/get).
55 lines
2.2 KiB
TOML
55 lines
2.2 KiB
TOML
[package]
|
|
name = "claw-store"
|
|
version = "0.3.0"
|
|
edition = "2021"
|
|
|
|
[[bin]]
|
|
name = "claw-store"
|
|
path = "src/main.rs"
|
|
|
|
[dependencies]
|
|
clap = { version = "4", features = ["derive"] }
|
|
serde = { version = "1", features = ["derive"] }
|
|
toml = "0.8"
|
|
tokio = { version = "1", features = ["full"] }
|
|
sysinfo = "0.30"
|
|
anyhow = "1"
|
|
tracing = "0.1"
|
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
|
chrono = { version = "0.4", features = ["serde"] }
|
|
axum = { version = "0.7", features = ["macros"] }
|
|
tower-http = { version = "0.5", features = ["cors", "fs"] }
|
|
tokio-stream = "0.1"
|
|
serde_json = "1"
|
|
# v0.2.0 — flock(2) wrapper for atomic+locked manifest writes
|
|
# (manifest.rs). Already a transitive dep; declaring it directly
|
|
# makes the call site obvious.
|
|
libc = "0.2"
|
|
# v0.2.0 — sibling-tempfile + rename for atomic manifest persistence.
|
|
# Was previously dev-dep only; promoted to main.
|
|
tempfile = "3"
|
|
# v0.11.1 — cluster membership via scuttlebutt gossip + phi-accrual failure
|
|
# detection. Wraps in cluster/gossip.rs. UDP transport, keyed KV state per
|
|
# node, seed_nodes bootstrap from [[cluster.peers]] config.
|
|
chitchat = "0.11"
|
|
# v0.11 — QUIC transport for peer RPC (Phase 1c). UDP-based; runs on a
|
|
# separate port from chitchat gossip. TLS 1.3 by default; we wire mTLS
|
|
# against a fleet root CA in cluster/transport.rs.
|
|
quinn = { version = "0.11", default-features = false, features = ["runtime-tokio", "rustls-ring"] }
|
|
# v0.23 — TLS 1.3 provider driving quinn's crypto. Pinned to the ring
|
|
# provider so a single CryptoProvider is installed process-wide.
|
|
rustls = { version = "0.23", default-features = false, features = ["ring"] }
|
|
# v0.13 — X.509 cert generation for the fleet CA + per-node leaf certs.
|
|
# Used both by production bootstrap (writes PEM to /etc/claw-store/tls/)
|
|
# and by tests (in-memory ephemeral CA).
|
|
rcgen = { version = "0.13", features = ["pem", "x509-parser"] }
|
|
# v2 — parse PEM files into DER for rustls consumption. Used in
|
|
# NodeIdentity::from_pem_files (Phase 1d) so persistent identity round-trips
|
|
# through the file system on daemon restart.
|
|
rustls-pemfile = "2"
|
|
|
|
[dev-dependencies]
|
|
tempfile = "3"
|
|
tower = { version = "0.5", features = ["util"] }
|
|
http-body-util = "0.1"
|