Skip to content

Attestation Strategies

QHx uses a two-phase attestation process to establish workload identity: node attestation (proving the node’s identity) followed by workload attestation (proving the process identity). This guide explains how these phases relate, when to use pre-registration versus automated registration, and how to select the right attestation strategy for your environment.

Target audience: Platform operators, security architects, and deployment teams configuring QHx identity issuance.

┌─────────────────────┐
│ Node Attestation │ Phase 1: "What machine is this?"
│ │
│ Establishes: │ ┌─────────────────────────┐
│ • Node identity │ │ Trusted authorities: │
│ • Agent SPIFFE ID │ │ • Cloud provider APIs │
│ • Node selectors │ │ • TPM measurements │
│ │ │ • X.509 certificates │
└─────────────────────┘ │ • Join tokens │
↓ └─────────────────────────┘
┌─────────────────────┐
│ Workload Attestation│ Phase 2: "What process is this?"
│ │
│ Establishes: │ ┌─────────────────────────┐
│ • Process identity │ │ Local authorities: │
│ • SPIFFE ID │ │ • Kernel (PID, UID/GID) │
│ • Workload SVID │ │ • Kubelet API │
│ │ │ • Container runtime │
└─────────────────────┘ └─────────────────────────┘

Key principle: Node attestation establishes trust in the platform; workload attestation builds on that trust to identify specific processes.

Every workload identity has a parent identity (the node it runs on):

# Node registration entry
spiffeID: spiffe://qhx.dev/agent/k8s/node-1
selectors:
- k8s_psat:cluster:production
- k8s_psat:agent_node_name:node-1
---
# Workload registration entry (child of node)
spiffeID: spiffe://qhx.dev/ns/production/sa/api
parentID: spiffe://qhx.dev/agent/k8s/node-1 # ← Links to node
selectors:
- k8s:ns:production
- k8s:sa:api

Why parent-child? Ensures workload can only run on authorized nodes. If node is compromised, only workloads with that parent are affected.

When to use:

  • Hardware root of trust required
  • Defense/classified workloads
  • Physical or bare-metal servers
  • Measured boot required

How it works:

  1. Node boots with UEFI Secure Boot enabled
  2. Boot measurements stored in TPM PCR registers
  3. PKI Agent reads TPM measurements
  4. PKI Server validates measurements against policy
  5. Agent receives node identity

Configuration:

# PKI Agent (node)
apiVersion: v1
kind: ConfigMap
metadata:
name: pki-agent-config
namespace: qhx-system
data:
agent.conf: |
agent {
data_dir = "/var/lib/qhx/agent"
trust_domain = "qhx.dev"
}
plugins {
NodeAttestor "tpm_devid" {
plugin_data {
devid_cert_path = "/opt/qhx/devid-cert.pem"
devid_priv_path = "/opt/qhx/devid-key.pem"
}
}
}

PKI Server configuration:

# PKI Server
data:
server.conf: |
server {
trust_domain = "qhx.dev"
}
plugins {
NodeAttestor "tpm_devid" {
plugin_data {
ca_path = "/opt/qhx/tpm-ca-bundle.pem"
devid_policy {
allowed_pcr_banks = ["SHA256"]
pcr_values {
# Boot integrity
"0" = ["<expected-hash>"] # BIOS/firmware
"7" = ["<expected-hash>"] # Secure Boot state
"14" = ["<expected-hash>"] # Boot order
}
}
}
}
}

Node selectors generated:

tpm:pub_hash:<hash-of-public-key>
tpm:pcr:<bank>:<index>:<value>

Advantages:

  • Hardware-rooted identity
  • Detects boot-time tampering
  • Prevents agent impersonation
  • Measured boot integrity

Disadvantages:

  • Requires TPM 2.0 hardware
  • PCR values change on firmware updates
  • Complex initial provisioning
  • Not available in all cloud environments

Operational notes:

  • Maintain PCR value database
  • Update policy after BIOS updates
  • Test boot integrity monitoring
  • Document TPM provisioning process

Strategy 2: Kubernetes PSAT (Projected Service Account Token)

