Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Architecture

This chapter describes the overall structure of Ahdapa, the key modules, and the full request lifecycle from a TCP connection to an HTTP response.

System architecture

graph TB
    subgraph clients["OAuth2 Clients / Browsers"]
        browser["Browser (Authorization Code)"]
        device["CLI / IoT (Device Flow)"]
        service["Service (Client Credentials)"]
    end

    subgraph ahdapa["Ahdapa Node"]
        direction TB
        oauth2["OAuth2 endpoints\n/authorize · /token · /jwks\n/revoke · /introspect · /userinfo\n/par · /device_authorization"]
        auth["Auth module\nGSSAPI/SPNEGO · Password\nSession cookie · AEAD codes"]
        crdt["CRDT state\nIdpCrdt (RwLock)\nsigning keys · clients · nodes"]
        db[("Local DB\nSQLite / Postgres / MariaDB\nCRDT snapshots + ephemeral tables")]
        webui["WebUI\nPreact SPA\n/ui/auth/* · /ui/admin/*"]
        admin["Admin API\n/api/admin/*\nclients · keys · nodes · families"]
        gossip["Gossip loop\n/api/gossip/sync\nCMS (ML-KEM-768 + ECDSA P-256)"]
    end

    subgraph infra["FreeIPA Infrastructure"]
        kdc["KDC (Kerberos)"]
        ldap["LDAP (FreeIPA)"]
    end

    subgraph peers["Peer Nodes"]
        node2["Ahdapa Node 2"]
        node3["Ahdapa Node 3"]
    end

    browser -->|"HTTPS"| oauth2
    device --> oauth2
    service --> oauth2
    oauth2 --> auth
    auth -->|"GSSAPI token"| kdc
    auth -->|"user lookup"| ldap
    oauth2 --> crdt
    crdt --> db
    admin --> crdt
    gossip -->|"POST /api/gossip/sync"| node2
    gossip -->|"POST /api/gossip/sync"| node3
    crdt --> gossip
    webui --> oauth2
    webui --> admin

Source layout

