Files
alknet/tasks/integration/phase2/api-keys-dynamic-config.md
glm-5.1 aafee72f4c Decompose Phase 2 (Core Bridge) into 8 dependency-ordered tasks
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)
2026-06-09 09:33:22 +00:00

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
credential-provider-trait
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 ApiKeyEntry struct: prefix, hash, scopes, description, optional ttl/expires_at
  • Add api_keys: Vec<ApiKeyEntry> to AuthPolicy (or a separate section on DynamicConfig)
  • Extend ConfigIdentityProvider::resolve_from_token() to check API keys: prefix match → hash verification → return Identity
  • API keys produce Identity { id: "<prefix>", scopes: <from entry>, resources: {} }
  • The AuthToken path (Ed25519 signed timestamp) is unchanged — both go through the same resolve_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

  • ApiKeyEntry struct defined with prefix, hash, scopes, description, expires_at: Option<u64> fields
  • AuthPolicy gains an api_keys: Vec<ApiKeyEntry> field (or DynamicConfig gains a separate api_keys section)
  • ConfigIdentityProvider::resolve_from_token() checks API keys: matches prefix, verifies SHA-256 hash of the full token, returns Identity on success
  • API key lookup: tokens starting with alk_ (or configured prefix) are treated as API keys; others go through the AuthToken verification path
  • Expired API keys (where expires_at is set and in the past) are rejected
  • API key scopes propagate to the returned Identity.scopes field
  • DynamicConfig::default() includes an empty api_keys list (no behavioral change)
  • ConfigReloadHandle reloads API keys along with the rest of AuthPolicy
  • 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_keys should live on AuthPolicy or be a separate section. Putting it on AuthPolicy keeps all auth-related config together and ensures atomic reloads. The ConfigIdentityProvider already has access to Arc<ArcSwap<DynamicConfig>> so it can read both authorized_keys and api_keys from 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 &str or a new enum that can be either an AuthToken string or an API key string. Alternatively, resolve_from_token() can accept &str and internally discriminate by prefix/format.

Summary

To be filled on completion