- Parse and validate from snapshot JSON, reject future/missing timestamps - Enforce snapshot age vs in cache and stale fallback (no lifetime extension on repeat S3 reads) - OCSP: , , - Remove unused from signer - Publisher heartbeats: , republish clean snapshots past interval, preserve dirty-only behavior and concurrency - Config/env/docker/docs updates for heartbeat Rolling upgrade: enable publisher heartbeats before deploying the age-enforcing responder so existing snapshots remain within the age window.
14 KiB
KCA — web PKI management
KCA is a browser-based reimplementation of the software-key workflows of XCA: private keys, PKCS#10 requests, X.509 certificates, certificate revocation lists and reusable templates, organised in private vaults (each vault is the web equivalent of one XCA database).
server/— Python + kaya backend: OIDC login, Redis sessions, personal API tokens, Postgres (Tortoise ORM + aerich migrations), AES-256-GCM private-key encryption, and a complete REST API. The pure PKI domain logic lives inpackages/kca-core; the deployable app ispackages/kca-app.web/— Rust + Sycamore compiled to WebAssembly by Trunk, styled with Bootstrap 5.3 (vendored underweb/vendor/) and a dark PKI-console theme.
Run the whole stack
docker compose up --build
Postgres, Redis, a mock OIDC provider (test users alice, bob, carol,
dave; any username works), the database migrator and the app all start
together. The app listens on http://127.0.0.1:8080.
The browser and the app both use the OIDC issuer
http://mockoauth:8180/kca, so add a host entry once:
echo "127.0.0.1 mockoauth" | sudo tee -a /etc/hosts
Then open http://localhost:8080, click Sign in with default, and type
alice (no password). The SPA and API are served from the same origin; the
session cookie authenticates both.
For a real deployment set at least:
# 32 random bytes, base64 or hex:
KCA_MASTER_KEY="$(openssl rand -base64 32)"
# Point these at your IdP and public callback URL:
OIDC_0_ISSUER=https://idp.example.com
OIDC_0_CLIENT_ID=kca
OIDC_0_CLIENT_SECRET=...
OIDC_0_REDIRECT_URI=https://kca.example.com/auth/default/callback
server/.env.example documents every setting.
What it does
Vaults
Every user owns a private set of vaults. A vault contains keys, certificates,
requests, CRLs and templates; non-owners get 404. Vaults can be renamed,
deleted and configured from Settings (default hash, serial length,
explicit/mandatory DN entries, default key type/size, default validity).
Keys
- Generate RSA (1024–8192), DSA (1024–3072), EC (P-192/P-224/P-256/P-384/P-521, secp256k1, brainpool) and Ed25519 keys.
- Import PEM/DER public or private keys, PKCS#8, OpenSSH, PKCS#12 and PVK.
- Export PEM public/private, traditional encrypted PEM, PKCS#8 (plain or encrypted), DER public/private, SSH2 public/private, JWK public/private and PVK.
- Private keys are stored encrypted with AES-256-GCM under
KCA_MASTER_KEY(AAD bound to the key row id). They are never included in normal API responses; only an explicit export decrypts them.
Requests and certificates
- Create PKCS#10 requests with subject, key, extensions and attributes (challenge password, unstructured name); verify imported requests.
- Issue self-signed certificates or certificates signed by a CA in the same vault, from scratch or from a request. Imported CSRs are marked signed once a certificate is issued from them.
- The certificate wizard covers subject entries in the vault's explicit DN order, additional subject attributes, mandatory DN enforcement, validity, basic constraints, key usage, extended key usage, SAN/IAN, subject/authority key identifiers, CRL distribution points, authority information access, name constraints, certificate policies, policy constraints, inhibit-any-policy, OCSP no-check, TLS feature (OCSP stapling), Netscape legacy extensions and raw OID/DER extensions. The SPA exposes typed controls for the common and advanced extensions plus an advanced JSON editor for raw/less common extensions, and a server-side Validate extensions action. OpenSSL config text is available on export.
- Certificate details are split into Overview, Extensions, Validation, Export, Metadata, Revoke, Transform and CA tabs, with hash deep links. They show subject/issuer, serial, validity, signatures, MD5/SHA-1/SHA-256 fingerprints, parsed extensions, OpenSSL validation errors, purposes and the issuer chain.
- Renew with a new or kept serial, optionally revoke/replace the old certificate. Revoke with a reason; manage the CA's revocation list manually, including revocation dates and invalidity dates. Certificate lists support bulk revocation with a shared reason and invalidity date. CA properties (CRL days and default signing template, including built-ins) are editable from the CA detail page, and the wizard offers the CA default template explicitly without overwriting edits.
- Absolute validity dates and XCA's "no well-defined expiration" mode are supported alongside duration-based validity.
- Transform certificates to a public key, a CSR or a template.
- Export PEM, PEM chain, PEM+key, PEM+encrypted PKCS#8, PKCS#7, PKCS#7 chain, DER, PKCS#12, PKCS#12 chain, OpenVPN, OpenSSL config, certificate index, vCalendar, JWK/JWK chain and the CA's OpenVPN tls-auth key.
CRLs
Generate a CRL from a CA's revocation entries with configurable hash, dates, reason inclusion, AKI and issuer alternative name. Imported CRLs are linked to their CA and their serials are merged into its revocation list. Export PEM, DER or vCalendar.
Templates
Built-in CA, TLS server and TLS client templates mirror XCA's presets. User templates store subject defaults, extension defaults and a relative validity range. XCA's binary template format is supported for import and export, and templates are applied to a certificate/request spec server-side with Apply all, Subject only, and Extensions + validity modes.
Import and migration
- Paste PEM or upload files: PEM bundles, DER certificates/requests/CRLs/keys, PKCS#7, PKCS#12, SSH keys and XCA templates.
- Preview imports with per-item selection before committing; duplicates are skipped by default.
- Dump a whole vault as a ZIP (
POST /api/vaults/{id}/export): manifest, certificate/request/CRL PEM, public keys and optionally encrypted private keys. - Import XCA SQLite
.xdbdatabases (schema 7/8): software keys, certificates, requests, CRLs, revocations, authorities and templates. PKCS#11 token items are skipped, and private keys encrypted with individual passwords are only imported when the supplied database password decrypts them.
Search and API tokens
Global search covers internal names, comments, subjects, issuers, serials,
fingerprints and extension text. Personal API tokens (Authorization: Bearer kca_…) let scripts use the REST API; tokens act as their owner, are stored
only as SHA-256 digests, and cannot mint/revoke tokens.
OCSP responder
KCA can publish full per-CA certificate-status snapshots to any S3-compatible
bucket and ships a standalone Rust/Axum responder on port 2560.
- KCA publishes
ocsp/<KCA CA UUID>.jsonwhenever certificates are issued, renewed, revoked, deleted or imported. Revocation reasons are included; this is a full status snapshot, not a revoked-only list. - The responder has no Postgres/Redis/KCA master-key access. It only reads the configured S3 prefix and uses a delegated OCSP signing certificate/key per CA.
- Each CA snapshot is cached for one second with a single-flight refresh, so
concurrent requests cause at most one S3
GetObjectper CA per second. - The publisher refreshes unchanged snapshots every 30 seconds by default. The
responder rejects snapshots older than its configured grace period even when
S3 is readable, and returns
tryLaterwhen no fresh snapshot is available. - OCSP
thisUpdatereflects the snapshot generation time andnextUpdatenever extends beyond the snapshot's hard expiry. - Gitea Actions publishes
gitea.woggioni.net/woggioni/kca-ocspon the samerelease/*tags as the server image. The responder image is a statically linked musl executable runningFROM scratchwith only the CA bundle.
Enable publishing in KCA with KCA_OCSP_S3_BUCKET and the other
KCA_OCSP_* variables in server/.env.example. Run the responder with a
configuration derived from ocsp-responder/responder.example.yaml, setting
each issuer's ca_id to its CA certificate UUID in KCA, or with Compose:
docker compose --profile ocsp up --build
See ocsp-responder/README.md for the JSON layout, signer requirements and
openssl ocsp verification.
REST API
OpenAPI JSON is at /api/openapi.json; Swagger UI at /api/docs.
| Area | Endpoints |
|---|---|
| Identity | GET /api/health, GET /api/me, GET /api/auth/providers, GET/POST/DELETE /api/tokens |
| Vaults | GET/POST /api/vaults, GET/PATCH/DELETE /api/vaults/{id}, GET/PATCH /api/vaults/{id}/settings, GET /api/vaults/{id}/summary, GET /api/vaults/{id}/options, GET /api/vaults/{id}/search, POST /api/vaults/{id}/export |
| Keys | GET/POST /api/vaults/{id}/keys, GET/PATCH/DELETE .../keys/{key_id}, POST .../keys/{key_id}/export |
| Requests | GET/POST /api/vaults/{id}/requests, GET/PATCH/DELETE .../{id}, POST .../{id}/export, POST .../{id}/transform/open-key, POST .../{id}/transform/template |
| Certificates | GET/POST /api/vaults/{id}/certificates, POST /api/vaults/{id}/certificates/preview, GET /api/vaults/{id}/certificates/{ca_id}/ocsp-status, GET/PATCH/DELETE .../{id}, POST .../{id}/export, POST .../{id}/renew, POST .../{id}/revoke, POST /api/vaults/{id}/certificates/bulk-revoke, GET/PATCH .../{id}/ca, GET/POST .../{id}/revocations, PATCH/DELETE .../revocations/{rev_id}, POST .../{id}/crl, POST .../{id}/tls-auth, transforms |
| CRLs | GET /api/vaults/{id}/crls, GET/DELETE .../{crl_id}, POST .../{crl_id}/export |
| Templates | GET/POST /api/vaults/{id}/templates, GET/PATCH/DELETE .../{template_id}, POST .../{template_id}/apply, POST .../{template_id}/export |
| Import | POST /api/vaults/{id}/import (dry_run, selected, xca_db) |
Errors are always {"error": "..."}. Mutating resources carry a version
for optimistic concurrency (409 on stale writes). Export endpoints return
JSON with content_base64 so the SPA can download or copy the bytes.
Development
Backend (from server/, using uv):
uv sync --package kca-app
uv run python -m unittest discover -s tests -t .
uv run python -m unittest discover -s packages/kca-core/tests
uv run mypy -p kca.core -p kca
Tests run in-process with in-memory sqlite, the in-memory session store and a fake OIDC user, exactly like the spendwise reference app.
Browser end-to-end tests (from the repository root, with the Compose stack up):
cd e2e
npm install
npx playwright install chromium
npm test
They use Playwright Chromium. Set KCA_BASE_URL to target another port, e.g.
KCA_BASE_URL=http://localhost:8083 npm test. See e2e/README.md.
OCSP responder (from ocsp-responder/):
cargo test
cargo run # uses OCSP_CONFIG or /etc/ocsp/responder.yaml
Frontend (from web/, with the backend running on :8080):
trunk serve # SPA on http://localhost:8000, /api and /auth proxied
cargo test
Migrations live in server/migrations/. After changing models.py:
DATABASE_URL=postgres://kca:password@localhost:5432/kca uv run aerich migrate --name describe_change
DATABASE_URL=postgres://kca:password@localhost:5432/kca uv run aerich upgrade
OpenTelemetry
OpenTelemetry is optional and disabled by default. The Docker image includes
the otel extra; for a non-image install run:
uv sync --package kca-app --extra otel
Enable export with:
OTEL_ENABLED=1
OTEL_SERVICE_NAME=kca
OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4318
# Optional comma-separated "Name=Value" pairs:
OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer token,X-Tenant=one"
The OTLP/HTTP endpoints are derived automatically (/v1/traces,
/v1/metrics, /v1/logs). KCA exports:
- HTTP server spans and metrics through
kaya-otel, with W3C trace-context propagation and health-check exclusion; - asyncpg and Redis client spans;
- the
kca.active_sessionsgauge plus process/GC system metrics; - application logs through the OTLP log exporter (OpenTelemetry's own SDK logs are filtered to avoid export loops);
session.id(truncated digest) anduser.idspan attributes for trace correlation.
OTEL_EXCLUDED_PATHS, OTEL_METRIC_EXPORT_INTERVAL_MILLIS and
OTEL_METRICS_POLL_INTERVAL_SECONDS are documented in server/.env.example.
Layout
server/
packages/kca-core/ # pure PKI domain (keys, names, extensions, certs, CSRs,
# CRLs, templates, imports/exports, .xdb, AES-GCM)
packages/kca-app/ # kaya app, config, telemetry, Tortoise models, routes, static
migrations/ # aerich schema history
tests/ # app integration suite
dev/mockoauth/ # dev OIDC provider image + test users
web/
src/pages/ # dashboard, vault, keys, requests, certificates,
# wizard, CRLs, templates, import, search, settings, tokens
src/components/ # nav, toast, shared UI
src/{api,auth,model,dom}.rs
pwa/ # manifest + app-shell assets
e2e/ # Playwright browser suite (Node)
ocsp-responder/ # standalone Rust/Axum OCSP responder
Limits
- Smart cards / PKCS#11 / HSMs are not supported. A browser app cannot use a local card reader; server-side PKCS#11 would require a separate HSM deployment and is deliberately out of scope.
- The desktop command-line interface is replaced by the REST API and personal access tokens.
- PKCS#12 export uses PBES2/AES via
cryptography; legacy RC2/3DES PKCS#12 output is not available. Legacy inputs are imported when the library supports them. - XCA's OpenSSL-config "Advanced" text editor is replaced by typed extension fields plus an advanced JSON editor carrying raw OID/DER and other less common extensions. OpenSSL config remains an export format.
- The PWA service worker is intentionally not registered: certificate and key data must never be served stale from a cache. The manifest and 192/512 icons remain available for installation.
- Column layouts, window geometry, language packs and other desktop-only XCA preferences are not part of the web UI.