src/
  main.rs          Entry point: load config, open DB, build AppState, start gossip, serve
  config.rs        TOML configuration structs (Config, ServerConfig, DbConfig, …)
  audit.rs         Audit event taxonomy (19 event types), recording, overflow/alarm policies,
                   journal query (journalctl subprocess or in-memory/file fallback)
  journal.rs       JournalWriter: systemd journal namespace via Unix datagram, JSONL file
                   fallback, in-memory daemon for tests
  crdt/
    mod.rs         IdpCrdt and the four CRDT primitives: GrowSet, LwwRegister, OrMap, LwwMap;
                   load_from_db / persist_to_db / merge
  db/
    mod.rs         Database initialization (open, migrations, WAL mode)
    schema.rs      Row types mirroring every CRDT and ephemeral table
  auth/
    mod.rs         AuthResult enum; AuthError; public re-exports
    aead.rs        AES-256-GCM seal / open helpers (native-ossl)
    code.rs        Authorization code payload: AEAD-encrypted JSON blob
    consent.rs     Consent cookie payload: AEAD-encrypted pending authorization state
    cookie.rs      Session cookie: seal / unseal (SessionClaims)
    gssapi.rs      SPNEGO acceptor via ahdapa-gssapi
    ipa.rs         FreeIPA attribute lookup: IPA JSON-RPC API (default) or LDAP direct
    passkey.rs     WebAuthn passkey assertion/registration verification; COSE↔SPKI DER conversion
    password.rs    Password-based authentication via LDAP simple bind
    pkce.rs        PKCE S256 code_challenge / code_verifier verification
    refresh.rs     Refresh token payload: AEAD-encrypted, rotation + replay detection
  routes/
    mod.rs         AppState definition; bootstrap helpers; build() — assembles the axum Router
    oauth2.rs      All OAuth2 / OIDC endpoints (authorize, token, jwks, revoke, …)
    discovery.rs   /.well-known/oauth-authorization-server and /.well-known/openid-configuration
    admin.rs       /api/admin/* endpoints (clients CRUD, key rotation, nodes, refresh families)
    gossip.rs      /api/gossip/sync handler and background gossip loop
    topology.rs    FreeIPA topology-based gossip peer discovery (ipa_topology)

webui/
  index.html       Entry HTML — inline script for flash-free theme application
  src/             Preact + TypeScript source
    main.tsx       Preact entry point — wraps App with ThemeProvider, ToastProvider
    App.tsx        React Router setup (basename="/ui")
    api.ts         Typed fetch helpers for admin and auth APIs
    pf.tsx         Custom PatternFly 6 component library (cx, NavSection, Breadcrumb,
                   Pagination, Modal with focus trap, FormSelect, Table, etc.)
    theme.tsx      ThemeProvider / useTheme() — dark mode with localStorage persistence
    toast.tsx      ToastProvider / useToast() — portal-based PF6 AlertGroup notifications
    auth/          User-facing auth pages (Login, Consent, Device, Error)
    admin/         Operator admin pages and AdminLayout (nav groups, breadcrumb, branding)
    user/          User self-service pages (ProfilePage)
  dist/            Built SPA (generated by `npm run build`)

migrations/
  sqlite/0001_initial.sql    SQLite DDL
  postgres/0001_initial.sql  Postgres DDL
  mariadb/0001_initial.sql   MariaDB DDL

crates/
  ahdapa-gssapi/  Safe Rust GSSAPI bindings (fork of akamu-gssapi)
  ahdapa-jose/    JWT signing / verification primitives (fork of akamu-jose)
  ahdapa-ldap/    Safe Rust OpenLDAP bindings (fork of akamu-ldap)
  hbac-crdt/       Identity HBAC policy engine: op-based CRDT rule set, OAuth2 evaluation

AppState

Defined in src/routes/mod.rs. Every axum handler receives a clone of AppState via axum’s State extractor. Cloning is cheap: all fields are Arc<T>.

FieldTypePurpose
configArc<Config>Immutable configuration parsed at startup
dbsqlx::AnyPoolDatabase connection pool for all persistence
crdtArc<tokio::sync::RwLock<IdpCrdt>>CRDT cluster state; protected by a tokio RwLock
key_pair_rwArc<Mutex<([u8; 32], [u8; 32])>>Pair of (wrapping key, refresh sub-key) held under one lock so key rotation is always atomic. wrapping_key() and refresh_key() are cheap helpers that take a read lock.
node_idArc<str>Stable node identifier (from HOSTNAME env or a fresh UUIDv4)
gss_credOption<Arc<GssServerCred>>GSSAPI server credential; None when keytab is unavailable
ipaArc<IpaState>All IPA/LDAP runtime state: server URI, pre-computed DN paths (from rootDSE discovery), GSSAPI initiator credential (gss_initiator) for S4U2Self and machine authentication (e.g. passkeyconfig_show), initiator credential re-acquisition mode (gss_initiator_mode), per-user S4U2Self credential cache, user-attribute cache, passkey UV policy cache, and optional IPA JSON-RPC API client
jti_cacheArc<DashMap<String, i64>>Lock-free JTI replay cache for private_key_jwt and client_secret_jwt assertions. check_and_insert_jti is synchronous.
static_usersOption<Arc<StaticUsers>>Parsed static users file; None when [users] is not configured.
rbacArc<RbacState>Parsed RBAC roles and group-role bindings from [rbac].
auth_limiterArc<…>Per-IP authentication rate limiter (server.auth_rate_limit).
node_kem_privArc<Vec<u8>>Node’s ML-KEM-768 private key (PKCS#8 DER). Used to decrypt inbound gossip messages.
node_gossip_signing_privArc<Vec<u8>>Node’s ECDSA P-256 gossip signing private key (PKCS#8 DER). Used to sign outbound gossip messages.
node_gossip_signing_certArc<Vec<u8>>Node’s self-signed X.509 certificate (DER) for the gossip signing key. Embedded in outbound SignedData.
local_wrapping_key_idArc<RwLock<String>>UUID of the cluster wrapping key held locally. Updated when the key is rotated or pulled from a peer.
base_pathArc<str>URL path prefix (e.g. "/idp") extracted from the issuer URL. Prepended to all route paths.
gossip_clientArc<reqwest::Client>Pre-configured HTTP client for outbound gossip requests (10-second timeout, optional custom CA).
dynamic_peersArc<tokio::sync::RwLock<Vec<String>>>Gossip peer URLs discovered from the IPA replication topology (ipa_topology = true). Empty when ipa_topology = false. Merged with the static gossip.peers list at each gossip round. Updated by topology::run_topology_refresh.
dynamic_allowed_nodesArc<tokio::sync::RwLock<HashSet<String>>>Hostnames of IPA replicas discovered from the topology, automatically added to the gossip admission allowlist. Merged with gossip.allowed_node_ids at each admission check. Updated by topology::run_topology_refresh.
hbac_logArc<tokio::sync::RwLock<hbac_crdt::OpLog>>Authoritative op-log for Identity HBAC policies. Mutations (create, patch, delete rule) are appended here and the materialised RuleSet is mirrored into crdt.hbac_rules for gossip. Read by the token endpoint to evaluate evaluate_oauth2 before token issuance.
ipa_upstream_idpsArc<tokio::sync::RwLock<Vec<UpstreamIdpConfig>>>IPA-sourced upstream IdP registrations, refreshed every 300 seconds from cn=idp,<suffix> LDAP objects. Empty when [ipa] gssapi is not configured. Searched by find_upstream() after the static config.federation.upstream_idps list; CRDT ACR/AMR overrides from crdt.ipa_idp_overrides are applied to the cloned entry before it is returned.
ipa_issuer_aliasesVec<String>Per-node issuer aliases auto-derived from [gssapi] initiator_principal (node FQDN) and the ipa-ca.<realm> DNS alias. Supplements any manually configured server.issuer_aliases. Used by accepted_origins() and accepted_issuers() for WebAuthn passkey origin validation, backchannel-logout aud, and client_assertion aud acceptance.
jwks_cacheArc<tokio::sync::RwLock<HashMap<String, JwksCacheEntry>>>Short-lived cache of remote JWKS responses keyed by jwks_uri. Prevents a round-trip to the client’s key server on every private_key_jwt or JWT-bearer token request. Entries are refreshed after JWKS_CACHE_TTL.
gossip_notifyArc<tokio::sync::Notify>Signals the gossip background task to push state immediately after a local CRDT write, rather than waiting for the next periodic interval.
crdt_changedArc<tokio::sync::Notify>Fires after every gossip merge (inbound sync or outbound response processing). Used by the /api/gossip/await-client long-poll endpoint to detect convergence without polling.
cached_signing_keyArc<tokio::sync::RwLock<HashMap<String, (String, BackendPrivateKey)>>>In-memory cache of this node’s JWT signing keys, keyed by algorithm. Populated on first use and invalidated on key rotation. Avoids a DB query on every token issuance.
gossip_statsArc<Mutex<GossipStats>>Runtime gossip statistics (rounds completed, persist errors, wrapping key pull errors, per-peer last-sync timestamps). Served by GET /api/gossip/stats.
spiffe_caOption<Arc<SpiffeCA>>SPIFFE CA state; None when [spiffe] trust_domain is not set. Used by the Workload API gRPC server to issue X.509-SVIDs and JWT-SVIDs.
journalArc<JournalWriter>Audit event writer: sends structured events to the systemd journal namespace (ahdapa) via Unix datagram, or to a JSONL file fallback, or to tracing when neither is available.
audit_stateArc<AuditState>In-memory audit state: event counter, rolling-window violation tracker, halt flag, and consecutive-insert-failure counter for FAU_STG.4 / FAU_ARP.1 policy enforcement.
audit_policyArc<AuditPolicy>Audit policy configuration (overflow threshold, alarm threshold, alarm action). Parsed from [audit] at startup.
has_revoked_sessionsArc<AtomicBool>Fast-path flag: false when the revoked_sessions table is empty. When false, check_session_revoked() skips the database query entirely, eliminating per-request I/O on clusters with no active revocations. Set to true on first revocation and after gossip merge if the peer has revocations.
client_cacheArc<DashMap<String, (u64, ClientEntry)>>In-memory client lookup cache keyed by client_id, with CRDT generation for staleness detection. Avoids taking the CRDT read lock on every token request.
scope_cacheArc<std::sync::RwLock<ScopeClaimsCache>>Cached scope-to-claims mapping rebuilt from the CRDT scope definitions. Invalidated on CRDT generation change.
revoked_tokens_cacheArc<DashMap<String, i64>>In-memory JTI revocation cache for token revocation checks. Entries are populated from the CRDT on startup and updated on gossip merge.

Request lifecycle

1. TCP accept and HTTP parsing

tokio accepts a TCP connection. axum passes it to the hyper HTTP/1.1 codec. TraceLayer emits a tracing span for each request.

2. Route dispatch

axum matches the request against the router assembled in routes::build:

Path prefixHandler module
/authorize, /token, /jwks, /revoke, /introspect, /userinfo, /par, /device_authorization, /device, /registerroutes::oauth2
/.well-known/oauth-authorization-server, /.well-known/openid-configurationroutes::discovery
/api/admin/*routes::admin — clients CRUD, key rotation, nodes, refresh families, HBAC policies (/api/admin/hbac)
/api/gossip/*routes::gossip
/ui/*tower-http::ServeDir (Preact SPA)

3. Authorization code flow

Browser → GET /authorize (with PKCE code_challenge)
  └─ SPNEGO attempt (401 Negotiate) or session cookie check
  └─ if unauthenticated: redirect to /ui/auth/login
  └─ if authenticated:
       if client.skip_consent:
           build AuthCodePayload → AEAD-encrypt → redirect to redirect_uri directly
       else:
           build ConsentPayload → seal as AEAD cookie → redirect to /ui/auth/consent

Browser → GET /ui/auth/consent (Preact SPA)       [skipped when skip_consent = true]
  └─ fetch GET /api/auth/consent → display client name + scopes
  └─ user clicks Allow → POST /api/auth/consent {allow: true}
       └─ decrypt consent cookie → build AuthCodePayload → AEAD-encrypt → return redirect_to URL

Browser → GET {redirect_uri}?code=<AEAD-encrypted-code>&iss=...

Client → POST /token (code + code_verifier)
  └─ decrypt auth code → verify PKCE → issue JWT access token + refresh token

4. Token issuance

All tokens are issued in routes/oauth2.rs. The actual cryptographic primitives live in src/auth/:

  • JWT access token — signed with the active JWT signing key from the CRDT (algorithm set by [server] jwt_signing_algorithm, default ES256).
  • Authorization codeAuthCodePayload sealed with AppState::wrapping_key() (the first element of key_pair_rw) via auth::aead.
  • Refresh tokenRefreshTokenPayload sealed with AppState::refresh_key() (the HKDF-derived second element of key_pair_rw).
  • Session cookieSessionClaims sealed with AppState::wrapping_key().

5. CRDT read/write

Handlers acquire a read lock for lookups and a write lock only for mutations:

#![allow(unused)]
fn main() {
// Read (non-blocking for other readers):
let crdt = state.crdt.read().await;
let client = crdt.clients.get(&client_id);

// Write (exclusive):
let mut crdt = state.crdt.write().await;
crdt.active_kid.set(kid, now, &state.node_id);
}

After any CRDT mutation, the handler (or gossip handler) calls crdt.persist_to_db(&state.db) to flush the snapshot.

6. Background gossip

A background tokio task (routes::gossip::run) wakes either on a gossip_notify signal (fired immediately after any local CRDT write) or after gossip.interval_secs seconds (passive fallback). It prepares all per-peer payloads under a single CRDT read lock, then sends HTTP POSTs to all peers concurrently via tokio::task::JoinSet, and processes responses sequentially. Wall-clock gossip time is O(max single-peer round-trip) regardless of cluster size. See Gossip Protocol for details.


Technology stack

ComponentLibrary
Async runtimetokio
HTTP frameworkaxum 0.8
Databasesqlx 0.8 (SQLite / PostgreSQL / MariaDB via Any backend)
Schema migrationssqlx built-in migrate!
Crypto / AEAD / HMAC / HKDF / RNGnative-ossl
Certificate / key managementsynta-certificate
CMS gossip encryption (ML-KEM-768 + ECDSA P-256)ahdapa-cms
GSSAPI / SPNEGOahdapa-gssapi (fork of akamu-gssapi)
LDAP (FreeIPA user lookup)ahdapa-ldap (fork of akamu-ldap)
System userdb lookup (optional)ahdapa-varlink via kirmes (io.systemd.UserDatabase); enabled with --features varlink
PAM authentication backend (optional)ahdapa-pam — inline libpam FFI; enabled with --features pam
Identity HBAC policy enginehbac-crdt — op-based CRDT rule set with OAuth2 axes
WebUIPreact 10 + TypeScript + PatternFly 6, built with Vite 6 (React 19 API via preact/compat)
Serializationserde + serde_json
ConfigurationTOML