QHx AI Chat Point-to-Point Tutorial
This tutorial provides a step-by-step worked example for deploying QHx and an application workload secured using it. The application is an AI chat client and AI inference server which communicate over a post-quantum secure QHx substrate.
Prepare the demo environment
Section titled “Prepare the demo environment”The manifests and terminal output below use QHx v0.6.1 demo artifacts.
Check compatibility with the QHx release in the selected cluster before
applying them; the versioned images and downloaded CLI are not automatically
matched to that installation. For installation of a selected release, follow
the installation guide.
You need Kubernetes administrative access, Bash, kubectl, and jq on the
administrative host. The cluster needs QHx and the SPIFFE CSI driver running,
registry credentials for the demo images, and enough resources to run Ollama.
The client setup downloads a CLI binary from the versioned demo URL and
installs operating-system and Python packages. The pods therefore need access
to those package services and the Ollama model download service. Confirm that
the CLI artifact matches the client node’s architecture before deploying.
Use nonsensitive data with the manifests as written: the example accepts broad SPIFFE identity patterns within trusted domains, and its notary database and model storage are ephemeral. Before adapting it for sensitive workloads, restrict peer identities and provide durable storage with an appropriate retention policy. The notary manifest also creates a cluster-wide pod-read role and binding; review that access and the resource names before applying it. Do not enable TLS key logging for sensitive traffic: its output exposes keys that can decrypt recorded connections.
Check QHx pod readiness before continuing. Pod names and container counts depend on the installed release and authority configuration:
kubectl get pods -n qhx-systemConfigure registry authentication
Section titled “Configure registry authentication”We will create a namespace called ai-chat for our purposes, and a namespace
called qhx-notary to host our notary.
kubectl create ns ai-chatkubectl create ns qhx-notaryWe now copy the M42 registry authentication secret from the qhx-system
namespace to our new namespaces:
for namespace in ai-chat qhx-notary; do kubectl get secret oci-secret -n qhx-system -o json | \ jq --arg namespace "$namespace" \ '{apiVersion, kind, type, data, metadata: {name: "oci-secret", namespace: $namespace}}' | \ kubectl apply -f -doneOnly copy credentials into namespaces authorized to use them. This command reads the Secret into a pipeline; protect the administrative session and its output as credential material.
Deploy the Notary
Section titled “Deploy the Notary”Deploy the QHx notary by pasting the below command:
kubectl apply -f - <<'END'---apiVersion: v1kind: ServiceAccountmetadata: name: notary-proxy namespace: qhx-notary---apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata: name: pod-readerrules: - apiGroups: [""] resources: ["pods"] verbs: ["get", "list", "watch"]---apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRoleBindingmetadata: name: notary-proxy-pod-readersubjects: - kind: ServiceAccount name: notary-proxy namespace: qhx-notaryroleRef: kind: ClusterRole name: pod-reader apiGroup: rbac.authorization.k8s.io---apiVersion: apps/v1kind: Deploymentmetadata: name: notary-proxy namespace: qhx-notary labels: app: notary-proxyspec: replicas: 1 selector: matchLabels: app: notary-proxy template: metadata: labels: app: notary-proxy spec: serviceAccountName: notary-proxy imagePullSecrets: - name: oci-secret containers: - name: qhx-proxy image: "oci.messier42.com/qhx/proxy:v0.6.1" imagePullPolicy: IfNotPresent ports: - containerPort: 8081 name: http env: - name: QHX_PROXY_CONFIG value: | spiffe: workload_socket_path: unix:///spiffe-workload-api/agent.sock notary: enable: true database: path: /tmp/notary.db listeners: - name: central address: "0.0.0.0:8081" protocol: http mode: central target: url: "https://ollama.ai-chat.svc.cluster.local:8081" spiffe_ids: - "spiffe://.*" middlewares: - type: notary-query - type: openai timeouts: read: 600s write: 600s idle: 600s volumeMounts: - name: spiffe-workload-api mountPath: /spiffe-workload-api readOnly: true resources: requests: memory: "64Mi" cpu: "100m" limits: memory: "256Mi" cpu: "500m" volumes: - name: workspace emptyDir: {} - name: spiffe-workload-api csi: driver: csi.spiffe.io readOnly: true---apiVersion: v1kind: Servicemetadata: name: notary-proxy namespace: qhx-notaryspec: selector: app: notary-proxy ports: - protocol: TCP port: 8081 targetPort: 8081 type: ClusterIPENDDeploy Example Workload
Section titled “Deploy Example Workload”Deploy the example workload by pasting the below command:
kubectl apply -f - <<'END'---apiVersion: v1kind: ServiceAccountmetadata: name: llm-client namespace: ai-chat---apiVersion: v1kind: ServiceAccountmetadata: name: ollama-server namespace: ai-chat---apiVersion: apps/v1kind: Deploymentmetadata: name: llm-client namespace: ai-chat labels: app: llm-clientspec: replicas: 1 selector: matchLabels: app: llm-client template: metadata: labels: app: llm-client spec: serviceAccountName: llm-client imagePullSecrets: - name: oci-secret containers: - name: llm-client image: "python:3.11-slim" command: ["/bin/sleep"] args: ["infinity"] env: - name: OLLAMA_HOST value: "http://localhost:8081" workingDir: /root lifecycle: postStart: exec: command: - /bin/bash - "-c" - | export DEBIAN_FRONTEND=noninteractive apt update && apt install -y curl wrk jq pip install llm && llm install llm-ollama cd /root curl -LO https://m42-prod-demo-artifacts.s3.us-west-2.amazonaws.com/v0.6.1/qhx mkdir -p /usr/local/bin cp -a ./qhx /usr/local/bin/ cat <<END > say-hello #!/bin/sh exec curl -i -X POST -H 'Content-Type: application/json' \ http://localhost:8081/v1/chat/completions \ -d "\$(cat hello.json)" END cat <<END > say-hello-notarized #!/bin/sh exec qhx curl -i -X POST -H 'Content-Type: application/json' \ http://localhost:8081/v1/chat/completions \ -d "\$(cat hello.json)" "\$@" END cat <<END > hello.json { "model": "llama3.2:1b", "messages": [ { "role": "user", "content": "Hello." } ] } END chmod +x qhx say-hello say-hello-notarized /usr/local/bin/qhx resources: {} volumeMounts: - name: workspace mountPath: /workspace - name: spiffe-workload-api mountPath: /spiffe-workload-api readOnly: true - name: qhx-proxy image: "oci.messier42.com/qhx/proxy:v0.6.1" imagePullPolicy: IfNotPresent ports: - containerPort: 8081 name: http env: - name: QHX_PROXY_CONFIG value: | spiffe: workload_socket_path: unix:///spiffe-workload-api/agent.sock listeners: - name: client address: "0.0.0.0:8081" protocol: http mode: client target: url: https://notary-proxy.qhx-notary.svc.cluster.local:8081 spiffe_ids: - 'spiffe://.*' middlewares: [] timeouts: read: 600s write: 600s idle: 600s volumeMounts: - name: spiffe-workload-api mountPath: /spiffe-workload-api readOnly: true volumes: - name: workspace emptyDir: {} - name: spiffe-workload-api csi: driver: csi.spiffe.io readOnly: true---apiVersion: apps/v1kind: Deploymentmetadata: name: ollama-server namespace: ai-chat labels: app: ollama-serverspec: replicas: 1 selector: matchLabels: app: ollama-server template: metadata: labels: app: ollama-server spec: serviceAccountName: ollama-server imagePullSecrets: - name: oci-secret containers: - name: ollama-server image: ollama/ollama:latest env: - name: OLLAMA_HOST value: "0.0.0.0" - name: OLLAMA_KEEP_ALIVE value: "-1" volumeMounts: - name: ollama-data mountPath: /root/.ollama resources: {} lifecycle: postStart: exec: command: - ollama - run - "llama3.2:1b" - "Hello." - name: qhx-proxy image: "oci.messier42.com/qhx/proxy:v0.6.1" imagePullPolicy: IfNotPresent ports: - containerPort: 8081 name: http env: - name: QHX_PROXY_CONFIG value: | spiffe: workload_socket_path: unix:///spiffe-workload-api/agent.sock listeners: - name: server address: "0.0.0.0:8081" protocol: http mode: server target: url: http://localhost:11434 source: spiffe_ids: - '^spiffe://.*$' middlewares: [] timeouts: read: 600s write: 600s idle: 600s volumeMounts: - name: spiffe-workload-api mountPath: /spiffe-workload-api readOnly: true securityContext: runAsNonRoot: true runAsUser: 1001 runAsGroup: 1001 allowPrivilegeEscalation: false capabilities: drop: - ALL volumes: - name: ollama-data emptyDir: {} - name: spiffe-workload-api csi: driver: csi.spiffe.io readOnly: true---apiVersion: v1kind: Servicemetadata: name: ollama namespace: ai-chatspec: selector: app: ollama-server ports: - protocol: TCP port: 8081 targetPort: 8081 type: ClusterIPENDWorking with the AI chat demo
Section titled “Working with the AI chat demo”Wait for both workloads and the notary to become ready. The model and client package downloads can delay startup:
kubectl rollout status deployment/notary-proxy -n qhx-notarykubectl rollout status deployment/ollama-server -n ai-chatkubectl rollout status deployment/llm-client -n ai-chatOpen a shell in the LLM client container:
kubectl exec -n ai-chat -it deployment/llm-client -c llm-client -- /bin/bashRun the following commands inside that container. The terminal transcripts show example responses; identities, IDs, timestamps, and model output will vary. Identity and receipt reports are written to standard error alongside the response shown on standard output.
View and run say-hello to make an AI chat request:
$ cat ./say-hello#!/bin/shexec curl -i -X POST -H 'Content-Type: application/json' http://localhost:8081/v1/chat/completions -d "$(cat hello.json)"
$ ./say-helloHTTP/1.1 200 OKContent-Length: 277Content-Type: application/jsonDate: Tue, 02 Dec 2025 18:35:18 GMTQhx-Internal-Upstream-Uri: spiffe://qhx.dev/ns/qhx-notary/sa/notary-proxy/pod/notary-proxy-7d9c6f95cf-zwrbg/930bbb0e-511a-4a5c-a6fb-63ba8f8cac2cQhx-Workload-Id: -wtqOQWEBHnhN6spDIh_HazuBC1jqwbr1AOYI0ivGGw=
{"id":"chatcmpl-18","object":"chat.completion","created":1764700518,"model":"llama3.2:1b","choices":[{"index":0,"message":{"role":"assistant","content":"How can I assist you today?"},"finish_reason":"stop"}],"usage":{"prompt_tokens":27,"completion_tokens":8,"total_tokens":35}}Use say-hello-notarized to inspect and validate request notarization with
qhx curl. Check the installed demo CLI before using optional flags:
qhx --helpqhx curl --helpThe CLI reference describes the command options;
your installed release’s help determines which are available in this demo.
Identify this demo binary by its v0.6.1 download artifact; that release does
not provide qhx --version.
$ cat say-hello-notarized#!/bin/shexec qhx curl -i -X POST -H 'Content-Type: application/json' http://localhost:8081/v1/chat/completions -d "$(cat hello.json)" "$@"Add --print-workload to view the workload identity statement after the CLI
validates it:
$ ./say-hello-notarized --print-workloadHTTP/1.1 200 OKContent-Length: 278Content-Type: application/jsonDate: Tue, 02 Dec 2025 18:36:53 GMTQhx-Internal-Upstream-Uri: spiffe://qhx.dev/ns/qhx-notary/sa/notary-proxy/pod/notary-proxy-7d9c6f95cf-zwrbg/930bbb0e-511a-4a5c-a6fb-63ba8f8cac2cQhx-Workload-Id: -wtqOQWEBHnhN6spDIh_HazuBC1jqwbr1AOYI0ivGGw=
{"id":"chatcmpl-864","object":"chat.completion","created":1764700613,"model":"llama3.2:1b","choices":[{"index":0,"message":{"role":"assistant","content":"How can I assist you today?"},"finish_reason":"stop"}],"usage":{"prompt_tokens":27,"completion_tokens":8,"total_tokens":35}}Workload Identity Statement "-wtqOQWEBHnhN6spDIh_HazuBC1jqwbr1AOYI0ivGGw=":===========================================================================Issuer: spiffe://qhx.dev/ns/qhx-notary/sa/notary-proxy/pod/notary-proxy-7d9c6f95cf-zwrbg/930bbb0e-511a-4a5c-a6fb-63ba8f8cac2cSubject: spiffe://qhx.dev/ns/ai-chat/sa/ollama-server/pod/ollama-server-6b4bbd4b68-r6h9c/36159fb9-6d67-49db-a575-199dec756a9dAudience: [https://qhx.dev/workload-statement]Issued At: 2025-12-02T18:31:17Z
Trust Domain: qhx.devNamespace: ai-chatPod Name: ollama-server-6b4bbd4b68-r6h9cPod UID: 36159fb9-6d67-49db-a575-199dec756a9dService Account Name: ollama-server
Containers: - ollama/ollama:latest - oci.messier42.com/qhx/proxy:v0.6.1
Labels: pod-template-hash: 6b4bbd4b68 app: ollama-server
Raw Claims (JSON):{ "iss": "spiffe://qhx.dev/ns/qhx-notary/sa/notary-proxy/pod/notary-proxy-7d9c6f95cf-zwrbg/930bbb0e-511a-4a5c-a6fb-63ba8f8cac2c", "sub": "spiffe://qhx.dev/ns/ai-chat/sa/ollama-server/pod/ollama-server-6b4bbd4b68-r6h9c/36159fb9-6d67-49db-a575-199dec756a9d", "aud": "https://qhx.dev/workload-statement", "iat": 1764700277, "qhx": { "trustDomain": "qhx.dev", "namespace": "ai-chat", "podName": "ollama-server-6b4bbd4b68-r6h9c", "podUID": "36159fb9-6d67-49db-a575-199dec756a9d", "serviceAccountName": "ollama-server", "labels": { "app": "ollama-server", "pod-template-hash": "6b4bbd4b68" }, "annotations": null, "initContainers": null, "containers": [ { "image": "ollama/ollama:latest" }, { "image": "oci.messier42.com/qhx/proxy:v0.6.1" } ] }}Add -n signRequest --print-receipt to request signing and display the verified
receipt. This checks the notary’s captured request and response; it does not
establish that the model’s answer is correct.
$ ./say-hello-notarized -n signRequest --print-receiptHTTP/1.1 200 OKQhx-Workload-Id: -wtqOQWEBHnhN6spDIh_HazuBC1jqwbr1AOYI0ivGGw=Content-Length: 354Content-Type: application/jsonDate: Tue, 02 Dec 2025 18:37:23 GMTQhx-Internal-Upstream-Uri: spiffe://qhx.dev/ns/qhx-notary/sa/notary-proxy/pod/notary-proxy-7d9c6f95cf-zwrbg/930bbb0e-511a-4a5c-a6fb-63ba8f8cac2cQhx-Request-Id: xURvwXcjqkLGnJ0TIEk7LQ==
{"id":"chatcmpl-930","object":"chat.completion","created":1764700643,"model":"llama3.2:1b","choices":[{"index":0,"message":{"role":"assistant","content":"Hello. Is there something I can help you with or would you like to talk about something in particular?"},"finish_reason":"stop"}],"usage":{"prompt_tokens":27,"completion_tokens":22,"total_tokens":49}}Signed Request Receipt "xURvwXcjqkLGnJ0TIEk7LQ==":==================================================Issuer: spiffe://qhx.dev/ns/qhx-notary/sa/notary-proxy/pod/notary-proxy-7d9c6f95cf-zwrbg/930bbb0e-511a-4a5c-a6fb-63ba8f8cac2cSubject:Audience: [https://qhx.dev/receipt]Issued At: 2025-12-02T18:37:23Z
Workload Statement ID: -wtqOQWEBHnhN6spDIh_HazuBC1jqwbr1AOYI0ivGGw=
HTTP Request: Method: POST Path: /v1/chat/completions Headers: Content-Type: application/json User-Agent: qhx-curl Body (109 bytes): { "model": "llama3.2:1b", "messages": [ { "role": "user", "content": "Hello." } ]}
HTTP Response: Status Code: 200 Headers: Content-Type: application/json Body (354 bytes): {"id":"chatcmpl-930","object":"chat.completion","created":1764700643,"model":"llama3.2:1b","choices":[{"index":0,"message":{"role":"assistant","content":"Hello. Is there something I can help you with or would you like to talk about something in particular?"},"finish_reason":"stop"}],"usage":{"prompt_tokens":27,"completion_tokens":22,"total_tokens":49}}
Raw Claims (JSON):{ "iss": "spiffe://qhx.dev/ns/qhx-notary/sa/notary-proxy/pod/notary-proxy-7d9c6f95cf-zwrbg/930bbb0e-511a-4a5c-a6fb-63ba8f8cac2c", "aud": "https://qhx.dev/receipt", "iat": 1764700643, "qhx": { "workloadStatementID": "-wtqOQWEBHnhN6spDIh_HazuBC1jqwbr1AOYI0ivGGw", "request": { "method": "POST", "path": "/v1/chat/completions", "headers": [ { "name": "Content-Type", "values": [ "application/json" ] }, { "name": "User-Agent", "values": [ "qhx-curl" ] } ], "body": "ewogICJtb2RlbCI6ICJsbGFtYTMuMjoxYiIsCiAgIm1lc3NhZ2VzIjogWwogICAgewogICAgICAicm9sZSI6ICJ1c2VyIiwKICAgICAgImNvbnRlbnQiOiAiSGVsbG8uIgogICAgfQogIF0KfQ" }, "response": { "statusCode": 200, "headers": [ { "name": "Content-Type", "values": [ "application/json" ] } ], "body": "eyJpZCI6ImNoYXRjbXBsLTkzMCIsIm9iamVjdCI6ImNoYXQuY29tcGxldGlvbiIsImNyZWF0ZWQiOjE3NjQ3MDA2NDMsIm1vZGVsIjoibGxhbWEzLjI6MWIiLCJjaG9pY2VzIjpbeyJpbmRleCI6MCwibWVzc2FnZSI6eyJyb2xlIjoiYXNzaXN0YW50IiwiY29udGVudCI6IkhlbGxvLiBJcyB0aGVyZSBzb21ldGhpbmcgSSBjYW4gaGVscCB5b3Ugd2l0aCBvciB3b3VsZCB5b3UgbGlrZSB0byB0YWxrIGFib3V0IHNvbWV0aGluZyBpbiBwYXJ0aWN1bGFyPyJ9LCJmaW5pc2hfcmVhc29uIjoic3RvcCJ9XSwidXNhZ2UiOnsicHJvbXB0X3Rva2VucyI6MjcsImNvbXBsZXRpb25fdG9rZW5zIjoyMiwidG90YWxfdG9rZW5zIjo0OX19" } }}