Phase 2 completes the interface-to-protocol bridge and adds core types that external crates depend on. The 8 tasks are organized into 5 generations with clear dependencies: - Gen 1: StreamInterface/MessageInterface trait split (must go first) - Gen 2: SshSession bridge, RawFraming impl, CredentialProvider (parallel) - Gen 3: API keys in DynamicConfig (depends on CredentialProvider) - Gen 4: ListenerConfig HTTP/DNS stubs + axum scaffold - Gen 5: Review gate before Phase 3 Key design decisions: - 2.4a/2.4b split: SecretStoreCredentialProvider deferred to Phase 3 - API keys (2.6) must land before axum scaffold (2.7) - ListenerConfig (2.5) must land before axum scaffold (2.7) - Gen 2 tasks are parallelizable (separate modules)
4.4 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | |
|---|---|---|---|---|---|---|---|---|
| api-keys-dynamic-config | Add API keys to DynamicConfig.auth and extend IdentityProvider token resolution | pending |
|
narrow | low | component | implementation |
Description
Add [[auth.api_keys]] support to DynamicConfig and extend ConfigIdentityProvider::resolve_from_token() to verify API keys alongside existing AuthTokens. API keys are shorter, simpler bearer strings (hash-verified, with optional TTL and scopes) for service accounts and automation — they don't require Ed25519 key pairs like AuthTokens do.
Per ADR-037 and research/phase2/interface-model.md (Config section):
API Key format: alk_<random_prefix>_<secret> (or similar). Storage uses SHA-256 hash of the full key. Lookup is by prefix (first N characters), then hash verification of the full key.
Config format:
[[auth.api_keys]]
prefix = "alk_dGhl"
hash = "sha256:abc123..."
scopes = ["relay:connect"]
description = "dashboard service account"
Key changes:
- Add
ApiKeyEntrystruct:prefix,hash,scopes,description,optional ttl/expires_at - Add
api_keys: Vec<ApiKeyEntry>toAuthPolicy(or a separate section onDynamicConfig) - Extend
ConfigIdentityProvider::resolve_from_token()to check API keys: prefix match → hash verification → returnIdentity - API keys produce
Identity { id: "<prefix>", scopes: <from entry>, resources: {} } - The
AuthTokenpath (Ed25519 signed timestamp) is unchanged — both go through the sameresolve_from_token()method, discriminated by format/prefix
Why this is Phase 2: The HTTP interface (task 2.7) needs bearer token auth, and API keys are the simplest mechanism for IdentityProvider::resolve_from_token(). Without this, HTTP auth has no config-based auth mechanism.
Acceptance Criteria
ApiKeyEntrystruct defined withprefix,hash,scopes,description,expires_at: Option<u64>fieldsAuthPolicygains anapi_keys: Vec<ApiKeyEntry>field (orDynamicConfiggains a separateapi_keyssection)ConfigIdentityProvider::resolve_from_token()checks API keys: matches prefix, verifies SHA-256 hash of the full token, returnsIdentityon success- API key lookup: tokens starting with
alk_(or configured prefix) are treated as API keys; others go through theAuthTokenverification path - Expired API keys (where
expires_atis set and in the past) are rejected - API key scopes propagate to the returned
Identity.scopesfield DynamicConfig::default()includes an emptyapi_keyslist (no behavioral change)ConfigReloadHandlereloads API keys along with the rest ofAuthPolicy- Unit test: valid API key authenticates via
resolve_from_token() - Unit test: expired API key is rejected
- Unit test: wrong hash is rejected
- Unit test: unknown prefix is rejected (falls through to AuthToken path)
- Unit test: API key scopes appear in the resolved
Identity - All existing auth tests continue to pass (no behavioral change for SSH key auth)
References
- docs/architecture/decisions/037-api-keys-dynamic-config.md — ADR-037
- docs/research/phase2/interface-model.md — API keys in config, auth table
- docs/research/integration-plan.md — Phase 2.6
- crates/alknet-core/src/config/dynamic_config.rs — DynamicConfig, AuthPolicy
- crates/alknet-core/src/auth/identity.rs — ConfigIdentityProvider, IdentityProvider trait
Notes
The prefix match approach means we don't store the full API key in config — just the first ~8 chars for fast lookup and the SHA-256 hash for verification. This mirrors how GitHub/personal access tokens work.
Consider whether
api_keysshould live onAuthPolicyor be a separate section. Putting it onAuthPolicykeeps all auth-related config together and ensures atomic reloads. TheConfigIdentityProvideralready has access toArc<ArcSwap<DynamicConfig>>so it can read bothauthorized_keysandapi_keysfrom the same reload.
The
resolve_from_token()method currently takes&AuthToken— API keys are NOT AuthTokens (they're simple bearer strings). The method signature may need to accept a generic&stror a new enum that can be either an AuthToken string or an API key string. Alternatively,resolve_from_token()can accept&strand internally discriminate by prefix/format.
Summary
To be filled on completion