docs: write Phase 0 architecture foundation — ADRs 026-034, spec docs, and task updates
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.
This commit is contained in:
192
docs/architecture/configuration.md
Normal file
192
docs/architecture/configuration.md
Normal file
@@ -0,0 +1,192 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-06-07
|
||||
---
|
||||
|
||||
# Configuration
|
||||
|
||||
## What
|
||||
|
||||
Alknet's configuration is split into `StaticConfig` (immutable after startup) and
|
||||
`DynamicConfig` (hot-reloadable at runtime), with `ArcSwap` providing lock-free
|
||||
reads on the hot path. `ConfigService` wraps reloads behind an irpc protocol
|
||||
for production deployments.
|
||||
|
||||
## Why
|
||||
|
||||
Three specific failures motivated the split (ADR-030):
|
||||
|
||||
1. No hot reload of authentication credentials — adding a key requires a restart.
|
||||
2. No port forwarding access control — any authenticated client has unrestricted
|
||||
access (ADR-031).
|
||||
3. No structured configuration beyond CLI flags — operators need config files
|
||||
and the NAPI layer needs programmatic reload.
|
||||
|
||||
The split is clean: anything that affects SSH handshake or socket binding is
|
||||
static; anything checked per-connection or per-channel is dynamic.
|
||||
|
||||
## Architecture
|
||||
|
||||
### StaticConfig
|
||||
|
||||
Immutable after startup. Constructed from `ServeOptions` (the builder pattern
|
||||
is preserved per ADR-011). Contains:
|
||||
|
||||
- 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:
|
||||
|
||||
- `AuthPolicy` — authorized keys, certificate authorities, token config
|
||||
- `ForwardingPolicy` — allow/deny rules for channel targets (ADR-031)
|
||||
- `RateLimitConfig` — rate limiting parameters
|
||||
|
||||
`ArcSwap` provides lock-free reads. Every `auth_publickey()` and
|
||||
`channel_open_direct_tcpip()` call does a single `Arc` dereference — zero cost
|
||||
compared to the current approach. Writes are atomic: `store()` swaps the
|
||||
pointer.
|
||||
|
||||
### ConfigReloadHandle
|
||||
|
||||
```rust
|
||||
pub struct ConfigReloadHandle {
|
||||
dynamic: Arc<ArcSwap<DynamicConfig>>,
|
||||
}
|
||||
|
||||
impl ConfigReloadHandle {
|
||||
pub fn reload(&self, new_config: DynamicConfig) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
Obtained from `Server::run()`. Passed to NAPI or CLI for explicit reload.
|
||||
|
||||
### ConfigService irpc Service
|
||||
|
||||
```rust
|
||||
enum ConfigProtocol {
|
||||
GetForwardingPolicy,
|
||||
GetRateLimits,
|
||||
ReloadForwarding { policy: ForwardingPolicy },
|
||||
ReloadRateLimits { limits: RateLimitConfig },
|
||||
}
|
||||
```
|
||||
|
||||
Behind the `irpc` feature flag. For production deployments that use the service
|
||||
layer. For minimal deployments, direct `ConfigReloadHandle::reload()` is
|
||||
sufficient.
|
||||
|
||||
### ForwardingPolicy
|
||||
|
||||
Part of DynamicConfig (ADR-031). Evaluated per-channel-open, matched against
|
||||
the authenticated `Identity`. Rules are evaluated in order; first match wins.
|
||||
Default determines fallback.
|
||||
|
||||
```rust
|
||||
pub struct ForwardingPolicy {
|
||||
pub default: ForwardingAction,
|
||||
pub rules: Vec<ForwardingRule>,
|
||||
}
|
||||
```
|
||||
|
||||
### TOML Config File
|
||||
|
||||
Optional convenience input format (amends ADR-011, does not replace
|
||||
programmatic API). Covers static config plus initial auth/forwarding paths.
|
||||
|
||||
```toml
|
||||
[server]
|
||||
transport = "tls"
|
||||
listen = "0.0.0.0:443"
|
||||
|
||||
[auth]
|
||||
host_key = "/etc/alknet/ssh/host_key"
|
||||
|
||||
[forwarding]
|
||||
default = "deny"
|
||||
|
||||
[[forwarding.rules]]
|
||||
target = "localhost:*"
|
||||
action = "allow"
|
||||
```
|
||||
|
||||
### NAPI Reload API
|
||||
|
||||
```typescript
|
||||
interface AlknetServer {
|
||||
reloadAuth(auth: { authorizedKeys?: Buffer, certAuthority?: Buffer }): void;
|
||||
reloadForwarding(policy: ForwardingPolicyConfig): void;
|
||||
reloadAll(config: DynamicConfig): void;
|
||||
}
|
||||
```
|
||||
|
||||
### Multi-Transport Listeners
|
||||
|
||||
A head node may accept connections on multiple transports simultaneously. The
|
||||
architecture supports `Vec<ListenerConfig>` instead of a single
|
||||
`ServeTransportMode`. `Server::run()` spawns one accept loop per listener,
|
||||
sharing `DynamicConfig`, `ConnectionRateLimiter`, sessions, and shutdown signal.
|
||||
|
||||
```toml
|
||||
[[listeners]]
|
||||
transport = "tls"
|
||||
listen = "0.0.0.0:443"
|
||||
stealth = true
|
||||
|
||||
[[listeners]]
|
||||
transport = "tcp"
|
||||
listen = "0.0.0.0:22"
|
||||
|
||||
[[listeners]]
|
||||
transport = "iroh"
|
||||
iroh_relay = "https://relay.alk.dev"
|
||||
```
|
||||
|
||||
### CLI vs Programmatic Behavior
|
||||
|
||||
| Interface | Static config | Dynamic config | Reload mechanism |
|
||||
|-----------|--------------|----------------|------------------|
|
||||
| CLI | Flags + optional `--config` file | Loaded at startup from `--authorized-keys` | None (restart to change) |
|
||||
| Core Rust | `StaticConfig` struct | `AuthService` (irpc) or `ArcSwap<DynamicConfig>` (minimal) | `ConfigService::reload()` or `ConfigReloadHandle::reload()` |
|
||||
| NAPI | `serve()` options | Same | `server.reloadAuth()`, `server.reloadForwarding()` |
|
||||
|
||||
## Constraints
|
||||
|
||||
- `StaticConfig` cannot be changed after startup. Changing transport mode,
|
||||
listen address, TLS config, or host key requires a restart.
|
||||
- `DynamicConfig` is reloaded atomically via `ArcSwap`. Existing connections
|
||||
continue with their current config; new connections get the new config.
|
||||
- Config file is optional. `ServeOptions` builder pattern remains the primary
|
||||
API (amends ADR-011, does not supersede it).
|
||||
- No file watching (OQ-13 resolved: potential attack vector, unnecessary
|
||||
complexity).
|
||||
- Client configuration stays as `ConnectOptions` — no `ArcSwap` needed.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- None. All configuration-related questions are resolved per ADR-030, ADR-031,
|
||||
and the resolved OQs in [open-questions.md](open-questions.md).
|
||||
|
||||
## Design Decisions
|
||||
|
||||
| ADR | Decision | Summary |
|
||||
|-----|----------|---------|
|
||||
| [030](decisions/030-static-dynamic-config-split.md) | Static/dynamic config split | Immutable transport vs. reloadable auth/forwarding |
|
||||
| [011](decisions/011-no-ssh-config-programmatic-api.md) | Programmatic-first API | Amended, not superseded — TOML is convenience layer |
|
||||
| [031](decisions/031-forwarding-policy.md) | Forwarding policy | Rule-based allow/deny, TransportKind-aware |
|
||||
| [029](decisions/029-identity-core-type.md) | Identity as core type | DynamicConfig.auth consumed by IdentityProvider |
|
||||
| [028](decisions/028-auth-irpc-service.md) | Auth as irpc service | ConfigService wraps DynamicConfig reloads |
|
||||
|
||||
## References
|
||||
|
||||
- [research/configuration.md](../research/configuration.md) — Full analysis and proposed solution
|
||||
- [identity.md](identity.md) — IdentityProvider trait, DynamicConfig.auth
|
||||
- [ADR-013](decisions/013-fail2ban-friendly-logging.md) — Rate limiting parameters
|
||||
Reference in New Issue
Block a user