Section titled “Strategy 2: Kubernetes PSAT (Projected Service Account Token)”

When to use:

  • Kubernetes-native deployments
  • Cloud environments (EKS, AKS, GKE)
  • No TPM hardware available
  • Rapid deployment required

How it works:

  1. PKI Agent pod has service account token mounted
  2. Token is cryptographically signed by Kubernetes
  3. PKI Server validates token via Kubernetes API
  4. Token contains node name, namespace, service account
  5. Agent receives node identity

Configuration:

# PKI Agent DaemonSet
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: qhx-pki-agent
namespace: qhx-system
spec:
template:
spec:
serviceAccountName: qhx-pki-agent
containers:
- name: agent
image: qhx/pki-agent:v0.6.1
volumeMounts:
- name: agent-config
mountPath: /etc/qhx
- name: agent-socket
mountPath: /run/pki/sockets
volumes:
- name: agent-config
configMap:
name: pki-agent-config
---
apiVersion: v1
kind: ConfigMap
metadata:
name: pki-agent-config
data:
agent.conf: |
plugins {
NodeAttestor "k8s_psat" {
plugin_data {
cluster = "production"
token_path = "/var/run/secrets/kubernetes.io/serviceaccount/token"
}
}
}

PKI Server configuration:

data:
server.conf: |
plugins {
NodeAttestor "k8s_psat" {
plugin_data {
clusters = {
"production" = {
service_account_allow_list = ["qhx-system:qhx-pki-agent"]
audience = ["qhx-server"]
kube_config_file = "/etc/kubernetes/kubeconfig"
}
}
}
}
}

Node selectors generated:

k8s_psat:cluster:production
k8s_psat:agent_ns:qhx-system
k8s_psat:agent_sa:qhx-pki-agent
k8s_psat:agent_node_name:node-1
k8s_psat:agent_node_uid:abc-def-123

Advantages:

  • Native Kubernetes integration
  • No hardware requirements
  • Automatic node discovery
  • Works in all K8s environments

Disadvantages:

  • Depends on Kubernetes API availability
  • Token theft possible (mitigated by short expiry)
  • No hardware root of trust
  • Cluster admin can forge tokens

Operational notes:

  • Rotate service account keys regularly
  • Monitor Kubernetes API availability
  • Restrict service account RBAC
  • Enable Kubernetes audit logging

Strategy 3: Cloud Provider IID (AWS, Azure, GCP)

Section titled “Strategy 3: Cloud Provider IID (AWS, Azure, GCP)”

When to use:

  • Cloud-native deployments
  • Need cloud metadata integration
  • Auto-scaling environments
  • Trust cloud provider platform

AWS Example:

# PKI Agent
plugins {
NodeAttestor "aws_iid" {
plugin_data {}
}
}

PKI Server:

plugins {
NodeAttestor "aws_iid" {
plugin_data {
access_key_id = "<AWS-ACCESS-KEY>"
secret_access_key = "<AWS-SECRET-KEY>"
skip_block_device = false
}
}
NodeResolver "aws_iid" {
plugin_data {
access_key_id = "<AWS-ACCESS-KEY>"
secret_access_key = "<AWS-SECRET-KEY>"
}
}
}

Node selectors generated:

aws:account:123456789012
aws:region:us-east-1
aws:instance:i-abc123def456
aws:ami:ami-xyz789
aws:sg:sg-abc123 (security group)
aws:tag:environment:production
aws:iam:role:my-instance-role

Advantages:

  • Rich metadata (tags, IAM role, VPC)
  • Automatic in cloud environments
  • No additional infrastructure
  • Well-tested and documented

Disadvantages:

  • Cloud provider lock-in
  • Requires IAM credentials
  • API rate limits
  • IID can be exfiltrated

When to use:

  • Initial agent deployment
  • No platform attestation available
  • Manual node provisioning
  • Testing/development

How it works:

  1. Administrator generates join token on PKI Server
  2. Token provided to agent at startup
  3. Agent presents token to server
  4. Server validates and immediately expires token
  5. Agent receives long-term SVID

