Update four existing specs (overview, server, napi-and-pubsub, call-protocol) to reflect Phase 0 decisions: three-layer model, IdentityProvider, ForwardingPolicy, OperationEnv, static/dynamic config split. Review all 9 Phase 0a ADRs (026-034) for consistency. Fix 4 critical issues from architecture review: missing OQ-SVC-05 in open-questions.md, deprecated hub terminology, undefined AuthService and noq terms. Replace inline OQ text with cross-references per format rules. Add ConfigServiceImpl definition to configuration.md. Port absolute workspace paths to project-relative links by copying referenced docs (feasibility, certbot, fail2ban, event_source_types) into docs/research/.
6.9 KiB
status, last_updated
| status | last_updated |
|---|---|
| draft | 2026-06-07 |
Secret Service
What
The alknet-secret crate provides BIP39 mnemonic generation, SLIP-0010 Ed25519
HD key derivation, AES-256-GCM encryption for external credentials, and the
SecretProtocol irpc service. It is the only component that holds the master
seed phrase.
Why
Operations like SSH key generation, API key storage, and Ethereum transaction signing all need deterministic key derivation from a single root of trust. The seed phrase is the single recovery mechanism — from it, all self-generated secrets can be derived on demand. External credentials (third-party API keys, OAuth tokens) cannot be derived and must be stored encrypted, with the encryption key itself derived from the seed.
The secret service isolates this responsibility: no other crate sees the seed, and derived keys are provided on demand through an irpc service interface.
Architecture
Security Model
| State | What's in memory | What's on disk |
|---|---|---|
| Locked | Nothing | Encrypted database, derivation path metadata |
| Unlocked | Master seed in RAM | Same (seed is never persisted) |
| After use | Derived keys cached in RAM | Derivation paths only |
The seed phrase is entered once (at node startup or via Unlock call), held
only in RAM, and never written to disk. The Lock call purges the seed and all
cached derived keys from memory.
SecretProtocol irpc Service
#[rpc_requests(message = SecretMessage)]
#[derive(Debug, Serialize, Deserialize)]
enum SecretProtocol {
#[rpc(tx=oneshot::Sender<DerivedKey>)]
#[wrap(DeriveEd25519)]
DeriveEd25519 { path: String },
#[rpc(tx=oneshot::Sender<DerivedKey>)]
#[wrap(DeriveEncryptionKey)]
DeriveEncryptionKey { path: String },
#[rpc(tx=oneshot::Sender<DerivedKey>)]
#[wrap(DeriveEthereumKey)]
DeriveEthereumKey { path: String },
#[rpc(tx=oneshot::Sender<Vec<u8>>)]
#[wrap(DerivePassword)]
DerivePassword { path: String, length: usize },
#[rpc(tx=oneshot::Sender<EncryptedData>)]
#[wrap(Encrypt)]
Encrypt { plaintext: String, key_version: u32 },
#[rpc(tx=oneshot::Sender<String>)]
#[wrap(Decrypt)]
Decrypt { encrypted: EncryptedData },
#[rpc(tx=oneshot::Sender<()>)]
#[wrap(Lock)]
Lock,
#[rpc(tx=oneshot::Sender<()>)]
#[wrap(Unlock)]
Unlock { passphrase: String },
}
#[derive(Debug, Serialize, Deserialize)]
struct DerivedKey {
key_type: KeyType,
private_key: Vec<u8>,
public_key: Vec<u8>,
}
#[derive(Debug, Serialize, Deserialize)]
enum KeyType {
Ed25519,
Aes256Gcm,
Secp256k1,
}
#[derive(Debug, Serialize, Deserialize)]
struct EncryptedData {
key_version: u32,
salt: String, // Base64-encoded
iv: String, // Base64-encoded
data: String, // Base64-encoded
}
BIP39 Mnemonic and Seed Derivation
let mnemonic = Mnemonic::from_phrase(&phrase, Language::English)?;
let seed = mnemonic.to_seed(Some(&passphrase));
let master_key = ExtendedPrivKey::new_master(Network::Alknet, &seed)?;
SLIP-0010 Ed25519 HD Key Derivation
The 74' coin type is unallocated per SLIP-0044 and reserved for alknet.
Derivation Path Constants
| Path | Purpose | Curve/Algorithm |
|---|---|---|
m/74'/0'/0'/0' |
Primary identity keypair | Ed25519 (alknet auth) |
m/74'/0'/0'/{n}' |
Worker/device identity | Ed25519 |
m/74'/0'/1'/0' |
SSH host key | Ed25519 |
m/74'/1'/0'/{hash}' |
Site-specific password | Deterministic |
m/74'/2'/0'/0' |
Encryption key for external credentials | AES-256-GCM |
m/44'/60'/0'/0/0 |
Ethereum signing key | secp256k1 |
AES-256-GCM Encryption for External Credentials
External credentials (API keys, OAuth tokens) that cannot be derived are
encrypted using a key derived from the seed at path m/74'/2'/0'/0'. The
EncryptedData type stores the key version, salt, IV, and ciphertext. This
format is compatible with the existing @alkdev/storage EncryptedDataSchema.
- The secret service derives an AES-256-GCM key via path
m/74'/2'/0'/0' - External credentials are encrypted with this key
- The encrypted data is stored as a
SecretNodein the metagraph - Only the derivation path and key version are stored in plain attributes
- The seed phrase (or derived encryption key) is held only by the secret service — never in the database
Deployment Topologies
Minimal (single node, CLI): Secret service runs in the same process. Seed phrase entered at startup. All keys derived locally. No irpc overhead.
Production (head node): Secret service runs on a dedicated node or as a local irpc service. Workers request derived keys via irpc over QUIC. The seed never leaves the secret service node.
Constraints
- The seed phrase is never persisted to disk. It is entered at startup or via
Unlockand held only in RAM. Lockpurges the seed and all cached derived keys from memory.- alknet-secret does not depend on alknet-core or alknet-storage. It is fully independent.
- The
EncryptedDatawire format (key_version, salt, iv, data) is shared with alknet-storage for compatibility, but this is type-level compatibility — not a crate dependency. - Per ADR-032, the secret service's Honker streams (key derivation notifications) stay within the service boundary. External consumers use irpc calls or call protocol operations that project to integration events.
- The irpc service defines the wire format for in-cluster communication
(postcard serialization). For call protocol exposure (e.g.,
/head/secrets/derive), the service is wrapped in an operation that serializes to JSON.
Open Questions
-
OQ-SVC-01: Should the secret service support multiple seed phrases (one per tenant)? See open-questions.md.
-
OQ-SVC-02: Should service protocols use postcard (binary) or JSON for remote calls? See open-questions.md.
-
OQ-SVC-03: How does the secret service integrate with the existing
EncryptedDataSchemafrom@alkdev/storage? See open-questions.md. -
OQ-SVC-04: Should workers cache derived keys locally? See open-questions.md.
Design Decisions
| ADR | Decision | Summary |
|---|---|---|
| 027 | Crate decomposition | alknet-secret is independent of core and storage |
| 032 | Event boundary | Secret service domain events stay internal |
References
- research/services.md — SecretProtocol definition, DerivedKey, KeyType
- research/storage.md — Secrets section, derivation paths, EncryptedData
- research/integration-plan.md — Phase 2.1
- SLIP-0010 — https://github.com/satoshilabs/slips/blob/master/slip-0010.md
- BIP39 — https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki