- 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)
10 KiB
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 rotationis_expired: Check TTL before useprovision: 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:
- Operator configures credentials:
alknet credential add vast-ai --type bearer --token-file ./key.txt - CLI encrypts via
SecretProtocol::Encrypt(AES-256-GCM, key at pathm/74'/2'/0'/0') - Encrypted credential stored as
EncryptedDatanode in metagraph, tagged with service name - At startup,
SecretStoreCredentialProvidercallsSecretProtocol::Decryptfor each configured service - 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
CredentialProviderandCredentialSetlive inalknet_core::credentials. No database dependency at the core level.CredentialProviderdoes not depend onIdentityProvider. They compose throughOperationEnv, not through dependency.ManagedCredentialProviderandCredentialManagerare Phase C concepts. They are defined as extension points but not implemented yet.- Identity-bound credentials use
Identity.idas the account key. In config-backed deployments, this is the fingerprint or key prefix. In database-backed deployments, this is the account UUID. SecretStoreCredentialProviderdepends onSecretProtocol::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
CredentialSetvariants cover all identified credential types (Phases A–C). 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
CredentialProvidersupport 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
CredentialSetinclude 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
- identity.md — IdentityProvider (inbound auth, opposite direction)
- secret-service.md — SecretProtocol, EncryptedData
- services.md — OperationEnv, OperationContext
- definitions.md — IdentityProvider vs CredentialProvider disambiguation
- research/phase2/credential-provider.md — Full analysis with rustfs/gitea integration