Generate token:

Terminal window
kubectl exec -n qhx-system qhx-pki-server-0 -- \
/opt/qhx/bin/qhx-server token generate \
-spiffeID spiffe://qhx.dev/agent/manual/node-1 \
-ttl 300

Agent configuration:

plugins {
NodeAttestor "join_token" {
plugin_data {
token_path = "/opt/qhx/join-token"
}
}
}

Advantages:

  • Works in any environment
  • No platform dependencies
  • Simple to understand
  • Useful for bootstrapping

Disadvantages:

  • Manual process (doesn’t scale)
  • Token theft risk
  • One-time use only
  • No ongoing attestation

Best practices:

  • Use only for initial bootstrap
  • Immediately delete used tokens
  • Rotate to stronger attestation method
  • Never reuse tokens

Workload attestation happens after node attestation establishes the agent’s identity.

How it works:

  1. Workload calls Workload API (Unix socket)
  2. PKI Agent identifies caller’s process ID
  3. Agent queries cgroup to find container ID
  4. Agent queries kubelet for pod/container metadata
  5. Agent matches metadata to registration entries
  6. Agent returns cached SVID to workload

Selectors available:

SelectorExampleDescription
k8s:nsk8s:ns:productionPod namespace
k8s:sak8s:sa:api-serverService account
k8s:pod-namek8s:pod-name:api-7f8c9Pod name (unstable for Deployments)
k8s:pod-uidk8s:pod-uid:abc-123Unique pod identifier
k8s:pod-labelk8s:pod-label:app:apiPod label (key:value)
k8s:container-namek8s:container-name:appContainer name within pod
k8s:container-imagek8s:container-image:api:v1.0Container image reference
k8s:node-namek8s:node-name:node-1Node pod is running on

PKI Agent configuration:

plugins {
WorkloadAttestor "k8s" {
plugin_data {
skip_kubelet_verification = false
kubelet_ca_path = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
token_path = "/var/run/secrets/kubernetes.io/serviceaccount/token"
node_name_env = "MY_NODE_NAME"
}
}
}

Agent DaemonSet must have:

spec:
template:
spec:
hostPID: true # Required to see workload PIDs
env:
- name: MY_NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName

When to use:

  • Non-containerized workloads
  • Bare metal servers
  • Traditional VMs
  • Process-level identity required

Selectors available:

SelectorExampleDescription
unix:uidunix:uid:1000User ID
unix:gidunix:gid:1000Group ID
unix:pathunix:path:/usr/bin/apiExecutable path
unix:sha256unix:sha256:<hash>Executable hash

Configuration:

plugins {
WorkloadAttestor "unix" {
plugin_data {}
}
}

Example registration:

Terminal window
qhx-server entry create \
-parentID spiffe://qhx.dev/agent/node-1 \
-spiffeID spiffe://qhx.dev/webapp \
-selector unix:uid:1000 \
-selector unix:path:/opt/webapp/bin/server

Selectors available:

SelectorExampleDescription
docker:labeldocker:label:app:apiContainer label
docker:image_iddocker:image_id:sha256:abcImage digest
docker:envdocker:env:ENVIRONMENT:prodEnvironment variable

Definition: Administrator explicitly creates registration entries before workloads start.

Method 1: CLI registration

Terminal window
# Node registration
kubectl exec -n qhx-system qhx-pki-server-0 -- \
/opt/qhx/bin/qhx-server entry create \
-node \
-spiffeID spiffe://qhx.dev/k8s-node-pool-prod \
-selector k8s_psat:cluster:production \
-selector k8s_psat:agent_ns:qhx-system
# Workload registration
kubectl exec -n qhx-system qhx-pki-server-0 -- \
/opt/qhx/bin/qhx-server entry create \
-parentID spiffe://qhx.dev/k8s-node-pool-prod \
-spiffeID spiffe://qhx.dev/ns/production/sa/api \
-selector k8s:ns:production \
-selector k8s:sa:api \
-selector k8s:pod-label:app:api

**Method 2: CRD-based registration **

