KIZARD (KMIP Wizard Lizard) is a learning tool for understanding how a KMIP (Key Management Interoperability Protocol) client integrates with Thales CipherTrust Manager acting as a KMIP server.
It is a web-based KMIP client built on top of PyKMIP that exposes common key-management operations through a REST API and a browser UI.
Status: the symmetric-key lifecycle is implemented end-to-end — locate, create, register (import), get, get-attributes, activate, rotate (rekey), revoke/deactivate, destroy, and encrypt/decrypt (AES/3DES/DES, CBC, PKCS5) — exposed over both the REST API and the UI. The whole KMIP connection is configured at runtime through the browser, so the app comes up before any CipherTrust details are known (
.envremains an optional way to pre-seed them). The UI walks through a guided, ordered setup:
- Reach the server — a pre-flight probe checks DNS, TCP, and the TLS handshake against the KMIP endpoint before any client material exists.
- Confirm the CA — review the CA that issued CipherTrust's certificate and approve it, supplying the full trust chain (Issuing CA + Root CA).
- Provide a client certificate — upload an existing mTLS identity, or generate a key + CSR in the browser (RSA 2048/4096 or ECDSA P-256) to be signed by your CA.
- Verify the connection — a live KMIP
Locateround-trip confirms the server accepts the client certificate and speaks KMIP end-to-end; the key pane unlocks only once this passes.The key pane names every object (auto-generating
Kizard-Key-…when none is given), flags objects a rotation has replaced, and greys out any action the key's current state would reject. Encrypt/Decrypt (AES/3DES/DES · CBC · PKCS5) runs client-side by default — KIZARD fetches the key with a KMIPGetand ciphers locally — or, with a checkbox, server-side via the KMIPEncrypt/Decryptoperations so the key never leaves CipherTrust. Each key uses a fixed IV, created once and reused for every operation.
┌──────────────┐ HTTPS/REST ┌──────────────────┐ KMIP/mTLS ┌──────────────────────┐
│ React (UI) │ ───────────────────> │ FastAPI backend │ ───────────────────> │ CipherTrust Manager │
│ frontend/ │ │ backend/ │ (PyKMIP client) │ (KMIP server) │
└──────────────┘ └──────────────────┘ └──────────────────────┘
- frontend/ — React single-page app (Vite). Calls the backend REST API.
- backend/ — Python FastAPI service that wraps PyKMIP's
ProxyKmipClientand exposes key operations as JSON endpoints. - KMIP server — CipherTrust Manager (or any KMIP 1.x/2.x server) reached over a mutually-authenticated TLS connection.
KIZARD/
├── backend/ FastAPI service wrapping PyKMIP
│ ├── app/
│ │ ├── main.py App entrypoint / FastAPI instance
│ │ ├── core/ Settings & configuration
│ │ ├── kmip/ PyKMIP client wrapper
│ │ ├── models/ Pydantic request/response schemas
│ │ └── api/routes/ REST endpoints (health, keys, ...)
│ ├── tests/
│ ├── requirements.txt
│ └── .env.example
├── frontend/ React (Vite) single-page app
│ └── src/
├── docs/ Architecture & design notes
├── LICENSE Apache License 2.0
└── README.md
- Python 3.10–3.13 (see Compatibility notes).
- Node.js 18+ and npm.
- git.
- Network access to a KMIP server (e.g. CipherTrust Manager) and a client certificate / private key trusted by that server.
- The host installer is a Bash script, so on Windows use Git Bash or WSL when running the installation commands below.
Use this path when you want to run KIZARD directly on a workstation or server instead of deploying it in containers.
Run these checks before installing:
git --version
python3 --version
node --version
npm --version
python3 -m venv --helpThe installer expects:
gitavailable- Python 3.10 or newer (3.10–3.13 recommended)
- Node.js 18 or newer
npmavailablevenvsupport available for Python
If any command fails, install the missing package(s) first and re-run the checks.
On Ubuntu/Debian:
sudo apt update
sudo apt install -y git python3 python3-venv python3-pip nodejs npmOn RHEL/Fedora/Rocky/Alma:
sudo dnf install -y git python3 python3-venv python3-pip nodejs npmOn openSUSE:
sudo zypper install -y git python3 python3-venv python3-pip nodejs npmIf your distribution uses yum instead of dnf, replace dnf with yum.
From a fresh clone, run the host installer. It checks for the runtime requirements, installs any missing system packages where supported, creates the backend virtual environment, installs frontend dependencies, and starts the app:
git clone <repo-url>
cd KIZARD
bash scripts/host-deploy.shUseful variations:
bash scripts/host-deploy.sh --nostart # install/setup only, do not start the app
scripts/host-manage.sh start # start the app later
scripts/host-manage.sh stop # stop the app
scripts/host-manage.sh status # check whether it is running
scripts/host-undeploy.sh # uninstall the host installThe installer is safe to re-run. On first use it creates backend/.env from the
example template; edit it with your KMIP host and TLS material before starting
the app.
- Python 3.10–3.13 are supported. PyKMIP 0.10 calls the module-level
ssl.wrap_socket(), which was removed in Python 3.12, so on 3.12+ the TLS connection would otherwise raiseAttributeError: module 'ssl' has no attribute 'wrap_socket'. The backend reinstalls a faithfulSSLContext-basedssl.wrap_socket(seeapp/core/ssl_compat.py, applied on import of the KMIP client) — preserving mutual-TLS and CA verification — so no Python-version pin is needed. The shim is a no-op on 3.10/3.11, which still ship the original function. - Use KMIP 1.4 with CipherTrust Manager. PyKMIP 0.10 cannot parse
CipherTrust's KMIP 2.0 responses (the response header fails to decode), so set
KMIP_VERSION=1.4. Versions 1.0–1.4 all work; 2.0 does not. - CipherTrust client-cert mapping. The KMIP interface authenticates the client by certificate. The cert's identity (e.g. its CN) must map to a CipherTrust user, and that user needs auto-registration enabled — otherwise the server completes the TLS handshake but closes every KMIP request.
The host-based installer covers the same steps as the manual setup below, but with a single command. The script checks for the runtime dependencies (git, Python ≥ 3.10, Node.js ≥ 18, npm), installs any that are missing via the system package manager, and sets up the backend virtualenv and frontend packages:
scripts/host-deploy.sh # install deps, set up venv/node_modules, then start the app
# backend :8000, UI :5173 (safe to re-run)
scripts/host-deploy.sh --nostart # ...or install/set up without starting
scripts/host-manage.sh stop # stop the app
scripts/host-manage.sh start # start it again later
scripts/host-manage.sh reset # clear the KMIP config so the guided setup starts over
scripts/host-manage.sh status # is it running? (also: logs [backend|frontend] [-f])
scripts/host-undeploy.sh # uninstall (removes venv/node_modules)These mirror the Kubernetes scripts (k8s-deploy.sh / k8s-manage.sh /
k8s-undeploy.sh) — see Running on Kubernetes.
On the first run backend/.env is seeded from the template — edit it with your
KMIP host and TLS material (under backend/certs/), then scripts/host-manage.sh start to connect to your KMIP server.
Supported package managers: apt, dnf, yum, zypper, pacman. To run the pieces by hand instead, follow the steps below.
cd backend
python -m venv .venv
# Windows: .venv\Scripts\activate
# Linux/Mac: source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # then edit .env with your KMIP server details
uvicorn app.main:app --reloadThe API listens on port 8000, with interactive docs at /docs — e.g.
http://<kizard-host>:8000/docs, or http://localhost:8000/docs when you are
on the machine itself.
cd frontend
npm install
npm run devThe UI listens on port 5173 and proxies API calls to the backend. Reach it at
http://<kizard-host>:5173, where <kizard-host> is the machine you installed
KIZARD on — localhost only from that machine itself.
CipherTrust Manager exposes a KMIP interface (default port 5696) that requires
mutual TLS. To connect, you will need:
- The CipherTrust Manager KMIP interface hostname and port.
- A KMIP client certificate and its private key (or generate a CSR in the UI and have your CA sign it).
- The CA chain that signed the server certificate (Issuing CA + Root CA).
The recommended path is the guided flow in the browser. Open the UI and work through the setup steps — reach the server, confirm the CA, provide the client certificate, then verify — each of which is described inline. Nothing needs to be known ahead of time; the app starts with no configuration.
Alternatively, pre-seed any of these in backend/.env (see
backend/.env.example) so the app comes up already configured — useful for a
repeatable demo. Values set in the UI at runtime take precedence.
KIZARD remembers what the guided flow configured — the server target, the CA
approval, and the mTLS material — under backend/data/. To walk the four setup
steps again from scratch, clear that state. Two equivalent ways:
- In the browser — the Start over button below the setup steps
(
DELETE /api/connection). Takes effect immediately, no restart. - On the command line —
scripts/host-manage.sh reset, which also restarts the backend if it is running:
scripts/host-manage.sh reset # prompts, then clears and restarts the backend
scripts/host-manage.sh reset --yes # no prompt
scripts/host-manage.sh reset --all # also forget each key's fixed IV (old ciphertext
# stops decrypting; keys on the server are untouched)Either way all four steps go back to un-done and the flow restarts at "Reach the server". Keys already created on CipherTrust are untouched — they live on the server, not in KIZARD.
A reset deliberately ignores anything pre-seeded in backend/.env, so a student
never skips a step: the server target is left explicitly blank rather than
re-seeded from KMIP_HOST, and client material at KMIP_CERT_PATH &c. stops
counting as configured. Those certificate files are yours, so they are
disregarded rather than deleted — nothing under backend/certs/ is removed. The
marker that suppresses them lives at backend/data/certs/ignore-env-material and
is dropped automatically the moment you supply a certificate through the UI.
The install — virtualenv, node modules, backend/.env — is left alone. To
uninstall instead, use scripts/host-undeploy.sh.
KIZARD can also run as two containers — the backend (FastAPI/uvicorn) and the
frontend (the React build served by nginx) — on a Rancher RKE2 cluster.
Each runs in its own Deployment, Pod, and Service; the frontend's nginx serves the
SPA and reverse-proxies /api to the backend Service, so the browser stays
same-origin (the production stand-in for the dev-only Vite proxy).
Browser ──HTTP──▶ ingress-nginx (host: kizard.test256.io)
└─▶ frontend Service :80 ── nginx ──┬─ / static SPA
└─ /api ─▶ backend Service :8000
└─ KMIP/mTLS ─▶ CipherTrust :5696
backend/Dockerfile,frontend/Dockerfile(+frontend/nginx.conf) — the two images.k8s/— manifests:namespace,configmap(KMIP settings),pvc(runtime state),backend,frontend,ingress, andsecret.example.yaml(optional pre-seeded mTLS material).scripts/k8s-deploy.sh/scripts/k8s-undeploy.sh— build, import, deploy, tear down.scripts/k8s-manage.sh— the cluster counterpart ofhost-manage.sh(start/stop/reset/status/logs) for a running deployment.
Run the deploy on an RKE2 node. Set up two things there first:
-
kubectlthat reaches the cluster. The script usesKUBECONFIGif set, else~/.kube/config, else the root-only/etc/rancher/rke2/rke2.yaml. Give your user a readable copy once:mkdir -p ~/.kube sudo cp /etc/rancher/rke2/rke2.yaml ~/.kube/config sudo chown "$(id -u):$(id -g)" ~/.kube/config export PATH="$PATH:/var/lib/rancher/rke2/bin" # so kubectl is found kubectl get nodes # verify
-
A build tool —
dockerornerdctl— with its daemon running. On a fresh node, install Docker; note it ships its owncontainerd(separate from RKE2's), which must be up:curl -fsSL https://get.docker.com | sudo sh sudo usermod -aG docker "$USER" && newgrp docker # use docker without sudo sudo systemctl enable --now containerd docker docker ps # verify
If Docker won't start with
connect: connection refusedon/run/containerd/containerd.sock, that socket was left behind as a stale directory:sudo rm -rf /run/containerd/containerd.sock, thensudo systemctl restart containerd docker.
Single-node cluster — images go into the local containerd:
scripts/k8s-deploy.sh --host <fqdn>Multi-node cluster — push to a registry so every node can pull (one-time registry setup below):
scripts/k8s-deploy.sh --host <fqdn> --registry <registry-host>:5000Other flags: --namespace NS (default kizard), --tag TAG (default latest),
--skip-build (re-apply manifests only). The script installs
local-path-provisioner as the default StorageClass if the cluster has none,
applies the manifests, and waits for both rollouts.
<fqdn> is just the ingress vhost — it can be the node's own name
(e.g. kube.test256.io) or a dedicated name. Point DNS (or /etc/hosts on the
browsing machine) for <fqdn> at an ingress node IP, then open http://<fqdn>/
and walk the guided setup. Verify from the node with:
kubectl -n kizard get pods # both Running
curl -s http://<fqdn>/api/health # {"status":"ok",...}Runtime state (server target, CA approval, per-key fixed IVs, UI-uploaded mTLS
material) lives on a PersistentVolume mounted at /app/data, so it survives pod
restarts. KMIP configuration is done through the browser by default; the
kizard-config ConfigMap can pre-seed KMIP_HOST/KMIP_VERSION, and
k8s/secret.example.yaml shows how to inject a client certificate instead. The
backend runs a single replica (it caches the KMIP target and IV map in memory
over a ReadWriteOnce volume).
Without --registry, the script imports the freshly-built images into only the
node it runs on. That is fine for a single-node cluster, but on a multi-node
cluster pods scheduled on other nodes fail with ImagePullBackOff. Use a registry:
scripts/k8s-deploy.sh --host <fqdn> --registry <registry-host:5000>This pushes the images and sets the Deployments to pull them cluster-wide
(imagePullPolicy: Always). One-time cluster setup for a plain-HTTP lab registry:
# 1. Run a registry (e.g. on the build node) and let docker push to it:
docker run -d --restart=always --name registry -p 5000:5000 \
-v /opt/registry:/var/lib/registry registry:2
echo '{ "insecure-registries": ["<registry-host>:5000"] }' | sudo tee /etc/docker/daemon.json
sudo systemctl restart docker
# 2. Let every RKE2 node's containerd pull over HTTP, then restart RKE2 on each:
# /etc/rancher/rke2/registries.yaml —
# mirrors:
# "<registry-host>:5000":
# endpoint: ["http://<registry-host>:5000"]
sudo systemctl restart rke2-server # control-plane node(s)
sudo systemctl restart rke2-agent # worker nodes(Alternatively, skip the registry and ctr images import the same image tar on
every node.)
Symptom: KIZARD reaches your KMIP server by IP address but not by name —
cannot resolve 'cm-kirk.example.lab': [Errno -2] Name or service not known —
even though every node resolves that name correctly.
Why: pods do not use the node's resolver. They query CoreDNS, which ships with
forward . /etc/resolv.conf
and so load-balances across every upstream the node lists. A typical node lists
the LAN resolver plus public fallbacks (1.1.1.1, 8.8.4.4). The public ones have
never heard of your private zone, so they answer NXDOMAIN — a perfectly valid
negative answer, which CoreDNS returns as-is rather than retrying another upstream.
Lookups fail whenever the query lands on a public resolver, and CoreDNS's negative
cache then makes the failure stick.
Fix: forward the private zone to the LAN resolver that actually knows it. On an
RKE2 control-plane node, drop a HelmChartConfig into the auto-applying manifests
directory so the change survives RKE2 upgrades:
# /var/lib/rancher/rke2/server/manifests/rke2-coredns-config.yaml
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
name: rke2-coredns
namespace: kube-system
spec:
valuesContent: |-
servers:
- zones:
- zone: .
port: 53
plugins:
- name: errors
- name: health
configBlock: |-
lameduck 10s
- name: ready
- name: kubernetes
parameters: in-addr.arpa ip6.arpa # chart adds the cluster domain
configBlock: |-
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
- name: prometheus
parameters: 0.0.0.0:9153
- name: forward
parameters: . /etc/resolv.conf
- name: cache
parameters: 30
- name: loop
- name: reload
- name: loadbalance
- zones:
- zone: example.lab # your private zone
port: 53
plugins:
- name: errors
- name: cache
parameters: 30
- name: forward
parameters: . 192.168.1.1 # your LAN DNS resolver
- name: reloadSubstitute your own zone and resolver — 192.168.1.1 is only this lab's router;
find yours with resolvectl status | grep 'DNS Servers' on a node.
Two things to watch:
- Overriding
serversreplaces the whole Corefile, so the first block reproduces the RKE2 default verbatim. Compare it against your cluster's current Corefile (kubectl -n kube-system get cm rke2-coredns-rke2-coredns -o jsonpath='{.data.Corefile}') before applying, in case your RKE2 version ships different defaults. - Do not repeat
cluster.localin thekubernetesplugin'sparameters— the chart injects the cluster domain itself, and passing it again duplicates the zone.
RKE2 reconciles within ~10s and restarts the CoreDNS pods. Verify:
kubectl -n kube-system get cm rke2-coredns-rke2-coredns -o jsonpath='{.data.Corefile}'
kubectl -n kube-system wait --for=condition=ready pod -l k8s-app=kube-dns --timeout=120s
kubectl run dnstest --rm -it --restart=Never --image=busybox:1.36 -- \
nslookup cm-kirk.example.labk8s-deploy.sh probes --host against cluster DNS on every run and warns if the
name does not resolve, since that is the same failure KIZARD will hit at KMIP time.
scripts/k8s-manage.sh is the Kubernetes counterpart of host-manage.sh — same
subcommands, but driving the cluster via kubectl instead of host processes:
scripts/k8s-manage.sh start # scale backend + frontend up (replicas=1)
scripts/k8s-manage.sh stop # scale down to 0 — keeps the PVC and config
scripts/k8s-manage.sh reset # clear the guided-setup config (DELETE /api/connection)
scripts/k8s-manage.sh reset --all # ...and forget each key's fixed IV, then restart
scripts/k8s-manage.sh status # deployments, pods, PVC, ingress
scripts/k8s-manage.sh logs backend -f # tail a component's logsreset calls the app's own "Start over" endpoint in-process (no restart, same as
the browser button); --all additionally clears the per-key IVs and restarts the
backend to drop its in-memory cache.
scripts/k8s-undeploy.sh # remove workloads, keep the data PVC
scripts/k8s-undeploy.sh --purge # also delete the PVC and namespace (destroys state)Licensed under the Apache License 2.0. PyKMIP is also distributed under the Apache License 2.0.