Build the application. Let the fabric carry trust.
Korium gives Rust applications self-certifying identity, encrypted node transport,
discovery, messaging, streams, pub-sub, tunnels, and optional service authorization
through one bounded runtime. Start with the interaction your application needs.
There is no hidden protected-mode flag. The identity evidence supplied at build time determines admission behavior.
Default
DID-only node
Use for chat, collaboration, agent meshes, device networks, and other decentralized applications.
mTLS did:korium identity and key-bound PoW
Encrypted, bounded protocol handling
Your app owns contacts, invitations, membership, spam controls, and action policy
Node::builder().build().await?
Explicit evidence
Verified service
Use for APIs, gateways, CI/CD services, edge ingestion, and cross-organization service calls.
Everything in a DID-only node
Cert-borne service identity and source allowlist
Fail-closed bi-stream admission plus optional L7 invocation grants
.with_service_identity(root, bundle)
If you need…
Start with
Primary API
Small RPC calls
DID-only or verified service
send / incoming_requests
Custom or long-lived protocols
DID-only or verified service
open_stream / incoming_streams
Many-to-many events
DID-only node
subscribe / publish
Stable service or resource names
Either posture
announce_* / lookup_*
Legacy TCP connectivity
Tunnel-enabled node
start_tunnel_entry
DNS-native service lookup
DNS-enabled node or gateway
start_dns_listener_at
Service-scoped authorization
Verified service
verify_action
02
Foundation
Configure one node
Construction starts the fabric and public DNS bootstrap by default. Persist both the keypair and its mined identity proof when identity must survive restarts; restoring a key without its matching proof fails the build.
Disable public bootstrap when your deployment distributes explicit seed identities and socket addresses. A join address is an untrusted reachability hint; the connection still authenticates the remote node with mTLS, DID binding, and PoW.
Identity verification, path validation, encrypted transport, bounded routing, and discovery records.
Your app handles
Who should be contacted, invitations and blocklists, node roles, and user-facing trust decisions.
04
Scenario
Request / response
Use PLAIN request streams for bounded request/response work. Requests are capped at 64 KiB; callers always provide their own end-to-end timeout.
Caller
use std::time::Duration;
let reply = node.send(
&remote_identity_hex,
br#"{"op":"status"}"#.to_vec(),
Duration::from_secs(5),
).await?;
Async server
let mut requests = node.incoming_requests().await?;
while let Some((from, pubkey, body, ctx, reply)) = requests.recv().await {
// Inspect ctx and authorize before changing state.
let response = handle(from, pubkey, body, ctx).await;
let _ = reply.send(response);
}
incoming_requests is take-once and is the only request-serving API; the synchronous set_request_handler shortcut was removed in 0.8.28. Own the receiving task, and keep draining it.
05
Scenario
Raw bidirectional streams
Use RAW streams for custom framing, file transfer, interactive sessions, or protocols that need full duplex. Korium authenticates and bounds stream admission; your protocol owns framing and payload limits.
Open and accept
use tokio::io::{AsyncReadExt, AsyncWriteExt};
// Caller
let (mut send, mut recv) = node.open_stream(&remote_identity_hex).await?;
send.write_all(b"hello").await?;
send.finish()?;
let reply = recv.read_to_end(64 * 1024).await?;
// Receiver: take this once, then keep draining it.
let mut incoming = node.incoming_streams().await?;
while let Some((from, mut stream)) = incoming.recv().await {
if stream.kind() != IncomingStreamKind::Raw { continue; }
let body = stream.recv.read_to_end(64 * 1024).await?;
stream.send.write_all(&process(from, body)).await?;
stream.send.shutdown().await?;
}
Keep the guard alive.
GuardedRawStream owns the per-node admission permit. Do not discard it while retaining only detached I/O handles unless you also retain the guard returned by into_parts().
06
Scenario
Signed pub-sub
Use GossipSub for many-to-many events where duplicate, delayed, or reordered delivery is acceptable. Messages are signed and replay-bounded; publication does not grant application membership.
Subscribe and publish
node.subscribe("events/alerts").await?;
let mut messages = node.messages().await?; // take once
node.publish(
"events/alerts",
b"system update available".to_vec(),
).await?;
while let Some(message) = messages.recv().await {
consume(message).await?;
}
Good fit
Presence, invalidation, event fan-out, replicated notifications, and eventually consistent signals.
Use RPC instead
Commands that require a direct result, request-specific authorization, or business-level success semantics.
07
Scenario
Discover stable services
Service URNs stay stable while node identities rotate. Announcements publish signed, expiring liveness; lookups return candidates, never authority.
Service liveness
let service = "urn:korium:acme/payments";
// Serving node
node.announce_service(service).await?;
// Consumer
for (identity, addresses) in node.lookup_service_endpoints(service).await? {
println!("{} at {:?}", identity.to_hex(), addresses);
}
Discovery is not authorization.
A fresh signed record proves which node published the hint. It does not prove permission, service authority, health, or membership. Connect and enforce the appropriate application or verified-service policy.
08
Scenario
Locate resources without a registry
Publish exact resources or bounded parent prefixes under a namespace or service URN. Consumers always query an exact resource and receive bounded fresh candidates.
Resource discovery
use korium::ResourceSelector;
let context = "urn:korium:acme/storage";
node.announce_resource(
context,
ResourceSelector::prefix("repos/project"),
).await?;
if let Some(found) = node
.lookup_resource(context, "repos/project/readme")
.await?
{
for candidate in found.candidates {
node.connect(&candidate.identity.to_hex()).await?;
}
}
Resource strings are canonical printable ASCII and capped at 1024 bytes.
No list-all, wildcard, regex, glob, or prefix-enumeration lookup exists.
Use opaque, keyed, or tenant-scoped names when discovery subjects are sensitive.
unannounce_resource stops republishing; old records expire naturally.
09
Scenario
Bridge a TCP application
TUNNEL lets a local TCP client reach an in-process stream handler on a destination node. The entry tries native TLS 1.3 mTLS/TCP first and falls back to a QUIC TUNNEL stream through the fabric.
Destination and entry
use std::net::SocketAddr;
use korium::{IncomingStreamKind, Node};
// Destination: enable the exit and consume Tunnel streams.
let destination = Node::builder().tunnel().build().await?;
let mut incoming = destination.incoming_streams().await?;
// Entry: bind a local port for the legacy TCP client.
let entry = Node::new().await?;
let listen: SocketAddr = "127.0.0.1:13306".parse()?;
let (handle, shutdown) = entry
.start_tunnel_entry(listen, &destination_identity_hex)
.await?;
// Later: stop accepting and await clean termination.
shutdown.notify_one();
handle.await??;
Destination contract
Drain incoming_streams, select IncomingStreamKind::Tunnel, and map each accepted stream to local application state.
Operational bounds
256 concurrent sessions globally, 16 streams per remote node, 5-minute per-direction idle timeout, and a 1-hour session cap.
10
Kubernetes scenario
Expose Korium discovery through DNS
The DNS shim lets DNS-native workloads consume Korium identity and service discovery without embedding the Rust API. It is an authoritative, non-recursive UDP listener backed by the node's bounded DHT lookups—not a replacement for CoreDNS or a general-purpose resolver.
Pod-local
Sidecar resolver
Bind to loopback and configure one application or a local forwarding proxy to query port 5353.
Smallest network exposure
Korium identity and lifecycle stay with the workload
Best when the client supports a custom DNS server and port
--dns 127.0.0.1:5353
Namespace or cluster
DNS gateway
Run a persistent Korium node behind an internal UDP Service and query it explicitly.
One stable Kubernetes service address
Shared DHT view and bounded lookup concurrency
Keep access internal and enforce network-level source controls
--dns 0.0.0.0:5353
Build and run
# The standalone binary requires both features.
cargo build --release --bin korium --features cli,dns
# CLI bootstrap is opt-in. The DNS listener defaults to loopback:5353
# when --dns is omitted.
korium \
--port 4433 \
--dns 0.0.0.0:5353 \
--bootstrap \
--identity-file /var/lib/korium/identity
Library embedding
use std::net::SocketAddr;
let node = Node::builder().build().await?;
// Default: loopback on UDP 5353.
let local_dns = node.start_default_dns_listener().await?;
// Or expose an explicit address for a controlled gateway.
let bind: SocketAddr = "0.0.0.0:5353".parse()?;
let gateway_dns = node.start_dns_listener_at(bind).await?;
Query service URNs
A single-segment service URN maps deterministically to a DNS owner. For example,
urn:korium:acme.example/payments becomes
_korium._udp.u1.acme.example._svc.payments.svc.
DNS client
# Ask the Kubernetes Service explicitly; do not replace cluster DNS.
dig @korium-dns.platform.svc.cluster.local \
_korium._udp.u1.acme.example._svc.payments.svc. SRV
dig @korium-dns.platform.svc.cluster.local \
_korium._udp.u1.acme.example._svc.payments.svc. SVCB
Record
Consumer use
Returned evidence
SRV
Connect to a node or stable service
Port, generated target, and A/AAAA additionals
SVCB
Discover Korium transport parameters
ALPN, port, IPv4 and IPv6 hints
TXT …contact
Fetch a node contact record
Bounded postcard + base64url signed contact
TXT …did
Read the self-certifying node URI
did=did:korium:…
Kubernetes gateway
The following skeleton keeps the Korium identity on persistent storage and publishes DNS only through an internal UDP Service. Replace the image and storage class for your environment.
Query the gateway explicitly or configure a narrowly scoped local forwarder. Do not replace the Pod's normal nameserver or forward the broad .svc suffix: Kubernetes already owns *.svc.<cluster-domain>.
External DNS exposure requires an explicit non-loopback bind plus source-spoofing and rate controls.
The listener is UDP-only, authoritative, non-recursive, and accepts exactly one IN-class question per packet.
Responses are capped at 1232 bytes, in-flight queries at 64, and query work at 2 seconds.
Records use a 15-second TTL; service lookup gets a bounded 1.5-second DHT budget.
DNS output remains discovery evidence, not service authority or authorization. Korium mTLS and application policy still apply.
Persist the identity file with mode 0600. A new identity changes the gateway's node identity and requires new PoW.
11
Scenario
Run a verified service
A verified service presents an offline-authored authority bundle bound to its node key. Build fails if the proof is malformed, expired, rooted in another namespace, or bound to a different keypair.
Service admission + L7 grant
let service = Node::builder()
.contact(persisted_keypair, persisted_pow)
.with_service_identity(namespace_root, signed_bundle)
.build()
.await?;
assert_eq!(service.service_urn(), Some("urn:korium:acme/payments"));
let mut requests = service.incoming_requests().await?;
while let Some((_from, _pubkey, body, ctx, reply)) = requests.recv().await {
let request = parse_request(body)?;
let grant = service.verify_action(
&ctx,
request.bearer_token.as_deref(),
"payment.capture",
&format!("payments/{}", request.payment_id),
)?;
let _ = reply.send(capture(request, grant).await?);
}
Korium defines no action vocabulary. Canonicalize the resource before calling verify_action. L7 Biscuit grants are single-issuer, single-audience, service-scoped, terminal, non-delegating, and capped at 300 seconds; there is no fabric-wide instant revocation. That L7 rule is separate from the v2 service-instance delegation below, which authorizes a child key to speak for the same exact service URN.
Scale out a verified service without the root secret
This workflow is for the service-instance holder or its control plane, not an ordinary
calling client. The namespace root grants a bounded delegation capability before its
private key is destroyed; the authorized instance can then issue exact-URN child
credentials without choosing a new service name or policy. Enable Korium's
authoring feature in the binary or control-plane tool that performs the
first two phases.
1. While the namespace-root secret still exists
The root must issue the parent a v2 bundle with explicit delegation permission. Keep
the root public key before destroying the private key.
max_depth: 1 is the normal scale-out posture: the original instance may
authorize children, but those children cannot delegate again. The value limits ancestry
depth, not the number of sibling replicas the parent may authorize.
Delegation permission cannot be added later.
An existing v1 authority bundle cannot delegate, and it cannot be upgraded after the namespace-root private key has been destroyed.
2. After the namespace-root secret has been destroyed
The authorized parent generates or receives a child key, then signs a child bundle.
authorize() verifies the supplied parent bundle before signing and requires
the child validity window to stay within the parent's.
Exact-URN child authorization
use korium::{
authoring::{ExactUrnInstanceDelegationSpec, LeaseTerms},
Keypair,
};
let (child_key, child_pow) = Keypair::generate()?;
let child_bundle = ExactUrnInstanceDelegationSpec {
namespace_root: namespace_root_public,
parent_key: &parent_key,
verified_parent_bundle: &parent_bundle,
child_public_key: child_key.public_key_bytes(),
terms: LeaseTerms {
not_before_unix_seconds: now,
expires_unix_seconds: now + 300,
},
nonce: child_delegation_nonce,
}
.authorize()?;
The child authoring API deliberately accepts no URN, allow_sources, or
grant_issuers; all three are inherited from the root-signed parent bundle.
Persist the child key, matching PoW proof, child bundle, root public key, and the
authorization nonces according to your control-plane durability and audit requirements.
The namespace-root private key is not required.
3. Start the child normally
Existing NodeBuilder integration
let child = korium::Node::builder()
.contact(child_key, child_pow)
.with_service_identity(namespace_root_public, child_bundle)
.build()
.await?;
This is the entire runtime integration. NodeBuilder checks that the bundle
terminates at the supplied child key. Korium then embeds and verifies the delegated
proof through its existing TLS, connection, destination, and stream paths; no
transport-specific delegation API is needed.
Rotate the namespace root
Removing a node from a namespace means redeploying that namespace under a new root. A node pins its
acceptance policy separately from the root its own credential chains to, so the rollout can proceed as a
three-wave rolling redeployment instead of a flag day.
Rolling root transition
// Waves 1 and 2 carry the identical acceptance policy, so no wave
// ever presents a credential the rest of the fleet cannot root.
let node = Node::builder()
.contact(persisted_keypair, persisted_pow)
.with_service_identity(own_credential_root, signed_bundle)
.accepting_namespace_root_transition(
new_primary_root,
old_retiring_root,
retire_after_unix_seconds,
)
.build()
.await?;
Wave
Own credential
Accepts credentials rooted at
1
old
new primary + old retiring until T
2
new
new primary + old retiring until T
3
new
new only
A chain rooted at the retiring root verifies only while now < retire_after; its verified expiry becomes min(chain_expiry, retire_after), so admitted connections close at retirement.
Exposure is bounded only once wave 1 has reached every node — a node never reached has no knowledge of T. Treat completion of wave 1, not its start, as the start of the bound.
Retirement is fail-closed at the deadline: now == retire_after is already refused.
The build rejects equal primary and retiring roots, a deadline not in the future, and a node whose own credential root neither entry accepts.
Persist keypairs and PoW proofs. Regenerating them changes every DID, invalidating grant_issuers entries and forcing PoW re-mining fleet-wide.
12
Consumer contract
Know what the proof means
Authenticated node
Key possession
The live node controls the private key bound to its did:korium identity. This says nothing about permission or behavior.
Discovery record
Signed hint
The named node published fresh, bounded reachability or liveness data. The data is not membership or authority.
Service identity
Right to speak as a URN
A signed lease chain rooted in the pinned namespace root binds this node key to this service URN until the earliest expiry in the chain.
Invocation grant
Exact authorized action
An approved issuer granted one audience an action on a canonical resource for one service until a short deadline. The terminal L7 Biscuit does not authorize further grants.
Design for these realities
Delivery can be duplicated, delayed, replayed, reordered, or lost. Make application operations idempotent where needed.
Live transport does not depend on remote clock claims. Signed records and credentials use bounded local Unix-UTC validity checks.
There is no per-node revocation. Removing a node means redeploying the namespace under a new root; lease expiry is a backstop, not the removal path. Author short lease windows if you cannot redeploy quickly.
Audit events are structured where verified context exists; collection, retention, timestamps, integrity, and complete denial coverage are deployment responsibilities.
Keep the node alive for as long as its receivers and sessions should run. Prefer explicit shutdown().await; dropping a node aborts remaining work as a last-resort safety boundary.
Telemetry
let t = node.telemetry().await?;
metrics.gauge("korium.connected_peers", t.connected_peers);
metrics.gauge("korium.dht.pressure", t.pressure);
metrics.count("korium.transport.errors", t.transport_errors);
metrics.count("korium.gossip.delivery_drops", t.gossipsub_delivery_drops);
metrics.gauge("korium.tunnel.active", t.tunnel_active_sessions);
node.shutdown().await;
01
Drain take-once receivers continuously; backpressure and channel limits are intentional.
02
Persist the keypair and matching PoW proof together when identity continuity matters.
03
Use application deadlines around work beyond Korium’s built-in transport bounds.
04
Renew service authority and grants before expiry; never treat timeouts as consensus.
05
Alert on pressure, replay-capacity drops, transport errors, and sustained tunnel saturation.