CipherMQ Project Architecture: A Secure Message Queue with mTLS, ACL, Rate Limiting, and Guaranteed Delivery
CipherMQ is a high-performance, secure message queue system built entirely in Rust the broker, the Sender reference client, the Receiver reference client, and the Admin Console are all Rust binaries. It is designed for encrypted message transmission with zero message loss and exactly-once delivery. It leverages Mutual Transport Layer Security (mTLS) for secure client-server communication, hybrid encryption (X25519 + XSalsa20-Poly1305 sealed box, then ChaCha20-Poly1305) for message confidentiality between sender and receiver, and AES-256-GCM for encrypting receivers' public keys at rest in a PostgreSQL database.
The system now includes role-based Access Control (ACL) that maps mTLS certificate Common Names to sender, receiver, or admin roles with strict per-command permission enforcement, token-bucket rate limiting with per-IP and per-CN connection caps, Prometheus-compatible metrics with lock-free atomic counters, a real-time admin dashboard, and heartbeat monitoring for production-grade deployments.
The project consists of:
- Server (
server/src/): A Rust/Tokio message broker for routing and delivering messages over mTLS, with ACL enforcement, rate limiting, metrics collection, heartbeat monitoring, and an mTLS-secured admin console. PostgreSQL-backed persistence for metadata and public keys. - Sender (
clients/sender_1/): A Rust/Tokio client that fetches receiver public keys, hybrid-encrypts messages, and sends them through an async, backpressure-bounded pipeline with retry logic. - Receiver (
clients/receiver_1/): A Rust/Tokio client that registers its public key, receives, decrypts, deduplicates, and persists messages, acknowledging each as it is processed, with periodic heartbeat reporting. - Admin Console (
clients/admin/): An Axum-based web dashboard that polls the server's admin endpoint over mTLS and renders real-time server health, queue details, binding maps, connection statistics, and Prometheus metrics. - Logging: Structured JSON logs with rotation and level-based filtering on the server; JSON file logs plus a human-readable console layer on both clients.
This document provides a comprehensive overview of the architecture, covering the server, the clients, mTLS, encryption, key distribution, acknowledgment mechanisms, access control, rate limiting, metrics, admin console, and storage, all based directly on the current Rust implementation.
CipherMQ operates as a message queue with queues and exchanges, exposed over mTLS connections via a simple line-oriented, text-based protocol. Key features include:
- Mutual TLS (mTLS): Secures client-server communication with two-way authentication. The server uses
tokio-rustlswith aWebPkiClientVerifier(server/src/auth.rs); all Rust clients usetokio-rustlson the client side. Server-side parameters (cert/key/CA paths, listen address) are configured viaconfig.toml; client-side parameters via each client'sconfig.json. - Role-Based Access Control (ACL): Each connecting client's CN is resolved to a
Role(Sender,Receiver,Admin, orUnknown) via theAclManager. Unknown roles are rejected at connection time. Each command is checked against a permission matrix before execution, and receivers are additionally restricted to resources matching their own CN. - Token-Bucket Rate Limiting: Per-CN global and publish-specific token buckets with configurable RPS and burst. Per-IP and per-CN connection count limits prevent resource exhaustion.
- Hybrid Encryption: For every message, the sender generates a random 256-bit session key and 96-bit nonce. The session key is wrapped for the receiver using an X25519 ECDH shared secret combined with XSalsa20-Poly1305 (the NaCl/libsodium "sealed box" construction); the message content itself is encrypted with ChaCha20-Poly1305 using that session key and nonce.
- Public Key Distribution: Receivers register their public key using the
register_public_keycommand; senders retrieve a receiver's public key usingget_public_key. Keys are stored AES-256-GCM-encrypted in PostgreSQL. - Zero Message Loss: The sender retries publishing until it receives
ACK <message_id>from the broker; the broker keeps a message in its queue (and retains its metadata in PostgreSQL) until the receiver sendsack <message_id>. - Exactly-Once Delivery: The receiver deduplicates messages in-memory using
message_idbefore processing or storing them. - Asynchronous Processing: Built with Tokio throughout server and all clients for concurrent, high-performance connection handling.
- Thread-Safe Data Structures: The server uses
DashMapfor its in-memory queue/exchange/binding/consumer/status maps, allowing lock-free concurrent access across client-handling tasks. - Flexible Routing: Supports exchanges, queues, and routing-key-based bindings, declared explicitly by clients before publishing or consuming.
- Heartbeat Monitoring: Receivers send periodic
heartbeatcommands; the server tracks liveness per client and automatically cleans up timed-out connections. - Prometheus-Compatible Metrics: Atomic counters (
AtomicU64) for messages published, acknowledged, delivered, errors, connections, and rate-limit rejections; gauges for connected clients, queue count, messages in queues, consumers, and uptime all exportable in Prometheus text exposition format. - Admin Console: A dedicated mTLS-secured TCP endpoint (
server/src/admin_server.rs) exposes node status as JSON and Prometheus metrics. A companion Axum web dashboard (clients/admin/) polls this endpoint and renders live server health, queue details, binding maps, connection statistics, and counter data. - Structured Logging: The server writes JSON logs split by level (
INFO,DEBUG,ERROR) into separate files, with configurable rotation (hourly,daily, ornever). Both clients write JSON logs to a single configured file plus a readable console layer, filtered by a single configured level. - Persistent Storage: The server stores message metadata and AES-256-GCM-encrypted public keys in PostgreSQL via a dedicated async storage actor task.
The server is the core of CipherMQ, managing message routing, delivery, secure connections via mTLS, access control, rate limiting, metrics, heartbeat monitoring, and persistent storage in PostgreSQL.
ServerState: The broker's in-memory state, shared behindArc<ServerState>(lock-free via DashMap):queues: DashMap<String, VecDeque<(String, EncryptedInputData)>>: Messages currently held in each declared queue, awaiting consumption. UsesVecDequefor O(1) push/pop.message_queues: DashMap<String, DashSet<String>>: Maps eachmessage_idto the set of queues it resides in, enabling efficient removal on ACK.bindings: DashMap<String, Vec<(String, String)>>: For each exchange, the list of(queue_name, routing_key)pairs bound to it.exchanges: DashMap<String, Vec<String>>: The set of declared exchanges.consumers: DashMap<String, Vec<mpsc::UnboundedSender<(String, EncryptedInputData)>>>: Per-queue channels used to push newly published messages directly to subscribed consumer connections.message_status: DashMap<String, MessageStatus>: Per-messagesent_time/delivered_time/acknowledged_time(asOption<std::time::Instant>), used to track in-flight messages; the entry is removed once the message is acknowledged.connected_clients: Arc<AtomicUsize>: Count of currently connected clients, incremented/decremented atomically (lock-free).storage: Arc<Storage>: Handle to the PostgreSQL-backed storage actor.max_queue_size: usize: Maximum number of messages per queue; oldest messages are evicted when exceeded.
EncryptedInputData: The wire format for one encrypted message body, exchanged in thepublish/publish_batch/consume/fetchcommands:message_id: String: Unique identifier for deduplication, constructed by the sender as"{sender_id}-{correlation_id}-{receiver_client_id}".receiver_client_id: String: The target receiver's client ID.enc_session_key: String: Base64-encoded sealed-box ciphertext (32-byte ephemeral X25519 public key followed by the XSalsa20-Poly1305-encrypted session key).nonce: String: Base64-encoded 96-bit ChaCha20-Poly1305 nonce for the message payload.ciphertext: String: Base64-encoded ChaCha20-Poly1305 ciphertext of the message content (the Poly1305 authentication tag is appended to this ciphertext by the AEAD crate).
MessageStatus: Tracks per-message lifecycle withsent_time,delivered_time, andacknowledged_timeasOption<Instant>. Thecan_deliver()method returnstrueif the message has not yet been acknowledged.
Background Tasks:
- Message Cleanup (
cleanup_old_messages): Runs every 60 seconds; removes messages from queues and status that were delivered more than 3000 seconds ago and never acknowledged. - Dead Consumer Cleanup (
cleanup_dead_consumers): Runs every 60 seconds; removes closedmpsc::UnboundedSenderchannels from the consumer map and cleans up empty consumer lists.
Roleenum:Sender,Receiver,Admin,Unknown.AuthResultenum:AllowedorDenied(&'static str).AclManager: Holds threeArc<HashSet<String>>sets for sender, receiver, and admin CNs. Provides:resolve_role(cn: &str) -> Role: Maps a CN to its role. Admins are checked first (for priority), then senders, then receivers; any unrecognized CN returnsUnknown.check_command(role, command) -> AuthResult: Enforces the per-role permission matrix (see §8 in README). Admins have full access; unknown roles are denied all commands.check_resource(role, cn, resource_type, resource_name) -> AuthResult: For receivers, verifies that the resource (queue, routing_key) matches their own CN prefix (e.g.,receiver_1can only declarereceiver_1_queue). Admins can access any resource.
Permission Matrix:
| Command | Sender | Receiver | Admin |
|---|---|---|---|
declare_queue |
❌ | ✅ (own resources only) | ✅ |
declare_exchange |
❌ | ✅ | ✅ |
bind |
❌ | ✅ (own resources only) | ✅ |
publish / publish_batch |
✅ | ❌ | ✅ |
consume |
❌ | ✅ | ✅ |
ack |
✅ | ✅ | ✅ |
register_public_key |
❌ | ✅ | ✅ |
get_public_key |
✅ | ❌ | ✅ |
resend |
✅ | ✅ | ✅ |
heartbeat |
❌ | ✅ | ✅ |
- mTLS: The server uses
tokio-rustlswith ECDSA P-384 certificates for both the server identity and client authentication. - Authentication (
auth.rs): Builds arustls::ServerConfigfromcert_path/key_path/ca_cert_pathand aWebPkiClientVerifierbuilt from the CA's root store, requiring every connecting client to present a certificate signed by that CA. - Client identity (
connection.rs): Extracts the client'sclient_iddirectly from the Common Name (CN) field of its TLS peer certificate once the handshake completes there is no separate login or identity command. - Protocol: A line-oriented text protocol over the TLS stream. Each connection is handled by
handle_client(server.rs) in a loop that reads one command per line and, concurrently, forwards any messages pushed to that client's consumer channel.
RateLimiter: Uses per-CN token buckets for global and publish-specific rate limiting. Buckets refill at the configured RPS rate up to the burst capacity.- Connection Limits: Tracks active connections per IP and per CN via
HashMap<String, usize>behindRwLock. Connections are incremented on accept and decremented on disconnect. RateLimitResult:Allowed,RateLimited, orConnectionLimit.- All rate limiter state is behind
Arc<RwLock<>>for concurrent access.
Metrics: HoldsAtomicU64counters for:messages_published,messages_acked,messages_deliverederrors_total,connections_totalconnections_rejected_rate_limit,connections_rejected_conn_limitrequests_rate_limited
ServerSnapshot: ARwLock-protected snapshot updated periodically withconnected_clients,queue_count,total_messages, andconsumer_count.render_prometheus(): Returns Prometheus text exposition format with# HELP,# TYPE, and metric values.render_status(): Returns a human-readable status summary.start_time:Instantrecorded at server startup for uptime calculation.
- mTLS Secured: Uses its own
TlsAcceptorbuilt from the same CA and server certificates. Only clients with an admin CN (verified viaAclManager::resolve_role) can connect. - Commands:
jsonFull JSON status including server info, counters, snapshot, live data, connection stats, queues, bindings, and exchanges.metricsPrometheus-format metrics output.statusHuman-readable status summary.queuesQueue count, total messages, total consumers.connectionsPer-IP and per-CN connection breakdown.quit/exitDisconnect.
HeartbeatMonitor: ADashMap<String, Instant>tracking the last heartbeat time per client. Configurable timeout (default 60 seconds).monitor_task(): Runs every 10 seconds, scanning for clients whose last heartbeat exceeds the timeout. Timed-out clients are removed and logged as warnings.record_heartbeat(client_id): Called on every received command (all data received counts as a heartbeat) and on initial connection.get_stats(): Returns(total_tracked, alive_count)for the admin console.
The server talks to PostgreSQL through a single background task (a "storage actor") that owns the database client and processes commands sent to it over an mpsc channel, so all database access is serialized through one place even though many client connections run concurrently.
- Connection Pool: Uses
sqlx::PgPoolwith up to 200 connections, retry logic (5 attempts with 5-second backoff), and automatic reconnection if the pool closes. - PostgreSQL schema, created automatically on startup if it does not already exist:
message_metadata:message_id(TEXT, PRIMARY KEY)client_id(TEXT): The sender's client ID.exchange_name(TEXT)routing_key(TEXT)sent_time(TEXT, RFC 3339)delivered_time(TEXT, RFC 3339, nullable)acknowledged_time(TEXT, RFC 3339, nullable)
public_keys:client_id(TEXT, PRIMARY KEY)public_key_ciphertext(TEXT): Base64-encoded AES-256-GCM ciphertext of the receiver's raw X25519 public key.nonce(TEXT): Base64-encoded 96-bit AES-GCM nonce, randomly generated per registration.tag(TEXT): Base64-encoded 128-bit AES-GCM authentication tag.
- Batched Updates: Delivered-time and acknowledged-time updates are buffered in
HashMap<String, String>and flushed to PostgreSQL in batch every 500ms or when the buffer reaches 50 items, using a singleUPDATE ... FROM (SELECT unnest(...))query. - Encryption at rest: Public keys are encrypted with AES-256-GCM using the 32-byte key configured at
[encryption].aes_keyinconfig.toml. The ciphertext and tag produced by theaes-gcmcrate are split apart before storage and concatenated again on read.
- Loads and parses
config.toml(sections:[server],[tls],[logging],[database],[encryption],[performance],[admin],[rate_limit],[acl]). - Validates that
tls.cert_path,tls.key_path, andtls.ca_cert_pathare present whenconnection_type = "tls". - Validates that
database.host,database.user, anddatabase.dbnameare non-empty. - Validates that
encryption.algorithmis exactly"x25519_chacha20_poly1305"and thatencryption.aes_keyBase64-decodes to exactly 32 bytes. - Applies defaults for any empty/zero logging fields (
level→"info",rotation→"daily", log file paths →logs/info.log/logs/debug.log/logs/error.log,max_size_mb→10). PerformanceConfigdefaults:max_queue_size→ 10,000,heartbeat_timeout→ 60s,cleanup_interval→ 3000s,consumer_cleanup_interval→ 60s,idle_timeout_sec→ 60s.AdminConfigdefaults:enabled→ false,address→127.0.0.1:9091,allowed_cns→ empty.RateLimitConfigdefaults: all values as listed in §3.1.4;enabled→ false.AclConfigdefaults: all CN lists empty (no ACL enforcement if unconfigured).
- Role: Fetches receiver public keys, hybrid-encrypts messages, and sends them through an asynchronous, backpressure-bounded pipeline with automatic retries.
- mTLS: Authenticates with
tls.client_cert_path/tls.client_key_pathfromconfig.json, verifies the server usingtls.certificate_path(the CA certificate). Whentls.check_hostnameisfalse, certificate-chain validation still runs, but the TLSServerNamehostname check is skipped via a customServerCertVerifier. - Identity: The sender's own
client_idis not configured explicitly it is extracted from the CN field of its own client certificate at startup (extract_client_id) and used to populate the{sender_id}template placeholder and the generatedmessage_id. - Encryption: For each message and each target receiver, generates a fresh random 256-bit session key and 96-bit nonce; wraps the session key for that receiver with X25519 + XSalsa20-Poly1305 sealed-box encryption (
sealed_box_encrypt), and encrypts the message body with ChaCha20-Poly1305 (encrypt_message). A separate ciphertext/session-key pair is generated per receiver, so compromising one receiver's private key does not expose messages addressed to other receivers. - Message Generation: Uses configurable
content_templatewith placeholders{sender_id},{correlation_id},{timestamp},{seq}, and customextra_fields. - Commands used:
declare_queue <queue>,declare_exchange <exchange>,bind <queue> <exchange> <routing_key>: Issued once per configured binding before publishing.get_public_key <receiver_client_id>: Retrieves a receiver's public key (and caches it tokeys/<receiver_client_id>_public.key).publish <exchange> <routing_key> <json_message>: Sends one encrypted message; the server replies withACK <message_id>or anError:line.
- Pipeline: Opens a dedicated mTLS connection for the send/ACK pipeline. A
tokio::sync::Semaphoresized tosender.max_inflightbounds how many messages may be outstanding at once; a separate task continuously reads ACK lines from the same connection and releases semaphore permits as ACKs arrive. After all messages have been sent, the sender drains remaining ACKs up to a timeout, then retries any still-unacknowledged messages up tosender.max_retriestimes with exponential backoff, before reporting final throughput and failure counts. - Logging: JSON logs to
logging.info_file_path, plus a readable console layer, both filtered bylogging.level.
- Role: Registers its public key, receives, decrypts, deduplicates, and stores messages in
data/<queue_name>_received_messages.jsonl, acknowledging each as it is processed, with periodic heartbeats. - mTLS: Authenticates with
tls.client_cert_path/tls.client_key_path, verifies the server usingtls.certificate_path. Samecheck_hostnamebehavior as the Sender. - Decryption: Loads its X25519 private key from
keys/receiver_private.key(Base64, 32 bytes). For each incoming message it reverses the sender's scheme: unwraps the session key with the sealed-box construction (X25519 ECDH + XSalsa20-Poly1305) using its own private key and the ephemeral public key embedded inenc_session_key, then decrypts the payload with ChaCha20-Poly1305 using that session key and the message'snonce. - Commands used:
register_public_key <base64_public_key>: Registers this receiver's X25519 public key with the broker; on success the server repliesPublic key registered.declare_queue <queue>,declare_exchange <exchange>,bind <queue> <exchange> <routing_key>: Issued once at startup usingqueue_name/exchange_name/routing_keyfromconfig.json.consume <queue>: Subscribes as a push consumer; the broker then streamsMessage: <id> <json>lines as they are published.ack <message_id>: Sent after a message has been successfully decrypted and queued for local storage.heartbeat: Sent every 30 seconds to maintain liveness with the server's heartbeat monitor.
- Deduplication: Maintains an in-memory
HashSet<String>of processedmessage_ids for the lifetime of the connection/process, skipping any message whose ID has already been seen. - Persistence: Decrypted messages are batched (up to 100 at a time, or on a 5-second idle timeout) and appended as JSON Lines to
data/<queue_name>_received_messages.jsonl, each record containingmessage_id, the decryptedmessagecontent, and a Unixtimestamp. - Concurrency: Uses separate Tokio tasks for message receiving, ACK sending, heartbeat reporting, and message persistence, coordinated via
mpscchannels andAtomicBoolrunning flag. - Resilience: Automatically reconnects (with a 1-second pause) if the TLS connection drops, and responds to
Ctrl+C(SIGINT) by draining in-flight work before exiting. - Logging: JSON logs to
logging.info_file_path, plus a readable console layer, filtered bylogging.level.
-
Architecture: A two-tier system:
- Background Polling Thread (
poll.rs): Establishes a persistent mTLS connection to the server's admin endpoint, sendsjsoncommands at the configuredpoll_interval_secs, and updates a sharedRwLock<NodeData>with the parsed JSON response. Automatically reconnects on failure. - Axum Web Server (
main.rs): Serves the HTML dashboard at/and a JSON API endpoint at/api/statusthat returns the latestNodeData. Uses CORS middleware for cross-origin access.
- Background Polling Thread (
-
NodeDatastructure: Mirrors the server's JSON status output exactly:server: name, uptime, health, max_queue_sizecounters: messages_published, messages_acked, messages_delivered, errors_total, connections_total, connections_rejected_rate_limit, connections_rejected_conn_limit, requests_rate_limitedsnapshot: connected_clients, queue_count, total_messages, consumer_countlive: connected_clients, queues, total_messages, total_consumers, exchangesconnection_stats: active_by_ip, active_by_cn, by_ip map, by_cn mapqueues: array of {name, message_count, consumer_count}bindings: array of {exchange, queue, routing_key}exchanges: array of exchange namesreachable: boolean (false if last poll failed)last_update: timestamp string
-
Dashboard (
dashboard.html): A self-contained HTML file with embedded CSS and JavaScript that fetches/api/statusperiodically and renders:- Server health badge (healthy/unreachable)
- Uptime and server name
- Connected clients, queues, consumers, exchanges
- Messages published/acked/delivered
- Rate-limit rejections and connection limits
- Per-queue table with message counts and consumer counts
- Full binding map
- Connection statistics by IP and CN
-
Configuration (
config.json):server_name: Display name for the dashboard.poll_interval_secs: How often to poll the server (default: 3).web_address: Dashboard listen address (default:127.0.0.1:8080).tls.*: CA cert, client cert, client key for mTLS.server.admin_address: Server's admin console endpoint.
- Sender (per message, per receiver):
- Looks up the receiver's X25519 public key (fetched live via
get_public_key, or read from a local cache underkeys/). - Generates a random 256-bit session key and 96-bit nonce.
- Wraps the session key for the receiver using a NaCl-style sealed box: an ephemeral X25519 key pair is generated, an X25519 shared secret is computed with the receiver's public key, a nonce is derived by hashing the ephemeral and receiver public keys with BLAKE2b, and the session key is encrypted with XSalsa20-Poly1305 under that shared secret and derived nonce. The ephemeral public key is prepended to the resulting ciphertext to form
enc_session_key. - Encrypts the plaintext message content with ChaCha20-Poly1305 under the session key and the random nonce, producing
ciphertext(with the Poly1305 tag appended) andnonce.
- Looks up the receiver's X25519 public key (fetched live via
- Receiver (per incoming message):
- Splits
enc_session_keyinto the embedded ephemeral public key and the sealed-box ciphertext. - Recomputes the X25519 shared secret using its own private key and the embedded ephemeral public key, rederives the same BLAKE2b-based nonce, and decrypts the sealed box with XSalsa20-Poly1305 to recover the session key.
- Decrypts
ciphertextwith ChaCha20-Poly1305 using the recovered session key andnonce, verifying the Poly1305 tag and recovering the plaintext message content.
- Splits
- Receiver:
- Generates an X25519 key pair offline using the
create_ca_key/Rust_Key_Maker_X25519utility (receiver_private.key,receiver_public.key). - On startup, sends
register_public_key <base64_public_key>to the server over its mTLS connection (itsclient_idis implicit from its certificate CN). - The server encrypts the raw public key bytes with AES-256-GCM and stores the result in the
public_keystable in PostgreSQL.
- Generates an X25519 key pair offline using the
- Sender:
- Sends
get_public_key <receiver_client_id>to retrieve a receiver's public key. - Caches the returned key locally as
keys/<receiver_client_id>_public.keyfor reuse if the server is later unreachable.
- Sends
- Security:
- Public keys are encrypted at rest in PostgreSQL with AES-256-GCM.
- mTLS restricts both key registration and key retrieval to clients holding a certificate signed by the configured CA.
- ACL restricts
register_public_keyto receiver-role clients andget_public_keyto sender-role clients.
-
Key and Certificate Generation:
create_ca_key/Rust_CA_Maker_ECDSA_P-384_Multi_Client(a Rust binary usingrcgen) generatesca.crt/ca.key,server.crt/server.key, and oneclient.crt/client.keypair per CN argument supplied on the command line (e.g.,sender_1,receiver_1,admin).create_ca_key/Rust_Key_Maker_X25519(a Rust binary usingx25519-dalek) generatesreceiver_public.keyandreceiver_private.key.
-
Server Startup:
- Loads
config.tomland initializes logging (JSON, per-level files + console). - Connects to PostgreSQL with retry logic; creates
message_metadataandpublic_keystables if they don't exist. - Initializes
ServerState(DashMap-based, with background cleanup tasks),Metrics,RateLimiter,AclManager, andHeartbeatMonitor. - Spawns the admin console task (if
[admin].enabled = true). - Spawns the heartbeat monitor task.
- Loads unacknowledged message metadata from PostgreSQL for resend requests.
- Begins accepting mTLS connections.
- Loads
-
Client Connection:
- mTLS handshake → CN extracted → ACL role resolved.
- If role is
Unknown, connection is rejected immediately. - Connection limits checked (per-IP, per-CN).
- Client enters command loop.
-
Receiver to Server:
- Receiver authenticates via mTLS, then sends
register_public_key <base64_public_key>. - Declares its queue and exchange, binds them with its routing key, and subscribes with
consume. - Sends
heartbeatevery 30 seconds.
- Receiver authenticates via mTLS, then sends
-
Sender to Server:
- Sender authenticates via mTLS, declares/binds its configured exchange and queues (these commands are denied by ACL for senders the sender uses a separate connection or relies on pre-declared resources).
- Sends
get_public_key <receiver_client_id>for each configured receiver. - Hybrid-encrypts and publishes messages with
publish <exchange> <routing_key> <json_message>. - Server replies
ACK <message_id>for each successfully queued message.
-
Server to Receiver:
- The server routes each published message to every queue bound to its exchange/routing-key pair and pushes it immediately to any subscribed consumer as
Message: <id> <json>. - The receiver decrypts, deduplicates by
message_id, appends the result to its JSONL output file, and sendsack <message_id>. - The server replies
ACK_CONFIRMED <message_id>, removes the message from the relevant queue(s), and removes its entry from in-memorymessage_status.
- The server routes each published message to every queue bound to its exchange/routing-key pair and pushes it immediately to any subscribed consumer as
-
Admin Console to Server:
- Admin client connects to the admin endpoint over mTLS.
- Server verifies the client CN is in
admin_cnsviaAclManager. - Admin client sends
json/metrics/status/queues/connectionscommands. - Web dashboard polls
/api/statusand renders the data in real-time.
-
Message Removal:
- The server removes a message from all queues once it has been acknowledged.
- The
message_queuesmap entry is also removed, and themessage_statusentry is cleared.
- Sender–Server ACK:
- The server replies
ACK <message_id>immediately after apublishis queued to at least one bound queue, or anError: ...line if publishing failed (e.g. duplicate message ID, or no queue bound to that exchange/routing key). - The sender tracks every published message in a
pendingmap until its ACK arrives; anmpscchannel carries ACK results from a dedicated reader task back to the main send loop. - After the send loop completes, the sender drains remaining ACKs (bounded by
ack_timeout_secs + 5), then explicitly retries any still-pending messages up tosender.max_retriestimes with exponential backoff.
- The server replies
- Receiver–Server ACK:
- The receiver sends
ack <message_id>once a message has been decrypted and handed off to its local storage task. - The server replies
ACK_CONFIRMED <message_id>, removes the message from its queues, and clears itsmessage_statusentry. - If the receiver's connection drops before an
ackis sent, the unacknowledged message remains queued on the broker (and its metadata persists in PostgreSQL) until a futureconsume/fetchredelivers it after reconnection.
- The receiver sends
- Independence: Sender↔server and receiver↔server acknowledgments are fully decoupled each side only needs to track its own outstanding requests.
- Retry responsibility: Both clients own their respective retry logic; the broker's role is limited to queuing, delivery, and tracking acknowledgment state.
The AclManager is constructed at server startup from the [acl] section of config.toml. It holds three HashSet<String> collections for sender, receiver, and admin CNs. When a client connects, its CN (extracted from the mTLS certificate) is looked up against these sets:
CN in admin_cns → Role::Admin
CN in sender_cns → Role::Sender
CN in receiver_cns → Role::Receiver
CN not found → Role::Unknown → connection rejected
Admin is checked first to ensure admin CNs take precedence if accidentally listed in multiple sets.
In server.rs, every command parsed from the client's input is checked via acl_manager.check_command(&role, command) before execution. If denied, the server responds with Access denied: <reason> (role=<role>) and the command is not executed.
For declare_queue and bind, an additional check_resource call verifies that receivers only manage resources matching their own CN. This prevents receiver_1 from declaring receiver_2_queue or binding with receiver_2_key.
The admin server (admin_server.rs) independently extracts the connecting client's CN from the TLS handshake and verifies it resolves to Role::Admin before serving any data. Non-admin clients are silently disconnected.
Each client CN gets two independent token buckets:
- Global bucket: Configured with
global_rps(refill rate) andglobal_burst(max tokens). Every command consumes one token. - Publish bucket: Configured with
publish_rpsandpublish_burst. Only checked forpublishandpublish_batchcommands.
Tokens refill at the RPS rate based on elapsed time since the last request, capped at the burst value.
- Per-IP: Maximum
max_connections_per_ipconcurrent connections from a single IP address. - Per-CN: Maximum
max_connections_per_cnconcurrent connections per client CN. - Connections are tracked in
HashMap<String, usize>behindRwLock, incremented on accept and decremented on disconnect.
- Connection acceptance (
main.rs):rate_limiter.check_connection(cn, ip)is called after TLS handshake. If rejected, the connection is closed andconnections_rejected_*counters are incremented. - Per-command (
server.rs):rate_limiter.check(cn, ip, is_publish)is called before each command. If rejected, the server responds withRate limitedandrequests_rate_limitedis incremented.
All counters use AtomicU64 with Ordering::Relaxed for maximum throughput. This means individual reads may see slightly stale values, but counters are monotonically increasing and never lose updates.
The ServerSnapshot is updated by periodic tasks that scan DashMap sizes. It provides a point-in-time view of connected clients, queue count, total messages, and consumer count.
The render_prometheus() method outputs standard Prometheus text exposition format:
# HELP ciphermq_messages_published_total Total messages published
# TYPE ciphermq_messages_published_total counter
ciphermq_messages_published_total 1234
This can be scraped by Prometheus or any compatible monitoring system.
- Sender:
- Logs key retrieval, per-message encryption, publish attempts, ACK receipt, retries, and final pipeline throughput/failure counts.
- Example:
INFO sender: Encrypted message <message_id> for <receiver_client_id> with routing_key <routing_key>.
- Receiver:
- Logs public-key registration, queue/exchange setup, message receipt, decryption, deduplication, ACK sending, heartbeat sending, and batch persistence.
- Example:
INFO receiver: Processed and decrypted message <message_id>.
- Server:
- Logs connections, ACL role resolution, command handling, queue/exchange/binding declarations, publish/ack outcomes, rate-limit rejections, heartbeat timeouts, and errors in structured JSON.
- Example:
{"timestamp":"2026-06-08T07:03:00Z","level":"ERROR","fields":{"message":"TLS handshake failed"}}.
- Admin Console:
- Logs connection establishment, TLS handshake results, ACL verification, polling cycles, and dashboard readiness.
- Verification:
- Check the sender's console/log output for messages that exhausted
max_retries("permanently failed after {n} retries"). - Check the receiver's
data/<queue_name>_received_messages.jsonlfor processed messages. - Check server logs for acknowledgment confirmations, ACL denials, or errors.
- Query PostgreSQL's
message_metadatatable for message timing/status andpublic_keysfor registered key material. - Check the admin dashboard for real-time server health, queue details, and rate-limit statistics.
- Query the admin server's
metricsendpoint for Prometheus-compatible metrics.
- Check the sender's console/log output for messages that exhausted