Files
alknet/docs/architecture/auth.md
glm-5.1 19b3d3a078 docs: write Phase 0 architecture foundation — ADRs 026-034, spec docs, and task updates
Phase 0a — ADRs (9 new):
- ADR-026: Transport/interface separation (three-layer model)
- ADR-027: Crate decomposition (core, secret, storage, flowgraph, napi, CLI)
- ADR-028: Auth as irpc service (AuthProtocol behind feature flag)
- ADR-029: Identity as core type (Identity + IdentityProvider in alknet-core)
- ADR-030: Static/dynamic config split (ArcSwap, ConfigReloadHandle)
- ADR-031: Forwarding policy (rule-based allow/deny, TransportKind-aware)
- ADR-032: Event boundary discipline (domain, irpc, call protocol boundaries)
- ADR-033: OperationEnv universal composition (three dispatch paths)
- ADR-034: Head/worker terminology (replace hub/spoke)

Phase 0b — New spec documents (7):
- identity.md, services.md, interface.md, configuration.md,
  storage.md, flowgraph.md, secret-service.md

Updated existing docs:
- auth.md: reference identity.md for canonical definitions, add AuthProtocol
- open-questions.md: resolve OQ-12, OQ-16, OQ-18, OQ-22, OQ-23-25
- README.md: add all new docs, ADRs 026-034

Marked 19 architecture tasks as completed.
2026-06-07 09:32:58 +00:00

11 KiB

status, last_updated
status last_updated
draft 2026-06-07

Authentication

What

A unified authentication layer that works across all transports — SSH-over-any- transport and WebTransport (non-SSH HTTP-level transports). The same key material (Ed25519 authorized keys and certificate authorities) is shared across both auth paths. Identity resolution produces a transport-agnostic Identity that carries scopes and resources for downstream authorization.

Why

Alknet currently authenticates connections exclusively through SSH public key auth. Non-SSH transports (WebTransport) cannot perform SSH key exchange — they need a different auth presentation that shares the same key material. The unified auth layer ensures one key set, one identity, one rotation mechanism across all transports. See ADR-023 for the decision context.

The canonical definitions of Identity and IdentityProvider are in identity.md. This document covers auth-specific behavior: auth presentation per transport, AuthPolicy structure, and the auth service relationship.

Architecture

Identity and IdentityProvider

See identity.md for the canonical definitions of:

  • Identity struct ({ id, scopes, resources })
  • IdentityProvider trait (resolve_from_fingerprint(), resolve_from_token())
  • ConfigIdentityProvider (default, ArcSwap-backed)
  • StorageIdentityProvider (production, SQLite-backed, in alknet-storage)
  • AuthProtocol irpc service (behind irpc feature flag)

The key relationship: IdentityProvider is the contract. ConfigIdentityProvider is the default implementation (reads from DynamicConfig.auth). AuthProtocol 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.

Auth Presentation Per Transport

Transport Auth presentation Verification
SSH (TCP, TLS, iroh) SSH public key auth in the SSH handshake ServerAuthConfig::authenticate_publickey() — key lookup in authorized set
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 verification result is the same: an authenticated identity with scopes.

Token Authentication

For non-SSH transports, the client constructs an authentication token:

AuthToken = base64url(key_id || timestamp || signature)

  key_id    = SHA-256 fingerprint of the Ed25519 public key (32 bytes)
  timestamp = Unix seconds, big-endian u64 (8 bytes)
  signature = Ed25519 sign(key_id || timestamp_bytes, private_key)

Wire format when passed in a WebTransport CONNECT request:

CONNECT https://server:443/alknet?token=<AuthToken>

Server verification:

  1. Base64url-decode the token
  2. Extract key_id (first 32 bytes)
  3. Look up key_id in the same authorized_keys set that SSH auth uses
  4. Verify the Ed25519 signature against (key_id || timestamp_bytes) using the matching public key
  5. Check timestamp is within the acceptable window (configurable, default ±300 seconds)
  6. Resolve to the same Identity that SSH pubkey auth would produce

The key fingerprint in the token serves double duty: it identifies which key to verify against, and it ties the signature to a specific key (swapping key_id invalidates the signature).

Replay Protection

V1 uses timestamp-only (±300s window, no server state). The replay trade-offs and future zero-replay options (nonce challenge-response) are documented in ADR-023.

IdentityProvider and Auth Service Relationship

The IdentityProvider trait (defined in identity.md) decouples alknet-core from any specific identity storage. Two implementations exist:

  • ConfigIdentityProvider (in alknet-core) — reads from ArcSwap<DynamicConfig.auth>. Every authorized key gets a default scope set. No database required. This is the default for minimal deployments.

  • StorageIdentityProvider (in alknet-storage) — backed by SQLite peer_credentials and api_keys tables plus the ACL graph. Resolves fingerprint → account → organization membership → effective scopes.

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. Both the trait path and the irpc path produce the same Identity result.

The trait is the contract. The backing store is pluggable. Alknet-core never depends on Honker, SQLite, or any specific database.

