Phase 0a — ADRs (9 new): - ADR-026: Transport/interface separation (three-layer model) - ADR-027: Crate decomposition (core, secret, storage, flowgraph, napi, CLI) - ADR-028: Auth as irpc service (AuthProtocol behind feature flag) - ADR-029: Identity as core type (Identity + IdentityProvider in alknet-core) - ADR-030: Static/dynamic config split (ArcSwap, ConfigReloadHandle) - ADR-031: Forwarding policy (rule-based allow/deny, TransportKind-aware) - ADR-032: Event boundary discipline (domain, irpc, call protocol boundaries) - ADR-033: OperationEnv universal composition (three dispatch paths) - ADR-034: Head/worker terminology (replace hub/spoke) Phase 0b — New spec documents (7): - identity.md, services.md, interface.md, configuration.md, storage.md, flowgraph.md, secret-service.md Updated existing docs: - auth.md: reference identity.md for canonical definitions, add AuthProtocol - open-questions.md: resolve OQ-12, OQ-16, OQ-18, OQ-22, OQ-23-25 - README.md: add all new docs, ADRs 026-034 Marked 19 architecture tasks as completed.
7.1 KiB
status, last_updated
| status | last_updated |
|---|---|
| draft | 2026-06-07 |
Identity
What
The Identity type and IdentityProvider trait are the core abstractions for
authentication and authorization in alknet. Identity is the unified result of
auth verification — whether via SSH public key, signed timestamp token, or
database lookup. IdentityProvider is the trait that resolves credentials to an
Identity, decoupling alknet-core from any specific identity storage.
Why
Auth, forwarding policy, and call protocol all need to know who is making a
request and what they are authorized to do. Without Identity in core, each
subsystem would define its own identity type, leading to duplication and
conversion boilerplate. Without IdentityProvider as a trait, alknet-core
would either hardcode config-file-based auth or take a database dependency —
neither acceptable for a library crate.
The IdentityProvider trait exists because the same auth verification concept
needs two implementations: ConfigIdentityProvider for minimal deployments (all
keys in memory via ArcSwap) and StorageIdentityProvider for production (SQLite
lookup via peer_credentials and ACL graph). The trait is the contract; the
backing store is pluggable.
Architecture
Identity Struct
pub struct Identity {
pub id: String, // Fingerprint or account UUID
pub scopes: Vec<String>, // e.g., ["relay:connect", "service:gitea:read"]
pub resources: HashMap<String, Vec<String>>, // e.g., {"service": ["gitea", "registry"]}
}
The id field serves dual purpose:
- Config-based auth (
ConfigIdentityProvider): holds the Ed25519 key fingerprint (e.g.,SHA256:abc123...) - Database-backed auth (
StorageIdentityProvider): holds the account UUID from theaccountstable
This keeps the type simple while accommodating both auth paths. Downstream
consumers (forwarding policy, call protocol ACL checks) use scopes and
resources without knowing whether the identity came from a config file or a
database.
IdentityProvider Trait
pub trait IdentityProvider: Send + Sync + 'static {
/// Resolve an SSH public key fingerprint to an identity.
fn resolve_from_fingerprint(&self, fingerprint: &str) -> Option<Identity>;
/// Resolve an auth token to an identity.
/// Returns None if the token is invalid, expired, or the key is not authorized.
fn resolve_from_token(&self, token: &AuthToken) -> Option<Identity>;
}
Both SSH key auth and token auth resolve to the same Identity type. The trait
lives in alknet_core::auth.
ConfigIdentityProvider (Default)
Reads from ArcSwap<DynamicConfig.auth> per ADR-030. Every authorized key gets
a default scope set. No database dependency. This is the default for CLI and
single-node deployments.
pub struct ConfigIdentityProvider {
auth_config: Arc<ArcSwap<DynamicConfig>>,
}
impl IdentityProvider for ConfigIdentityProvider {
fn resolve_from_fingerprint(&self, fingerprint: &str) -> Option<Identity> {
let config = self.auth_config.load();
config.auth.ssh.authorized_keys.get(fingerprint)
.map(|key_entry| Identity {
id: fingerprint.to_string(),
scopes: key_entry.scopes.clone(),
resources: key_entry.resources.clone(),
})
}
fn resolve_from_token(&self, token: &AuthToken) -> Option<Identity> {
// Verify Ed25519 signature against the same authorized_keys set
// Resolve to the same Identity as SSH auth would produce
}
}
StorageIdentityProvider (Production)
Implemented in alknet-storage (not in alknet-core). Backed by SQLite
peer_credentials and api_keys tables plus the ACL graph. Resolves
fingerprint → account → organization membership → effective scopes. Uses the
IdentityProvider trait defined in alknet-core, providing the concrete impl via
the trait.
AuthProtocol irpc Service
The AuthProtocol irpc service (behind the irpc feature flag per ADR-028)
provides an async boundary for auth verification. It is one way to satisfy the
IdentityProvider trait, not a replacement for it:
enum AuthProtocol {
VerifyPubkey { fingerprint: String, key_data: Vec<u8> },
VerifyToken { token_bytes: Vec<u8>, timestamp: u64 },
ReloadKeys,
CheckAccess { identity: Identity, operation: String },
}
enum AuthResult {
Ok(Identity),
Denied(String),
}
The relationship:
- Trait-based path: Handler calls
identity_provider.resolve_from_fingerprint()directly. Zero overhead. Used when irpc is disabled or when the implementation is local. - irpc path: Handler calls
identity_provider.resolve_from_fingerprint(), which internally delegates toAuthProtocol::VerifyPubkeyvia an irpc client. Used in production deployments with SQLite-backed auth.
Both paths produce the same Identity result.
Auth Flows
SSH key auth (existing, unchanged):
Client connects → SSH handshake → auth_publickey() callback
→ IdentityProvider::resolve_from_fingerprint(fingerprint)
→ Some(Identity) or None
Token auth (new, for non-SSH transports):
Browser connects → WebTransport CONNECT request
→ Extract token from URL path or Authorization header
→ IdentityProvider::resolve_from_token(token)
→ Some(Identity) or None
Both paths produce an Identity. The Identity is attached to the connection
and used by ForwardingPolicy and call protocol for authorization decisions.
Constraints
IdentityandIdentityProviderlive inalknet_core::auth. No database dependency at the core level (ADR-029).- alknet-storage implements the core trait — the dependency goes from storage to core, not the other way.
- The
idfield inIdentityserves dual purpose (fingerprint or UUID). This is a deliberate simplification — downstream consumers don't need to know the source. - Certificate authority tokens are not supported for token auth in v1 (ADR-023).
- The irpc feature flag means nodes that only do SSH tunneling don't need the service layer overhead.
Open Questions
- None specific to this spec. See open-questions.md for general auth questions (OQ-15, OQ-19).
Design Decisions
| ADR | Decision | Summary |
|---|---|---|
| 029 | Identity as core type | Identity and IdentityProvider live in alknet-core, not storage |
| 028 | Auth as irpc service | AuthProtocol behind feature flag; IdentityProvider is the contract |
| 023 | Unified auth | Same key material for SSH and token auth; same Identity result |
References
- auth.md — Token authentication, AuthPolicy, WebTransport session handling
- research/services.md — AuthService, AuthProtocol definition
- research/integration-plan.md — Phase 1.2
- ADR-030 — DynamicConfig (ConfigIdentityProvider reads from it)
- ADR-031 — ForwardingPolicy consumes Identity.scopes