buildonai-key-serverv2.1.1AGPL-3.0-only + Commercial

BuildOnAI Key Server

ed25519 signature-per-request authentication. No tokens to issue, no sessions to manage, no JWT to operate. The key is the identity.

Overview

An HTTP service that does two jobs:

  1. Verifies ed25519-signed requests. A caller signs(method, path, timestamp, nonce, body-hash) with its private key. The server holds the matching public key in a file and answers { valid: true | false }. Stateless. No session store. No token issuance. No expiry to manage.
  2. Hands out secrets on demand. SSH private keys for deploy/push, API tokens for outbound calls. IP allow-list is the first gate; every sensitive vault or audit request also requires a valid ed25519 signature.

Designed as a sidecar for Consciousness Server and Cortex, but works standalone for any HTTP service that wants key-based authentication without standing up Vault, OAuth, or a JWT issuer.

Why this exists

If you run a small group of services or agents that talk HTTP to each other, your auth options today are bad in three different ways:

  • Bearer tokens in headers. Now you have token issuance, token storage, token rotation, token revocation, refresh flows, and a token database to back up. Half of "auth" is now "token lifecycle".
  • mTLS between every pair. Cleaner trust model, but every operator hits the same wall: certificate authority, intermediate certs, rotation, and a hard story for revocation.
  • Shared secret in .env. Works until the day it leaks — then everyone learns about it at the same time.

Signed requests sit in the middle. The agent's public key is a file on the verifier's disk. Removing the file revokes the agent. The signature is per-request, so there is no token to steal that grants ongoing access. The verifier holds no state about who signed what — only "is this signature valid for this canonical message".

This pattern is well-known (AWS SigV4, GitHub Apps JWT, SSH agent forwarding). BuildOnAI Key Server is the self-contained implementation of it.

Install

terminal
git clone https://github.com/build-on-ai/buildonai-key-server.git
cd buildonai-key-server
cp auth/allowed-clients.json.example auth/allowed-clients.json
# edit auth/allowed-clients.json: add the IPs your callers will use
docker compose up -d
curl http://localhost:3040/health

Standalone deployment: this repository exposes port 3040 by default. When Key Server is deployed as part of Consciousness Server, its default host port is13040. Change the standalone port withKEY_SERVER_PORT.

Register an agent (ed25519)

terminal
# On the agent's host:
ssh-keygen -t ed25519 -C "agent1@$(hostname)" \
  -f ~/.ssh/buildonai-agent1 -N ""

# Copy the PUBLIC key to the key-server host:
scp ~/.ssh/buildonai-agent1.pub \
    operator@key-server-host:/opt/buildonai-key-server/keys/agents/agent1.pub

# Confirm the server picked it up:
curl http://key-server-host:3040/api/agents/identity
# {"agents": ["agent1"]}

Private key stays on the agent's host. It never leaves. Revocation is rm keys/agents/agent1.pub — takes effect immediately, there is no cache.

Signed routes

Sensitive routes — vault reads and the audit log — require both an allowed caller IP and a valid ed25519 signature. Missing or invalid signatures receive 401. The health, public-identity and verification endpoints remain open to the IP allow-list by design.

Use the shipped bin/sign-request helper or implement the documented canonical-message protocol in your client. There is no unsigned compatibility mode.

Concepts

Canonical message

Agent, gated service, and key-server must reconstruct the same bytes to sign and verify. Five fields joined by literal \n (LF):

canonical message
<METHOD>\n<PATH>\n<X-Timestamp>\n<X-Nonce>\nSHA256(body)

SHA256(body) is hex-encoded. PATH is the request path without query string. For a GET with no body, SHA256("") = e3b0c442...52b855.

Verifying a signed request

A gated service (Consciousness Server, your API, your webhook receiver) extracts the four headers + body hash from an incoming request and POSTs them to /api/verify:

terminal
# A gated service forwards the agent's headers + body hash to /api/verify.
curl -s -X POST http://key-server-host:3040/api/verify \
  -H 'Content-Type: application/json' \
  -d '{
    "agent_id": "agent1",
    "method": "POST",
    "path": "/api/notes/create",
    "timestamp": "2026-05-31T10:00:00Z",
    "nonce": "a7b3c1d9e2f64a18b3c1d9e2f64a18b3",
    "signature": "base64-ed25519-sig-here",
    "body_sha256": "e3b0c44298fc1c149afbf4c8996fb924..."
  }'
