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.
Install QHx
Section titled “Install QHx”Deploy QHx onto the target Kubernetes cluster by following the install guide.
After installation, you can verify that QHx is installed and working:
kubectl get pods -n qhx-systemNAME READY STATUS RESTARTSmanager-6f89bb4796-9kz5g 1/1 Running 0pki-agent-csxgb 3/3 Running 0pki-agent-jxcn5 3/3 Running 0pki-server-0 2/2 Running 0Configure 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 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); doneDeploy 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.4.0" 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: 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/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.4.0" 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/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.4.0" 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: 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”We can drop into our LLM client pod to get a shell:
kubectl exec -n ai-chat -it "$(kubectl get pod -o name -n ai-chat -l app=llm-client)" -- /bin/bashllm-client$We can view, and run, the say-hello script to perform an example AI chat inference 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}}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/shexec 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:
$ ./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.4.0
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.4.0" } ] }}By adding -n signRequest, we can request request-level signing, and view the resulting signed request receipt by adding --print-receipt:
$ ./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" } }}