Skip to content

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.

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:

Terminal window
kubectl get pods -n qhx-system

We will create a namespace called ai-chat for our purposes, and a namespace called qhx-notary to host our notary.

Terminal window
kubectl create ns ai-chat
kubectl create ns qhx-notary

We now copy the M42 registry authentication secret from the qhx-system namespace to our new namespaces:

Terminal window
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 -
done

Only 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 QHx notary by pasting the below command:

Terminal window
kubectl apply -f - <<'END'
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: notary-proxy
namespace: qhx-notary
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: pod-reader
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: notary-proxy-pod-reader
subjects:
- kind: ServiceAccount
name: notary-proxy
namespace: qhx-notary
roleRef:
kind: ClusterRole
name: pod-reader
apiGroup: rbac.authorization.k8s.io
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: notary-proxy
namespace: qhx-notary
labels:
app: notary-proxy
spec:
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: v1
kind: Service
metadata:
name: notary-proxy
namespace: qhx-notary
spec:
selector:
app: notary-proxy
ports:
- protocol: TCP
port: 8081
targetPort: 8081
type: ClusterIP
END

Deploy the example workload by pasting the below command:

Terminal window
kubectl apply -f - <<'END'
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: llm-client
namespace: ai-chat
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: ollama-server
namespace: ai-chat
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: llm-client
namespace: ai-chat
labels:
app: llm-client
spec:
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/v1
kind: Deployment
metadata:
name: ollama-server
namespace: ai-chat
labels:
app: ollama-server
spec:
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: v1
kind: Service
metadata:
name: ollama
namespace: ai-chat
spec:
selector:
app: ollama-server
ports:
- protocol: TCP
port: 8081
targetPort: 8081
type: ClusterIP
END

Wait for both workloads and the notary to become ready. The model and client package downloads can delay startup:

Terminal window
kubectl rollout status deployment/notary-proxy -n qhx-notary
kubectl rollout status deployment/ollama-server -n ai-chat
kubectl rollout status deployment/llm-client -n ai-chat

Open a shell in the LLM client container:

Terminal window
kubectl exec -n ai-chat -it deployment/llm-client -c llm-client -- /bin/bash

Run 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/sh
exec curl -i -X POST -H 'Content-Type: application/json' http://localhost:8081/v1/chat/completions -d "$(cat hello.json)"
$ ./say-hello
HTTP/1.1 200 OK
Content-Length: 277
Content-Type: application/json
Date: Tue, 02 Dec 2025 18:35:18 GMT
Qhx-Internal-Upstream-Uri: spiffe://qhx.dev/ns/qhx-notary/sa/notary-proxy/pod/notary-proxy-7d9c6f95cf-zwrbg/930bbb0e-511a-4a5c-a6fb-63ba8f8cac2c
Qhx-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:

Terminal window
qhx --help
qhx curl --help

The 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/sh
exec 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-workload
HTTP/1.1 200 OK
Content-Length: 278
Content-Type: application/json
Date: Tue, 02 Dec 2025 18:36:53 GMT
Qhx-Internal-Upstream-Uri: spiffe://qhx.dev/ns/qhx-notary/sa/notary-proxy/pod/notary-proxy-7d9c6f95cf-zwrbg/930bbb0e-511a-4a5c-a6fb-63ba8f8cac2c
Qhx-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-63ba8f8cac2c
Subject: spiffe://qhx.dev/ns/ai-chat/sa/ollama-server/pod/ollama-server-6b4bbd4b68-r6h9c/36159fb9-6d67-49db-a575-199dec756a9d
Audience: [https://qhx.dev/workload-statement]
Issued At: 2025-12-02T18:31:17Z
Trust Domain: qhx.dev
Namespace: ai-chat
Pod Name: ollama-server-6b4bbd4b68-r6h9c
Pod UID: 36159fb9-6d67-49db-a575-199dec756a9d
Service 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-receipt
HTTP/1.1 200 OK
Qhx-Workload-Id: -wtqOQWEBHnhN6spDIh_HazuBC1jqwbr1AOYI0ivGGw=
Content-Length: 354
Content-Type: application/json
Date: Tue, 02 Dec 2025 18:37:23 GMT
Qhx-Internal-Upstream-Uri: spiffe://qhx.dev/ns/qhx-notary/sa/notary-proxy/pod/notary-proxy-7d9c6f95cf-zwrbg/930bbb0e-511a-4a5c-a6fb-63ba8f8cac2c
Qhx-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-63ba8f8cac2c
Subject:
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"
}
}
}