6.1 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | |
|---|---|---|---|---|---|---|---|---|
| core/config | Implement StaticConfig, DynamicConfig, AuthPolicy, ApiKeyEntry, ConfigReloadHandle, TlsIdentity | completed |
|
moderate | low | component | implementation |
Description
Implement the configuration types in src/config.rs. These are the config
structures consumed by the endpoint and the CLI binary. StaticConfig is
immutable at startup; DynamicConfig is hot-reloadable via ArcSwap.
StaticConfig
pub struct StaticConfig {
pub listen_addr: Option<SocketAddr>,
pub tls_identity: Option<TlsIdentity>,
pub iroh_relay: Option<RelayUrl>,
pub drain_timeout: Duration,
}
Immutable configuration resolved at startup. listen_addr is None for
iroh-only nodes. tls_identity is required if listen_addr is Some.
TlsIdentity
pub enum TlsIdentity {
X509 { cert: PathBuf, key: PathBuf },
RawKey(iroh::SecretKey),
SelfSigned,
}
Three modes (OQ-12):
X509: domain certificate for browser/WebTransport clientsRawKey: RFC 7250 raw Ed25519 public key — default for P2P, no domain/CASelfSigned: development only
RawKey uses iroh::SecretKey (Ed25519) — re-exported from iroh, which
alknet-core depends on (feature-gated). The key can be derived from
alknet-vault at the assembly layer or generated fresh.
DynamicConfig
#[derive(Debug, Clone)]
pub struct DynamicConfig {
pub auth: AuthPolicy,
pub rate_limits: RateLimitConfig,
}
Runtime-reloadable via ArcSwap.
AuthPolicy
pub struct AuthPolicy {
pub authorized_fingerprints: HashSet<String>,
pub api_keys: Vec<ApiKeyEntry>,
}
Fingerprints stored as strings (no russh dependency in core — ADR-003). Certificate authority entries deferred to alknet-ssh (omitted from v1 to avoid referencing an undefined type; adding back is additive).
ApiKeyEntry
pub struct ApiKeyEntry {
pub prefix: String,
pub hash: String,
pub scopes: Vec<String>,
pub description: String,
pub expires_at: Option<u64>,
}
Carries forward from reference implementation. Prefix (first 8 chars) for O(1) lookup, SHA-256 hash for verification.
RateLimitConfig
pub struct RateLimitConfig {
pub max_connections_per_ip: usize,
pub max_auth_attempts: usize,
}
ArcSwap pattern
let dynamic = Arc::new(ArcSwap::new(Arc::new(DynamicConfig::default())));
- Reads:
dynamic.load()returnsArc<DynamicConfig>— lock-free - Writes:
dynamic.store(Arc::new(new_config))— atomic swap - No locks: ArcSwap uses atomic operations
ConfigReloadHandle
pub struct ConfigReloadHandle {
dynamic: Arc<ArcSwap<DynamicConfig>>,
}
impl ConfigReloadHandle {
pub fn reload(&self, new_config: DynamicConfig);
pub fn dynamic(&self) -> Arc<DynamicConfig>;
}
reload(): atomically replaces the dynamic configdynamic(): returns current config asArc<DynamicConfig>
Config reload is a privilege-escalation path. A reload that adds an authorized fingerprint or API key grants access immediately. The reload trigger must be authenticated/local-only (SIGHUP, file watch, or admin call protocol operation). The implementation must not ship a reload endpoint with no auth "for convenience."
ConfigError
pub enum ConfigError {
InvalidFlag { name: String },
KeyFileNotFound { path: String },
BindFailed(io::Error),
TlsConfig(io::Error),
IncompatibleOptions,
}
Defaults
drain_timeout: 2 secondsmax_connections_per_ip: implementation default (reference uses a reasonable value)max_auth_attempts: implementation defaultDynamicConfig::default(): empty auth policy, default rate limits
What NOT to include
Per the spec, StaticConfig does NOT include: host_key, host_key_algorithm,
proxy_config, stealth, transport_mode, listeners. These are removed in
the new model (ALPN dispatch replaces them — see config.md Key Differences).
Acceptance Criteria
StaticConfigstruct with all fields per config.mdTlsIdentityenum with X509, RawKey, SelfSigned variantsDynamicConfigstruct withauthandrate_limitsfieldsAuthPolicystruct withauthorized_fingerprintsandapi_keysApiKeyEntrystruct with all 5 fieldsRateLimitConfigstruct with both fieldsConfigReloadHandlewithreload()anddynamic()methodsConfigErrorenum with all variantsDynamicConfigderivesClone,Debug(for ArcSwap)- Default values match config.md (drain_timeout = 2s, etc.)
- No russh dependency (fingerprints as strings)
- Unit tests for Default impls
- Unit test: ConfigReloadHandle reload swaps config atomically
cargo test -p alknet-coresucceedscargo clippy -p alknet-coresucceeds with no warnings
References
- docs/architecture/crates/core/config.md — all type definitions
- docs/architecture/decisions/003-crate-decomposition.md — ADR-003 (no russh in core)
- docs/architecture/decisions/010-alpn-router-and-endpoint.md — ADR-010 (no ListenerConfig)
Notes
Config reload is a privilege-escalation path — do not ship an unauthenticated reload endpoint. The ArcSwap pattern carries forward from the reference implementation. StaticConfig removes all SSH-centric fields (host_key, stealth, transport_mode, listeners) — those are handler concerns now.
Summary
Implemented all configuration types in config.rs: StaticConfig
(drain_timeout=2s default), TlsIdentity (X509/RawKey[iroh-gated]/SelfSigned),
DynamicConfig (Clone/Debug/Default, ArcSwap-reloadable), AuthPolicy (String
fingerprints, no russh), ApiKeyEntry (5 fields), RateLimitConfig (100/5
defaults), ConfigReloadHandle (reload/dynamic via ArcSwap), ConfigError
(thiserror, all variants). iroh_relay and RawKey feature-gated to iroh.
Preserved AuthPolicy::resolve_identity_from_fingerprint/resolve_api_key
methods from the parallel core/auth task. 41 total tests pass; clippy clean on
default + iroh features. Merged to develop (resolved config.rs conflicts with
core/auth).