apiVersion: spire.spiffe.io/v1alpha1
kind: ClusterStaticEntry
metadata:
name: production-api-identity
spec:
spiffeID: spiffe://qhx.dev/ns/production/sa/api
parentID: spiffe://qhx.dev/k8s-node-pool-prod
selectors:
- k8s:ns:production
- k8s:sa:api
x509SVIDTTL: 1h
federatesWith:
- partner-domain.com
admin: false
downstream: false

Advantages:

  • Explicit control over identities
  • Audit trail via GitOps
  • Policy-as-code
  • Works without cluster access

Disadvantages:

  • Manual process (doesn’t scale for many workloads)
  • Must anticipate all workloads
  • Stale entries for deleted workloads
  • Requires server access or CRD permissions

When to use:

  • Small number of well-known services
  • High-security environments requiring approval
  • Regulatory compliance needs
  • GitOps-managed infrastructure

Best practices:

  • Store entries in version control
  • Use namespace/service account patterns
  • Include MLS labels in comments
  • Document selector rationale

Definition: System automatically creates registration entries based on observed workloads.

QHx Admission Controller Integration:

When QHx admission controller applies MLS labels, it can automatically create registration entries:

# User creates Deployment (no registration entry exists yet)
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: production
spec:
template:
spec:
serviceAccountName: api
containers:
- name: api
image: api:v1.0

Admission controller mutates:

metadata:
labels:
mls.qhx.dev/level: "us:s" # Applied automatically
mls.qhx.dev/releasability: "us"
app: api

Admission controller creates registration entry:

apiVersion: spire.spiffe.io/v1alpha1
kind: ClusterStaticEntry
metadata:
name: production-api-abc123
ownerReferences:
- apiVersion: apps/v1
kind: Deployment
name: api
uid: abc-123-def-456
spec:
spiffeID: spiffe://qhx.dev/ns/production/sa/api
parentID: spiffe://qhx.dev/k8s-node-pool-prod
selectors:
- k8s:ns:production
- k8s:sa:api
- k8s:pod-label:app:api

Lifecycle management:

  • Entry created when Deployment created
  • Entry updated when Deployment updated
  • Entry deleted when Deployment deleted (via ownerReference)

Advantages:

  • Zero operator intervention
  • Scales to thousands of workloads
  • No stale entries (garbage collected)
  • Immediate identity issuance

Disadvantages:

  • Less explicit control
  • Harder to audit (many entries)
  • Requires cluster permissions
  • May create unnecessary entries

When to use:

  • Large number of dynamic workloads
  • Kubernetes-native applications
  • Development/staging environments
  • Microservices architectures

Combine pre-registration and automation:

Pre-register:

  • Node identities (stable, few nodes)
  • Critical services (database, API gateway)
  • Cross-namespace services
  • Federated identities

Auto-register:

  • Application workloads (many, ephemeral)
  • Development services
  • Batch jobs
  • Sidecar proxies

Example:

# Pre-registered: Node pool identity
---
apiVersion: spire.spiffe.io/v1alpha1
kind: ClusterStaticEntry
metadata:
name: production-node-pool
spec:
spiffeID: spiffe://qhx.dev/k8s-node-pool-prod
selectors:
- k8s_psat:cluster:production
x509SVIDTTL: 24h
---
# Pre-registered: Database identity
apiVersion: spire.spiffe.io/v1alpha1
kind: ClusterStaticEntry
metadata:
name: production-postgres
spec:
spiffeID: spiffe://qhx.dev/ns/production/sa/postgres
parentID: spiffe://qhx.dev/k8s-node-pool-prod
selectors:
- k8s:ns:production
- k8s:sa:postgres
x509SVIDTTL: 12h
---
# Auto-registered: Application workloads
# (created by QHx admission controller as Deployments are created)
CriterionTPMK8s PSATCloud IIDJoin Token
Hardware root of trust✅❌❌❌
Works in cloud⚠️ Limited✅✅✅
Works bare metal✅⚠️ K8s required❌✅
Auto-scaling friendly❌✅✅❌
Setup complexityHighLowLowVery low
Operational burdenHighLowLowHigh