AuthPolicy Structure

AuthPolicy in DynamicConfig holds both auth paths, sharing key material:

pub struct AuthPolicy {
    pub ssh: SshAuthConfig,
    pub token: TokenAuthConfig,
}

pub struct SshAuthConfig {
    pub authorized_keys: HashSet<PublicKey>,
    pub cert_authorities: Vec<CertAuthorityEntry>,
    // Existing fields from current ServerAuthConfig
}

pub struct TokenAuthConfig {
    pub enabled: bool,
    pub max_token_age: Duration,  // Timestamp window (default: 300s)
    pub key_source: TokenKeySource,
}

pub enum TokenKeySource {
    /// Share the same authorized_keys set with SshAuthConfig.
    /// Default and recommended for v1.
    Shared,
    /// Separate key set for non-SSH transports.
    /// For deployments that want distinct access control per transport.
    Separate(HashSet<PublicKey>),
}

When TokenKeySource::Shared (the default), adding a key to authorized_keys immediately grants access via both SSH and WebTransport. One key set, one reloadAuth() call, one rotation.

Auth Flow in the Server

SSH transport (existing, unchanged):

Client connects → SSH handshake → auth_publickey() callback
  → ServerAuthConfig::authenticate_publickey() or authenticate_certificate()
  → Auth::Accept or Auth::Reject

WebTransport transport (new):

Browser connects → WebTransport CONNECT request
  → SessionRequest inspection: extract token from URL path or header
  → TokenAuthConfig verification: decode token → lookup key_id → verify signature → check timestamp
  → session_request.accept() or session_request.forbidden()

After auth, both paths produce an Identity. The Identity is attached to the connection and used by ForwardingPolicy and the call protocol to make authorization decisions.

WebTransport SessionRequest Inspection

The wtransport library's SessionRequest provides:

  • path() — URL path (e.g., /alknet?token=...)
  • headers() — HTTP headers (for Authorization: Bearer ...)
  • origin() — Browser origin (for CORS-like restrictions)
  • remote_address() — Client UDP address

Token extraction from URL path is preferred for browser WebTransport because the W3C API (new WebTransport(url)) naturally includes query parameters. For native clients (Deno, CLI), the Authorization header is also supported.

Browser-Side Token Construction

// Illustrative — see client SDK for production implementation
async function createAuthToken(keyPair) {
    const publicKey = await crypto.subtle.exportKey('raw', keyPair.publicKey);
    const keyId = new Uint8Array(await crypto.subtle.digest('SHA-256', publicKey));

    const timestamp = new ArrayBuffer(8);
    new DataView(timestamp).setBigUint64(0, BigInt(Math.floor(Date.now() / 1000)));

    const message = new Uint8Array([...keyId, ...new Uint8Array(timestamp)]);
    const signature = await crypto.subtle.sign('Ed25519', keyPair.privateKey, message);

    const token = new Uint8Array([...keyId, ...new Uint8Array(timestamp), ...new Uint8Array(signature)]);
    return btoa(String.fromCharCode(...token))
        .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

Browsers support Ed25519 key generation and signing via SubtleCrypto (Chrome 105+, Firefox 130+, Safari 17+). Deno supports it natively. No external dependencies needed.

Constraints

  • Auth tokens are Ed25519-signed with the same key pair used for SSH auth. No separate key management for non-SSH transports.
  • IdentityProvider is the only interface between alknet-core and identity storage. No database dependency at the core level.
  • The SSH auth path is unchanged. auth_publickey() continues to work exactly as it does today. Token auth is additive.
  • Certificate authority tokens are not supported for token auth in v1. CA verification requires the full OpenSSH certificate structure, which doesn't fit in a simple signed timestamp. This can be added later if needed.
  • 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 exclusively.

Security Considerations

Token in URL: The auth token is passed as a URL query parameter (?token=...) for browser WebTransport compatibility. This is a known web security consideration:

  • Server logs: The token may appear in HTTP access logs. Servers MUST strip or redact the token query parameter before logging the request URL.
  • Browser history: The token may appear in browser history. Timestamps limit exposure to the token window (±300s).
  • Referrer headers: WebTransport does not send referrer headers, so the token does not leak via HTTP Referer.
  • Native clients: Deno and native clients SHOULD prefer the Authorization: Bearer header over URL parameters when the client supports custom headers.

Open Questions

  • OQ-18: Source of Identity.scopes Resolved per ADR-029 and ADR-031. IdentityProvider owns scopes, ForwardingPolicy uses scopes from Identity. See open-questions.md.

  • OQ-19: Should the WebTransport listener require its own TLS identity (separate from the SSH-over-TLS listener), or can they share the same certificate? Deferred to Phase 4. See open-questions.md.

Design Decisions

ADR Decision Summary
012 Ed25519 + cert-authority Key-based auth, no passwords
023 Unified auth, shared key material Same keys for SSH and token auth
028 Auth as irpc service AuthProtocol behind feature flag; IdentityProvider is the contract
029 Identity as core type Identity and IdentityProvider in alknet-core

References