Sync architecture specs with Phase 2 research findings
- Add definitions.md: normative terminology disambiguation (Interface, Service, Transport, Token, Identity, Domain, Scope, CredentialProvider, etc.) - Add credentials.md: CredentialProvider trait and CredentialSet enum for outbound auth, mirroring IdentityProvider pattern for inbound auth - Rewrite interface.md: StreamInterface/MessageInterface split (ADR-035), InterfaceRequest/InterfaceResponse, HttpInterface/DnsInterface stubs, ListenerConfig with Stream/Http/Dns variants, credential presentation table - Update auth.md: API keys in DynamicConfig (ADR-037), credential presentation per (Transport, Interface) pair, ApiKeyEntry struct in AuthPolicy - Update configuration.md: API keys, ListenerConfig with Http/Dns variants, expanded TOML config examples - Update call-protocol.md: resolve OQ-IF-01 (InterfaceEvent carries EventEnvelope + Identity), add MessageInterface awareness to protocol adapter layer - Update overview.md: three-layer model now includes StreamInterface/ MessageInterface, CredentialProvider/CredentialSet exports, definitions.md reference, ADRs 035-037 - Update open-questions.md: resolve OQ-IF-01, OQ-IF-02, add OQ-P2-01 through OQ-P2-04, add OQ-CP-01 through OQ-CP-04, add OQ-DEF-01, OQ-DEF-03, OQ-DEF-08 - Update README.md: add definitions.md, credentials.md, ADRs 035-037, phase2 research docs, current state description Key architectural decisions: - ADR-035: StreamInterface/MessageInterface split (two Layer 2 traits) - ADR-036: CredentialProvider as core type (outbound auth, alknet_core::credentials) - ADR-037: API keys as DynamicConfig auth (hash-verified bearer tokens)
This commit is contained in:
@@ -1,18 +1,21 @@
|
|||||||
---
|
---
|
||||||
status: draft
|
status: draft
|
||||||
last_updated: 2026-06-07
|
last_updated: 2026-06-09
|
||||||
---
|
---
|
||||||
|
|
||||||
# Alknet Architecture
|
# Alknet Architecture
|
||||||
|
|
||||||
## Current State
|
## Current State
|
||||||
|
|
||||||
Architecture specification in active development. Phase 0 foundation complete:
|
Architecture spec sync in progress. Phase 0 foundation complete (ADRs 001–037).
|
||||||
ADRs 001–034 accepted, new spec documents created for all components, existing
|
Phase 1 core modifications partially implemented (interface trait, config split,
|
||||||
specs updated for the three-layer model, crate decomposition, unified identity,
|
identity provider, forwarding policy). Phase 2 core bridge research complete;
|
||||||
OperationEnv, and forwarding policy. Remaining open questions: OQ-15 (QUIC
|
spec documents updated to reflect StreamInterface/MessageInterface split,
|
||||||
coexistence), OQ-19 (WebTransport TLS), OQ-20 (worker registration), OQ-IF-01
|
CredentialProvider as core type, and API keys in DynamicConfig.
|
||||||
(Interface session/EventEnvelope), OQ-IF-02 (ForwardingPolicy placement). See
|
|
||||||
|
Remaining open questions: OQ-15 (QUIC coexistence), OQ-19 (WebTransport TLS),
|
||||||
|
OQ-20 (worker registration), OQ-CP-01 (per-identity credentials), OQ-CP-02
|
||||||
|
(OIDC provider location), OQ-CP-03 (credential rotation). See
|
||||||
[open-questions.md](open-questions.md).
|
[open-questions.md](open-questions.md).
|
||||||
|
|
||||||
## Architecture Documents
|
## Architecture Documents
|
||||||
@@ -21,7 +24,7 @@ coexistence), OQ-19 (WebTransport TLS), OQ-20 (worker registration), OQ-IF-01
|
|||||||
|----------|--------|-------------|
|
|----------|--------|-------------|
|
||||||
| [overview.md](overview.md) | reviewed | Package purpose, crate structure, three-layer model, exports, dependencies |
|
| [overview.md](overview.md) | reviewed | Package purpose, crate structure, three-layer model, exports, dependencies |
|
||||||
| [transport.md](transport.md) | reviewed | Transport abstraction: TCP, TLS, iroh |
|
| [transport.md](transport.md) | reviewed | Transport abstraction: TCP, TLS, iroh |
|
||||||
| [auth.md](auth.md) | draft | Unified auth: SSH + token, IdentityProvider trait |
|
| [auth.md](auth.md) | draft | Unified auth: SSH + token + API keys, credential presentation per interface |
|
||||||
| [call-protocol.md](call-protocol.md) | draft | Bidirectional call/event protocol, OperationEnv, three dispatch paths |
|
| [call-protocol.md](call-protocol.md) | draft | Bidirectional call/event protocol, OperationEnv, three dispatch paths |
|
||||||
| [client.md](client.md) | reviewed | Client connection, SOCKS5, port forwarding |
|
| [client.md](client.md) | reviewed | Client connection, SOCKS5, port forwarding |
|
||||||
| [server.md](server.md) | reviewed | Server acceptance, IdentityProvider, ForwardingPolicy, channel handling |
|
| [server.md](server.md) | reviewed | Server acceptance, IdentityProvider, ForwardingPolicy, channel handling |
|
||||||
@@ -29,11 +32,13 @@ coexistence), OQ-19 (WebTransport TLS), OQ-20 (worker registration), OQ-IF-01
|
|||||||
| [napi-and-pubsub.md](napi-and-pubsub.md) | reviewed | NAPI wrapper, reload API, pubsub event target adapter |
|
| [napi-and-pubsub.md](napi-and-pubsub.md) | reviewed | NAPI wrapper, reload API, pubsub event target adapter |
|
||||||
| [identity.md](identity.md) | draft | Identity type, IdentityProvider trait, auth flows |
|
| [identity.md](identity.md) | draft | Identity type, IdentityProvider trait, auth flows |
|
||||||
| [services.md](services.md) | draft | irpc service layer, OperationEnv, three dispatch paths |
|
| [services.md](services.md) | draft | irpc service layer, OperationEnv, three dispatch paths |
|
||||||
| [interface.md](interface.md) | draft | Layer 2: Interface trait, SshInterface, RawFramingInterface |
|
| [interface.md](interface.md) | draft | StreamInterface, MessageInterface, credential presentation, ListenerConfig |
|
||||||
| [configuration.md](configuration.md) | draft | StaticConfig, DynamicConfig, forwarding policy, reload |
|
| [configuration.md](configuration.md) | draft | StaticConfig, DynamicConfig, API keys, forwarding policy, reload |
|
||||||
| [storage.md](storage.md) | draft | alknet-storage: metagraph, identity, ACL, honker |
|
| [storage.md](storage.md) | draft | alknet-storage: metagraph, identity, ACL, honker |
|
||||||
| [flowgraph.md](flowgraph.md) | draft | alknet-flowgraph: call graph, operation graph, petgraph |
|
| [flowgraph.md](flowgraph.md) | draft | alknet-flowgraph: call graph, operation graph, petgraph |
|
||||||
| [secret-service.md](secret-service.md) | draft | alknet-secret: BIP39, SLIP-0010, AES-GCM, SecretProtocol |
|
| [secret-service.md](secret-service.md) | draft | alknet-secret: BIP39, SLIP-0010, AES-GCM, SecretProtocol |
|
||||||
|
| [credentials.md](credentials.md) | draft | CredentialProvider, CredentialSet (outbound auth) |
|
||||||
|
| [definitions.md](definitions.md) | draft | Terminology disambiguation and concept mapping |
|
||||||
|
|
||||||
## Research Documents
|
## Research Documents
|
||||||
|
|
||||||
@@ -48,6 +53,10 @@ coexistence), OQ-19 (WebTransport TLS), OQ-20 (worker registration), OQ-IF-01
|
|||||||
| [feasibility/](../research/feasibility/) | — | SSH tunnel feasibility assessment and related analyses |
|
| [feasibility/](../research/feasibility/) | — | SSH tunnel feasibility assessment and related analyses |
|
||||||
| [event-sourcing/](../research/event-sourcing/) | — | Event sourcing patterns and event-driven architecture reference |
|
| [event-sourcing/](../research/event-sourcing/) | — | Event sourcing patterns and event-driven architecture reference |
|
||||||
| [ops/](../research/ops/) | — | Production ops reference: certbot, fail2ban |
|
| [ops/](../research/ops/) | — | Production ops reference: certbot, fail2ban |
|
||||||
|
| [phase2/definitions.md](../research/phase2/definitions.md) | draft | Terminology disambiguation (promoted to architecture/definitions.md) |
|
||||||
|
| [phase2/interface-model.md](../research/phase2/interface-model.md) | draft | StreamInterface/MessageInterface analysis (promoted to interface.md) |
|
||||||
|
| [phase2/credential-provider.md](../research/phase2/credential-provider.md) | draft | CredentialProvider research (promoted to credentials.md) |
|
||||||
|
| [phase2/tls-transport.md](../research/phase2/tls-transport.md) | draft | HTTP interface, stealth handoff, ListenerConfig (promoted to interface.md, auth.md) |
|
||||||
|
|
||||||
## ADR Table
|
## ADR Table
|
||||||
|
|
||||||
@@ -84,6 +93,9 @@ coexistence), OQ-19 (WebTransport TLS), OQ-20 (worker registration), OQ-IF-01
|
|||||||
| [032](decisions/032-event-boundary-discipline.md) | Event boundary discipline (domain, irpc, call protocol) | Accepted |
|
| [032](decisions/032-event-boundary-discipline.md) | Event boundary discipline (domain, irpc, call protocol) | Accepted |
|
||||||
| [033](decisions/033-operationenv-irpc-call-protocol.md) | OperationEnv as universal composition mechanism | Accepted |
|
| [033](decisions/033-operationenv-irpc-call-protocol.md) | OperationEnv as universal composition mechanism | Accepted |
|
||||||
| [034](decisions/034-head-worker-terminology.md) | Head/worker terminology replacing hub/spoke | Accepted |
|
| [034](decisions/034-head-worker-terminology.md) | Head/worker terminology replacing hub/spoke | Accepted |
|
||||||
|
| [035](decisions/035-streaminterface-messageinterface-split.md) | StreamInterface / MessageInterface split | Accepted |
|
||||||
|
| [036](decisions/036-credentialprovider-core-type.md) | CredentialProvider as core type (outbound auth) | Accepted |
|
||||||
|
| [037](decisions/037-api-keys-dynamic-config.md) | API keys as DynamicConfig auth | Accepted |
|
||||||
|
|
||||||
> ADR numbers 020–022 were allocated to proposals that were withdrawn before
|
> ADR numbers 020–022 were allocated to proposals that were withdrawn before
|
||||||
> acceptance and are not listed.
|
> acceptance and are not listed.
|
||||||
@@ -93,15 +105,16 @@ coexistence), OQ-19 (WebTransport TLS), OQ-20 (worker registration), OQ-IF-01
|
|||||||
See [open-questions.md](open-questions.md) for all open and resolved questions.
|
See [open-questions.md](open-questions.md) for all open and resolved questions.
|
||||||
Key resolved questions from Phase 0: OQ-12, OQ-16, OQ-18 (forwarding policy
|
Key resolved questions from Phase 0: OQ-12, OQ-16, OQ-18 (forwarding policy
|
||||||
and identity scopes), OQ-17 (transport-aware auth), OQ-23 (irpc feature flag),
|
and identity scopes), OQ-17 (transport-aware auth), OQ-23 (irpc feature flag),
|
||||||
OQ-24 (DNS control channel scope), OQ-25 (crate irpc dependencies). Key open
|
OQ-24 (DNS control channel scope), OQ-25 (crate irpc dependencies), OQ-IF-01
|
||||||
questions: OQ-15 (QUIC coexistence), OQ-19 (WebTransport TLS), OQ-20 (worker
|
(Interface session / EventEnvelope relationship), OQ-IF-02 (ForwardingPolicy
|
||||||
registration).
|
placement). Key open questions: OQ-15 (QUIC coexistence), OQ-19 (WebTransport
|
||||||
|
TLS), OQ-20 (worker registration).
|
||||||
|
|
||||||
## Lifecycle Definitions
|
## Lifecycle Definitions
|
||||||
|
|
||||||
| Status | Meaning | Transitions |
|
| Status | Meaning | Transitions |
|
||||||
|--------|---------|-------------|
|
|--------|---------|-------------|
|
||||||
| `draft` | Under active development. May change significantly. | → `reviewed` when open questions resolved |
|
| `draft` | Under active development. May change significantly. | → `reviewed` when open questions resolved |
|
||||||
| `reviewed` | Architecture final. Implementation may begin. Changes require review. | → `stable` when implementation verified |
|
| `reviewed` | Architecture final. Implementation may begin. Changes require review. | → `stable` when implementation is complete and verified |
|
||||||
| `stable` | Locked. Changes require review and may warrant an ADR. | → `deprecated` when superseded |
|
| `stable` | Locked. Changes require review and may warrant an ADR. | → `deprecated` when superseded |
|
||||||
| `deprecated` | Superseded. Kept for reference. | Removed when no longer referenced |
|
| `deprecated` | Superseded. Kept for reference. | Removed when no longer referenced |
|
||||||
@@ -42,16 +42,30 @@ is the default implementation (reads from `DynamicConfig.auth`). `AuthProtocol`
|
|||||||
irpc service is one way to satisfy the trait, behind a feature flag. Both paths
|
irpc service is one way to satisfy the trait, behind a feature flag. Both paths
|
||||||
produce the same `Identity` result. See ADR-028 and ADR-029.
|
produce the same `Identity` result. See ADR-028 and ADR-029.
|
||||||
|
|
||||||
### Auth Presentation Per Transport
|
### Credential Presentation Per Interface
|
||||||
|
|
||||||
| Transport | Auth presentation | Verification |
|
Each (Transport, Interface) pair presents credentials differently, but all
|
||||||
|-----------|-------------------|-------------|
|
resolve to the same `Identity` through `IdentityProvider`. See
|
||||||
| SSH (TCP, TLS, iroh) | SSH public key auth in the SSH handshake | `ServerAuthConfig::authenticate_publickey()` — key lookup in authorized set |
|
[definitions.md](definitions.md) for the full terminology rules.
|
||||||
| WebTransport (HTTP/3) | Signed timestamp token in CONNECT request | Token auth — same authorized set verifies the Ed25519 signature |
|
|
||||||
| Future (WebSocket, etc.) | Signed timestamp token in headers/query | Same token verification |
|
|
||||||
|
|
||||||
The **key material is shared**. The **presentation differs per transport**. The
|
| (Transport, Interface) | Credential presentation | Resolves via |
|
||||||
**verification result is the same**: an authenticated identity with scopes.
|
|------------------------|------------------------|-------------|
|
||||||
|
| (TLS, SshInterface) | SSH public key handshake | `resolve_from_fingerprint()` |
|
||||||
|
| (TCP, SshInterface) | SSH public key handshake | `resolve_from_fingerprint()` |
|
||||||
|
| (iroh, SshInterface) | SSH public key handshake | `resolve_from_fingerprint()` |
|
||||||
|
| (TLS, RawFramingInterface) | AuthToken in frame header | `resolve_from_token()` |
|
||||||
|
| (TCP, RawFramingInterface) | AuthToken in frame header | `resolve_from_token()` |
|
||||||
|
| (WebTransport, RawFramingInterface) | AuthToken in CONNECT request | `resolve_from_token()` |
|
||||||
|
| (—, HttpInterface) | `Authorization: Bearer` header | `resolve_from_token()` |
|
||||||
|
| (—, DnsInterface) | AuthToken in query labels | `resolve_from_token()` |
|
||||||
|
|
||||||
|
The **key material is shared**. The **credential presentation** differs per
|
||||||
|
(Transport, Interface) pair. The **verification result is the same**: an
|
||||||
|
authenticated `Identity` with scopes.
|
||||||
|
|
||||||
|
`resolve_from_token()` handles both AuthTokens (Ed25519-signed) and API keys
|
||||||
|
(hash-verified bearer tokens). The implementation discriminates by prefix or
|
||||||
|
format — see ADR-037.
|
||||||
|
|
||||||
### Token Authentication
|
### Token Authentication
|
||||||
|
|
||||||
@@ -112,14 +126,46 @@ irpc path produce the same `Identity` result.
|
|||||||
The trait is the contract. The backing store is pluggable. Alknet-core never
|
The trait is the contract. The backing store is pluggable. Alknet-core never
|
||||||
depends on Honker, SQLite, or any specific database.
|
depends on Honker, SQLite, or any specific database.
|
||||||
|
|
||||||
|
### API Keys
|
||||||
|
|
||||||
|
For service accounts, automation, and HTTP interface auth, Ed25519 AuthTokens
|
||||||
|
are inconvenient — they require client-side key generation and signing. API keys
|
||||||
|
provide a simpler bearer token format (ADR-037):
|
||||||
|
|
||||||
|
```
|
||||||
|
API key: "alk_dGhlX3NlY3JldA" (~20 chars, configurable prefix)
|
||||||
|
Storage: SHA-256 hash of the full key
|
||||||
|
Lookup: prefix match → hash verification → Identity
|
||||||
|
```
|
||||||
|
|
||||||
|
API keys are configured in `DynamicConfig.auth.api_keys`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[auth.api_keys]]
|
||||||
|
prefix = "alk_"
|
||||||
|
hash = "sha256:abc..."
|
||||||
|
scopes = ["relay:connect"]
|
||||||
|
description = "dashboard service account"
|
||||||
|
ttl = "30d" # optional
|
||||||
|
```
|
||||||
|
|
||||||
|
Both AuthTokens and API keys go through `IdentityProvider::resolve_from_token()`.
|
||||||
|
The implementation discriminates by prefix (default `alk_`): if the token starts
|
||||||
|
with the API key prefix, it's verified by SHA-256 hash lookup; otherwise, it's
|
||||||
|
verified as an Ed25519 AuthToken. Both paths produce the same `Identity`.
|
||||||
|
|
||||||
|
See [configuration.md](configuration.md) for the full `DynamicConfig.auth`
|
||||||
|
structure and ADR-037 for the decision context.
|
||||||
|
|
||||||
### AuthPolicy Structure
|
### AuthPolicy Structure
|
||||||
|
|
||||||
`AuthPolicy` in `DynamicConfig` holds both auth paths, sharing key material:
|
`AuthPolicy` in `DynamicConfig` holds all auth paths, sharing key material:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
pub struct AuthPolicy {
|
pub struct AuthPolicy {
|
||||||
pub ssh: SshAuthConfig,
|
pub ssh: SshAuthConfig,
|
||||||
pub token: TokenAuthConfig,
|
pub token: TokenAuthConfig,
|
||||||
|
pub api_keys: Vec<ApiKeyEntry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SshAuthConfig {
|
pub struct SshAuthConfig {
|
||||||
@@ -142,6 +188,14 @@ pub enum TokenKeySource {
|
|||||||
/// For deployments that want distinct access control per transport.
|
/// For deployments that want distinct access control per transport.
|
||||||
Separate(HashSet<PublicKey>),
|
Separate(HashSet<PublicKey>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct ApiKeyEntry {
|
||||||
|
pub prefix: String, // e.g., "alk_"
|
||||||
|
pub hash: String, // e.g., "sha256:abc..."
|
||||||
|
pub scopes: Vec<String>, // e.g., ["relay:connect", "secrets:derive"]
|
||||||
|
pub description: Option<String>, // e.g., "dashboard service account"
|
||||||
|
pub expires_at: Option<u64>, // Unix timestamp, optional TTL
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
When `TokenKeySource::Shared` (the default), adding a key to
|
When `TokenKeySource::Shared` (the default), adding a key to
|
||||||
@@ -220,6 +274,13 @@ dependencies needed.
|
|||||||
- Token auth is only available on transports that carry HTTP metadata (URL
|
- Token auth is only available on transports that carry HTTP metadata (URL
|
||||||
path, headers). SSH-over-TCP/TLS/iroh continues to use SSH native auth
|
path, headers). SSH-over-TCP/TLS/iroh continues to use SSH native auth
|
||||||
exclusively.
|
exclusively.
|
||||||
|
- API keys are bearer tokens — anyone who obtains the key has the associated
|
||||||
|
permissions. The hash storage and optional TTL mitigate but do not eliminate
|
||||||
|
this risk. Ed25519 AuthTokens remain the preferred auth method for interactive
|
||||||
|
clients. See ADR-037.
|
||||||
|
- API keys are verified by SHA-256 hash lookup in `DynamicConfig.auth.api_keys`
|
||||||
|
(or the `api_keys` database table in production). The full key is provided to
|
||||||
|
the client exactly once at creation time.
|
||||||
|
|
||||||
### Security Considerations
|
### Security Considerations
|
||||||
|
|
||||||
@@ -254,6 +315,8 @@ security consideration:
|
|||||||
| [023](decisions/023-unified-auth-shared-key-material.md) | Unified auth, shared key material | Same keys for SSH and token auth |
|
| [023](decisions/023-unified-auth-shared-key-material.md) | Unified auth, shared key material | Same keys for SSH and token auth |
|
||||||
| [028](decisions/028-auth-irpc-service.md) | Auth as irpc service | AuthProtocol behind feature flag; IdentityProvider is the contract |
|
| [028](decisions/028-auth-irpc-service.md) | Auth as irpc service | AuthProtocol behind feature flag; IdentityProvider is the contract |
|
||||||
| [029](decisions/029-identity-core-type.md) | Identity as core type | `Identity` and `IdentityProvider` in alknet-core |
|
| [029](decisions/029-identity-core-type.md) | Identity as core type | `Identity` and `IdentityProvider` in alknet-core |
|
||||||
|
| [035](decisions/035-streaminterface-messageinterface-split.md) | StreamInterface/MessageInterface | Credential presentation differs per (Transport, Interface) pair |
|
||||||
|
| [037](decisions/037-api-keys-dynamic-config.md) | API keys in DynamicConfig | Hash-verified bearer tokens for service accounts |
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
@@ -261,6 +324,8 @@ security consideration:
|
|||||||
- [server.md](server.md) — Current SSH auth handler
|
- [server.md](server.md) — Current SSH auth handler
|
||||||
- [transport.md](transport.md) — Transport abstraction
|
- [transport.md](transport.md) — Transport abstraction
|
||||||
- [configuration.md](configuration.md) — DynamicConfig, AuthPolicy, ConfigReloadHandle
|
- [configuration.md](configuration.md) — DynamicConfig, AuthPolicy, ConfigReloadHandle
|
||||||
|
- [interface.md](interface.md) — Credential presentation per (Transport, Interface) pair
|
||||||
|
- [definitions.md](definitions.md) — Terminology disambiguation (IdentityProvider vs CredentialProvider, AuthToken vs API key)
|
||||||
- [services.md](services.md) — AuthProtocol irpc service
|
- [services.md](services.md) — AuthProtocol irpc service
|
||||||
- [open-questions.md](open-questions.md) — OQ-17 (resolved), OQ-18 (resolved), OQ-19
|
- [open-questions.md](open-questions.md) — OQ-17 (resolved), OQ-18 (resolved), OQ-19
|
||||||
- [wtransport](https://github.com/BiagioFesta/wtransport) — Rust WebTransport library
|
- [wtransport](https://github.com/BiagioFesta/wtransport) — Rust WebTransport library
|
||||||
|
|||||||
@@ -311,8 +311,18 @@ periodically.
|
|||||||
|
|
||||||
### Protocol Adapter Layer
|
### Protocol Adapter Layer
|
||||||
|
|
||||||
The call protocol is transport-agnostic by design. It maps to any transport
|
The call protocol is transport-agnostic and interface-agnostic by design. It
|
||||||
that carries `EventEnvelope` frames:
|
receives input from two interface categories (ADR-035):
|
||||||
|
|
||||||
|
**StreamInterface** produces `InterfaceEvent` frames from a continuous byte
|
||||||
|
stream (SSH channel, raw framing). The call protocol handler calls `recv()`
|
||||||
|
on the session to get events.
|
||||||
|
|
||||||
|
**MessageInterface** handles individual `InterfaceRequest` → `InterfaceResponse`
|
||||||
|
pairs (HTTP, DNS). The call protocol handler constructs an `OperationContext`
|
||||||
|
from the request and invokes the registry directly.
|
||||||
|
|
||||||
|
Both paths resolve to the same `OperationRegistry` and `OperationEnv`:
|
||||||
|
|
||||||
| Transport | Channel mechanism | Direction |
|
| Transport | Channel mechanism | Direction |
|
||||||
|-----------|-------------------|-----------|
|
|-----------|-------------------|-----------|
|
||||||
@@ -494,9 +504,16 @@ agent service itself is built on top, not into the core.
|
|||||||
in gRPC terms)?~~ Resolved — deferred. Current model covers all identified use
|
in gRPC terms)?~~ Resolved — deferred. Current model covers all identified use
|
||||||
cases. See [open-questions.md](open-questions.md).
|
cases. See [open-questions.md](open-questions.md).
|
||||||
|
|
||||||
- **OQ-IF-01**: How does the `Interface` session type relate to the call
|
- **~~OQ-IF-01~~**: ~~How does the `Interface` session type relate to the call
|
||||||
protocol's `EventEnvelope` stream? This needs design during Phase 1.8
|
protocol's `EventEnvelope` stream?~~ Resolved — `InterfaceSession::recv()`
|
||||||
implementation. See [open-questions.md](open-questions.md).
|
returns `Option<InterfaceEvent>` where `InterfaceEvent` carries
|
||||||
|
`EventEnvelope` + `Identity`. `InterfaceSession::send()` accepts `EventEnvelope`.
|
||||||
|
The `SshSession` bridge implements this over the `alknet-control:0` channel.
|
||||||
|
For `MessageInterface`, `InterfaceRequest`/`InterfaceResponse` normalize
|
||||||
|
request/response pairs. See [interface.md](interface.md) and ADR-035.
|
||||||
|
|
||||||
|
- **OQ-P2-01**: Should `MessageInterface` and `StreamInterface` share a common
|
||||||
|
trait? See [interface.md](interface.md) and [open-questions.md](open-questions.md).
|
||||||
|
|
||||||
## Design Decisions
|
## Design Decisions
|
||||||
|
|
||||||
@@ -507,6 +524,7 @@ agent service itself is built on top, not into the core.
|
|||||||
| [025](decisions/025-handler-spec-separation.md) | Handler/spec separation | Downstream registers operations without modifying core |
|
| [025](decisions/025-handler-spec-separation.md) | Handler/spec separation | Downstream registers operations without modifying core |
|
||||||
| [028](decisions/028-auth-irpc-service.md) | Auth as irpc service | irpc is one dispatch backend for OperationEnv |
|
| [028](decisions/028-auth-irpc-service.md) | Auth as irpc service | irpc is one dispatch backend for OperationEnv |
|
||||||
| [033](decisions/033-operationenv-irpc-call-protocol.md) | OperationEnv | Universal composition with three dispatch paths |
|
| [033](decisions/033-operationenv-irpc-call-protocol.md) | OperationEnv | Universal composition with three dispatch paths |
|
||||||
|
| [035](decisions/035-streaminterface-messageinterface-split.md) | StreamInterface/MessageInterface | Call protocol accepts events from both interface categories |
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,25 @@ Hot-reloadable at runtime via `ArcSwap<DynamicConfig>`. Contains:
|
|||||||
compared to the current approach. Writes are atomic: `store()` swaps the
|
compared to the current approach. Writes are atomic: `store()` swaps the
|
||||||
pointer.
|
pointer.
|
||||||
|
|
||||||
|
### API Keys
|
||||||
|
|
||||||
|
`DynamicConfig.auth` also includes API keys for service accounts and HTTP
|
||||||
|
interface auth (ADR-037):
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[auth.api_keys]]
|
||||||
|
prefix = "alk_"
|
||||||
|
hash = "sha256:abc..."
|
||||||
|
scopes = ["relay:connect"]
|
||||||
|
description = "dashboard service account"
|
||||||
|
ttl = "30d" # optional
|
||||||
|
```
|
||||||
|
|
||||||
|
API keys are verified by `ConfigIdentityProvider::resolve_from_token()` — if
|
||||||
|
the token starts with the configured prefix, it's treated as an API key and
|
||||||
|
verified by SHA-256 hash lookup. Otherwise, it's treated as an Ed25519 AuthToken.
|
||||||
|
Both paths produce the same `Identity` result.
|
||||||
|
|
||||||
### ConfigReloadHandle
|
### ConfigReloadHandle
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
@@ -137,12 +156,67 @@ programmatic API). Covers static config plus initial auth/forwarding paths.
|
|||||||
|
|
||||||
```toml
|
```toml
|
||||||
[server]
|
[server]
|
||||||
|
# Stream-based listener: TLS + SSH on port 443
|
||||||
|
[[listeners]]
|
||||||
|
type = "stream"
|
||||||
transport = "tls"
|
transport = "tls"
|
||||||
|
interface = "ssh"
|
||||||
listen = "0.0.0.0:443"
|
listen = "0.0.0.0:443"
|
||||||
|
|
||||||
|
[server.tls]
|
||||||
|
cert = "/etc/alknet/tls/cert.pem"
|
||||||
|
key = "/etc/alknet/tls/key.pem"
|
||||||
|
|
||||||
|
# Stream-based listener: TCP + SSH on port 22
|
||||||
|
[[listeners]]
|
||||||
|
type = "stream"
|
||||||
|
transport = "tcp"
|
||||||
|
interface = "ssh"
|
||||||
|
listen = "0.0.0.0:22"
|
||||||
|
|
||||||
|
# Stream-based listener: iroh P2P
|
||||||
|
[[listeners]]
|
||||||
|
type = "stream"
|
||||||
|
transport = "iroh"
|
||||||
|
iroh_relay = "https://relay.alk.dev"
|
||||||
|
|
||||||
|
# Message-based listener: HTTP on port 443 (with stealth)
|
||||||
|
[[listeners]]
|
||||||
|
type = "http"
|
||||||
|
listen = "0.0.0.0:443"
|
||||||
|
tls = true
|
||||||
|
stealth = true
|
||||||
|
|
||||||
|
# Message-based listener: HTTP on port 8080 (separate, no stealth)
|
||||||
|
# [[listeners]]
|
||||||
|
# type = "http"
|
||||||
|
# listen = "0.0.0.0:8080"
|
||||||
|
# tls = false
|
||||||
|
# stealth = false
|
||||||
|
|
||||||
|
# Message-based listener: DNS on port 53
|
||||||
|
# [[listeners]]
|
||||||
|
# type = "dns"
|
||||||
|
# listen = "0.0.0.0:53"
|
||||||
|
# tls = false
|
||||||
|
|
||||||
[auth]
|
[auth]
|
||||||
host_key = "/etc/alknet/ssh/host_key"
|
host_key = "/etc/alknet/ssh/host_key"
|
||||||
|
|
||||||
|
[auth.ssh]
|
||||||
|
authorized_keys = [...]
|
||||||
|
|
||||||
|
[auth.token]
|
||||||
|
enabled = true
|
||||||
|
max_token_age = "5m"
|
||||||
|
|
||||||
|
[[auth.api_keys]]
|
||||||
|
prefix = "alk_"
|
||||||
|
hash = "sha256:abc..."
|
||||||
|
scopes = ["relay:connect"]
|
||||||
|
description = "dashboard service account"
|
||||||
|
ttl = "30d"
|
||||||
|
|
||||||
[forwarding]
|
[forwarding]
|
||||||
default = "deny"
|
default = "deny"
|
||||||
|
|
||||||
@@ -163,10 +237,32 @@ interface AlknetServer {
|
|||||||
|
|
||||||
### Multi-Transport Listeners
|
### Multi-Transport Listeners
|
||||||
|
|
||||||
A head node may accept connections on multiple transports simultaneously. The
|
A head node may accept connections on multiple transports and interfaces simultaneously.
|
||||||
architecture supports `Vec<ListenerConfig>` instead of a single
|
Listeners come in two categories: stream-based (Transport + StreamInterface pairs) and
|
||||||
`ServeTransportMode`. `Server::run()` spawns one accept loop per listener,
|
message-based (self-contained HTTP or DNS servers).
|
||||||
sharing `DynamicConfig`, `ConnectionRateLimiter`, sessions, and shutdown signal.
|
|
||||||
|
```rust
|
||||||
|
pub enum ListenerConfig {
|
||||||
|
Stream {
|
||||||
|
transport: TransportKind,
|
||||||
|
interface: StreamInterfaceKind,
|
||||||
|
},
|
||||||
|
Http {
|
||||||
|
bind_addr: SocketAddr,
|
||||||
|
tls: bool,
|
||||||
|
stealth: bool, // byte-peek protocol detection on shared port
|
||||||
|
},
|
||||||
|
Dns {
|
||||||
|
bind_addr: SocketAddr,
|
||||||
|
tls: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For stream-based listeners, `Server::run()` spawns one accept loop per listener.
|
||||||
|
For HTTP listeners, it spawns an axum server. For DNS listeners, it spawns a DNS
|
||||||
|
server. All share `DynamicConfig`, `ConnectionRateLimiter`, sessions, and
|
||||||
|
shutdown signal.
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[[listeners]]
|
[[listeners]]
|
||||||
|
|||||||
263
docs/architecture/credentials.md
Normal file
263
docs/architecture/credentials.md
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
---
|
||||||
|
status: draft
|
||||||
|
last_updated: 2026-06-09
|
||||||
|
---
|
||||||
|
|
||||||
|
# Credentials (Outbound Auth)
|
||||||
|
|
||||||
|
## What
|
||||||
|
|
||||||
|
The `CredentialProvider` trait and `CredentialSet` enum handle **outbound**
|
||||||
|
authentication: how alknet authenticates _to_ external and self-hosted services.
|
||||||
|
This is the complement to `IdentityProvider`, which handles **inbound**
|
||||||
|
authentication (who is calling alknet).
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
Without `CredentialProvider`, each service wrapper would independently solve
|
||||||
|
credential retrieval, caching, and lifecycle management. Cloud API integrations
|
||||||
|
(vast.ai, runpod) need API keys. Self-hosted services (rustfs, gitea) need
|
||||||
|
S3 access keys or OIDC tokens. The secret service can store these at rest, but
|
||||||
|
the wiring between "decrypt a credential from storage" and "use it in an HTTP
|
||||||
|
request" doesn't exist yet.
|
||||||
|
|
||||||
|
`CredentialProvider` provides a unified abstraction — just as `IdentityProvider`
|
||||||
|
unifies inbound auth, `CredentialProvider` unifies outbound auth. Handlers
|
||||||
|
access credentials through `OperationEnv`, not by reaching into storage directly.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Direction: Inbound vs Outbound
|
||||||
|
|
||||||
|
| | IdentityProvider | CredentialProvider |
|
||||||
|
|---|---|---|
|
||||||
|
| **Direction** | Inbound (who is calling alknet) | Outbound (how alknet calls others) |
|
||||||
|
| **Resolves** | Fingerprint/token → `Identity` | Service name → `CredentialSet` |
|
||||||
|
| **Storage** | `peer_credentials`, `api_keys` | Encrypted nodes in metagraph |
|
||||||
|
| **Lifecycle** | Stateless lookup | May need refresh (OIDC tokens, S3 sessions) |
|
||||||
|
| **Location** | `alknet_core::auth` | `alknet_core::credentials` |
|
||||||
|
|
||||||
|
Both live at the same architectural layer. A handler receives an
|
||||||
|
`OperationContext` with `identity` (who called us) and can access credentials
|
||||||
|
through `context.env` (how we call out).
|
||||||
|
|
||||||
|
### CredentialProvider Trait
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub trait CredentialProvider: Send + Sync + 'static {
|
||||||
|
fn get_credentials(&self, service: &str) -> Option<CredentialSet>;
|
||||||
|
fn refresh_credentials(&self, service: &str) -> Option<CredentialSet>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The trait is intentionally narrow. It returns credentials for a named service.
|
||||||
|
It does not abstract the auth mechanism — that stays with the service wrapper
|
||||||
|
that knows the protocol (S3 signing, OAuth2 refresh, etc.).
|
||||||
|
|
||||||
|
### CredentialSet
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub enum CredentialSet {
|
||||||
|
ApiKey {
|
||||||
|
header_name: String,
|
||||||
|
token: String,
|
||||||
|
},
|
||||||
|
Basic {
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
},
|
||||||
|
Bearer {
|
||||||
|
token: String,
|
||||||
|
},
|
||||||
|
S3AccessKey {
|
||||||
|
access_key: String,
|
||||||
|
secret_key: String,
|
||||||
|
session_token: Option<String>,
|
||||||
|
},
|
||||||
|
OidcToken {
|
||||||
|
access_token: String,
|
||||||
|
refresh_token: Option<String>,
|
||||||
|
expires_at: Option<u64>,
|
||||||
|
},
|
||||||
|
Custom {
|
||||||
|
scheme: String,
|
||||||
|
params: HashMap<String, String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Each variant carries the data needed for a specific auth mechanism. The service
|
||||||
|
wrapper that requested the credentials knows what variant it expects and how to
|
||||||
|
use it.
|
||||||
|
|
||||||
|
### CredentialProvider vs IdentityProvider
|
||||||
|
|
||||||
|
These are opposite-direction abstractions that compose through `OperationEnv`:
|
||||||
|
|
||||||
|
```
|
||||||
|
Incoming Request
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
IdentityProvider (credential → Identity)
|
||||||
|
│
|
||||||
|
├── SSH fingerprint → Identity.id, .scopes, .resources
|
||||||
|
├── Bearer AuthToken → Identity.id, .scopes, .resources
|
||||||
|
└── API key → Identity.id, .scopes, .resources
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
OperationContext { identity, env, ... }
|
||||||
|
│
|
||||||
|
├── context.env.invoke("git", "push", input)
|
||||||
|
│ └── GitService handler
|
||||||
|
│ └── CredentialProvider (outbound)
|
||||||
|
│ └── get_credentials("rustfs")
|
||||||
|
│ └── S3AccessKey { access_key, secret_key }
|
||||||
|
│
|
||||||
|
└── context.env.invoke("secrets", "derive", input)
|
||||||
|
└── local dispatch to SecretProtocol
|
||||||
|
|
||||||
|
Two directions: Inbound (who is calling us)
|
||||||
|
Outbound (how we call others)
|
||||||
|
```
|
||||||
|
|
||||||
|
### SecretStoreCredentialProvider (Phase 1 Default)
|
||||||
|
|
||||||
|
The default `CredentialProvider` implementation. Decrypts credentials via
|
||||||
|
`SecretProtocol::Decrypt` and holds them in RAM:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct SecretStoreCredentialProvider {
|
||||||
|
credentials: ArcSwap<HashMap<String, CredentialSet>>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
At startup, the CLI or NAPI assembly loads credentials from the secret service
|
||||||
|
and populates the `ArcSwap`. The `refresh_credentials()` method re-decrypts
|
||||||
|
after a `Lock`/`Unlock` cycle on the secret service.
|
||||||
|
|
||||||
|
### ManagedCredentialProvider (Phase C Future)
|
||||||
|
|
||||||
|
For self-hosted services that need active lifecycle management (S3 session
|
||||||
|
token rotation, OIDC token refresh). Wraps `SecretStoreCredentialProvider`
|
||||||
|
with per-service `CredentialManager` instances:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct ManagedCredentialProvider {
|
||||||
|
base: SecretStoreCredentialProvider,
|
||||||
|
managers: HashMap<String, Arc<dyn CredentialManager>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait CredentialManager: Send + Sync + 'static {
|
||||||
|
fn refresh(&self, current: &CredentialSet) -> Option<CredentialSet>;
|
||||||
|
fn is_expired(&self, current: &CredentialSet) -> bool;
|
||||||
|
fn provision(&self, identity: &Identity) -> Option<CredentialSet>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `refresh`: OIDC token refresh, S3 session token rotation
|
||||||
|
- `is_expired`: Check TTL before use
|
||||||
|
- `provision`: Create credentials on a self-hosted service for a given identity
|
||||||
|
|
||||||
|
This is a Phase C concept. The spec defines the extension point but defers
|
||||||
|
implementation.
|
||||||
|
|
||||||
|
### Integration with OperationEnv
|
||||||
|
|
||||||
|
Handlers access credentials through `OperationEnv`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Handler needs outbound credentials for a service
|
||||||
|
let creds = context.env.get_credentials("rustfs");
|
||||||
|
```
|
||||||
|
|
||||||
|
This is analogous to how `context.env.invoke(namespace, op, input)` works for
|
||||||
|
operation dispatch — the handler doesn't know whether the credential comes from
|
||||||
|
config, the secret service, or a managed provider.
|
||||||
|
|
||||||
|
### Integration with SecretProtocol
|
||||||
|
|
||||||
|
Credentials are stored encrypted in the metagraph via `SecretProtocol`:
|
||||||
|
|
||||||
|
1. Operator configures credentials: `alknet credential add vast-ai --type bearer --token-file ./key.txt`
|
||||||
|
2. CLI encrypts via `SecretProtocol::Encrypt` (AES-256-GCM, key at path `m/74'/2'/0'/0'`)
|
||||||
|
3. Encrypted credential stored as `EncryptedData` node in metagraph, tagged with service name
|
||||||
|
4. At startup, `SecretStoreCredentialProvider` calls `SecretProtocol::Decrypt` for each configured service
|
||||||
|
5. Decrypted credentials held in RAM with same lifecycle as the seed (purged on `Lock`)
|
||||||
|
|
||||||
|
The `EncryptedData` wire format is shared with alknet-storage by type-level
|
||||||
|
compatibility, not a crate dependency.
|
||||||
|
|
||||||
|
### Identity-Bound Credentials (Phase B+ Future)
|
||||||
|
|
||||||
|
For multi-tenant setups where different alknet users have different access levels
|
||||||
|
on the same external service:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Service-level credential (all users share one key):
|
||||||
|
credential_provider.get_credentials("rustfs")
|
||||||
|
|
||||||
|
// Identity-bound credential (per-user key):
|
||||||
|
credential_provider.get_credentials_for("rustfs", &identity.id)
|
||||||
|
```
|
||||||
|
|
||||||
|
The trait-level method is service-level. The identity-bound method is an
|
||||||
|
extension in alknet-storage that uses `Identity.id` (the account UUID in
|
||||||
|
database-backed deployments) as the lookup key. No separate `account_id` field
|
||||||
|
needed — `Identity.id` IS the account identifier.
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
- `CredentialProvider` and `CredentialSet` live in `alknet_core::credentials`.
|
||||||
|
No database dependency at the core level.
|
||||||
|
- `CredentialProvider` does not depend on `IdentityProvider`. They compose
|
||||||
|
through `OperationEnv`, not through dependency.
|
||||||
|
- `ManagedCredentialProvider` and `CredentialManager` are Phase C concepts.
|
||||||
|
They are defined as extension points but not implemented yet.
|
||||||
|
- Identity-bound credentials use `Identity.id` as the account key. In
|
||||||
|
config-backed deployments, this is the fingerprint or key prefix. In
|
||||||
|
database-backed deployments, this is the account UUID.
|
||||||
|
- `SecretStoreCredentialProvider` depends on `SecretProtocol::Decrypt`, which
|
||||||
|
requires the alknet-secret crate. A stub impl that reads from config is
|
||||||
|
sufficient for Phase 2 when alknet-secret isn't available.
|
||||||
|
- The `CredentialSet` variants cover all identified credential types (Phases
|
||||||
|
A–C). Phase D (alknet as OIDC provider) is additive.
|
||||||
|
|
||||||
|
## Phase Progression
|
||||||
|
|
||||||
|
| Phase | CredentialProvider Scope | Notes |
|
||||||
|
|-------|-------------------------|-------|
|
||||||
|
| Phase 2 (now) | Trait + `CredentialSet` in core. `SecretStoreCredentialProvider` stub reads from config. | Enables Phase 2 HTTP auth |
|
||||||
|
| Phase A | `SecretStoreCredentialProvider` backed by `SecretProtocol::Decrypt`. CLI command for credential management. | Full secret service integration |
|
||||||
|
| Phase B | `FromOpenAPI` integration. `CredentialProvider` populates `HttpServiceConfig.auth`. | Auto-registration of external services |
|
||||||
|
| Phase C | `ManagedCredentialProvider` + `CredentialManager`. S3 signing, OIDC refresh, identity-bound credentials. | Production self-hosted services |
|
||||||
|
| Phase D | Alknet as OIDC provider. Eliminates stored credentials for OIDC-compatible services. | Long-term goal |
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
- **OQ-CP-01**: Should `CredentialProvider` support per-identity credentials
|
||||||
|
(`get_credentials(service, identity)`)? See [open-questions.md](open-questions.md).
|
||||||
|
|
||||||
|
- **OQ-CP-02**: Where should OIDC provider operations live if alknet becomes
|
||||||
|
an OIDC provider (Phase D)? See [open-questions.md](open-questions.md).
|
||||||
|
|
||||||
|
- **OQ-CP-03**: How do credential rotations propagate across a cluster? See
|
||||||
|
[open-questions.md](open-questions.md).
|
||||||
|
|
||||||
|
- **OQ-CP-04**: Should `CredentialSet` include request-signing capability?
|
||||||
|
See [open-questions.md](open-questions.md).
|
||||||
|
|
||||||
|
## Design Decisions
|
||||||
|
|
||||||
|
| ADR | Decision | Summary |
|
||||||
|
|-----|----------|---------|
|
||||||
|
| [036](decisions/036-credentialprovider-core-type.md) | CredentialProvider as core type | Outbound credentials in `alknet_core::credentials`, parallel to IdentityProvider |
|
||||||
|
| [029](decisions/029-identity-core-type.md) | Identity as core type | Inbound auth — the opposite direction |
|
||||||
|
| [032](decisions/032-event-boundary-discipline.md) | Event boundary | Secret service domain events stay internal |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [identity.md](identity.md) — IdentityProvider (inbound auth, opposite direction)
|
||||||
|
- [secret-service.md](secret-service.md) — SecretProtocol, EncryptedData
|
||||||
|
- [services.md](services.md) — OperationEnv, OperationContext
|
||||||
|
- [definitions.md](definitions.md) — IdentityProvider vs CredentialProvider disambiguation
|
||||||
|
- [research/phase2/credential-provider.md](../research/phase2/credential-provider.md) — Full analysis with rustfs/gitea integration
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# ADR-035: StreamInterface and MessageInterface Split
|
||||||
|
|
||||||
|
## Status
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The `Interface` trait (ADR-026) assumes a persistent byte stream from a `Transport`. It produces a `Session` that yields `InterfaceEvent` frames. This works for SSH and raw framing — both run over duplex streams.
|
||||||
|
|
||||||
|
However, HTTP and DNS do not fit this model. They handle individual request/response pairs, not persistent sessions. HTTP runs over a TLS connection after byte-peek protocol detection (extending the existing stealth mode pattern). DNS runs its own server on port 53. Both are stateless per-request, not session-oriented.
|
||||||
|
|
||||||
|
The three-layer model (Transport, Interface, Protocol) remains correct. The issue is that Layer 2 has two distinct patterns: stream-based (SSH, raw framing) where the transport provides a continuous byte stream, and message-based (HTTP, DNS) where the interface manages its own transport and handles discrete requests.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Split the `Interface` trait into two independent traits:
|
||||||
|
|
||||||
|
1. **`StreamInterface`** — consumes a `TransportStream`, produces a long-lived `Session` that yields `InterfaceEvent` frames. Existing `SshInterface` and `RawFramingInterface` become `StreamInterface` implementations.
|
||||||
|
|
||||||
|
2. **`MessageInterface`** — handles individual `InterfaceRequest` → `InterfaceResponse` pairs. Manages its own transport (HTTP server, DNS server). `HttpInterface` and `DnsInterface` are `MessageInterface` implementations.
|
||||||
|
|
||||||
|
The traits are independent. They have different signatures (`accept(stream)` vs `handle_request(req)`), different lifecycles (long-lived session vs stateless per-request), and different transport ownership (provided by caller vs self-managed).
|
||||||
|
|
||||||
|
`ListenerConfig` gains variants for both:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub enum ListenerConfig {
|
||||||
|
Stream {
|
||||||
|
transport: TransportKind,
|
||||||
|
interface: StreamInterfaceKind,
|
||||||
|
},
|
||||||
|
Http {
|
||||||
|
bind_addr: SocketAddr,
|
||||||
|
tls: bool,
|
||||||
|
stealth: bool,
|
||||||
|
},
|
||||||
|
Dns {
|
||||||
|
bind_addr: SocketAddr,
|
||||||
|
tls: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`TransportKind::Dns` is removed. DNS is a `MessageInterface` that manages its own transport (UDP/TCP port 53), not a transport variant.
|
||||||
|
|
||||||
|
The call protocol handler (Layer 3) is interface-agnostic: it processes `InterfaceEvent` frames from `StreamInterface` sessions and `InterfaceRequest` → `InterfaceResponse` from `MessageInterface` handlers. The dispatch logic is the same — only the framing differs.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
**Positive**: HTTP and DNS are first-class interfaces with proper type signatures. No forcing stateless protocols into a session model. The existing stealth mode byte-peek pattern naturally extends to `HttpInterface`. The `InterfaceRequest` / `InterfaceResponse` types normalize calls across message-based interfaces.
|
||||||
|
|
||||||
|
**Positive**: Removing `TransportKind::Dns` prevents a breaking change later — code should never depend on DNS as a transport variant.
|
||||||
|
|
||||||
|
**Positive**: `ListenerConfig` correctly models the server's accept loop: stream listeners spawn one accept loop per (transport, interface) pair, while HTTP and DNS listeners each manage their own server.
|
||||||
|
|
||||||
|
**Negative**: Two traits where there was one. But they serve fundamentally different purposes. A common super-trait would add complexity (`accept_stream` + `handle_request` + `transport_kind`) without practical benefit — implementations satisfy one trait or the other, never both.
|
||||||
|
|
||||||
|
**Negative**: The `accept()` method on the current `Interface` trait needs to be renamed. This is a rename of an existing method signature, not a semantic change — `SshInterface` and `RawFramingInterface` implementations become `StreamInterface` implementations with the same `accept()` logic.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- ADR-026 (transport/interface separation — updated by this ADR)
|
||||||
|
- [interface.md](../interface.md) — Interface layer spec
|
||||||
|
- [research/phase2/interface-model.md](../../research/phase2/interface-model.md) — Full analysis
|
||||||
|
- [research/phase2/tls-transport.md](../../research/phase2/tls-transport.md) — HTTP interface, ListenerConfig
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# ADR-036: CredentialProvider as Core Type
|
||||||
|
|
||||||
|
## Status
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Alknet's `IdentityProvider` resolves **inbound** authentication: given a
|
||||||
|
credential (fingerprint or token), produce an `Identity`. But there is no
|
||||||
|
corresponding abstraction for **outbound** credentials: how does alknet
|
||||||
|
authenticate _to_ external services (vast.ai, rustfs, gitea)?
|
||||||
|
|
||||||
|
Without `CredentialProvider`, each service wrapper would independently solve
|
||||||
|
credential retrieval, caching, and lifecycle management. This leads to
|
||||||
|
duplicated effort and inconsistent security practices across service wrappers.
|
||||||
|
|
||||||
|
The pattern mirrors the existing `IdentityProvider` pattern: trait in core,
|
||||||
|
default impl using simple storage, production impl using the secret service
|
||||||
|
and database.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Define `CredentialProvider` trait and `CredentialSet` enum in
|
||||||
|
`alknet_core::credentials`.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub trait CredentialProvider: Send + Sync + 'static {
|
||||||
|
fn get_credentials(&self, service: &str) -> Option<CredentialSet>;
|
||||||
|
fn refresh_credentials(&self, service: &str) -> Option<CredentialSet>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum CredentialSet {
|
||||||
|
ApiKey { header_name: String, token: String },
|
||||||
|
Basic { username: String, password: String },
|
||||||
|
Bearer { token: String },
|
||||||
|
S3AccessKey { access_key: String, secret_key: String, session_token: Option<String> },
|
||||||
|
OidcToken { access_token: String, refresh_token: Option<String>, expires_at: Option<u64> },
|
||||||
|
Custom { scheme: String, params: HashMap<String, String> },
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The trait is intentionally narrow. It returns credentials for a named service.
|
||||||
|
It does not try to abstract the auth mechanism itself — that stays with the
|
||||||
|
service wrapper that knows the protocol (S3 signing, OAuth2 refresh, etc.).
|
||||||
|
|
||||||
|
Phase 1 provides `SecretStoreCredentialProvider` (reads from
|
||||||
|
`SecretProtocol::Decrypt`, holds in RAM). Phase 2+ adds
|
||||||
|
`ManagedCredentialProvider` (with `CredentialManager` for lifecycle management:
|
||||||
|
refresh, expiration, provisioning).
|
||||||
|
|
||||||
|
`CredentialProvider` does not depend on `IdentityProvider`, though
|
||||||
|
`ManagedCredentialProvider` may use `Identity.id` for identity-bound credential
|
||||||
|
lookups.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
**Positive**: Outbound auth has a unified abstraction, just as inbound auth
|
||||||
|
has `IdentityProvider`. Service wrappers retrieve credentials through one
|
||||||
|
interface. `OperationEnv` can expose credentials through `context.env`.
|
||||||
|
|
||||||
|
**Positive**: The `CredentialSet` enum covers all identified credential types
|
||||||
|
(API keys, bearer tokens, S3 access keys, OIDC tokens, basic auth, custom).
|
||||||
|
This is sufficient for Phases A-C. Phase D (alknet as OIDC provider) is additive.
|
||||||
|
|
||||||
|
**Positive**: The trait in core, impl in service crate pattern is consistent
|
||||||
|
with `IdentityProvider` (trait in core, `ConfigIdentityProvider` in core,
|
||||||
|
`StorageIdentityProvider` in alknet-storage).
|
||||||
|
|
||||||
|
**Negative**: Adds a new core type and a new module (`credentials`). But this
|
||||||
|
is the same pattern as `IdentityProvider` and `auth` — a small, narrow trait
|
||||||
|
with a clear contract.
|
||||||
|
|
||||||
|
**Negative**: `ManagedCredentialProvider` and `CredentialManager` are Phase C
|
||||||
|
concepts. The spec should define them as future extensions, not implement them
|
||||||
|
now.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- ADR-029 (Identity as core type — same pattern)
|
||||||
|
- [credentials.md](../credentials.md) — CredentialProvider spec
|
||||||
|
- [research/phase2/credential-provider.md](../../research/phase2/credential-provider.md) — Full analysis
|
||||||
|
- [identity.md](../identity.md) — IdentityProvider (inbound, opposite direction)
|
||||||
83
docs/architecture/decisions/037-api-keys-dynamic-config.md
Normal file
83
docs/architecture/decisions/037-api-keys-dynamic-config.md
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# ADR-037: API Keys as DynamicConfig Auth
|
||||||
|
|
||||||
|
## Status
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Alknet's token auth uses Ed25519-signed `AuthToken`s — the same key material
|
||||||
|
used for SSH auth. This is appropriate for interactive clients (browsers, CLI)
|
||||||
|
that can generate and sign Ed25519 key pairs.
|
||||||
|
|
||||||
|
But for service accounts, automation, and simple integrations, Ed25519 key
|
||||||
|
pairs are inconvenient. A dashboard backend, a CI/CD pipeline, or a monitoring
|
||||||
|
script needs a simple bearer token that can be stored in an environment variable
|
||||||
|
or config file without managing cryptographic key pairs.
|
||||||
|
|
||||||
|
The HTTP interface (Phase 2+) requires bearer token auth for `Authorization:
|
||||||
|
Bearer <token>` headers. `AuthToken` works but requires client-side Ed25519
|
||||||
|
signing. API keys offer a simpler alternative: short bearer tokens verified by
|
||||||
|
SHA-256 hash lookup, with optional scope restrictions and TTL.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Add `[[auth.api_keys]]` section to `DynamicConfig`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[auth.api_keys]]
|
||||||
|
prefix = "alk_"
|
||||||
|
hash = "sha256:abc..."
|
||||||
|
scopes = ["relay:connect", "secrets:derive"]
|
||||||
|
description = "dashboard service account"
|
||||||
|
ttl = "30d" # optional
|
||||||
|
```
|
||||||
|
|
||||||
|
`ConfigIdentityProvider::resolve_from_token()` handles both token types:
|
||||||
|
- If the input starts with the configured prefix (default `alk_`), treat it as
|
||||||
|
an API key: hash it with SHA-256 and look up the hash in the `api_keys` table.
|
||||||
|
- Otherwise, treat it as an `AuthToken`: decode, verify Ed25519 signature,
|
||||||
|
check timestamp, resolve from `authorized_keys`.
|
||||||
|
|
||||||
|
Both paths produce the same `Identity` result. In database-backed deployments,
|
||||||
|
both resolve to the same account UUID.
|
||||||
|
|
||||||
|
API keys are stored as SHA-256 hashes (like password hashing — the cleartext
|
||||||
|
key is never stored, only its hash). The prefix enables O(1) routing between
|
||||||
|
AuthToken and API key verification without trying both paths.
|
||||||
|
|
||||||
|
The full key is provided to the client exactly once (at creation time). Subsequent
|
||||||
|
verifications only compare hashes.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
**Positive**: Simple bearer token auth for HTTP and other non-SSH interfaces.
|
||||||
|
No cryptographic key management for service accounts. Consistent with industry
|
||||||
|
practice (Stripe, GitHub, AWS all use prefixed API keys).
|
||||||
|
|
||||||
|
**Positive**: Both AuthTokens and API keys go through `resolve_from_token()`.
|
||||||
|
The caller doesn't need to know which type they're using. This keeps the
|
||||||
|
authentication layer unified.
|
||||||
|
|
||||||
|
**Positive**: Scoped API keys enable fine-grained access control for service
|
||||||
|
accounts. A monitoring tool gets `["monitoring:read"]`, not full access.
|
||||||
|
|
||||||
|
**Negative**: API keys are bearer tokens — anyone who obtains the key has the
|
||||||
|
associated permissions. The hash storage and optional TTL mitigate but do not
|
||||||
|
eliminate this risk. Ed25519 AuthTokens remain the preferred auth method for
|
||||||
|
interactive clients.
|
||||||
|
|
||||||
|
**Negative**: API key rotation requires updating `DynamicConfig` (or the
|
||||||
|
`api_keys` database table). The `ConfigReloadHandle` / `ConfigService` reload
|
||||||
|
mechanism handles this, but it's a deliberate operation, not automatic.
|
||||||
|
|
||||||
|
**Negative**: No rate limiting on API key verification is built into this ADR.
|
||||||
|
Rate limiting on the HTTP interface is a separate concern.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- ADR-023 (unified auth, shared key material)
|
||||||
|
- ADR-029 (Identity as core type)
|
||||||
|
- ADR-030 (static/dynamic config split)
|
||||||
|
- [auth.md](../auth.md) — Token auth, AuthPolicy, API keys
|
||||||
|
- [configuration.md](../configuration.md) — DynamicConfig, AuthPolicy
|
||||||
|
- [research/phase2/interface-model.md](../../research/phase2/interface-model.md) — API keys in config
|
||||||
226
docs/architecture/definitions.md
Normal file
226
docs/architecture/definitions.md
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
---
|
||||||
|
status: draft
|
||||||
|
last_updated: 2026-06-09
|
||||||
|
---
|
||||||
|
|
||||||
|
# Definitions: Terminology and Concept Disambiguation
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Several terms are overloaded across alknet's architecture. This document defines
|
||||||
|
each term precisely and states the rule for using it in architecture specs. When
|
||||||
|
ambiguity is possible, specs must use the full qualifier.
|
||||||
|
|
||||||
|
This is a normative reference — other architecture documents link here rather
|
||||||
|
than repeating definitions inline.
|
||||||
|
|
||||||
|
## Term Definitions
|
||||||
|
|
||||||
|
### Interface (Layer 2)
|
||||||
|
|
||||||
|
An **Interface** consumes a Transport stream (Layer 1) or manages its own
|
||||||
|
transport, and produces call protocol sessions or handles discrete requests.
|
||||||
|
It is a _protocol parser_, not a network service.
|
||||||
|
|
||||||
|
Two subtypes:
|
||||||
|
|
||||||
|
| Subtype | Trait | Lifecycle | Transport ownership | Examples |
|
||||||
|
|---------|-------|-----------|---------------------|----------|
|
||||||
|
| `StreamInterface` | `accept(stream) → Session` | Long-lived session | Provided by caller | SshInterface, RawFramingInterface |
|
||||||
|
| `MessageInterface` | `handle_request(req) → Response` | Stateless per-request | Self-managed | HttpInterface, DnsInterface |
|
||||||
|
|
||||||
|
**Rule**: In alknet architecture docs, "Interface" (capitalized) refers to
|
||||||
|
Layer 2. Rust trait definitions use "trait" or "contract." Network URLs use
|
||||||
|
"endpoint." When discussing auth mechanisms per transport/interface pair, use
|
||||||
|
"credential presentation" (not "auth interface").
|
||||||
|
|
||||||
|
See: [interface.md](interface.md), ADR-035.
|
||||||
|
|
||||||
|
### Transport (Layer 1)
|
||||||
|
|
||||||
|
A **Transport** produces a byte stream (`AsyncRead + AsyncWrite + Unpin + Send`).
|
||||||
|
It is a _wire mechanism_, not a protocol. `TransportKind` enumerates:
|
||||||
|
`Tcp`, `Tls`, `Iroh`, `WebTransport`.
|
||||||
|
|
||||||
|
DNS is **not** a transport — it is a `MessageInterface` that manages its own
|
||||||
|
transport (UDP/TCP port 53).
|
||||||
|
|
||||||
|
**Rule**: Never use "transport" to refer to HTTP, DNS, or any protocol that
|
||||||
|
doesn't produce a `TransportStream`. Use "MessageInterface" instead.
|
||||||
|
|
||||||
|
See: [transport.md](transport.md), ADR-026, ADR-035.
|
||||||
|
|
||||||
|
### Service (irpc service)
|
||||||
|
|
||||||
|
An **irpc service** is an in-cluster, Rust-to-Rust service defined by an irpc
|
||||||
|
protocol enum. Dispatched by enum variant with postcard serialization. Examples:
|
||||||
|
`AuthProtocol`, `SecretProtocol`, `ConfigProtocol`.
|
||||||
|
|
||||||
|
**Rule**: Always qualify: "irpc service" (in-cluster, enum-dispatched),
|
||||||
|
"application service" (operation-registered handler), or "external service"
|
||||||
|
(third-party endpoint). Never use bare "service" in architecture docs.
|
||||||
|
|
||||||
|
See: [services.md](services.md), ADR-028, ADR-033.
|
||||||
|
|
||||||
|
### Operation (call protocol)
|
||||||
|
|
||||||
|
An **operation** is a path-based handler registered in `OperationRegistry`,
|
||||||
|
dispatched by `namespace + name`. Cross-node, cross-language, JSON
|
||||||
|
`EventEnvelope` framing.
|
||||||
|
|
||||||
|
**Rule**: Use "operation" for call protocol handlers. Use "irpc service method"
|
||||||
|
for enum-dispatched calls. These are different dispatch mechanisms unified by
|
||||||
|
OperationEnv.
|
||||||
|
|
||||||
|
See: [call-protocol.md](call-protocol.md), ADR-033.
|
||||||
|
|
||||||
|
### Identity (core type)
|
||||||
|
|
||||||
|
The `Identity` struct `{ id, scopes, resources }` represents an authenticated
|
||||||
|
principal. Produced by `IdentityProvider` (inbound auth resolution).
|
||||||
|
|
||||||
|
| Identity field | Config-backed auth | Database-backed auth |
|
||||||
|
|---------------|-------------------|---------------------|
|
||||||
|
| `id` | SSH key fingerprint | Account UUID |
|
||||||
|
| `scopes` | From authorized_keys entry | From peer_credentials + ACL |
|
||||||
|
| `resources` | From authorized_keys entry | From organization membership |
|
||||||
|
|
||||||
|
**Rule**: "Identity" (capitalized, code font) = the alknet struct. "identity
|
||||||
|
service" = a full identity management system (Keystone, etc.). Never conflate
|
||||||
|
the two.
|
||||||
|
|
||||||
|
See: [identity.md](identity.md), ADR-029.
|
||||||
|
|
||||||
|
### IdentityProvider (inbound auth)
|
||||||
|
|
||||||
|
`IdentityProvider` resolves **inbound** authentication: given a credential
|
||||||
|
(fingerprint or token), produce an `Identity`.
|
||||||
|
|
||||||
|
**Direction**: Inbound (who is calling alknet).
|
||||||
|
|
||||||
|
**Rule**: Never use "IdentityProvider" to describe outbound auth. That is
|
||||||
|
`CredentialProvider`.
|
||||||
|
|
||||||
|
See: [identity.md](identity.md), ADR-029.
|
||||||
|
|
||||||
|
### CredentialProvider (outbound auth)
|
||||||
|
|
||||||
|
`CredentialProvider` resolves **outbound** credentials: given a service name,
|
||||||
|
produce a `CredentialSet` for authenticating _to_ that service.
|
||||||
|
|
||||||
|
**Direction**: Outbound (how alknet calls others).
|
||||||
|
|
||||||
|
**Rule**: Never use "CredentialProvider" for inbound auth. That is
|
||||||
|
`IdentityProvider`.
|
||||||
|
|
||||||
|
See: [credentials.md](credentials.md), ADR-036.
|
||||||
|
|
||||||
|
### AuthToken
|
||||||
|
|
||||||
|
`AuthToken = base64url(key_id || timestamp || signature)` — an Ed25519-signed
|
||||||
|
timestamp token used for non-SSH auth. Self-signed by the client, verified
|
||||||
|
server-side.
|
||||||
|
|
||||||
|
**Rule**: Use "AuthToken" (capitalized) for this specific format. Use "API key"
|
||||||
|
for hash-verified bearer tokens. Never use bare "token" in architecture docs.
|
||||||
|
|
||||||
|
See: [auth.md](auth.md), ADR-023.
|
||||||
|
|
||||||
|
### API Key
|
||||||
|
|
||||||
|
A hash-verified bearer token with a prefix like `alk_...`. Simpler than
|
||||||
|
AuthToken (no Ed25519 key pair needed). Stored as SHA-256 hash in
|
||||||
|
`DynamicConfig.auth.api_keys` or `api_keys` table.
|
||||||
|
|
||||||
|
**Rule**: Always "API key" (two words) for hash-verified bearer tokens.
|
||||||
|
"AuthToken" for Ed25519-signed tokens.
|
||||||
|
|
||||||
|
See: [auth.md](auth.md), ADR-037.
|
||||||
|
|
||||||
|
### Domain Event vs Integration Event
|
||||||
|
|
||||||
|
| Type | Scope | Serialization | Example |
|
||||||
|
|------|-------|---------------|---------|
|
||||||
|
| Domain event | Within a service boundary | Any format (Honker streams) | `KeyRotated`, `InventoryAdjusted` |
|
||||||
|
| Integration event | Across service or node boundaries | JSON `EventEnvelope` | `call.requested`, `UserCreated` |
|
||||||
|
|
||||||
|
irpc service calls are synchronous request-response, not events.
|
||||||
|
|
||||||
|
**Rule**: "Domain event" for internal Honker streams. "Integration event" for
|
||||||
|
call protocol `EventEnvelope`. "irpc call" for synchronous in-cluster calls.
|
||||||
|
Per ADR-032, domain events never cross service boundaries without projection.
|
||||||
|
|
||||||
|
See: ADR-032, [services.md](services.md).
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
|
||||||
|
A permission string attached to an `Identity`. Flat strings like
|
||||||
|
`"relay:connect"`, `"secrets:derive"`. Used by `ForwardingPolicy` and
|
||||||
|
operation-level ACL.
|
||||||
|
|
||||||
|
**Rule**: Use "scope" for `Identity.scopes` flat strings. Use "resource" for
|
||||||
|
`Identity.resources` entries. Do not conflate with hierarchical role models
|
||||||
|
unless explicitly noting a comparison to Keystone.
|
||||||
|
|
||||||
|
See: [identity.md](identity.md), ADR-031.
|
||||||
|
|
||||||
|
### OperationRegistry
|
||||||
|
|
||||||
|
The central registry mapping `(namespace, operation_name)` to handlers and
|
||||||
|
specs. All interfaces resolve to the same registry.
|
||||||
|
|
||||||
|
**Rule**: "OperationRegistry" for this specific data structure. "Service
|
||||||
|
catalog" only when explicitly comparing to Keystone or similar external systems.
|
||||||
|
|
||||||
|
See: [call-protocol.md](call-protocol.md), ADR-025.
|
||||||
|
|
||||||
|
### Credential Presentation
|
||||||
|
|
||||||
|
The mechanism by which credentials are presented on each (Transport, Interface)
|
||||||
|
pair:
|
||||||
|
|
||||||
|
| (Transport, Interface) | Credential presentation | Resolves via |
|
||||||
|
|----------------------|----------------------|-------------|
|
||||||
|
| (TLS, SSH) | SSH key handshake | `resolve_from_fingerprint()` |
|
||||||
|
| (TCP, SSH) | SSH key handshake | `resolve_from_fingerprint()` |
|
||||||
|
| (iroh, SSH) | SSH key handshake | `resolve_from_fingerprint()` |
|
||||||
|
| (TLS, raw framing) | AuthToken in frame header | `resolve_from_token()` |
|
||||||
|
| (TCP, raw framing) | AuthToken in frame header | `resolve_from_token()` |
|
||||||
|
| (WebTransport, raw framing) | AuthToken in CONNECT request | `resolve_from_token()` |
|
||||||
|
| (—, HTTP) | `Authorization: Bearer` header | `resolve_from_token()` |
|
||||||
|
| (—, DNS) | AuthToken in query labels | `resolve_from_token()` |
|
||||||
|
|
||||||
|
**Rule**: Use "credential presentation" for the mechanism of presenting
|
||||||
|
credentials on a specific (Transport, Interface) pair. Not "auth interface"
|
||||||
|
(which overloads "Interface").
|
||||||
|
|
||||||
|
See: [auth.md](auth.md), [interface.md](interface.md).
|
||||||
|
|
||||||
|
## Cross-cutting Open Questions
|
||||||
|
|
||||||
|
These questions affect multiple specs and need resolution before or during
|
||||||
|
Phase 2 implementation:
|
||||||
|
|
||||||
|
- **OQ-DEF-03**: Should `Identity.scopes` be hierarchical (Keystone implied roles)
|
||||||
|
or stay flat? Recommendation: Stay flat. Add implied scope resolution in
|
||||||
|
alknet-storage when multi-tenant deployment requires it.
|
||||||
|
|
||||||
|
- **OQ-DEF-07**: Should the on-chain `IdentityProvider` be a separate impl or a
|
||||||
|
`CredentialProvider` extension? Recommendation: Separate `IdentityProvider`
|
||||||
|
impl (`OnChainIdentityProvider`). `IdentityProvider` resolves inbound auth,
|
||||||
|
not outbound credentials.
|
||||||
|
|
||||||
|
- **OQ-DEF-08**: Should "credential presentation" replace overloaded "interface" in
|
||||||
|
auth contexts? Recommendation: Yes. Adopted in this document.
|
||||||
|
|
||||||
|
See: [open-questions.md](open-questions.md) for tracking.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [interface.md](interface.md) — StreamInterface / MessageInterface
|
||||||
|
- [auth.md](auth.md) — AuthToken, credential presentation per interface
|
||||||
|
- [identity.md](identity.md) — Identity, IdentityProvider
|
||||||
|
- [credentials.md](credentials.md) — CredentialProvider, CredentialSet
|
||||||
|
- [services.md](services.md) — irpc services vs application services
|
||||||
|
- [call-protocol.md](call-protocol.md) — Operations, OperationEnv
|
||||||
|
- [research/phase2/definitions.md](../research/phase2/definitions.md) — Full research with cross-domain mappings
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
status: draft
|
status: draft
|
||||||
last_updated: 2026-06-07
|
last_updated: 2026-06-09
|
||||||
---
|
---
|
||||||
|
|
||||||
# Interface (Layer 2)
|
# Interface (Layer 2)
|
||||||
@@ -8,24 +8,33 @@ last_updated: 2026-06-07
|
|||||||
## What
|
## What
|
||||||
|
|
||||||
The Interface layer sits between Transport (Layer 1) and Protocol (Layer 3).
|
The Interface layer sits between Transport (Layer 1) and Protocol (Layer 3).
|
||||||
An Interface consumes a `Transport::Stream` and produces call protocol sessions.
|
Interfaces consume byte streams from Transports or manage their own transports,
|
||||||
SSH is an interface, not a transport — it wraps a byte stream in session
|
and produce call protocol sessions or handle discrete requests. SSH is an
|
||||||
semantics. Raw framing (4-byte length prefix + JSON `EventEnvelope`) is another
|
interface, not a transport — it wraps a byte stream in session semantics. Raw
|
||||||
interface, one without SSH overhead.
|
framing (4-byte length prefix + JSON `EventEnvelope`) is another interface.
|
||||||
|
HTTP and DNS are message-based interfaces that handle individual request/response
|
||||||
|
pairs without persistent sessions.
|
||||||
|
|
||||||
## Why
|
## Why
|
||||||
|
|
||||||
In the current architecture, SSH is deeply embedded in `ServerHandler`. This
|
In the original architecture, SSH was deeply embedded in `ServerHandler`. This
|
||||||
tangling of transport, interface, and protocol makes it impossible to:
|
tangling of transport, interface, and protocol made it impossible to:
|
||||||
|
|
||||||
- Run the call protocol over DNS queries without wrapping SSH inside DNS
|
- Run the call protocol over DNS queries without wrapping SSH inside DNS
|
||||||
- Use raw framing for local service mesh (no SSH overhead)
|
- Use raw framing for local service mesh (no SSH overhead)
|
||||||
- Support WebTransport direct call protocol for browsers
|
- Support WebTransport direct call protocol for browsers
|
||||||
- Separate auth mechanics from channel management
|
- Separate auth mechanics from channel management
|
||||||
|
- Accept HTTP requests and map them to call protocol operations
|
||||||
|
|
||||||
The three-layer model (ADR-026) cleanly separates these concerns. Transport
|
The three-layer model (ADR-026) cleanly separates these concerns. Transport
|
||||||
produces bytes. Interface parses bytes into sessions. Protocol carries
|
produces bytes. Interface parses bytes into sessions or handles requests.
|
||||||
semantics. A connection is always a (Transport, Interface) pair.
|
Protocol carries semantics. A connection is always a (Transport, Interface)
|
||||||
|
pair for stream-based interfaces, or a standalone message-based interface.
|
||||||
|
|
||||||
|
Phase 2 research identified that HTTP and DNS don't fit the persistent session
|
||||||
|
model — they're stateless per-request. This led to the StreamInterface /
|
||||||
|
MessageInterface split (ADR-035), which gives each interface category its own
|
||||||
|
trait with the right lifecycle and ownership model.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -33,37 +42,103 @@ semantics. A connection is always a (Transport, Interface) pair.
|
|||||||
|
|
||||||
```
|
```
|
||||||
Layer 3: Protocol (Call protocol, Operations, OperationEnv)
|
Layer 3: Protocol (Call protocol, Operations, OperationEnv)
|
||||||
Layer 2: Interface (SSH, raw framing, HTTP/WS, DNS control channel)
|
Layer 2: Interface (StreamInterface: SSH, raw framing | MessageInterface: HTTP, DNS)
|
||||||
Layer 1: Transport (TCP, TLS, iroh, DNS, WebTransport)
|
Layer 1: Transport (TCP, TLS, iroh, WebTransport)
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Layer 1: Transport** — produces byte streams (`AsyncRead + AsyncWrite + Unpin
|
- **Layer 1: Transport** — produces byte streams (`AsyncRead + AsyncWrite + Unpin
|
||||||
+ Send`). Unchanged per ADR-001.
|
+ Send`). Unchanged per ADR-001. DNS is NOT a transport.
|
||||||
- **Layer 2: Interface** — consumes a `Transport::Stream` and produces call
|
- **Layer 2: Interface** — two categories:
|
||||||
protocol sessions. SSH does handshake + auth + channel multiplexing. Raw
|
- **StreamInterface**: consumes a `TransportStream` and produces a long-lived
|
||||||
framing does length-prefix parsing.
|
session that yields `InterfaceEvent` frames.
|
||||||
|
- **MessageInterface**: handles individual `InterfaceRequest` →
|
||||||
|
`InterfaceResponse` pairs. Manages its own transport.
|
||||||
- **Layer 3: Protocol** — carries semantics. Call protocol events, operation
|
- **Layer 3: Protocol** — carries semantics. Call protocol events, operation
|
||||||
registry, service calls. Agnostic to both Transport and Interface below it.
|
registry, service calls. Agnostic to both Transport and Interface below it.
|
||||||
|
|
||||||
### Interface Trait
|
### StreamInterface Trait
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait Interface: Send + Sync + 'static {
|
pub trait StreamInterface: Send + Sync + 'static {
|
||||||
type Session;
|
type Session: InterfaceSession;
|
||||||
async fn accept(stream: TransportStream, config: &InterfaceConfig) -> Result<Self::Session>;
|
|
||||||
|
async fn accept(
|
||||||
|
&self,
|
||||||
|
stream: Box<dyn TransportStream>,
|
||||||
|
config: &InterfaceConfig,
|
||||||
|
) -> Result<Self::Session>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The session produced by an interface is consumed by the call protocol handler.
|
The session produced by a `StreamInterface` is consumed by the call protocol
|
||||||
Different interfaces produce different session types, but the call protocol
|
handler. Different stream interfaces produce different session types, but the
|
||||||
handler receives `EventEnvelope` frames from any interface.
|
call protocol handler receives `InterfaceEvent` frames from any stream
|
||||||
|
interface.
|
||||||
|
|
||||||
### SshInterface
|
### MessageInterface Trait
|
||||||
|
|
||||||
Wraps the existing `ServerHandler` logic. This is the most complex interface
|
```rust
|
||||||
because SSH provides channel multiplexing, auth negotiation, and proxy
|
#[async_trait]
|
||||||
management within a single session.
|
pub trait MessageInterface: Send + Sync + 'static {
|
||||||
|
async fn handle_request(&self, request: InterfaceRequest) -> Result<InterfaceResponse>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Message-based interfaces handle individual requests without persistent sessions.
|
||||||
|
They manage their own transport (HTTP server, DNS server) and normalize requests
|
||||||
|
into `InterfaceRequest` / `InterfaceResponse`.
|
||||||
|
|
||||||
|
### InterfaceRequest / InterfaceResponse
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct InterfaceRequest {
|
||||||
|
pub operation_path: String, // e.g., "/head/auth/verify"
|
||||||
|
pub input: Value, // JSON input payload
|
||||||
|
pub auth_token: Option<AuthToken>, // Extracted from wire format
|
||||||
|
pub metadata: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct InterfaceResponse {
|
||||||
|
pub result: Result<Value, CallError>,
|
||||||
|
pub status: u16, // HTTP status, DNS result code, etc.
|
||||||
|
pub headers: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The call protocol handler processes `InterfaceRequest` the same way it processes
|
||||||
|
`InterfaceEvent` frames — both resolve to operation invocations through
|
||||||
|
`OperationEnv`. The difference is framing: stream interfaces produce `InterfaceEvent`
|
||||||
|
frames from a continuous byte stream, message interfaces construct `InterfaceRequest`
|
||||||
|
from their wire format.
|
||||||
|
|
||||||
|
### InterfaceSession
|
||||||
|
|
||||||
|
Every stream interface session implements `InterfaceSession`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct InterfaceEvent {
|
||||||
|
pub envelope: EventEnvelope,
|
||||||
|
pub identity: Option<Identity>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait InterfaceSession: Send {
|
||||||
|
async fn recv(&mut self) -> Option<InterfaceEvent>;
|
||||||
|
async fn send(&mut self, envelope: EventEnvelope) -> Result<()>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`InterfaceEvent` carries an `EventEnvelope` and the authenticated `Identity`.
|
||||||
|
The call protocol handler (Layer 3) receives `InterfaceEvent` frames and
|
||||||
|
processes them uniformly, regardless of whether they arrived over SSH or raw
|
||||||
|
framing.
|
||||||
|
|
||||||
|
### SshInterface (StreamInterface)
|
||||||
|
|
||||||
|
Wraps the existing `ServerHandler` logic. This is the most complex stream
|
||||||
|
interface because SSH provides channel multiplexing, auth negotiation, and
|
||||||
|
proxy management within a single session.
|
||||||
|
|
||||||
What stays in SshInterface (Layer 2):
|
What stays in SshInterface (Layer 2):
|
||||||
- SSH handshake and session management
|
- SSH handshake and session management
|
||||||
@@ -79,7 +154,11 @@ What moves to Layer 3 (call protocol handler):
|
|||||||
What moves to per-connection state:
|
What moves to per-connection state:
|
||||||
- Port forwarding proxy logic
|
- Port forwarding proxy logic
|
||||||
|
|
||||||
### RawFramingInterface
|
**Current implementation note**: `SshSession::recv()` and `SshSession::send()`
|
||||||
|
are stubs. The bridge from SSH channels to `InterfaceEvent` frames is
|
||||||
|
scheduled for Phase 2 implementation (see integration-plan.md Phase 2.1).
|
||||||
|
|
||||||
|
### RawFramingInterface (StreamInterface)
|
||||||
|
|
||||||
Reads 4-byte big-endian length prefix + JSON `EventEnvelope` frames directly
|
Reads 4-byte big-endian length prefix + JSON `EventEnvelope` frames directly
|
||||||
from the transport stream. No SSH wrapping. No channel multiplexing — the
|
from the transport stream. No SSH wrapping. No channel multiplexing — the
|
||||||
@@ -88,134 +167,210 @@ entire stream is a single call protocol channel.
|
|||||||
```rust
|
```rust
|
||||||
pub struct RawFramingInterface;
|
pub struct RawFramingInterface;
|
||||||
|
|
||||||
impl Interface for RawFramingInterface {
|
impl StreamInterface for RawFramingInterface {
|
||||||
type Session = RawFramingSession;
|
type Session = RawFramingSession;
|
||||||
// Reads length-prefixed EventEnvelope frames from the stream
|
// Reads length-prefixed EventEnvelope frames from the stream
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Used for:
|
Used for:
|
||||||
- DNS control channel (DNS transport + raw framing)
|
|
||||||
- Local service mesh (TCP + raw framing, no SSH overhead)
|
- Local service mesh (TCP + raw framing, no SSH overhead)
|
||||||
- Browser direct call protocol (WebTransport + raw framing, future)
|
- Secure mesh (TLS + raw framing)
|
||||||
|
- WebTransport direct call protocol (future: WebTransport + raw framing)
|
||||||
|
|
||||||
### DNS Control Channel
|
Auth for raw framing: `AuthToken` in frame header, resolved via
|
||||||
|
`IdentityProvider::resolve_from_token()`.
|
||||||
|
|
||||||
A (DNS transport, raw framing interface) pair. The DNS transport encodes
|
**Current implementation note**: `RawFramingInterface::accept()` returns an
|
||||||
`EventEnvelope` frames as DNS query/response pairs. The raw framing interface
|
error. Frame reading/writing is scheduled for Phase 2 implementation (see
|
||||||
parses them directly — **NOT** SSH inside DNS.
|
integration-plan.md Phase 2.2).
|
||||||
|
|
||||||
|
### HttpInterface (MessageInterface)
|
||||||
|
|
||||||
|
Accepts standard HTTP requests and maps them to call protocol operations:
|
||||||
|
|
||||||
```
|
```
|
||||||
Client: Encode EventEnvelope as base32 DNS query labels
|
POST /v1/{namespace}/{op} → registry.invoke(namespace, op, input) (mutation)
|
||||||
→ DNS Transport → DNS Server → Raw Framing Interface → Call Protocol Handler
|
GET /v1/{namespace}/{op} → registry.invoke(namespace, op, input) (query)
|
||||||
|
GET /v1/{namespace}/{op} SSE → registry.subscribe(namespace, op, input) (subscription)
|
||||||
Server: Return EventEnvelope as DNS TXT record response
|
GET /v1/schema → registry.list_operations()
|
||||||
← Raw Framing Interface ← DNS Transport ← Call Protocol Handler
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Valid (Transport, Interface) Pairs
|
Auth: `Authorization: Bearer <token>` header, resolved via
|
||||||
|
`IdentityProvider::resolve_from_token()`. Both AuthTokens and API keys are
|
||||||
|
accepted.
|
||||||
|
|
||||||
| Transport | Interface | Use case |
|
The HTTP interface runs inside the existing stealth mode byte-peek architecture:
|
||||||
|-----------|-----------|----------|
|
after a TLS handshake, the server peeks at the first bytes. If they're
|
||||||
| TLS | SSH | Standard alknet tunnel |
|
`SSH-2.0-`, the stream goes to `SshInterface`. Otherwise, the stream goes to
|
||||||
| TCP | SSH | Plain SSH tunnel |
|
the axum HTTP router.
|
||||||
| iroh | SSH | P2P SSH tunnel |
|
|
||||||
| DNS | raw framing | DNS control channel |
|
|
||||||
| WebTransport | SSH | Browser SSH tunnel (future) |
|
|
||||||
| WebTransport | raw framing | Browser call protocol (future) |
|
|
||||||
| TCP | raw framing | Direct call protocol, local mesh |
|
|
||||||
|
|
||||||
### InterfaceConfig
|
**Phase 2 scope**: Auth middleware, stealth handoff, and default 404 handler
|
||||||
|
only. Specific operation routes and path conventions are Phase 5+. The
|
||||||
|
`ListenerConfig::Http` variant spawns an axum router that reaches auth context;
|
||||||
|
routing inside axum is a later concern.
|
||||||
|
|
||||||
Different interfaces require different configuration:
|
### DnsInterface (MessageInterface)
|
||||||
|
|
||||||
|
A DNS server that encodes/decodes `EventEnvelope` frames as DNS query/response
|
||||||
|
pairs. AuthToken is embedded in DNS query labels. Resolution via
|
||||||
|
`IdentityProvider::resolve_from_token()`.
|
||||||
|
|
||||||
|
This is a `MessageInterface` — it manages its own transport (UDP/TCP port 53)
|
||||||
|
and handles individual DNS queries as request/response pairs. DNS is NOT a
|
||||||
|
transport.
|
||||||
|
|
||||||
|
**Phase**: DNS interface implementation is Phase 5+. The `ListenerConfig::Dns`
|
||||||
|
variant and `DnsInterface` stub are defined now; implementation is deferred.
|
||||||
|
|
||||||
|
### Stream-Based Interface Pairs
|
||||||
|
|
||||||
|
| Transport | StreamInterface | Credential Presentation | Use case |
|
||||||
|
|-----------|---------------|------------------------|----------|
|
||||||
|
| TLS | SshInterface | SSH key handshake | Standard alknet tunnel |
|
||||||
|
| TCP | SshInterface | SSH key handshake | Plain SSH tunnel |
|
||||||
|
| iroh | SshInterface | SSH key handshake | P2P SSH tunnel |
|
||||||
|
| TCP | RawFramingInterface | AuthToken in frame header | Local service mesh |
|
||||||
|
| TLS | RawFramingInterface | AuthToken in frame header | Secure mesh |
|
||||||
|
| WebTransport | RawFramingInterface | AuthToken in CONNECT request | Browser call protocol (future) |
|
||||||
|
|
||||||
|
### Message-Based Interface Pairs
|
||||||
|
|
||||||
|
| MessageInterface | Credential Presentation | Owns transport? | Use case |
|
||||||
|
|-----------------|------------------------|----------------|----------|
|
||||||
|
| HttpInterface | `Authorization: Bearer` header | Yes (axum) | REST API, dashboard, integrations |
|
||||||
|
| DnsInterface | AuthToken in query labels | Yes (DNS server) | Censorship-resistant control channel |
|
||||||
|
| WebSocketInterface | AuthToken in handshake | Yes (WS server) | Browser persistent connection (future) |
|
||||||
|
|
||||||
|
Message-based interfaces manage their own transport. They don't need a
|
||||||
|
`Transport` from Layer 1 — they ARE the transport+interface combined.
|
||||||
|
|
||||||
|
### ListenerConfig
|
||||||
|
|
||||||
|
The server's accept loop configuration covers both stream and message interfaces:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
pub enum InterfaceConfig {
|
pub enum ListenerConfig {
|
||||||
Ssh(SshInterfaceConfig),
|
Stream {
|
||||||
RawFraming(RawFramingConfig),
|
transport: TransportKind,
|
||||||
|
interface: StreamInterfaceKind,
|
||||||
|
},
|
||||||
|
Http {
|
||||||
|
bind_addr: SocketAddr,
|
||||||
|
tls: bool,
|
||||||
|
stealth: bool, // byte-peek protocol detection on shared port
|
||||||
|
},
|
||||||
|
Dns {
|
||||||
|
bind_addr: SocketAddr,
|
||||||
|
tls: bool,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SshInterfaceConfig {
|
pub enum StreamInterfaceKind {
|
||||||
pub auth: Arc<dyn IdentityProvider>,
|
Ssh,
|
||||||
pub forwarding: Arc<ArcSwap<DynamicConfig>>, // for ForwardingPolicy
|
RawFraming,
|
||||||
pub host_key: Arc<PrivateKey>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RawFramingConfig {
|
pub enum TransportKind {
|
||||||
// No SSH-specific config needed
|
Tcp,
|
||||||
// Auth is handled by the transport layer (e.g., token auth for WebTransport)
|
Tls { server_name: Option<String> },
|
||||||
// or by the call protocol layer
|
Iroh { endpoint_id: String },
|
||||||
|
WebTransport, // Phase 5+: tag only, no acceptor yet
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Auth Across Interfaces
|
Note: `TransportKind::Dns` does NOT exist. DNS is a `MessageInterface`, not a
|
||||||
|
transport. The `ListenerConfig::Dns` variant handles DNS listener configuration
|
||||||
|
directly.
|
||||||
|
|
||||||
- **SshInterface**: Auth happens during SSH handshake via
|
### Credential Presentation Across Interfaces
|
||||||
`IdentityProvider::resolve_from_fingerprint()`. The authenticated `Identity`
|
|
||||||
is attached to the session.
|
|
||||||
- **RawFramingInterface**: Auth is handled by the transport (e.g., token auth
|
|
||||||
for WebTransport via `IdentityProvider::resolve_from_token()`) or by the call
|
|
||||||
protocol layer (operation-level ACL).
|
|
||||||
|
|
||||||
Both paths produce the same `Identity` type (ADR-029).
|
Every interface resolves to the same `Identity` through `IdentityProvider`:
|
||||||
|
|
||||||
|
```
|
||||||
|
SSH fingerprint → IdentityProvider::resolve_from_fingerprint → Identity
|
||||||
|
AuthToken (Bearer) → IdentityProvider::resolve_from_token → Identity
|
||||||
|
API key (Bearer) → IdentityProvider::resolve_from_token → Identity
|
||||||
|
DNS embedded token → IdentityProvider::resolve_from_token → Identity
|
||||||
|
```
|
||||||
|
|
||||||
|
The credential presentation differs per (Transport, Interface) pair, but the
|
||||||
|
resolution result is always an `Identity`. See [definitions.md](definitions.md)
|
||||||
|
for the full table and terminology rules.
|
||||||
|
|
||||||
### Server Accept Loop
|
### Server Accept Loop
|
||||||
|
|
||||||
With the Interface trait, the accept loop becomes:
|
With both stream and message interfaces, the accept loop becomes:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
for listener in listeners {
|
for listener in listeners {
|
||||||
let (transport, interface) = listener;
|
match listener {
|
||||||
tokio::spawn(async move {
|
ListenerConfig::Stream { transport, interface } => {
|
||||||
loop {
|
// Spawn accept loop: transport.accept() → interface.accept(stream)
|
||||||
let stream = transport.accept().await?;
|
|
||||||
let session = interface.accept(stream, &config).await?;
|
|
||||||
// session produces call protocol events
|
|
||||||
// call protocol handler is interface-agnostic
|
|
||||||
}
|
}
|
||||||
});
|
ListenerConfig::Http { bind_addr, tls, stealth } => {
|
||||||
|
// Spawn axum HTTP server on bind_addr
|
||||||
|
// If stealth: byte-peek after TLS, route SSH vs HTTP
|
||||||
|
}
|
||||||
|
ListenerConfig::Dns { bind_addr, tls } => {
|
||||||
|
// Spawn DNS server on bind_addr
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Constraints
|
## Constraints
|
||||||
|
|
||||||
- The Interface trait must accommodate both SSH's channel multiplexing and raw
|
- `StreamInterface` and `MessageInterface` are independent traits with different
|
||||||
framing's single-stream model through the same abstraction.
|
signatures, lifecycles, and transport ownership. No common super-trait (ADR-035).
|
||||||
- `SshInterface` is the most invasive refactoring in Phase 1. The existing
|
- `SshInterface` is the most invasive refactoring. The existing `SshHandler`
|
||||||
`ServerHandler` owns auth, channel management, and proxy logic — extracting
|
owns auth, channel management, and proxy logic — extracting these cleanly
|
||||||
these cleanly requires careful design (integration-plan, Phase 1.8).
|
requires careful design (integration-plan Phase 1.8, completed in Phase 1).
|
||||||
- DNS transport implementation is Phase 4 work. The `TransportKind::Dns` variant
|
- DNS interface implementation is Phase 5 work. `DnsInterface` is defined as a
|
||||||
and `RawFramingInterface` are defined now; implementation is deferred.
|
`MessageInterface` stub; implementation is deferred.
|
||||||
- WebTransport is Phase 4 work. The `TransportKind::WebTransport` variant is a
|
- HTTP interface Phase 2 scope is limited to auth middleware and stealth handoff.
|
||||||
tag only for now.
|
Specific operation routes are Phase 5+.
|
||||||
|
- WebTransport is Phase 5 work. `TransportKind::WebTransport` and
|
||||||
|
`StreamInterfaceKind::WebTransport` are tags only for now.
|
||||||
|
- `TransportKind::Dns` does not exist. DNS is a `MessageInterface`, not a
|
||||||
|
transport. This was `TransportKind` enum pollution from an earlier design.
|
||||||
|
- The `Interface` trait (singular) in the current codebase needs to be renamed
|
||||||
|
to `StreamInterface`. This is a rename, not a semantic change.
|
||||||
|
|
||||||
## Open Questions
|
## Open Questions
|
||||||
|
|
||||||
- **OQ-IF-01**: How does the `Interface` session type relate to the call
|
- **OQ-IF-02**: ~~Should `SshInterface` own the `ForwardingPolicy` check for
|
||||||
protocol's `EventEnvelope` stream? Does every session implement
|
`channel_open_direct_tcpip`, or should that move to Layer 3?~~ **Resolved**:
|
||||||
`Stream<Item=EventEnvelope>`? This needs design during Phase 1.8.
|
ForwardingPolicy is Layer 3, but channel open/close lifecycle is Layer 2.
|
||||||
|
SshInterface reports channel requests to Layer 3; Layer 3 applies policy.
|
||||||
|
|
||||||
- **OQ-IF-02**: Should `SshInterface` own the `ForwardingPolicy` check for
|
- **OQ-P2-01**: Should `MessageInterface` and `StreamInterface` share a common
|
||||||
`channel_open_direct_tcpip`, or should that move to Layer 3? Current thinking:
|
trait? **Recommendation**: No. Independent traits with different signatures,
|
||||||
the forwarding check is a Layer 3 concern (it's policy, not session mechanics),
|
lifecycles, and transport ownership. A common super-trait adds complexity
|
||||||
but the channel open/close lifecycle is Layer 2. The Interface reports channel
|
without clear benefit. (See ADR-035.)
|
||||||
open requests to Layer 3; Layer 3 applies `ForwardingPolicy` and tells
|
|
||||||
Layer 2 whether to proxy.
|
- **OQ-P2-02**: Should the HTTP interface share a port with the SSH listener?
|
||||||
|
**Recommendation**: Start with separate ports. ALPN multiplexing on port 443
|
||||||
|
is a future optimization that doesn't change the interface abstraction.
|
||||||
|
Stealth mode byte-peek already handles shared-port detection for the common
|
||||||
|
case.
|
||||||
|
|
||||||
## Design Decisions
|
## Design Decisions
|
||||||
|
|
||||||
| ADR | Decision | Summary |
|
| ADR | Decision | Summary |
|
||||||
|-----|----------|---------|
|
|-----|----------|---------|
|
||||||
| [026](decisions/026-transport-interface-separation.md) | Three-layer model | SSH is Layer 2, not Layer 1 |
|
| [026](decisions/026-transport-interface-separation.md) | Three-layer model | SSH is Layer 2, not Layer 1 |
|
||||||
|
| [035](decisions/035-streaminterface-messageinterface-split.md) | StreamInterface / MessageInterface | Two trait categories at Layer 2 |
|
||||||
| [033](decisions/033-operationenv-irpc-call-protocol.md) | OperationEnv | Protocol is interface-agnostic |
|
| [033](decisions/033-operationenv-irpc-call-protocol.md) | OperationEnv | Protocol is interface-agnostic |
|
||||||
| [029](decisions/029-identity-core-type.md) | Identity as core type | Auth resolution across interfaces |
|
| [029](decisions/029-identity-core-type.md) | Identity as core type | Auth resolution across interfaces |
|
||||||
| [031](decisions/031-forwarding-policy.md) | Forwarding policy | Layer 3 policy applied to Layer 2 channel requests |
|
| [031](decisions/031-forwarding-policy.md) | Forwarding policy | Layer 3 policy applied to Layer 2 channel requests |
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- [research/integration-plan.md](../research/integration-plan.md) — Phase 1.8, valid (Transport, Interface) pairs
|
- [definitions.md](definitions.md) — Terminology disambiguation, credential presentation
|
||||||
- [research/core.md](../research/core.md) — DNS transport, three-layer model
|
- [research/phase2/interface-model.md](../research/phase2/interface-model.md) — Full StreamInterface/MessageInterface analysis
|
||||||
- [ADR-026](decisions/026-transport-interface-separation.md) — Transport/interface separation
|
- [research/phase2/tls-transport.md](../research/phase2/tls-transport.md) — HTTP interface, stealth handoff, ListenerConfig
|
||||||
|
- [research/integration-plan.md](../research/integration-plan.md) — Phase 1.8, Phase 2.1-2.7
|
||||||
- [transport.md](transport.md) — Transport trait (unchanged at Layer 1)
|
- [transport.md](transport.md) — Transport trait (unchanged at Layer 1)
|
||||||
- [server.md](server.md) — Current ServerHandler (will become SshInterface)
|
- [auth.md](auth.md) — Credential presentation per (Transport, Interface) pair
|
||||||
- [identity.md](identity.md) — IdentityProvider, auth across interfaces
|
- [identity.md](identity.md) — IdentityProvider, auth across interfaces
|
||||||
@@ -237,14 +237,95 @@ last_updated: 2026-06-07
|
|||||||
|
|
||||||
### OQ-IF-01: How does the Interface session type relate to the call protocol's EventEnvelope stream?
|
### OQ-IF-01: How does the Interface session type relate to the call protocol's EventEnvelope stream?
|
||||||
- **Origin**: [interface.md](interface.md)
|
- **Origin**: [interface.md](interface.md)
|
||||||
- **Status**: open
|
- **Status**: ~~resolved~~
|
||||||
- **Priority**: high
|
- **Priority**: ~~high~~ —
|
||||||
- **Resolution**: (pending — needs design during Phase 1.8 implementation)
|
- **Resolution**: `InterfaceSession::recv()` returns `Option<InterfaceEvent>` where `InterfaceEvent` carries `EventEnvelope` + `Identity`. `InterfaceSession::send()` accepts `EventEnvelope`. The `SshSession` bridge implements this over the `alknet-control:0` channel. For `MessageInterface`, `InterfaceRequest`/`InterfaceResponse` normalize request/response pairs. See [interface.md](interface.md) and ADR-035.
|
||||||
- **Cross-references**: [interface.md](interface.md), [ADR-026](decisions/026-transport-interface-separation.md)
|
- **Cross-references**: [ADR-035](decisions/035-streaminterface-messageinterface-split.md), [interface.md](interface.md)
|
||||||
|
|
||||||
### OQ-IF-02: Should SshInterface own ForwardingPolicy checks or should they move to Layer 3?
|
### OQ-IF-02: Should SshInterface own ForwardingPolicy checks or should they move to Layer 3?
|
||||||
- **Origin**: [interface.md](interface.md)
|
- **Origin**: [interface.md](interface.md)
|
||||||
- **Status**: open
|
- **Status**: ~~resolved~~
|
||||||
|
- **Priority**: ~~medium~~ —
|
||||||
|
- **Resolution**: ForwardingPolicy is Layer 3 (it's policy, not session mechanics). Channel open/close lifecycle is Layer 2. The Interface reports channel open requests to Layer 3; Layer 3 applies ForwardingPolicy. The current `SshHandler` implementation checks policy in `channel_open_direct_tcpip`, which already delegates to `Identity.scopes` from the authenticated identity — this is consistent with the resolution.
|
||||||
|
- **Cross-references**: [ADR-031](decisions/031-forwarding-policy.md), [interface.md](interface.md)
|
||||||
|
|
||||||
|
### OQ-P2-01: Should MessageInterface and StreamInterface share a common trait?
|
||||||
|
- **Origin**: [research/phase2/interface-model.md](../research/phase2/interface-model.md)
|
||||||
|
- **Status**: resolved
|
||||||
- **Priority**: medium
|
- **Priority**: medium
|
||||||
- **Resolution**: (pending — current thinking: forwarding check is Layer 3 policy, but channel open/close lifecycle is Layer 2. The Interface reports channel open requests to Layer 3; Layer 3 applies ForwardingPolicy.)
|
- **Resolution**: Independent traits. Different signatures (`handle_request` vs `accept` + session lifecycle), different transport ownership (self-managed vs provided), different lifecycles (stateless per-request vs long-lived session). A common super-trait adds complexity without benefit. See ADR-035.
|
||||||
- **Cross-references**: [interface.md](interface.md), [ADR-031](decisions/031-forwarding-policy.md)
|
- **Cross-references**: [ADR-035](decisions/035-streaminterface-messageinterface-split.md), [interface.md](interface.md)
|
||||||
|
|
||||||
|
### OQ-P2-02: Should the HTTP interface share a port with the SSH listener?
|
||||||
|
- **Origin**: [research/phase2/interface-model.md](../research/phase2/interface-model.md)
|
||||||
|
- **Status**: resolved
|
||||||
|
- **Priority**: low
|
||||||
|
- **Resolution**: Start with separate ports. Stealth mode byte-peek on a shared port is already implemented for SSH vs HTTP detection. `ListenerConfig::Http { stealth: true }` enables the existing peek pattern. ALPN multiplexing on port 443 is a future optimization that doesn't change the interface abstraction.
|
||||||
|
- **Cross-references**: [interface.md](interface.md), [research/phase2/tls-transport.md](../research/phase2/tls-transport.md)
|
||||||
|
|
||||||
|
### OQ-P2-03: Should the HTTP interface auto-generate OpenAPI specs from OperationRegistry?
|
||||||
|
- **Origin**: [research/phase2/interface-model.md](../research/phase2/interface-model.md)
|
||||||
|
- **Status**: resolved
|
||||||
|
- **Priority**: low
|
||||||
|
- **Resolution**: Yes, but Phase 5+. The HTTP interface needs to exist first (Phase 5.3 in the integration plan). `GET /v1/schema` producing an OpenAPI spec from registered `OperationSpec`s is the natural end state. This creates symmetry with `FromOpenAPI` (inbound spec consumption).
|
||||||
|
- **Cross-references**: [call-protocol.md](call-protocol.md), [interface.md](interface.md)
|
||||||
|
|
||||||
|
### OQ-P2-04: How do self-hosted services authenticate via alknet?
|
||||||
|
- **Origin**: [research/phase2/credential-provider.md](../research/phase2/credential-provider.md), [research/phase2/definitions.md](../research/phase2/definitions.md)
|
||||||
|
- **Status**: resolved
|
||||||
|
- **Priority**: medium
|
||||||
|
- **Resolution**: Three-phase approach. Phase A: shared secret (`CredentialSet::Bearer` or `S3AccessKey`). Phase C: identity-bound credentials via `ManagedCredentialProvider`. Phase D: alknet as OIDC provider. The `CredentialProvider` trait in core enables Phase A immediately; Phases C and D are additive.
|
||||||
|
- **Cross-references**: [ADR-036](decisions/036-credentialprovider-core-type.md), [credentials.md](credentials.md)
|
||||||
|
|
||||||
|
## Credentials
|
||||||
|
|
||||||
|
### OQ-CP-01: Should CredentialProvider support per-identity credentials?
|
||||||
|
- **Origin**: [credentials.md](credentials.md)
|
||||||
|
- **Status**: open
|
||||||
|
- **Priority**: low
|
||||||
|
- **Resolution**: Start with service-level credentials (`get_credentials(service)`). Add identity-level resolution (`get_credentials_for(service, identity_id)`) when the need is concrete. `Identity.id` already serves as the account UUID in database-backed mode.
|
||||||
|
- **Cross-references**: [credentials.md](credentials.md), [ADR-036](decisions/036-credentialprovider-core-type.md)
|
||||||
|
|
||||||
|
### OQ-CP-02: Where should OIDC provider operations live?
|
||||||
|
- **Origin**: [credentials.md](credentials.md)
|
||||||
|
- **Status**: open
|
||||||
|
- **Priority**: low
|
||||||
|
- **Resolution**: Application service (Phase D). OIDC is an application concern, not a core concern. The call protocol and OperationRegistry provide the transport; OIDC is just another set of operations.
|
||||||
|
- **Cross-references**: [credentials.md](credentials.md)
|
||||||
|
|
||||||
|
### OQ-CP-03: How do credential rotations propagate across a cluster?
|
||||||
|
- **Origin**: [credentials.md](credentials.md)
|
||||||
|
- **Status**: open
|
||||||
|
- **Priority**: low
|
||||||
|
- **Resolution**: TBD. Likely TTL-based caching with a refresh threshold. Workers call `CredentialProvider::get_credentials()` which checks `is_expired()` and calls `refresh_credentials()` if needed.
|
||||||
|
- **Cross-references**: [credentials.md](credentials.md)
|
||||||
|
|
||||||
|
### OQ-CP-04: Should CredentialSet include request-signing capability?
|
||||||
|
- **Origin**: [credentials.md](credentials.md)
|
||||||
|
- **Status**: resolved
|
||||||
|
- **Priority**: low
|
||||||
|
- **Resolution**: No. `CredentialSet` is pure data. Request signing (e.g., AWS Signature V4) is a separate utility function in the service wrapper or a shared `alknet-s3` crate. Credentials are data; signing is protocol behavior.
|
||||||
|
- **Cross-references**: [credentials.md](credentials.md)
|
||||||
|
|
||||||
|
## Definitions
|
||||||
|
|
||||||
|
### OQ-DEF-01: Should alknet adopt a "Service Catalog" concept like Keystone?
|
||||||
|
- **Origin**: [research/phase2/definitions.md](../research/phase2/definitions.md)
|
||||||
|
- **Status**: resolved
|
||||||
|
- **Priority**: low
|
||||||
|
- **Resolution**: Keep `OperationRegistry` global, check scope at invocation time. Add scope-filtered discovery (`GET /v1/schema?scope=...`) when multi-tenant deployment requires it. The unfiltered registry is sufficient for current needs.
|
||||||
|
- **Cross-references**: [call-protocol.md](call-protocol.md)
|
||||||
|
|
||||||
|
### OQ-DEF-03: Should Identity.scopes be hierarchical or stay flat?
|
||||||
|
- **Origin**: [research/phase2/definitions.md](../research/phase2/definitions.md)
|
||||||
|
- **Status**: resolved
|
||||||
|
- **Priority**: low
|
||||||
|
- **Resolution**: Stay flat. Add implied scope resolution in alknet-storage when multi-tenant deployment requires it. A full policy language (like Rustfs IAM JSON policies) is Phase D territory.
|
||||||
|
- **Cross-references**: [identity.md](identity.md)
|
||||||
|
|
||||||
|
### OQ-DEF-08: Should "credential presentation" replace "auth interface" in terminology?
|
||||||
|
- **Origin**: [research/phase2/definitions.md](../research/phase2/definitions.md)
|
||||||
|
- **Status**: resolved
|
||||||
|
- **Priority**: medium
|
||||||
|
- **Resolution**: Yes. Adopted in [definitions.md](definitions.md). Use "credential presentation" for the mechanism of presenting credentials on a (Transport, Interface) pair. Never use "auth interface" (overloads "Interface").
|
||||||
|
- **Cross-references**: [definitions.md](definitions.md), [auth.md](auth.md)
|
||||||
@@ -35,17 +35,22 @@ irpc is behind a feature flag in alknet-core. Nodes that only do SSH tunneling d
|
|||||||
|
|
||||||
## Three-Layer Model
|
## Three-Layer Model
|
||||||
|
|
||||||
Alknet uses a three-layer model (ADR-026):
|
Alknet uses a three-layer model (ADR-026, ADR-035):
|
||||||
|
|
||||||
| Layer | Responsibility | Examples |
|
| Layer | Responsibility | Examples |
|
||||||
|-------|---------------|----------|
|
|-------|---------------|----------|
|
||||||
| **Layer 1: Transport** | Produces byte streams (`AsyncRead + AsyncWrite + Unpin + Send`) | TCP, TLS, iroh, DNS (future), WebTransport (future) |
|
| **Layer 1: Transport** | Produces byte streams (`AsyncRead + AsyncWrite + Unpin + Send`) | TCP, TLS, iroh, WebTransport (future) |
|
||||||
| **Layer 2: Interface** | Consumes a transport stream and produces call protocol sessions | SSH (handshake + auth + channel multiplexing), raw framing (length-prefix + JSON) |
|
| **Layer 2: Interface** | Two categories: StreamInterface (consumes transport stream, produces session) and MessageInterface (handles discrete requests, manages own transport) | Stream: SSH, raw framing. Message: HTTP, DNS |
|
||||||
| **Layer 3: Protocol** | Carries semantics — operation registry, service calls, events | Call protocol, OperationEnv, operation dispatch |
|
| **Layer 3: Protocol** | Carries semantics — operation registry, service calls, events | Call protocol, OperationEnv, operation dispatch |
|
||||||
|
|
||||||
SSH is an interface, not a transport. The three-layer model enables DNS control channels (DNS transport + raw framing), local service mesh (TCP + raw framing), and browser direct call protocol (WebTransport + raw framing) without wrapping SSH inside those transports.
|
SSH is an interface, not a transport. DNS is a message interface, not a transport.
|
||||||
|
The three-layer model enables HTTP interfaces (stealth mode byte-peek),
|
||||||
|
DNS control channels, and local service mesh (raw framing) without wrapping SSH
|
||||||
|
inside those transports.
|
||||||
|
|
||||||
A connection is always a (Transport, Interface) pair. The protocol layer is agnostic to both.
|
A stream-based connection is always a (Transport, StreamInterface) pair.
|
||||||
|
Message-based interfaces manage their own transport. The protocol layer is
|
||||||
|
agnostic to both.
|
||||||
|
|
||||||
## Service Layer
|
## Service Layer
|
||||||
|
|
||||||
@@ -93,15 +98,21 @@ The `alknet-core` crate exports the pluggable components for embedding or progra
|
|||||||
- `TcpTransport` — direct TCP connection
|
- `TcpTransport` — direct TCP connection
|
||||||
- `TlsTransport` — TCP + tokio-rustls TLS
|
- `TlsTransport` — TCP + tokio-rustls TLS
|
||||||
- `IrohTransport` — iroh QUIC P2P connection
|
- `IrohTransport` — iroh QUIC P2P connection
|
||||||
- `Interface` trait — consumes transport stream, produces call protocol session
|
- `Interface` trait → `StreamInterface` trait and `MessageInterface` trait (ADR-035)
|
||||||
|
- `InterfaceSession` trait — `recv()`/`send()` producing/consuming `InterfaceEvent` frames
|
||||||
|
- `InterfaceRequest` / `InterfaceResponse` — normalized request/response for message interfaces
|
||||||
- `Socks5Server` — local SOCKS5 proxy that forwards through SSH channels
|
- `Socks5Server` — local SOCKS5 proxy that forwards through SSH channels
|
||||||
- `PortForwarder` — manages local/remote port forwards
|
- `PortForwarder` — manages local/remote port forwards
|
||||||
- `ServerHandler` — russh server handler with configurable auth and channel policies
|
- `ServerHandler` → `SshInterface` — russh server handler with configurable auth and channel policies
|
||||||
- `Identity` / `IdentityProvider` — core identity types (ADR-029)
|
- `Identity` / `IdentityProvider` — core identity types (ADR-029)
|
||||||
|
- `CredentialProvider` / `CredentialSet` — outbound credential types (ADR-036)
|
||||||
- `OperationSpec` — operation registration for call protocol (ADR-025)
|
- `OperationSpec` — operation registration for call protocol (ADR-025)
|
||||||
|
- `OperationEnv` / `OperationContext` — universal composition and operation context
|
||||||
- `ConnectOptions` / `ServeOptions` — programmatic configuration structs
|
- `ConnectOptions` / `ServeOptions` — programmatic configuration structs
|
||||||
- `StaticConfig` / `DynamicConfig` — static/immutable vs. hot-reloadable config (ADR-030)
|
- `StaticConfig` / `DynamicConfig` — static/immutable vs, hot-reloadable config (ADR-030)
|
||||||
- `ConfigReloadHandle` — programmatic reload of dynamic config
|
- `ConfigReloadHandle` — programmatic reload of dynamic config
|
||||||
|
- `ForwardingPolicy` — rule-based allow/deny for channel targets (ADR-031)
|
||||||
|
- `ListenerConfig` — stream and message listener configuration
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
@@ -134,7 +145,7 @@ The `alknet-core` crate exports the pluggable components for embedding or progra
|
|||||||
|
|
||||||
1. **SSH runs over transport, not alongside** — The transport layer produces a single `AsyncRead+AsyncWrite+Unpin+Send` stream. SSH runs over that stream via `russh::client::connect_stream()` / `russh::server::run_stream()`. The SSH layer never knows what transport it's on. (ADR-001, ADR-004)
|
1. **SSH runs over transport, not alongside** — The transport layer produces a single `AsyncRead+AsyncWrite+Unpin+Send` stream. SSH runs over that stream via `russh::client::connect_stream()` / `russh::server::run_stream()`. The SSH layer never knows what transport it's on. (ADR-001, ADR-004)
|
||||||
|
|
||||||
2. **Three-layer model: Transport, Interface, Protocol** — SSH is an interface (Layer 2), not a transport (Layer 1). A connection is always a (Transport, Interface) pair. The call protocol (Layer 3) is agnostic to both. This enables DNS control channels, raw framing, and WebTransport direct call protocol without wrapping SSH inside those transports. (ADR-026)
|
2. **Three-layer model: Transport, Interface, Protocol** — SSH is a StreamInterface (Layer 2), not a transport (Layer 1). HTTP and DNS are MessageInterfaces (Layer 2). A connection is always a (Transport, StreamInterface) pair for stream-based interfaces, or a standalone MessageInterface for message-based ones. The call protocol (Layer 3) is agnostic to both. This enables HTTP interfaces, DNS control channels, and local service mesh without wrapping SSH. (ADR-026, ADR-035)
|
||||||
|
|
||||||
3. **SOCKS5 is the primary client interface** — Port forwarding is built on top of SOCKS5-like channel management. For VPN-like "route all traffic" behavior, users run `tun2proxy` alongside alknet's SOCKS5 proxy. TUN is not in the project scope. (ADR-005, ADR-014)
|
3. **SOCKS5 is the primary client interface** — Port forwarding is built on top of SOCKS5-like channel management. For VPN-like "route all traffic" behavior, users run `tun2proxy` alongside alknet's SOCKS5 proxy. TUN is not in the project scope. (ADR-005, ADR-014)
|
||||||
|
|
||||||
@@ -193,6 +204,9 @@ The `alknet-core` crate exports the pluggable components for embedding or progra
|
|||||||
| [032](decisions/032-event-boundary-discipline.md) | Event boundary | Domain events never cross service boundaries |
|
| [032](decisions/032-event-boundary-discipline.md) | Event boundary | Domain events never cross service boundaries |
|
||||||
| [033](decisions/033-operationenv-irpc-call-protocol.md) | OperationEnv | Universal composition, three dispatch paths |
|
| [033](decisions/033-operationenv-irpc-call-protocol.md) | OperationEnv | Universal composition, three dispatch paths |
|
||||||
| [034](decisions/034-head-worker-terminology.md) | Head/worker | Replaces hub/spoke terminology |
|
| [034](decisions/034-head-worker-terminology.md) | Head/worker | Replaces hub/spoke terminology |
|
||||||
|
| [035](decisions/035-streaminterface-messageinterface-split.md) | StreamInterface/MessageInterface | Two Layer 2 trait categories for stream vs message |
|
||||||
|
| [036](decisions/036-credentialprovider-core-type.md) | CredentialProvider as core type | Outbound credentials in `alknet_core::credentials` |
|
||||||
|
| [037](decisions/037-api-keys-dynamic-config.md) | API keys in DynamicConfig | Hash-verified bearer tokens for service accounts |
|
||||||
|
|
||||||
## Open Questions
|
## Open Questions
|
||||||
|
|
||||||
@@ -204,10 +218,12 @@ relationship).
|
|||||||
## References
|
## References
|
||||||
|
|
||||||
- [transport.md](transport.md) — Transport abstraction (Layer 1)
|
- [transport.md](transport.md) — Transport abstraction (Layer 1)
|
||||||
- [interface.md](interface.md) — Interface layer (Layer 2)
|
- [interface.md](interface.md) — StreamInterface and MessageInterface (Layer 2)
|
||||||
- [call-protocol.md](call-protocol.md) — Call protocol (Layer 3)
|
- [call-protocol.md](call-protocol.md) — Call protocol (Layer 3)
|
||||||
- [auth.md](auth.md) — Unified authentication
|
- [auth.md](auth.md) — Unified authentication, API keys, credential presentation
|
||||||
- [identity.md](identity.md) — Identity and IdentityProvider
|
- [identity.md](identity.md) — Identity and IdentityProvider
|
||||||
|
- [credentials.md](credentials.md) — CredentialProvider and CredentialSet (outbound auth)
|
||||||
|
- [definitions.md](definitions.md) — Terminology disambiguation
|
||||||
- [configuration.md](configuration.md) — StaticConfig, DynamicConfig, ForwardingPolicy
|
- [configuration.md](configuration.md) — StaticConfig, DynamicConfig, ForwardingPolicy
|
||||||
- [services.md](services.md) — irpc service layer, OperationEnv
|
- [services.md](services.md) — irpc service layer, OperationEnv
|
||||||
- [server.md](server.md) — Server acceptance, channel handling
|
- [server.md](server.md) — Server acceptance, channel handling
|
||||||
|
|||||||
Reference in New Issue
Block a user