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.

Deploy QHx onto the target Kubernetes cluster by following the install guide.

After installation, you can verify that QHx is installed and working:

Terminal window
kubectl get pods -n qhx-system
NAME READY STATUS RESTARTS
manager-6f89bb4796-9kz5g 1/1 Running 0
pki-agent-csxgb 3/3 Running 0
pki-agent-jxcn5 3/3 Running 0
pki-server-0 2/2 Running 0

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 x in ai-chat qhx-notary; do kubectl create secret -n "$x" docker-registry oci-secret --from-file=<(kubectl get -n qhx-system secret oci-secret -o json | jq -r '.data.".dockerconfigjson"' | base64 -d); done

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
keylog: /tmp/qhx-proxy.keylog
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
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
keylog: /tmp/qhx-proxy.keylog
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
keylog: /tmp/qhx-proxy.keylog
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

We can drop into our LLM client pod to get a shell:

Terminal window
kubectl exec -n ai-chat -it "$(kubectl get pod -o name -n ai-chat -l app=llm-client)" -- /bin/bash
llm-client$

We can view, and run, the say-hello script to perform an example AI chat inference 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}}

We can substitute the say-hello tool for the say-hello-notarized tool to inspect and validate request notarization. This makes use of the qhx CLI and its curl subcommand.

$ 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)" "$@"

By adding the --print-workload command, we can view the workload identity statement which qhx curl validates:

Terminal window
$ ./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"
}
]
}
}

By adding -n signRequest, we can request request-level signing, and view the resulting signed request receipt by adding --print-receipt:

Terminal window
$ ./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"
}
}
}