This guide gets you from zero to verifying capability tokens in a real service. It covers two paths: the Hessra managed service and the self-hosted open core.
Which path is right for you
Managed service: Hessra runs the root authority for you. You get an identity token, a gitops policy repo, and an authority endpoint. Your services use hessra-client to request tokens and verify them locally. This is the fastest path to running capability security without managing infrastructure.
Open core: You embed the hessra-cap engine directly in your code. Good for understanding the model, local development, or systems where you want to own the full stack. See the hessra-cap examples for a complete self-contained demo.
The token format and verification logic are identical on both paths. If you start with the managed service and want to self-host later, or vice versa, the only thing that changes is how you request tokens.
Managed service setup
The managed service is in early access. To get provisioned, reach out at hello@hessra.net. You will receive:
- A private gitops repository with a
clients.tomlpolicy file - Your root authority URL (e.g.
yourco.hessra.net) - The server CA certificate for verifying the authority's TLS
mTLS certificates are also supported as an authentication mechanism if you already have them. For most new users, identity tokens are easier to get started with.
Defining policy
Your clients.toml in the gitops repo defines what each service is allowed to do. Each client is identified by the subject in its identity token and is granted capabilities over named resources.
[cacerts]
cert_chain = '''-----BEGIN CERTIFICATE-----
<your CA cert>
-----END CERTIFICATE-----'''
[[clients]]
identifier = { type = "san_uri", value = "uri:urn:yourco:api-gateway" }
resources = [
{ name = "orders", operations = ["read", "write"] },
{ name = "inventory", operations = ["read"] },
]
[[clients]]
identifier = { type = "san_uri", value = "uri:urn:yourco:orders-service" }
resources = [
{ name = "db.orders", operations = ["read", "write"] },
]
Push a commit to the repo and the root authority picks up the changes automatically.
Requesting capability tokens
Add hessra-client to your service:
[dependencies]
hessra-client = "0.1"
Build a client using your identity token and request a capability before each outbound call:
use hessra_client::{HessraClient, types::{TokenRequest, IdentityTokenRequest}};
// Your service's identity token. Provision this at startup from an env var or secret store
let identity_token = std::env::var("HESSRA_IDENTITY_TOKEN")?;
let client = HessraClient::builder()
.base_url("yourco.hessra.net")
.server_ca(include_str!("certs/ca.pem"))
.build()?;
let response = client.request_token_with_identity(
&TokenRequest {
resource: "orders".to_string(),
operation: "read".to_string(),
},
&identity_token,
).await?;
let token = response.token.expect("no token returned");
If you are using mTLS certificates instead of an identity token, use request_token with the client built with .mtls_cert and .mtls_key.
Send the capability token as a bearer header with your request:
reqwest::Client::new()
.get("https://orders-service.internal/orders")
.header("Authorization", format!("Bearer {token}"))
.send()
.await?;
Verifying capability tokens
The receiving service verifies tokens locally using the root authority's public key. No network call is required for verification itself.
Add the verification crates:
[dependencies]
hessra-cap-token = "1.0"
hessra-token-core = "1.0"
reqwest = { version = "0.12", features = ["rustls-tls", "json"] }
Public key and rotation
The root authority's public key rotates on a regular schedule. For the managed service this happens every 45. You have three reasonable strategies for handling this:
Fetch on failure (recommended for most services): cache the key at startup, and if verification fails, refetch and retry once before rejecting the request. This adds one extra HTTP call only when a rotation has occurred, which is rare.
Fetch on every request: simplest to implement, highest overhead. Fine for low-traffic services.
Parse expiry and prefetch: parse the certificate to determine when the key expires and schedule a refresh before that point. Most operationally robust but more code.
The example below uses the fetch-on-failure approach, which is what the Hessra waitlist service uses in production:
use hessra_cap_token::CapabilityVerifier;
use hessra_token_core::PublicKey;
use tokio::sync::RwLock;
use std::sync::Arc;
struct KeyCache {
key: RwLock<Option<PublicKey>>,
authority_url: String,
ca_pem: String,
}
impl KeyCache {
async fn get_or_fetch(&self) -> Result<PublicKey, Box<dyn std::error::Error>> {
if let Some(key) = *self.key.read().await {
return Ok(key);
}
self.refresh().await
}
async fn refresh(&self) -> Result<PublicKey, Box<dyn std::error::Error>> {
let certs = reqwest::Certificate::from_pem_bundle(self.ca_pem.as_bytes())?;
let mut builder = reqwest::Client::builder();
for cert in certs {
builder = builder.add_root_certificate(cert);
}
let client = builder.build()?;
let resp: serde_json::Value = client
.get(format!("{}/public_key", self.authority_url))
.send().await?
.json().await?;
let key = PublicKey::from_pem(resp["public_key"].as_str().unwrap())?;
*self.key.write().await = Some(key);
Ok(key)
}
}
async fn verify(
token: &str,
key_cache: &KeyCache,
resource: &str,
operation: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let key = key_cache.get_or_fetch().await?;
let result = CapabilityVerifier::new(
token.to_string(),
key,
resource.to_string(),
operation.to_string(),
).verify();
if result.is_err() {
// Verification failed. The key may have rotated. Refetch and retry once.
let fresh_key = key_cache.refresh().await?;
CapabilityVerifier::new(
token.to_string(),
fresh_key,
resource.to_string(),
operation.to_string(),
).verify()?;
}
Ok(())
}
Putting it together: Axum middleware
Here is a minimal Axum middleware that derives the expected resource and operation from the request and verifies the incoming capability token:
use axum::{body::Body, extract::State, http::Request, middleware::Next,
http::StatusCode, response::Response};
use hessra_cap_token::CapabilityVerifier;
use hessra_token_core::PublicKey;
use std::sync::Arc;
struct AppState {
key_cache: KeyCache,
}
async fn capability_middleware(
State(state): State<Arc<AppState>>,
request: Request<Body>,
next: Next,
) -> Result<Response, StatusCode> {
let token = request
.headers()
.get("authorization")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "))
.ok_or(StatusCode::UNAUTHORIZED)?;
let (resource, operation) = match (
request.method().as_str(),
request.uri().path(),
) {
("GET", "/orders") | ("GET", p) if p.starts_with("/orders/") =>
("orders", "read"),
("POST", "/orders") =>
("orders", "write"),
_ => return Err(StatusCode::FORBIDDEN),
};
let key = state.key_cache.get_or_fetch().await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let result = CapabilityVerifier::new(
token.to_string(),
key,
resource.to_string(),
operation.to_string(),
).verify();
if result.is_err() {
// Retry once with a fresh key in case of rotation
let fresh_key = state.key_cache.refresh().await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
CapabilityVerifier::new(
token.to_string(),
fresh_key,
resource.to_string(),
operation.to_string(),
)
.verify()
.map_err(|_| StatusCode::UNAUTHORIZED)?;
}
Ok(next.run(request).await)
}
What is next
The quickstart shows the simple case: flat service-to-service capabilities where the root authority knows the full resource names. For systems with dynamic naming such as multi-tenant apps, per-user resources, hierarchical objects, see the webapp auth guide which covers designation and how objects control their own namespaces.