Decision tree:

Is hardware root of trust required?
├─ Yes → TPM attestation
└─ No
└─ Running on Kubernetes?
├─ Yes → K8s PSAT attestation
└─ No
└─ Running in cloud (AWS/Azure/GCP)?
├─ Yes → Cloud IID attestation
└─ No → Join token (then upgrade)
EnvironmentStrategySelectors
KubernetesK8s workload attestork8s:ns, k8s:sa, k8s:pod-label
Docker (non-K8s)Docker workload attestordocker:label, docker:image_id
Bare metal / VMUnix workload attestorunix:uid, unix:path, unix:sha256
WindowsWindows workload attestorwindows:*

Selector selection criteria:

Most stable (recommended):

  • k8s:ns + k8s:sa (doesn’t change across restarts)
  • unix:sha256 (pinned to binary)

Moderately stable:

  • k8s:pod-label (changes if labels change)
  • unix:path (changes if binary moves)

Unstable (avoid):

  • k8s:pod-name (changes on every restart for Deployments)
  • k8s:container-name (changes if container renamed)
FactorPre-RegistrationAutomatedHybrid
Workload count< 50> 50050-500
Change frequencyWeekly or lessHourly or moreDaily
Compliance needsHigh (audit trail)Low (dynamic)Medium
Expertise requiredLow (manual)High (automation)Medium
GitOps-friendly✅ Very⚠️ Generated✅ Partial

QHx admission controller applies MLS labels that can be used as selectors:

apiVersion: spire.spiffe.io/v1alpha1
kind: ClusterStaticEntry
spec:
spiffeID: spiffe://qhx.dev/ns/classified/sa/mission-app
selectors:
- k8s:ns:classified
- k8s:sa:mission-app
- k8s:pod-label:mls.qhx.dev/level:us:ts
- k8s:pod-label:mls.qhx.dev/compartment:us:quantum

Why this matters:

  • Flowspecs can match on SPIFFE ID
  • Identity encodes classification level
  • Network policy generated from labels
  • Defense in depth (labels + identity)

Multiple PKI Servers (Algorithm Diversity)

Section titled “Multiple PKI Servers (Algorithm Diversity)”

QHx runs multiple PKI Servers for different algorithms:

# Node registration for ML-DSA-65 server
---
apiVersion: spire.spiffe.io/v1alpha1
kind: ClusterStaticEntry
metadata:
name: production-nodes-mldsa65
annotations:
qhx.dev/pki-server: mldsa65
spec:
spiffeID: spiffe://qhx.dev/k8s-node-pool-prod
className: mldsa65 # Routes to ML-DSA-65 PKI Server
selectors:
- k8s_psat:cluster:production
---
# Node registration for EC-P384 server (legacy)
apiVersion: spire.spiffe.io/v1alpha1
kind: ClusterStaticEntry
metadata:
name: production-nodes-ecp384
annotations:
qhx.dev/pki-server: ec-p384
spec:
spiffeID: spiffe://qhx.dev/k8s-node-pool-legacy
className: ec-p384 # Routes to EC-P384 PKI Server
selectors:
- k8s_psat:cluster:production
- k8s_psat:agent_node_label:crypto:traditional

Namespace routing:

apiVersion: qhx.dev/v1
kind: QHxPolicy
metadata:
namespace: production-critical
spec:
signatureAlgorithm: mldsa87
# Workloads in this namespace get identities from mldsa87 PKI Server
1. User creates Deployment
↓
2. QHx Admission Controller intercepts
↓
3. Extract user groups from authentication
↓
4. Evaluate QHxClusterPolicy
↓
5. Apply MLS labels
↓
6. Create/update ClusterStaticEntry (if automated)
↓
7. Deployment created
↓
8. Pod scheduled to node
↓
9. PKI Agent attests node (if first pod on node)
↓
10. Container starts, calls Workload API
↓
11. PKI Agent attests workload
↓
12. Agent matches selectors to registration entries
↓
13. Agent requests SVID from PKI Server
↓
14. PKI Server signs SVID
↓
15. Agent returns SVID to workload

