Encryptoglyph is an open-source encryption-at-rest library and self-hosted service skeleton for managed service providers. It keeps field encryption, key custody, and encrypted backup in one place: an XChaCha20-Poly1305 AEAD envelope, an OpenBao custody service, a restic backup target, and an MSP console in front of them.
Komboe is the vault product that adopts this envelope. This repository is the shared crypto library and the deploy skeleton. It is licensed under the Apache License 2.0.
- XChaCha20-Poly1305 AEAD. New ciphertext uses a 192-bit nonce and a 256-bit key. The header carries a version, an algorithm id, and a key id. Legacy AES-256-GCM envelopes still decrypt, and
rewrapupgrades them. Rust (crates/encryptoglyph) and Python (api/eg_crypto) share the wire format and the vectors intestvectors/envelope.json. - Key custody. The compose file runs OpenBao on an internal network for Transit keys and KV. The library itself has no escrow key and no master override. Root keys are 32 bytes, derived with HKDF-SHA256 per tenant, purpose, and algorithm, or from a password with Argon2id. The console and
/v1sidecar do not call OpenBao. The custody design is in docs/plan.md and docs/SECURITY-HARDENING.md. - Encrypted backup.
restic/rest-serversits on the same internal network as the backup target. Host ports are not published. Backup scheduling and restore evidence are described in the plan and are not implemented in this tree. - Console.
console/is the operator UI, served by the API at/. It seals fields, rotates and shreds keys, and runs a deterministic assistant. OpenBao and restic stay unwired until a deployment supplies them. The compose service builds the same image as the API.
| Piece | In this repository |
|---|---|
| Rust envelope, HKDF, Argon2id, key rotation | Implemented and tested |
| Python module, wire-compatible with Rust | Implemented and tested |
| TypeScript wrapper over the WASM build | Source in bindings/ts; wasm-pack output is not committed |
eg-api container |
/healthz, the /v1 sidecar, and the console. Swagger, ReDoc, and /openapi.json are not served |
| OpenBao, Postgres 16, restic rest-server | Images and config in deploy/docker-compose.yml. The console reports them as not connected |
| Console | console/, served by the API. See INTEGRATION.md |
Traefik is external. Only eg-console and eg-api join proxy-net. OpenBao, Postgres, and the restic rest-server stay on the internal network (internal: true), which has no route off the Docker host. The API container is the only app process that joins both networks. The crypto libraries are not containers. The API image copies the console, eg_crypto, and the /v1 service.
flowchart TB
operator[MSP operator]
subgraph proxy [proxy-net]
traefik[Traefik TLS, external]
console["eg-console MSP console"]
api["eg-api console and /v1"]
end
subgraph internal [internal network, no host ports]
openbao[OpenBao key custody]
postgres[Postgres 16]
restic[restic rest-server]
end
subgraph crypto [shared XChaCha20-Poly1305 envelope]
rust[Rust crate]
python[Python eg_crypto]
ts[TypeScript WASM]
end
operator --> traefik
traefik --> console
traefik --> api
api --- openbao
api --- postgres
api --- restic
rust --- python
rust --- ts
python --- api
The lines from eg-api to OpenBao, Postgres, and rest-server are placement in the compose file, not live calls from the health handler.
The SVG is the same layout: docs/img/architecture.svg.
Docker Compose is the deploy entrypoint. Secrets stay in deploy/.env, which is gitignored. deploy/.env.example lists the names and leaves the values empty.
cp deploy/.env.example deploy/.env
docker network create proxy-net
docker compose -f deploy/docker-compose.yml configproxy-net must already exist because the compose file marks it external. docker compose config is the check CI runs. Do not commit .env.
docker compose up builds eg-api and eg-console from api/Dockerfile. OpenBao, Postgres, and rest-server still expect real values in .env before they will start. The console does not require them.
The API image is buildable on its own. It listens on port 8000 inside the container and, in the compose file, publishes no host port. This loopback publish is only for a local health check:
docker build -t encryptoglyph-api:local -f api/Dockerfile api
docker run --rm -p 127.0.0.1:8000:8000 \
-e BUILD_SHA="$(git rev-parse --short=12 HEAD)" \
encryptoglyph-api:localcurl -sS http://127.0.0.1:8000/healthzA healthy process returns {"status":"ok","build":"<12 hex chars>"}. /docs, /redoc, /openapi.json, and /swagger are not served. That is checked by scripts/assert-exposure.sh.
Without Docker, from api/ with Python 3.12. This serves /healthz, /v1, and the console at /:
python -m pip install -r requirements.txt
BUILD_SHA="$(git rev-parse --short=12 HEAD)" python -m uvicorn main:app --host 127.0.0.1 --port 8000Open http://127.0.0.1:8000. Adoption snippets are in INTEGRATION.md. The assistant uses deterministic commands with no model. Set OLLAMA_BASE_URL when a local Ollama should choose those commands. It never receives plaintext or keys.
The HTTP API in this repository is the health check above. Envelope encryption is the library surface, with the same record context in each language: tenant, purpose, table, record id, field, and schema version. Those fields are authenticated as additional data, so a ciphertext cannot be moved to another row, field, or tenant.
crates/encryptoglyph is the source of truth. unsafe is forbidden. Default writes are version 3, XChaCha20-Poly1305 with a key commitment.
use encryptoglyph::{decrypt, encrypt, Keyring, RecordContext, VERSION_V2};
fn seal_email() -> Result<Vec<u8>, encryptoglyph::Error> {
let key = Keyring::generate_key()?;
let kr = Keyring::new("k1", &key)?;
let ctx = RecordContext {
tenant: "acme".into(),
purpose: "field".into(),
table: "customers".into(),
record_id: "42".into(),
field: "email".into(),
schema_version: 1,
};
let blob = encrypt(&kr, &ctx, b"ada@example.com")?;
let plain = decrypt(&kr, &ctx, &blob, VERSION_V2)?;
Ok(plain.to_vec())
}cd crates/encryptoglyph
cargo test --lockedapi/eg_crypto matches the Rust wire format. Tests run it directly. The API container image does not copy this package.
From the api directory:
from eg_crypto import Keyring, RecordContext, encrypt, decrypt
key = Keyring.generate_key()
kr = Keyring({"k1": key}, "k1")
ctx = RecordContext(
tenant="acme",
purpose="field",
table="customers",
record_id="42",
field="email",
)
blob = encrypt(kr, ctx, b"ada@example.com")
assert decrypt(kr, ctx, blob) == b"ada@example.com"cd api
python -m pip install -r requirements-dev.txt
pytestbindings/ts is a thin wrapper around the WASM build of the Rust crate. Crypto stays in Rust. Build the package with wasm-pack, then compile the wrapper:
cd bindings/ts
npm run build:wasm
npm run buildimport { Keyring } from "@godesk/encryptoglyph";
const key = crypto.getRandomValues(new Uint8Array(32));
const kr = new Keyring("k1", key);
const blob = kr.encrypt(
{ tenant: "acme", purpose: "field", table: "customers", recordId: "42", field: "email" },
new TextEncoder().encode("ada@example.com"),
);The package is private and is not published to npm from this repository. bindings/ts/pkg is gitignored.
- Authenticated encryption. Version 2 is XChaCha20-Poly1305-IETF. The header (magic, version, algorithm, key id, nonce) is part of the additional data, together with the canonical record context. Flipping the version or moving the blob between records fails authentication.
- Key separation. A root key never encrypts plaintext directly. HKDF-SHA256 derives a data key from the root key, the purpose, the tenant, and the algorithm. Password root keys use Argon2id with a 16-byte salt.
- Rotation. A
Keyringencrypts with the current key id and can decrypt older ids.rewrapwrites a fresh version-2 blob. The current key cannot be retired. - Uniform failures. Decrypt errors do not distinguish a bad tag from a malformed header.
- No backdoor in the library. There is no escrow or override key in the Rust or Python code. Custody of root keys is an OpenBao operation. This console keeps session keys in process memory and says so.
- Network. Compose does not publish host ports. OpenBao's own UI is disabled. Postgres passwords come from the gitignored env file.
- Dependency licenses.
cargo denyallows Apache-2.0, MIT, BSD, ISC, MPL-2.0, Unicode, Zlib, and CC0. The check runs in CI.
Report vulnerabilities to security@komboe.com. The process is in SECURITY.md. Hardening notes and the Komboe adoption plan are in docs/SECURITY-HARDENING.md and docs/design/komboe-integration.md.
Encryptoglyph is open source under the Apache License, Version 2.0. See LICENSE and NOTICE. Copyright 2026 GoDesk.
Third-party components stay under their own licenses. See THIRD_PARTY_NOTICES.md.
Contributions are licensed under Apache-2.0. Read CONTRIBUTING.md and the Contributor Covenant before opening a pull request.
