Files
alknet/docs/architecture/credentials.md
glm-5.1 cfc44008d3 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)
2026-06-09 08:09:45 +00:00

10 KiB
Raw Permalink Blame History

status, last_updated
status last_updated
draft 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

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

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:

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:

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:

// 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:

// 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 AC). 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.

  • OQ-CP-02: Where should OIDC provider operations live if alknet becomes an OIDC provider (Phase D)? See open-questions.md.

  • OQ-CP-03: How do credential rotations propagate across a cluster? See open-questions.md.

  • OQ-CP-04: Should CredentialSet include request-signing capability? See open-questions.md.

Design Decisions

ADR Decision Summary
036 CredentialProvider as core type Outbound credentials in alknet_core::credentials, parallel to IdentityProvider
029 Identity as core type Inbound auth — the opposite direction
032 Event boundary Secret service domain events stay internal

References