Check node attestation:

Terminal window
# List attested nodes
kubectl exec -n qhx-system qhx-pki-server-0 -- \
/opt/qhx/bin/qhx-server agent list
# Output:
# SPIFFE ID: spiffe://qhx.dev/agent/k8s/node-1
# Attestation type: k8s_psat
# Expiration: 2026-02-03 10:00:00 +0000 UTC
# Selectors:
# k8s_psat:cluster:production
# k8s_psat:agent_node_name:node-1

Check workload registration:

Terminal window
# List registration entries
kubectl exec -n qhx-system qhx-pki-server-0 -- \
/opt/qhx/bin/qhx-server entry show \
-parentID spiffe://qhx.dev/agent/k8s/node-1
# Output:
# Entry ID: abc-123-def-456
# SPIFFE ID: spiffe://qhx.dev/ns/production/sa/api
# Parent ID: spiffe://qhx.dev/agent/k8s/node-1
# Selectors:
# k8s:ns:production
# k8s:sa:api

Check workload received SVID:

Terminal window
# From inside workload container
ls -la /run/secrets/qhx.dev/
# Output:
# svid.pem (X.509 certificate)
# svid-key.pem (Private key)
# bundle.pem (Trust bundle)

Node attestation fails:

Terminal window
# Check agent logs
kubectl logs -n qhx-system -l app=qhx-pki-agent
# Common issues:
# - "failed to attest node: context deadline exceeded"
# → PKI Server unreachable, check NetworkPolicy
# - "failed to attest node: invalid token"
# → Service account token expired or invalid
# - "failed to attest node: PCR values do not match"
# → TPM measurements changed (BIOS update?)

Workload attestation fails:

Terminal window
# Check agent logs for workload PID
kubectl logs -n qhx-system -l app=qhx-pki-agent | grep "pid=<PID>"
# Common issues:
# - "workload does not match any registration entry"
# → No registration entry with matching selectors
# - "failed to query kubelet"
# → Agent cannot reach kubelet, check hostPID setting
# - "container not found"
# → cgroup detection failed, check containerd version

Prometheus metrics:

# Node attestation rate
rate(qhx_node_attestations_total[5m])
# Node attestation failures
rate(qhx_node_attestations_failed_total[5m])
# Workload attestation rate
rate(qhx_workload_attestations_total[5m])
# Workload attestation failures
rate(qhx_workload_attestations_failed_total[5m])
# Registration entries per node
qhx_registration_entries_per_node

Alert examples:

groups:
- name: qhx-attestation
rules:
- alert: NodeAttestationFailureHigh
expr: rate(qhx_node_attestations_failed_total[5m]) > 0.1
annotations:
summary: "High node attestation failure rate"
- alert: WorkloadAttestationFailureHigh
expr: rate(qhx_workload_attestations_failed_total[5m]) > 1
annotations:
summary: "High workload attestation failure rate"
- alert: NoRegistrationEntriesForNode
expr: qhx_registration_entries_per_node == 0
for: 5m
annotations:
summary: "Node has no registration entries"

Threat: Agent impersonation

  • Mitigation: Use TPM or Kubernetes PSAT (cryptographically verified)
  • Don’t: Rely solely on join tokens in production

Threat: Token theft (Kubernetes)

  • Mitigation: Short token TTL, rotate service account keys
  • Don’t: Use long-lived service account tokens

Threat: IID replay (cloud)

  • Mitigation: PKI Server validates IID freshness
  • Don’t: Disable IID validation checks

Threat: Selector spoofing

  • Mitigation: Use cryptographically verifiable selectors (image hash, not label)
  • Don’t: Rely on user-controlled selectors (pod labels can be set by user)

Threat: Container escape

  • Mitigation: Use AppArmor/SELinux, see Security Hardening Guide
  • Don’t: Run privileged containers with workload identities

Threat: SVID theft

  • Mitigation: Short SVID TTL (1 hour), memory-locked keys
  • Don’t: Write SVIDs to disk or logs