Phase 0a — ADRs (9 new): - ADR-026: Transport/interface separation (three-layer model) - ADR-027: Crate decomposition (core, secret, storage, flowgraph, napi, CLI) - ADR-028: Auth as irpc service (AuthProtocol behind feature flag) - ADR-029: Identity as core type (Identity + IdentityProvider in alknet-core) - ADR-030: Static/dynamic config split (ArcSwap, ConfigReloadHandle) - ADR-031: Forwarding policy (rule-based allow/deny, TransportKind-aware) - ADR-032: Event boundary discipline (domain, irpc, call protocol boundaries) - ADR-033: OperationEnv universal composition (three dispatch paths) - ADR-034: Head/worker terminology (replace hub/spoke) Phase 0b — New spec documents (7): - identity.md, services.md, interface.md, configuration.md, storage.md, flowgraph.md, secret-service.md Updated existing docs: - auth.md: reference identity.md for canonical definitions, add AuthProtocol - open-questions.md: resolve OQ-12, OQ-16, OQ-18, OQ-22, OQ-23-25 - README.md: add all new docs, ADRs 026-034 Marked 19 architecture tasks as completed.
5.7 KiB
ADR-030: Static/Dynamic Configuration Split
Status
Accepted
Context
Alknet's configuration is loaded once at startup and never changes. This causes three specific failures:
-
No hot reload of authentication credentials. Adding or removing an authorized key requires restarting the server process. In head/worker deployments where keys are managed via a database, the process must be restarted every time a key is added, revoked, or rotated. This is operationally unacceptable.
-
No port forwarding access control. Any authenticated client can open a
direct-tcpipchannel to any destination. There is no policy governing which hosts, ports, or alknet control channels a client may access. A compromised key grants unrestricted network access through the tunnel. -
No structured configuration beyond CLI flags. ADR-011 chose programmatic-first configuration for the alpha — correct at the time. But as alknet moves toward publishable releases, operators need config files for reproducible deployments, and the NAPI layer needs programmatic reload capability that
ServeOptionsdoesn't currently support.
Not all configuration should be reloadable. Transport-level settings (listen address, TLS certificates, host key) require socket/TLS renegotiation to change at runtime — effectively a restart. Auth and forwarding policy can change atomically without disrupting existing connections.
Decision
Split configuration into StaticConfig and DynamicConfig.
StaticConfig
Immutable after startup. Constructed from ServeOptions (the builder pattern is
preserved). Contains everything that affects socket binding, TLS handshakes, or
SSH session negotiation:
- Transport mode, listen address
- TLS config (cert, key)
- iroh config (relay URL)
- Stealth mode flag
- Host key, host key algorithm
- Max auth attempts, max connections per IP
- Proxy config
Changing any of these requires a restart.
DynamicConfig
Hot-reloadable at runtime via ArcSwap<DynamicConfig>. Contains everything
checked per-connection or per-channel:
AuthPolicy— authorized keys, certificate authorities, token configForwardingPolicy— allow/deny rules for channel targets (ADR-031)RateLimitConfig— rate limiting parameters
ArcSwap provides lock-free reads on the hot path (every auth_publickey() and
every channel_open_direct_tcpip() call does an Arc dereference — zero cost
compared to the current approach). Writes are atomic: store() swaps the
pointer. Existing connections finish with their current config; new connections
get the new config.
ConfigReloadHandle
pub struct ConfigReloadHandle {
dynamic: Arc<ArcSwap<DynamicConfig>>,
}
impl ConfigReloadHandle {
pub fn reload(&self, new_config: DynamicConfig) { ... }
}
The handle is obtained from Server::run() and passed to NAPI or the CLI.
ConfigService
The ConfigService wraps ArcSwap<DynamicConfig> reloads behind an irpc
protocol (behind the irpc feature flag) for production deployments that use
the service layer. For minimal deployments (CLI, single-node), direct
ConfigReloadHandle::reload() is sufficient.
TOML Config File
An optional TOML config file covers static config plus initial auth/forwarding paths. This amends ADR-011 (does not supersede it) — the programmatic-first API remains primary. The config file is a convenience input format:
[server]
transport = "tls"
listen = "0.0.0.0:443"
stealth = false
max_connections_per_ip = 5
max_auth_attempts = 3
[server.tls]
cert = "/etc/alknet/tls/cert.pem"
key = "/etc/alknet/tls/key.pem"
[auth]
host_key = "/etc/alknet/ssh/host_key"
[forwarding]
default = "deny"
NAPI Reload API
interface AlknetServer {
reloadAuth(auth: { authorizedKeys?: Buffer, certAuthority?: Buffer }): void;
reloadForwarding(policy: ForwardingPolicyConfig): void;
reloadAll(config: DynamicConfig): void;
}
The NAPI layer parses key data and constructs a new DynamicConfig, then calls
ConfigReloadHandle::reload().
Client Configuration
Client configuration stays as ConnectOptions — no ArcSwap needed. Client
config is almost entirely static (which server to connect to, which key to use).
Consequences
- Positive: Auth credentials and forwarding policy can be reloaded without
restarting the server. Adding a key via
reloadAuth()takes effect on the next connection attempt. - Positive: ADR-011's programmatic-first intent is preserved. The TOML
config file is an optional convenience layer, not a replacement for
ServeOptions. - Positive:
ArcSwapprovides zero-cost reads on the hot path. Every auth check and every channel open is a singleArcdereference. - Positive: The
ConfigServiceirpc protocol (behind feature flag) allows production deployments to integrate config reload into their service mesh without taking a direct dependency onDynamicConfiginternals. - Positive: Forwarding policy is now part of
DynamicConfig— operators can restrict access per identity, per destination, per transport (ADR-031). - Negative: Two config structs where there was one. The split is clean (transport vs. policy) but adds surface area.
- Negative: Config file introduces
tomlas a dependency in the CLI crate. This is acceptable for a CLI binary.
References
- research/configuration.md — Full analysis
- ADR-011 — Programmatic-first API (amended, not superseded)
- ADR-031 — Forwarding policy (part of DynamicConfig)
- ADR-029 — Identity as core type (DynamicConfig.auth uses IdentityProvider)
- integration-plan.md — Phase 1.1