Changelog¶
What shipped when. The canonical source is the project-root CHANGELOG.md, kept in
Keep a Changelog format.
Changelog¶
All notable changes to Aegis will be documented here. This project follows Keep a Changelog and Semantic Versioning.
Unreleased¶
No entries yet — open the next PR with its Added / Changed / Fixed block.
0.1.1 — 2026-05-09¶
First public, taggable release. Everything below shipped between the
v0.1.0-rc.2 candidate and this tag — the full key-lifecycle and
crypto surface (sign / verify / encrypt / decrypt / wrap / unwrap /
rotate / compromise), JWT bearer auth, Postgres event journal,
Prometheus + OpenTelemetry observability, anomaly-detector baselines,
and the OpenAPI / Swagger UI documentation surface. v0.1.0 final was
never cut — what we'd planned as v0.1.0 is folded into this release.
Changed¶
Servernow boots inside aResource[IO, Unit](closes #12). Refactored the entry point fromdef main+unsafeRunSynctoIOApp.Simple+ a single composedResourcechain. Each piece of the boot — Prometheus meter registry, journal connection pool, PekkoActorSystem, HTTP binding — is acquired with a matching finalizer, so SIGTERM / SIGINT now unwinds the stack in reverse: HTTP unbind (5 s grace) → actor system terminate → journal pool close → meter registry close. v0.1.0's boot calledPostgresEventJournal.make(...).allocated.unsafeRunSync()._1and discarded the finalizer, leaking the connection pool until JVM exit; that's gone. NewBootResourceSpecacquires the full stack against a free local port, hits the listener, and verifies that releasing the resource closes the binding (no 200 on a subsequent connect).
Added¶
- Anomaly detector expansion: time-of-day, source-IP, op-histogram baselines (closes #13).
BaselineDetectornow ships five detectors instead of two — addsOpHistogramBaseline(actor performed anOperationit has never used),TimeOfDayBaseline(actor active in a UTC hour outside their seen set), andSourceIpBaseline(request from a new IP, read fromAuditRecord.context("source.ip")). Each detector has a cold-start guard: it requires the actor to have at least one prior observation in that dimension, so the first call doesn't alert. A single anomalous record can fire multiple detectors at once (compound anomalies — see the README's "Claude goes rogue" path).ActorBaselinegainedhoursSeen: Set[Int]andsourceIpsSeen: Set[String].AuditRecordgained an additivecontext: Map[String, String] = Map.emptyfield; theSourceIpBaselinedetector readsBaselineDetector.SourceIpContextKey("source.ip") from it. The HTTP layer doesn't yet populate the context — that's a follow-up; until then the SourceIp detector is shape-complete and tested but inert in production. - OpenAPI 3.1 spec + Swagger UI on the REST plane (closes #52).
HttpRoutesnow generates an OpenAPI document from the liveEndpoints.alllist and mounts the standard Swagger UI bundle at/docs/, with the raw YAML at/docs/docs.yaml. Because the spec is derived from the same Tapir endpoint definitions the routes interpret, drift between the docs and the wire shape is impossible by construction. Thetapir-openapi-docsandtapir-swagger-ui-bundledeps were already inDependencies.scalatapir; this PR is purely the route plumbing + a regression test that asserts every shipped path appears in the rendered spec. - Maven Central publishing — POM metadata + operator runbook (closes #14).
Each library module (
aegis-core,aegis-persistence,aegis-crypto,aegis-iam,aegis-audit,aegis-sdk-scala,aegis-sdk-java,aegis-kmip,aegis-http,aegis-agent-ai,aegis-mcp-server) now declares its own one-linedescriptionso Sonatype's POM-validation staging gate accepts the artifact.aegis-serverandaegis-clikeeppublish / skip := truesince they ship as a Docker image and a Universal tarball respectively. AThisBuild / descriptionfallback prevents an unnamed jar from regressing the gate. NewRELEASING.mddocuments the one-time maintainer setup (Sonatype OSSRH account, GPG key generation + keyserver publication, the four GitHub Action secretsPGP_SECRET/PGP_PASSPHRASE/SONATYPE_USERNAME/SONATYPE_PASSWORD) plus the per-release workflow (CHANGELOG bump,git tag v0.1.1 && git push origin v0.1.1, what to expect on the Actions page) and a troubleshooting matrix. The existingrelease.ymlworkflow already gates the Maven publish step onPGP_SECRET != '', so a release without secrets ships Docker + CLI only with a clear::notice. - OpenTelemetry tracing — application-level spans + autoconfigured SDK (closes #11). New
TracingKeyServicedecorator wraps eachKeyService[IO]call in an OTel span namedkms.<operation>with attributesaegis.operation,aegis.key.id(when applicable),aegis.principal.subject,aegis.principal.kind(humanoragent), andaegis.outcome(success/error_<code>). Span status is set toERRORwith theKmsErrormessage on failure. NewTracingRegistrybootstraps the OTel SDK viaAutoConfiguredOpenTelemetrySdk— configuration is driven entirely by the standardOTEL_*env vars / system properties (OTEL_SERVICE_NAME,OTEL_TRACES_EXPORTER,OTEL_EXPORTER_OTLP_ENDPOINT,OTEL_TRACES_SAMPLER,OTEL_RESOURCE_ATTRIBUTES). The decorator slots betweenMeteredKeyServiceandAuditingKeyService. For full request-graph coverage (pekko-http server spans, JDBC client spans, AWS SDK client spans), attach the OpenTelemetry Java Agent at JVM start (-javaagent:opentelemetry-javaagent.jar) — the agent and the SDK both read the sameOTEL_*env vars, so configuration is unchanged and our manual spans become children of the agent's via W3C trace-context propagation. NewTracingKeyServiceSpecuses the OTelInMemorySpanExporterto assert span names, attributes, status codes, and the locate-specificaegis.locate.hitsattribute. Adds theopentelemetry-api+-sdk+-exporter-otlp+-sdk-extension-autoconfiguredeps (server-tier only — library modules unaffected) plusopentelemetry-sdk-testingat test scope. - Docker Compose hardening: no default Postgres password (closes #51).
deploy/docker/docker-compose.ymlno longer ships theaegis-dev-password-change-medefault. Both the Postgres container and theaegis-serverJDBC password now reference${POSTGRES_PASSWORD:?...}— Compose fails fast with a clear error if the operator hasn't exported the variable.SECURITY.mdgains a new "Deploy-time configuration" section enumerating the env vars that must be supplied (POSTGRES_PASSWORD,AEGIS_AUTH_HMAC_SECRETwhen JWT auth is on, AWS creds when the KMS root-of-trust is configured) and noting that TLS termination is the fronting proxy's responsibility until the v0.4.0 KMIP plane ships native mTLS. - Prometheus
/metricsendpoint (closes #10). NewMeteredKeyServicedecorator slots betweenAuditingKeyServiceandAuthorizingKeyServicein the boot wiring and records three series perKeyServiceoperation:aegis_keys_op_total{operation}(counter),aegis_keys_op_duration_seconds{operation, outcome}(timer with percentile histogram so dashboards can compute p50/p95/p99), andaegis_keys_op_errors_total{operation, code}(counter tagged by theKmsError.code, so denies surface ascode="PermissionDenied"). The metrics layer sits outside auth so denies are countable; audit stays the outermost decorator so the audit row still reflects the true outcome. NewMetricsRegistry.make()builds aPrometheusMeterRegistryand binds the standard JVM/GC/threads/classloader/processor/uptime collectors. NewMetricsRoutes.routeexposesGET /metricsin Prometheus exposition format (text/plain; version=0.0.4) on the same pekko-http port as the application routes — it lives inaegis-serverrather thanaegis-httpso the Tapir API module stays Micrometer-free.Server.scalabuilds the registry once at boot and stitches the metrics route into the application route viaconcat(...). Adds themicrometer-core+micrometer-registry-prometheusdeps (server-tier only — library modules unaffected).
Fixed¶
- Server boot hung on first launch.
aegis-serverused a Pekko user-guardian + Promise pattern to expose theKeyOpsActor'sActorRefto the main thread. On some JDK + sbt + Pekko combinations, the guardian'sBehaviors.setupblock was never dispatched, soAwait.result(initialized.future, …)hung past every reasonable timeout. The fix makes the user guardian be theKeyOpsActordirectly (ActorSystem[T] <: ActorRef[T]in Pekko Typed) and removes the Promise/Await dance entirely. This affected thesbt 'server / run'README quickstart and the Docker image's startup. - CLI launcher script was named
bin/aegis-cli, notbin/aegis. sbt-native-packager defaults to the project name; we now setexecutableScriptName := "aegis"so the published tarball matches the README's./aegis-cli-0.1.0/bin/aegis versioninstructions. Server.scalaran sbt'sruntask in-process (no fork). Addedrun / fork := truefor theservermodule so the run task gets an isolated JVM. Previously this entangled Pekko's dispatcher with sbt's classloader.
Added¶
- Sign / verify across the whole stack (closes #5). New
sign(id, message, alg, by)andverify(id, message, signature, by)methods onKeyService[F[_]]inaegis-core, withOperation.Sign/Operation.Verifyadded to the IAM allowlist enum, a newSignaturetype +SigAlgorithmenum (RsaPssSha256,EcdsaSha256for v0.1.1), and matchingAuditingKeyServicedecorator records that capture the algorithm andvalid=true|falseoutcome. TheRootOfTrustSPI gained the same operations;AwsKmsRootOfTrustimplements them via the AWS KMSSign/VerifyAPIs (mappingRsaPssSha256→RSASSA_PSS_SHA_256,EcdsaSha256→ECDSA_SHA_256). On the wire:POST /v1/keys/{id}/sign(request:{messageBase64, algorithm}, response:{signatureBase64, algorithm}) andPOST /v1/keys/{id}/verify(request addssignatureBase64, response is{valid, algorithm}). The CLI gainedaegis keys sign --id <id> --message <text|@file> [--alg RsaPssSha256]andaegis keys verify --id <id> --message <text|@file> --signature <base64> [--alg RsaPssSha256]; verify exits 0 forvalid:true, 3 forvalid:false. The in-memoryKeyServiceuses a deterministic HMAC-SHA-256 keyed by the KeyId so the dev REST surface has a working round-trip without a real KMS. Sign requires the key to be inKeyState.Active; calls against PreActive keys returnKmsError(IllegalOperation, ...)and produce aFailedaudit record. ReadmeQuickstartSpecinaegis-core. Compiles + runs the embedded-library example fromREADME.mdso that snippet can never silently bitrot. If you change the README's "Quickstart — embedding as a library" Scala block, mirror the change in this test.- Rotate(id, policy) across the whole stack (closes #8). New
rotate(id, policy, by)method onKeyService[F[_]].ManagedKeygainscurrentVersion: Int = 1(additive — defaulted for back-compat); rotation increments it by one. Legal source state isActiveonly; rotating from any other state returnsKmsError(IllegalOperation, ...). The new value typeRotationPolicy(Manual | TimeBased(FiniteDuration) | OpCountBased(Long)) is recorded on the rotation event and audit row —Manualfor explicit calls today, the auto variants reserved for the v0.2.0 scheduler. NewKeyEvent.Rotated(newVersion, policy)journal event with circe codec so replays restorecurrentVersiondeterministically. The "old version stays verifiable/decryptable after rotation" contract fromdocs/ARCHITECTURE.md§3 is preserved without per-version material storage: the in-memory dev backend keys its deterministic MAC byKeyIdonly (so byte output is version-stable), and AWS KMS handles per-version material internally — the same CMK decrypts both pre- and post-rotation ciphertexts. AddedOperation.Rotateto the IAM allowlist enum;AuthorizingKeyServiceguards via the policy engine;AuditingKeyServicerecordsnewVersion=N policy=...;ActorBackedKeyService.rotateroutes through the actor mailbox for journal-serialized state changes;PostgresEventJournallearns the new event kind. On the wire:POST /v1/keys/{id}/rotate(request{policy?}, response fullManagedKeyDtowith the bumpedcurrentVersion). The CLI gainedaegis keys rotate --id <id> [--policy Manual|TimeBased:7days|OpCountBased:N].ManagedKeyDto(HTTP + CLI wire shapes) gained thecurrentVersionfield; existing JSON without the field decodes ascurrentVersion=1via the case-class default. - Compromise operator override across the whole stack (closes #9). New
compromise(id, reason, by)method onKeyService[F[_]]. Marks the key asCompromised; from this state every cryptographic operation — includingverify— refuses withKmsError(IllegalOperation, ...). (Note:verifywas previously permitted on any state; this PR tightens it to refuseCompromisedandDestroyed, matching the lock-down semantics described indocs/ARCHITECTURE.md§3.) Compromise is one-way: from{PreActive, Active, Deactivated}→Compromised;Destroyedkeys cannot be compromised. The mandatoryreasonis a non-empty human-readable justification (e.g. "discovered in S3 audit leak 2026-05-08") and ends up on the audit row atseverity=Critical. AddedOperation.Compromiseto the IAM allowlist enum and a newKeyEvent.Compromisedjournal event with circe codec so the journal replays the state transition deterministically. The state-mutating call routes throughKeyOpsActorso the journal append + state transition are serialized with the rest of the lifecycle. On the wire:POST /v1/keys/{id}/compromise(request:{reason}, response: fullManagedKeyDto); blank reasons are rejected with 400InvalidField. The CLI gainedaegis keys compromise --id <id> --reason "<text>". - Wrap / unwrap across the whole stack (closes #7). New
wrap(id, dek, by)andunwrap(id, wrappedDek, by)methods onKeyService[F[_]]for KMIP-style envelope encryption, withOperation.Wrap/Operation.Unwrapadded to the IAM allowlist enum and a newWrappedDekvalue type. TheRootOfTrustSPI gainedwrap/unwrapDek;AwsKmsRootOfTrustimplements them by delegating to the existing AWS KMSEncrypt/Decryptcalls with an emptyEncryptionContext(AWS doesn't expose separate Wrap/Unwrap APIs for symmetric CMKs — this is the conventional wire-up). On the wire:POST /v1/keys/{id}/wrap(request:{dekBase64}, response:{wrappedDekBase64}) andPOST /v1/keys/{id}/unwrap(request:{wrappedDekBase64}, response:{dekBase64}). The CLI gainedaegis keys wrap --id <id> --dek <text|@file>andaegis keys unwrap --id <id> --wrapped <b64>. Same state-gate as encrypt/decrypt: wrap requiresActive; unwrap is permitted onActive+Deactivatedso historical wrapped DEKs remain recoverable across rotations, refused onCompromised/Destroyed. TheAuditingKeyServicedecorator recordsdekLen(not the bytes) so audit logs show what was protected without leaking key material. - Encrypt / decrypt across the whole stack (closes #6). New
encrypt(id, plaintext, context, by)anddecrypt(id, ciphertext, context, by)methods onKeyService[F[_]], withOperation.Encrypt/Operation.Decryptadded to the IAM allowlist enum and a newCiphertextvalue type. Encryption context (theMap[String, String]AAD) is carried as a separate parameter — not embedded in the ciphertext — so the same context must be supplied to both sides, mirroring AWS KMS semantics. A context mismatch on decrypt returnsKmsError(CryptographicFailure, ...). TheRootOfTrustSPI gained the same operations;AwsKmsRootOfTrustimplements them via the AWS KMSEncrypt/DecryptAPIs withEncryptionContextplumbed throughAwsKmsPort. On the wire:POST /v1/keys/{id}/encrypt(request:{plaintextBase64, context}, response:{ciphertextBase64, context}) andPOST /v1/keys/{id}/decrypt(request:{ciphertextBase64, context}, response:{plaintextBase64, context}). The CLI gainedaegis keys encrypt --id <id> --plaintext <text|@file> [--context k=v,k2=v2]andaegis keys decrypt --id <id> --ciphertext <b64> [--context k=v,k2=v2]. The in-memoryKeyServiceuses a deterministic HMAC-keyed XOR-keystream layout (HMAC(id, ctx) || pt XOR keystream(id, ctx)) so the dev REST surface has a working round-trip without a real KMS. Encrypt requires the key to be inKeyState.Active; decrypt is permitted onActiveandDeactivatedkeys (so existing ciphertexts remain readable after a future rotation lands), but refused onCompromised/Destroyed. TheAuditingKeyServicedecorator records the context keys (not values) and the plaintext length on success, so audit logs surface what was protected without leaking the AAD's payload.
Documentation¶
- README accuracy pass. Each section that described future capabilities is now explicitly
marked 🚧 WIP (status column in tables, design-preview callouts above example/demo transcripts).
The "Modules" table now lists per-module v0.1.0 status. The library-embedding example was rewritten
to actually compile (the previous version used
KeyService.inMemory[IO]which doesn't typecheck —KeyService.inMemoryreturnsIO[KeyService[IO]]). Added a callout under "Docker Compose quickstart" telling users how to build the image locally before v0.1.0 hits GHCR.
0.1.0 — 2026-04-29¶
The first tagged release. Pre-alpha — interfaces will change before 1.0.
What ships¶
Library tier (no Pekko, embeddable in any JVM app):
aegis-core—KeyService[F[_]]algebra, typed domain ADTs (Principal,KeyId,KeySpec,OperationResult,KeyEvent), in-memory reference implementation, circe codecs forKeyEvent.aegis-iam—RoleBasedPolicyEngine(allowlist with recursive parent-check that blocks agent-scope escalation),AuthorizingKeyServicedecorator, JWT bearer auth (JwtVerifier/JwtIssuer— HMAC-SHA256),PrincipalResolverSPI (dev / jwt).aegis-audit—AuditingKeyServicedecorator that writes oneAuditRecordper call (including denied/failed),InMemoryAuditSinkandStdoutAuditSinkreference impls.aegis-persistence—EventJournalSPI with two implementations:InMemoryEventJournal(dev) andPostgresEventJournal(Doobie/Hikari) with idempotent schema bootstrap.aegis-crypto—RootOfTrustSPI plusAwsKmsRootOfTrustadapter for layered-mode deployments fronting an existing AWS KMS CMK.aegis-sdk-scala/aegis-sdk-java— skeleton clients (REST surface; further polish in 0.2.0).
Server tier (Pekko-based):
aegis-http— Tapir + pekko-http REST endpoints forPOST/GET/POST-activate/DELETE /v1/keys.aegis-server— boot wiring tying it all together: REST routes → audit fan-out (StdoutAuditSink + W1 anomaly detector) → authorization → PekkoKeyOpsActor(single-actor key state) → durableEventJournal. Configurable journal (in-memory|postgres) and auth (dev|hmac) via HOCON.aegis-agent-ai— W1 anomaly detector MVP (BaselineDetectorwith scope + rate-spike heuristics),AgentRecommendationevents,RecommendationSinkSPI + in-memory impl,TappedAuditSink.aegis-cli—aegisadmin CLI withversion,login,keys create/get/activate/destroy. Stubs printing "not yet wired up" foragent issue,audit tail,advisor scan(back-ends in 0.2.0).
Operator-facing knobs¶
aegis.persistence.journal.kind—"in-memory"(default) or"postgres"(env:AEGIS_JOURNAL_KIND).aegis.persistence.journal.postgres.{jdbc-url, username, password, pool-size}— env-overridable.aegis.auth.kind—"dev"(default) or"hmac".aegis.auth.hmac.secret— required whenkind=hmac; ≥32 bytes (env:AEGIS_AUTH_HMAC_SECRET).aegis.http.{host, port}— env-overridable.
Distribution¶
- Docker image:
ghcr.io/sharma-bhaskar/aegis-server:0.1.0. - Library jars:
dev.aegiskms:aegis-{core,iam,audit,crypto,persistence,sdk-scala,sdk-java}:0.1.0on Maven Central. - CLI tarball: attached to the GitHub Release for v0.1.0.
Known limitations (deferred)¶
- No live OIDC / JWKS verification. v0.1.0 ships HS256 only — operators issue self-signed tokens to themselves. RSA / ES256 + JWKS rotation are scoped for v0.2.0.
- No agent-token issuance HTTP endpoint.
aegis agent issuein the CLI prints a clear "not yet wired up" message; the trait (JwtIssuer) is in place. Endpoint lands in v0.2.0 (PR A1). - No MCP server, no KMIP server. Module skeletons exist in
aegis-mcp-serverandaegis-kmipso they can land additively in v0.2.0+. aegis-serverPostgres path leaks the connection pool until JVM exit. A properResource[IO, Unit]boot scope is on the F1.b follow-up.- GCP / Azure / Vault / PKCS#11 root-of-trust adapters are not yet shipped. AWS KMS only.
- Audit fan-out to Postgres / Kafka / SIEM webhooks is not yet shipped. Stdout sink only.
- Risk scorer (W2), auto-responder (W3), LLM advisor (W4) are not yet shipped. The W1 anomaly detector
emits
AgentRecommendationevents; consuming them is manual. - No Helm chart yet.
deploy/helm/aegis-kms/is a placeholder;deploy/docker/docker-compose.ymlbrings the server up against a local Postgres for hands-on testing.
Repository scaffolding (already in main before this release)¶
- sbt multi-project layout, Apache-2.0 license, CI workflow (
ci.yml), contribution and security policies, scalafmt + scalafix configured. apply-pr-backlog.shfor splitting working-tree changes into one commit per PR.