# {"valid": true}

Anti-replay

  • Timestamp window: now-300s to now+60s. Outside → rejected.
  • Nonce cache: each X-Nonce accepted at most once per 5 minutes (Redis SET NX EX 300).
  • Body binding: the canonical message includes SHA256(body) — a captured request cannot be replayed against a different endpoint or with a tampered body.

Vault layout

The on-disk layout under keys/ is deliberately simple — ls tells you what's there. Per-agent ed25519 public keys live at keys/agents/<AGENT>.pub. SSH private keys at keys/ssh/<name>; API tokens atkeys/<service>/api-key.txt. Setchmod 600 on private material.

Audit log

Every request — verify, vault read, success or failure — appends one line to logs/audit.log plus a JSONL entry tologs/audit.jsonl. Self-rotates at 50 MB.

logs/audit.log
[2026-05-31T08:00:01Z] IP=192.0.2.20 ENDPOINT=/api/verify RESULT=VALID agent=agent1
[2026-05-31T08:00:03Z] IP=192.0.2.30 ENDPOINT=/api/verify RESULT=VALID agent=agent2
[2026-05-31T08:00:18Z] IP=192.0.2.30 ENDPOINT=/api/verify RESULT=INVALID reason=nonce_replayed
[2026-05-31T14:22:05Z] IP=192.0.2.99 ENDPOINT=/keys/ssh/git-deploy RESULT=FORBIDDEN IP_not_whitelisted
[2026-05-31T14:22:18Z] IP=192.0.2.20 ENDPOINT=/keys/ssh/../etc/passwd RESULT=REJECTED path_traversal_attempt

Common use cases

Inter-service auth in a small monorepo

Each service holds its own ed25519 private key and signs outbound requests. The verifier is one container. Removes the per-serviceSHARED_SECRET env var that nobody rotates.

Webhook authentication

Your source signs the payload before sending; the receiver forwards the headers to /api/verify. Cryptographic proof of origin without a shared HMAC secret per source. Stolen pub key file? Public — there is no shared secret to steal.

IoT device auth

Each device generates its own ed25519 keypair on first boot. Public key registered out of band. Telemetry is signed per request. Stolen device → rm its .pub file, telemetry stops being accepted, the device's stored private key is now useless against you.

Vault read (legacy, optional)

any allow-listed host
# The helper emits the four signature headers for this exact request.
eval curl -s $(bin/sign-request agent1 GET /keys/ssh/github-deploy) \
  http://localhost:3040/keys/ssh/github-deploy > ~/.ssh/id_deploy
chmod 600 ~/.ssh/id_deploy

The vault endpoints (/keys/ssh/<name>,/keys/api/<service>) inherit the same trust model as /api/verify: IP allow-list plus a valid ed25519 signature for every sensitive request.

API

Seven endpoints. The whole surface area.

MethodPathPurpose
GET/healthLiveness probe
GET/api/agents/identityList registered agent ids
GET/api/agents/identity/:agentPublic key + fingerprint for one agent
POST/api/verifyVerify a signed request (called by gated services)
GET/keys/ssh/:nameFetch an SSH private key from the vault
GET/keys/api/:serviceFetch an API token from the vault
GET/auditLast 100 audit-log entries

Threat model

Defended against

  • Replay on the same LAN — nonce + timestamp window.
  • Forged origin from a foreign host — IP allow-list as layer one.
  • Captured-then-replayed at a different endpoint — canonical message binds method + path + body hash.
  • Compromised agent whose private key is now in attacker handsrm keys/agents/<AGENT>.pub on the verifier. Takes effect immediately, no cache.
  • Accidental commit of secrets — shipped.gitignore excludes keys/,logs/, and the real auth/allowed-clients.json.

Not defended against (deliberate scope)

  • Network eavesdropping on the request body. ed25519 is a signature scheme, not encryption. Wrap behind TLS (Caddy, nginx) if requests carry sensitive payloads.
  • An attacker with root on the host. They can read private keys, the pub-key vault, and the audit log directly from disk.
  • High-volume DoS on /api/verify. The server rate-limits nothing; put it behind a reverse proxy if untrusted callers can reach it.

For deployments outside a trusted LAN/VPN, the obvious upgrades are: terminate TLS at a reverse proxy, run behind a private network, set tight OS file permissions on keys/, and rotate agent keys on a schedule. The threat model is written for low-friction LAN use; adjust as the perimeter widens.

Next steps