Usage Walkthrough¶
A linear, hands-on walk through every operation Aegis-KMS supports — Step 0 (Docker installed) through Step 19 (clean up). Every step has the exact command to run and the expected output. By the end you'll have created a key, used every crypto operation, rotated it, marked it as compromised, inspected the audit trail, scraped Prometheus, and looked at OpenTelemetry traces.
Time to complete: 15-20 minutes if you copy-paste; 30-45 minutes if you read along.
Prefer reference docs?
This page is the linear walkthrough. For a per-audience reference (app developer, operator,
AI agent, storage vendor), see POSITIONING.md. For deep architecture, see
ARCHITECTURE.md. For the REST surface, the live OpenAPI spec is at
http://localhost:8080/docs/ once the server is running.
Step 0 — Prerequisites¶
You need:
| Tool | Version | Why |
|---|---|---|
| Docker | any recent | Brings up aegis-server + Postgres |
curl |
any | All examples use curl |
jq |
optional but recommended | Pretty-prints JSON responses |
openssl |
optional | Used to mint a JWT secret |
Verify:
bash
docker --version && curl --version | head -1 && jq --version 2>/dev/null || true
If Docker isn't installed: https://docs.docker.com/get-docker/.
Step 1 — Get the code¶
bash
git clone https://github.com/sharma-bhaskar/aegis-kms.git
cd aegis-kms
You don't need a JDK or sbt for this walkthrough — we'll use the published Docker image. (If you do want to build from source, see Developer Guide.)
Step 2 — Boot Aegis¶
Aegis-KMS requires you to set the Postgres password explicitly. There is no insecure default.
bash
export POSTGRES_PASSWORD="$(openssl rand -base64 24)"
docker compose -f deploy/docker/docker-compose.yml up -d
Expected output (last 5 lines):
[+] Running 3/3
✔ Network aegis-kms_default Created
✔ Container aegis-postgres Started
✔ Container aegis-server Started
Stream the logs in another terminal so you can see what the server is doing:
bash
docker compose -f deploy/docker/docker-compose.yml logs -f aegis-server
You should see something like:
INFO Started Aegis server on 0.0.0.0:8080
INFO Persistence: Postgres event journal at jdbc:postgresql://postgres:5432/aegis
INFO Crypto: AwsKmsRootOfTrust (or InMemoryRootOfTrust if AWS not configured)
INFO Auth: dev mode (X-Aegis-User header)
INFO Tracing: OpenTelemetry SDK auto-configured (OTEL_TRACES_EXPORTER=none)
INFO Boot complete in 2.34s
Leave that log tail running — you'll watch real audit + trace events appear in it as you work.
Step 3 — Verify the server is up¶
There's no dedicated /health endpoint in v0.1.1 — instead, watch the log tail from Step 2
for the "Boot complete" line, or hit the Swagger UI (next step). You can also confirm the
server is listening on 8080:
```bash curl -sI http://localhost:8080/docs/ | head -1
→ HTTP/1.1 200 OK¶
```
If you get connection refused, the server hasn't finished booting — give it ~5 seconds
and retry. If it still fails, check the log tail; most failures are configuration issues
(bad POSTGRES_PASSWORD, AWS credentials missing when AEGIS_ROT_KIND=aws).
A /healthz endpoint with structured component health is targeted for v0.1.2.
Step 4 — Open the Swagger UI¶
bash
open http://localhost:8080/docs/
(On Linux, xdg-open; or just paste the URL in your browser.)
You should see the OpenAPI 3.1 spec rendered as Swagger UI, listing every endpoint Aegis
serves: /v1/keys, /v1/keys/{id}/sign, /v1/keys/{id}/encrypt, etc. The spec is generated
from the same Tapir endpoint values the runtime interprets, so it's impossible for the docs to
drift from the wire shape.
You can drive every operation in this walkthrough from Swagger UI's "Try it out" buttons
instead of curl if you prefer.
Step 5 — Authenticate (dev mode)¶
By default, Aegis-KMS in this Docker Compose setup uses dev auth: pass an
X-Aegis-User: <name> header and the server treats you as that human principal. Use this for
the walkthrough; we'll switch to JWT in Step 17.
Set a shell variable so you don't have to retype it:
bash
export AEGIS=http://localhost:8080
export USER_HEADER="X-Aegis-User: alice"
We'll use $USER_HEADER on every request. If you forget it, you'll get 401 Unauthorized —
that's by design. There is no anonymous access.
(In v0.1.1 there's no GET /v1/keys list endpoint to confirm against — only
GET /v1/keys/{id} for a specific key. Step 6 creates a key we can fetch.)
Step 6 — Create your first key¶
We'll create a 256-bit AES symmetric key, named walkthrough-demo.
bash
KEY_RESPONSE=$(curl -s -X POST "$AEGIS/v1/keys" \
-H "$USER_HEADER" \
-H "Content-Type: application/json" \
-d '{
"spec": {
"name": "walkthrough-demo",
"algorithm": "AES",
"sizeBits": 256,
"objectType": "SymmetricKey"
}
}')
echo "$KEY_RESPONSE" | jq
Expected output:
json
{
"id": "8a3f1e25-6b0a-4f5d-8e92-1c4f6a83b2d9",
"spec": {
"name": "walkthrough-demo",
"algorithm": "AES",
"sizeBits": 256,
"objectType": "SymmetricKey"
},
"state": "PreActive",
"currentVersion": 1,
"createdAt": "2026-05-09T01:23:45Z"
}
What just happened:
- Aegis recorded a
KeyCreatedevent in the Postgres event journal. - The key is in state
PreActive— by design. A key starts inPreActiveso a policy engine, auditor, or human operator has a chance to review before it goes live. You can't sign or encrypt with aPreActivekey — you must explicitly activate it (Step 7). - An audit row was emitted to stdout (look at the log tail from Step 2).
Capture the id for the rest of the walkthrough:
bash
export KEY_ID=$(echo "$KEY_RESPONSE" | jq -r '.id')
echo "KEY_ID=$KEY_ID"
Other key specs¶
The same endpoint accepts several spec shapes:
algorithm |
Variants | Typical use |
|---|---|---|
AES |
128, 192, 256 | Symmetric encrypt/decrypt, AEAD, envelope DEKs |
RSA |
2048, 3072, 4096 | Wrap/unwrap, sign/verify |
EC |
P-256, P-384, P-521, secp256k1 |
Sign/verify, ECDH |
EdDSA |
Ed25519, Ed448 |
Sign/verify |
HMAC |
256, 384, 512 | MAC, JWT signing (HS*) |
For this walkthrough we'll stick with AES-256.
Step 7 — Activate the key¶
bash
curl -s -X POST "$AEGIS/v1/keys/$KEY_ID/activate" -H "$USER_HEADER" | jq
Expected output:
json
{
"id": "8a3f1e25-...",
"state": "Active",
"activatedAt": "2026-05-09T01:24:01Z"
}
State machine:
rotate compromise
PreActive ──activate──► Active ───────► Deactivated ───────────► Compromised ──destroy──► Destroyed
│ ▲
└────────────────────────────compromise──────────────────────────────────┘
Sign / encrypt / wrap require Active. Verify / decrypt / unwrap are permitted on Active
and Deactivated (so material produced before a rotation remains readable). Compromised
and Destroyed refuse every op.
Step 8 — Sign a message¶
v0.1.1 supports two signature algorithms: RsaPssSha256 and EcdsaSha256. The dev-mode
in-memory Root of Trust accepts either against any key spec, so we'll use RsaPssSha256 here.
```bash MSG_B64=$(echo -n 'invoice #1234 total $42,000' | base64)
curl -s -X POST "$AEGIS/v1/keys/$KEY_ID/sign" \ -H "$USER_HEADER" -H "Content-Type: application/json" \ -d "{\"messageBase64\":\"$MSG_B64\",\"algorithm\":\"RsaPssSha256\"}" | jq ```
Expected output:
json
{
"signatureBase64": "kY3Z0bH9eXqPQ8a7cF6V4t5RmN1pL2rS3sO9wJxK8uM=",
"algorithm": "RsaPssSha256",
"keyVersion": 1
}
Capture a signature of "hello" for Step 9:
```bash HELLO_B64=$(echo -n 'hello' | base64)
export SIG=$(curl -s -X POST "$AEGIS/v1/keys/$KEY_ID/sign" \ -H "$USER_HEADER" -H "Content-Type: application/json" \ -d "{\"messageBase64\":\"$HELLO_B64\",\"algorithm\":\"RsaPssSha256\"}" \ | jq -r '.signatureBase64') echo "SIG=$SIG" ```
Step 9 — Verify the signature¶
bash
curl -s -X POST "$AEGIS/v1/keys/$KEY_ID/verify" \
-H "$USER_HEADER" -H "Content-Type: application/json" \
-d "{\"messageBase64\":\"$HELLO_B64\",\"signatureBase64\":\"$SIG\",\"algorithm\":\"RsaPssSha256\"}" \
| jq
Expected output:
json
{
"valid": true,
"algorithm": "RsaPssSha256"
}
Try tampering — change 'hello' to 'helloo':
```bash TAMPERED_B64=$(echo -n 'helloo' | base64)
curl -s -X POST "$AEGIS/v1/keys/$KEY_ID/verify" \ -H "$USER_HEADER" -H "Content-Type: application/json" \ -d "{\"messageBase64\":\"$TAMPERED_B64\",\"signatureBase64\":\"$SIG\",\"algorithm\":\"RsaPssSha256\"}" \ | jq ```
Expected output (note: HTTP status is still 200 — valid: false is a successful
verification result, not an error):
json
{
"valid": false,
"algorithm": "RsaPssSha256"
}
Step 10 — Encrypt + decrypt with EncryptionContext (AAD)¶
EncryptionContext is bound to the ciphertext as Additional Authenticated Data — to
decrypt successfully, the caller must present the same context. This is what protects against
ciphertext-substitution attacks where an attacker swaps in another tenant's ciphertext.
bash
ENC_RESPONSE=$(curl -s -X POST "$AEGIS/v1/keys/$KEY_ID/encrypt" \
-H "$USER_HEADER" -H "Content-Type: application/json" \
-d '{
"plaintextBase64": "'"$(echo -n 'q2 financial summary: $42M revenue' | base64)"'",
"context": {"dataset":"q2-financials","tenant":"acme"}
}')
echo "$ENC_RESPONSE" | jq
Expected output:
json
{
"ciphertextBase64": "AQEBAHj...",
"keyVersion": 1,
"algorithm": "AesGcm256"
}
Decrypt with the matching context:
```bash CIPHERTEXT=$(echo "$ENC_RESPONSE" | jq -r '.ciphertextBase64')
curl -s -X POST "$AEGIS/v1/keys/$KEY_ID/decrypt" \ -H "$USER_HEADER" -H "Content-Type: application/json" \ -d '{ "ciphertextBase64": "'"$CIPHERTEXT"'", "context": {"dataset":"q2-financials","tenant":"acme"} }' | jq -r '.plaintextBase64' | base64 -d echo ```
Expected output:
q2 financial summary: $42M revenue
Now try with the wrong context — say a different tenant:
bash
curl -s -X POST "$AEGIS/v1/keys/$KEY_ID/decrypt" \
-H "$USER_HEADER" -H "Content-Type: application/json" \
-d '{
"ciphertextBase64": "'"$CIPHERTEXT"'",
"context": {"dataset":"q2-financials","tenant":"different-tenant"}
}' | jq
Expected output:
json
{
"error": "DecryptionFailed",
"message": "EncryptionContext mismatch — refusing to decrypt"
}
This is the correct behaviour: the AAD bound the context to the ciphertext at encrypt time; presenting different AAD at decrypt time is a forgery and rejected.
Step 11 — Wrap + unwrap a data encryption key (DEK)¶
Envelope encryption is the standard pattern for encrypting large objects: you generate a
short-lived data encryption key (DEK) per object, encrypt the object with the DEK locally, and
then wrap the DEK under your KMS key. Aegis exposes wrap / unwrap for this.
bash
WRAP_RESPONSE=$(curl -s -X POST "$AEGIS/v1/keys/$KEY_ID/wrap" \
-H "$USER_HEADER" -H "Content-Type: application/json" \
-d '{
"plaintextBase64": "'"$(openssl rand -base64 32)"'"
}')
echo "$WRAP_RESPONSE" | jq
Expected output:
json
{
"wrappedKeyBase64": "AQECAHg...",
"keyVersion": 1,
"algorithm": "AesKeyWrap256"
}
Unwrap:
```bash WRAPPED=$(echo "$WRAP_RESPONSE" | jq -r '.wrappedKeyBase64')
curl -s -X POST "$AEGIS/v1/keys/$KEY_ID/unwrap" \ -H "$USER_HEADER" -H "Content-Type: application/json" \ -d '{"wrappedKeyBase64":"'"$WRAPPED"'"}' | jq ```
Expected output:
json
{
"plaintextBase64": "<original 32-byte random key>",
"keyVersion": 1
}
Step 12 — Rotate the key¶
Rotation creates a new key version under the same KeyId. New writes go to the new
version; old ciphertexts and signatures remain readable because verify / decrypt /
unwrap happily accept material from any active or deactivated prior version.
bash
curl -s -X POST "$AEGIS/v1/keys/$KEY_ID/rotate" \
-H "$USER_HEADER" -H "Content-Type: application/json" \
-d '{"reason":"scheduled-rotation"}' | jq
Expected output:
json
{
"id": "8a3f1e25-...",
"previousVersion": 1,
"currentVersion": 2,
"rotatedAt": "2026-05-09T01:30:12Z"
}
Now sign with the rotated key — it'll use version 2:
bash
curl -s -X POST "$AEGIS/v1/keys/$KEY_ID/sign" \
-H "$USER_HEADER" -H "Content-Type: application/json" \
-d '{"messageBase64":"'"$(echo -n 'after rotation' | base64)"'","algorithm":"RsaPssSha256"}' \
| jq '{algorithm, keyVersion}'
Expected output:
json
{
"algorithm": "RsaPssSha256",
"keyVersion": 2
}
But you can still verify a signature made by version 1:
bash
curl -s -X POST "$AEGIS/v1/keys/$KEY_ID/verify" \
-H "$USER_HEADER" -H "Content-Type: application/json" \
-d "{\"messageBase64\":\"$HELLO_B64\",\"signatureBase64\":\"$SIG\",\"algorithm\":\"RsaPssSha256\"}" \
| jq
Returns valid: true because Aegis tracks per-version key material and routes the verify to
the version that signed it.
Step 13 — Mark the key as compromised¶
This is a one-way transition. Compromised keys refuse all operations, and the audit row is
emitted with severity Critical.
bash
curl -s -X POST "$AEGIS/v1/keys/$KEY_ID/compromise" \
-H "$USER_HEADER" -H "Content-Type: application/json" \
-d '{"reason":"leaked in S3 audit on 2026-05-08"}' | jq
Expected output:
json
{
"id": "8a3f1e25-...",
"state": "Compromised",
"compromisedAt": "2026-05-09T01:31:00Z",
"reason": "leaked in S3 audit on 2026-05-08"
}
Try to sign — it will refuse:
bash
curl -s -X POST "$AEGIS/v1/keys/$KEY_ID/sign" \
-H "$USER_HEADER" -H "Content-Type: application/json" \
-d '{"messageBase64":"'"$(echo -n 'too late' | base64)"'","algorithm":"RsaPssSha256"}' \
| jq
Expected output:
json
{
"error": "IllegalOperation",
"message": "Cannot sign with key in state Compromised"
}
This is what the v0.2.0 auto-responder will do automatically when the anomaly detector fires
a Critical alert — for now, it's an explicit operator action.
Step 14 — Inspect the audit log¶
The default AuditSink writes to stdout. In your log-tail terminal (Step 2) you'll have seen
rows like:
{"ts":"2026-05-09T01:23:45.234Z","actor":"alice","actorKind":"Human","op":"KeyCreated","keyId":"8a3f1e25-...","outcome":"Success","keyVersion":1}
{"ts":"2026-05-09T01:24:01.567Z","actor":"alice","actorKind":"Human","op":"KeyActivated","keyId":"8a3f1e25-...","outcome":"Success"}
{"ts":"2026-05-09T01:24:30.890Z","actor":"alice","actorKind":"Human","op":"Sign","keyId":"8a3f1e25-...","outcome":"Success","keyVersion":1,"alg":"RsaPssSha256"}
{"ts":"2026-05-09T01:25:00.123Z","actor":"alice","actorKind":"Human","op":"Verify","keyId":"8a3f1e25-...","outcome":"Success","keyVersion":1}
{"ts":"2026-05-09T01:25:15.456Z","actor":"alice","actorKind":"Human","op":"Verify","keyId":"8a3f1e25-...","outcome":"InvalidSignature"}
{"ts":"2026-05-09T01:30:12.111Z","actor":"alice","actorKind":"Human","op":"Rotate","keyId":"8a3f1e25-...","outcome":"Success","previousVersion":1,"currentVersion":2}
{"ts":"2026-05-09T01:31:00.222Z","actor":"alice","actorKind":"Human","op":"Compromise","keyId":"8a3f1e25-...","outcome":"Success","severity":"Critical","reason":"leaked in S3 audit on 2026-05-08"}
{"ts":"2026-05-09T01:31:15.333Z","actor":"alice","actorKind":"Human","op":"Sign","keyId":"8a3f1e25-...","outcome":"IllegalOperation","reason":"key state=Compromised"}
Every state change and every crypto operation lands in this log. Including the failures — note the last row, which records the rejected sign attempt against the compromised key.
In v0.2.0 these will fan out to Kafka / S3 / Webhook / Postgres sinks instead of (or in
addition to) stdout. The AuditSink SPI is in place; the adapters are next.
Step 15 — Inspect Prometheus metrics¶
bash
curl -s http://localhost:8080/metrics | grep -E '^aegis_keys_op_(total|errors)' | head -20
Expected output (roughly):
aegis_keys_op_total{operation="Create"} 1.0
aegis_keys_op_total{operation="Activate"} 1.0
aegis_keys_op_total{operation="Sign"} 3.0
aegis_keys_op_total{operation="Verify"} 2.0
aegis_keys_op_total{operation="Encrypt"} 1.0
aegis_keys_op_total{operation="Decrypt"} 2.0
aegis_keys_op_total{operation="Wrap"} 1.0
aegis_keys_op_total{operation="Unwrap"} 1.0
aegis_keys_op_total{operation="Rotate"} 1.0
aegis_keys_op_total{operation="Compromise"} 1.0
aegis_keys_op_errors_total{operation="Decrypt",code="DecryptionFailed"} 1.0
aegis_keys_op_errors_total{operation="Sign",code="IllegalOperation"} 1.0
aegis_keys_op_errors_total{operation="Verify",code="InvalidSignature"} 1.0
You'll also see standard JVM metrics: jvm_memory_used_bytes, jvm_gc_pause_seconds,
process_uptime_seconds, etc. Point your Prometheus scrape config at
http://aegis-server:8080/metrics in production.
Step 16 — Inspect OpenTelemetry traces (optional)¶
By default, traces are exported to nowhere (OTEL_TRACES_EXPORTER=none). To wire them to a
local Jaeger:
```bash docker run -d --name jaeger \ -p 16686:16686 -p 4318:4318 \ jaegertracing/all-in-one:latest
Restart Aegis with OTel pointing at Jaeger¶
docker compose -f deploy/docker/docker-compose.yml down export OTEL_TRACES_EXPORTER=otlp export OTEL_EXPORTER_OTLP_ENDPOINT=http://host.docker.internal:4318 export OTEL_SERVICE_NAME=aegis-server docker compose -f deploy/docker/docker-compose.yml up -d
Make some requests so there's something to see¶
curl -s -X POST "$AEGIS/v1/keys" \ -H "$USER_HEADER" -H "Content-Type: application/json" \ -d '{"spec":{"name":"trace-demo","algorithm":"AES","sizeBits":256,"objectType":"SymmetricKey"}}' \
/dev/null
Open Jaeger UI¶
open http://localhost:16686 ```
In Jaeger, select service: aegis-server. You'll see spans named kms.Create, kms.Sign,
kms.Encrypt etc. with attributes:
aegis.operation—Sign,Encrypt,Rotate, ...aegis.key.id— the KeyIdaegis.principal.subject—aliceaegis.principal.kind—humanoragentaegis.outcome—successorerror_<code>
For full request-graph coverage (HTTP server spans, JDBC spans, AWS SDK spans), see Operations → Observability.
Step 17 — Use the aegis CLI¶
Everything you just did via curl is also available via the aegis CLI. Download the tarball
from the latest release:
```bash TAG=0.1.1 curl -L -o aegis-cli.tgz \ "https://github.com/sharma-bhaskar/aegis-kms/releases/download/v${TAG}/aegis-cli-${TAG}.tgz" tar -xzf aegis-cli.tgz export AEGIS_CLI=./aegis-cli-${TAG}/bin/aegis
$AEGIS_CLI version
→ aegis 0.1.1¶
```
Run the same end-to-end flow as Steps 5-13:
bash
$AEGIS_CLI login --server $AEGIS --principal alice
$AEGIS_CLI keys create --alg AES-256 --name cli-demo
$AEGIS_CLI keys list
$AEGIS_CLI keys activate <id>
$AEGIS_CLI keys sign --id <id> --message "hello"
$AEGIS_CLI keys encrypt --id <id> --plaintext "secret" --context dataset=q2
$AEGIS_CLI keys rotate --id <id>
$AEGIS_CLI keys compromise --id <id> --reason "leaked"
Every verb has --help. The login command stores the server URL + principal in
~/.config/aegis/config.json, so subsequent commands don't need to repeat them.
v0.1.1 stub commands
aegis agent issue, aegis audit tail, and aegis advisor scan print "not yet wired
up" — they're the surface for v0.2.0 work. Don't be surprised when they don't do
anything yet.
Step 18 — Switch from dev auth to JWT (for production)¶
Dev mode (X-Aegis-User) is fine for the walkthrough but unsafe for anything else. Production
uses JWT bearer tokens.
```bash
Pick a strong shared secret (at least 32 bytes)¶
export AEGIS_AUTH_HMAC_SECRET="$(openssl rand -base64 48)"
Restart the server with JWT auth¶
docker compose -f deploy/docker/docker-compose.yml down docker run --rm -d --name aegis-server-jwt \ -p 8080:8080 \ -e POSTGRES_PASSWORD="$POSTGRES_PASSWORD" \ -e AEGIS_AUTH_KIND=hmac \ -e AEGIS_AUTH_HMAC_SECRET="$AEGIS_AUTH_HMAC_SECRET" \ ghcr.io/sharma-bhaskar/aegis-server:0.1.1 ```
Mint a token (from a Scala REPL or a small program — Aegis ships JwtIssuer.hmac for this):
scala
import dev.aegiskms.iam.JwtIssuer
val token = JwtIssuer.hmac(secret = sys.env("AEGIS_AUTH_HMAC_SECRET"))
.issue(subject = "alice", groups = Set("admins"), ttl = 1.hour)
println(token)
Then use it in requests:
bash
export TOKEN="<paste minted token>"
curl -s "$AEGIS/v1/keys" -H "Authorization: Bearer $TOKEN" | jq
For OIDC / JWKS / Auth0 / Okta integration, the AEGIS_AUTH_KIND=oidc mode is in WIP for
v0.2.0 — see Operations → Security for the deploy-time
configuration matrix.
Step 19 — Configure AWS KMS as the Root of Trust (production)¶
The InMemory RoT is fine for the walkthrough. In production, swap in AwsKmsRootOfTrust so
key material lives in AWS KMS hardware-backed CMKs and never touches Aegis's process memory.
Required AWS IAM permissions:
json
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"kms:CreateKey", "kms:DescribeKey", "kms:ScheduleKeyDeletion",
"kms:Sign", "kms:Verify",
"kms:Encrypt", "kms:Decrypt",
"kms:GenerateDataKey", "kms:GenerateDataKeyWithoutPlaintext",
"kms:GetPublicKey",
"kms:CreateAlias", "kms:UpdateAlias",
"kms:TagResource"
],
"Resource": "*"
}]
}
Boot Aegis with AWS RoT:
bash
docker run --rm -d --name aegis-server-aws \
-p 8080:8080 \
-e POSTGRES_PASSWORD="$POSTGRES_PASSWORD" \
-e AEGIS_ROT_KIND=aws \
-e AWS_REGION=us-east-1 \
-e AWS_ACCESS_KEY_ID=... \
-e AWS_SECRET_ACCESS_KEY=... \
ghcr.io/sharma-bhaskar/aegis-server:0.1.1
(In production, use IAM Roles / IRSA on Kubernetes / ECS task roles instead of static credentials.)
GCP / Azure / Vault / PKCS#11 RoT adapters are designed and roadmapped for v0.2.0 — see Status.
Step 20 — Clean up¶
bash
docker compose -f deploy/docker/docker-compose.yml down -v # -v also removes the Postgres volume
docker rm -f aegis-server-jwt aegis-server-aws jaeger 2>/dev/null || true
-v removes the Postgres data volume. If you skip it, your keys persist for the next run.
Reference — REST endpoints¶
You touched all of these. Full list:
| Method | Path | Purpose |
|---|---|---|
POST |
/v1/keys |
Create a new key (state: PreActive) |
GET |
/v1/keys/{id} |
Get one key + state + version |
POST |
/v1/keys/{id}/activate |
Transition PreActive → Active |
POST |
/v1/keys/{id}/sign |
Sign a message (RsaPssSha256 | EcdsaSha256) |
POST |
/v1/keys/{id}/verify |
Verify a signature |
POST |
/v1/keys/{id}/encrypt |
Encrypt with EncryptionContext AAD |
POST |
/v1/keys/{id}/decrypt |
Decrypt with matching AAD |
POST |
/v1/keys/{id}/wrap |
Envelope-wrap a DEK |
POST |
/v1/keys/{id}/unwrap |
Envelope-unwrap a DEK |
POST |
/v1/keys/{id}/rotate |
Create a new key version |
POST |
/v1/keys/{id}/compromise |
Mark key as Compromised (one-way) |
DELETE |
/v1/keys/{id} |
Schedule destruction |
GET |
/metrics |
Prometheus exposition |
GET |
/docs/ |
Swagger UI |
GET |
/docs/docs.yaml |
Raw OpenAPI 3.1 spec |
Not in v0.1.1: there's no
GET /v1/keys(list) endpoint and no/health//healthzendpoint. Both are tracked for v0.1.2.
Reference — error codes¶
All error responses share this shape:
json
{ "error": "<code>", "message": "<human-readable>" }
| Code | HTTP | When |
|---|---|---|
KeyNotFound |
404 | Unknown KeyId |
IllegalOperation |
409 | Operation not allowed in current state (e.g., sign on Compromised) |
InvalidSignature |
200 | Verify returned valid: false (this is not an HTTP error) |
DecryptionFailed |
400 | EncryptionContext mismatch, bad ciphertext, or algorithm mismatch |
PermissionDenied |
403 | Principal not authorized for this operation on this key |
Unauthorized |
401 | Missing or invalid bearer token / dev header |
BadRequest |
400 | Schema validation error |
InternalError |
500 | Unexpected error — open an issue with the request id from the response |
Where to next¶
- Architecture — how the decorator stack, the actor model, and the two-tier module split fit together
- Operations → Observability — Prometheus + OpenTelemetry
- Swagger UI deep-dive
- Operations → Security — JWT, OIDC, IAM permissions, TLS termination
- Comparison — Aegis vs. AWS KMS / Vault / OpenBao / GCP / Azure
- Developer Guide — if you want to contribute
- Roadmap — what lands when