Skip to content

Signed Resources

Signed resources enable external authorities to control Kubernetes resources through cryptographic signatures. This allows policy updates and application deployments in environments where field operators should not have the ability to make arbitrary changes.

Use cases:

  • Air-gapped environments receiving updates via USB drive
  • Central authority distributing policy to remote sites
  • Trusted application deployment where only signed apps can run
  • Immutable security policies that can only be updated by specific keys
  1. Create Resources: Define Kubernetes resources (policies, deployments, services)
  2. Sign Bundle: Sign the resource set with a trusted private key
  3. Create QHxSignedResource: Encapsulate signed resources in a QHxSignedResource CRD
  4. Apply to Cluster: Load QHxSignedResource via kubectl (or USB drive)
  5. Automatic Verification: QHx Manager validates signature and applies resources
  6. Status Reporting: Success or failure is reported in the resource status

A signed resource encapsulates one or more Kubernetes resources:

apiVersion: qhx.dev/v1
kind: QHxSignedResource
metadata:
name: signed-policy-update
namespace: qhx-system
spec:
resources:
- apiVersion: qhx.dev/v1
kind: QHxClusterPolicy
metadata:
name: qhx-cluster-policy
spec:
autolabelling: true
groupMapping:
classification:
- group: "mls:classification:secret"
level: "us:s"
resourceVersion: "2026-02-02T10:00:00Z"
resourceSignature: "eyJhbGciOiJFUzI1NiIs..."

Fields:

  • resources: List of Kubernetes resources to create or update
  • resourceVersion: Version string for rollback protection
  • resourceSignature: JWS signature over canonicalized resource bundle

Signed resources use monotonic versioning to prevent rollback attacks.

Each signed resource has a resourceVersion (arbitrary string, compared lexicographically):

resourceVersion: "2026-02-02T10:00:00Z"

When a signed resource is applied, QHx adds an annotation to every created resource:

metadata:
annotations:
qhx.dev/signedResourceVersion: "2026-02-02T10:00:00Z"

Attempting to apply an older signed version fails:

Terminal window
# Applied version: "2026-02-02T10:00:00Z"
# Attempt to apply: "2026-02-01T10:00:00Z"
# Result: Rejected (earlier version)

Lexicographic comparison determines ordering:

  • "2026-02-02" > "2026-02-01" ✓
  • "v2" > "v1" ✓
  • "2.0.0" > "1.9.9" ✓ (string comparison, not semantic versioning)

Recommendation: Use ISO 8601 timestamps for unambiguous ordering.

Resources can be sealed to prevent modification except via signed updates.

Add the qhx.dev/sealed annotation:

apiVersion: qhx.dev/v1
kind: QHxClusterPolicy
metadata:
name: qhx-cluster-policy
annotations:
qhx.dev/sealed: "true"
spec:
# ... policy configuration

Once sealed:

  • Resource cannot be modified via normal kubectl operations
  • Resource cannot be deleted via normal kubectl operations
  • Status updates and some system annotations are still allowed
  • Can only be updated via QHxSignedResource

Include the sealed annotation in a QHxSignedResource:

spec:
resources:
- apiVersion: qhx.dev/v1
kind: QHxClusterPolicy
metadata:
name: qhx-cluster-policy
annotations:
qhx.dev/sealed: "true"

Prevent ad-hoc sealing by setting restrictSealing in QHxClusterPolicy:

apiVersion: qhx.dev/v1
kind: QHxClusterPolicy
metadata:
name: qhx-cluster-policy
spec:
restrictSealing: true

With restrictSealing: true:

  • Only signed resources can seal resources
  • Prevents accidental sealing that could wedge the cluster
  • Operators cannot seal arbitrary resources

Trusted signing keys are configured in QHxClusterPolicy:

apiVersion: qhx.dev/v1
kind: QHxClusterPolicy
metadata:
name: qhx-cluster-policy
spec:
trustedSigners:
- kid: "authority-key-2026"
kty: "EC"
crv: "P-256"
x: "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4"
y: "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM"
- kid: "backup-key-2026"
kty: "EC"
crv: "P-256"
x: "..."
y: "..."

Trust anchor format: JWK (JSON Web Key)

Multiple keys enable:

  • Key rotation
  • Multiple authorities (e.g., security team and ops team)
  • Backup keys

Create the resources you want to sign:

resources.yaml
apiVersion: qhx.dev/v1
kind: QHxClusterPolicy
metadata:
name: qhx-cluster-policy
annotations:
qhx.dev/sealed: "true"
spec:
autolabelling: true
groupMapping:
classification:
- group: "mls:classification:secret"
level: "us:s"
---
apiVersion: v1
kind: Namespace
metadata:
name: production
annotations:
qhx.dev/sealedForKinds: "*"
Terminal window
# Generate EC P-256 key pair
openssl ecparam -name prime256v1 -genkey -noout -out private-key.pem
# Extract public key in JWK format
qhx sign export-jwk --private-key private-key.pem > public-key.jwk
Terminal window
qhx sign create \
--resources resources.yaml \
--private-key private-key.pem \
--version "2026-02-02T10:00:00Z" \
--output signed-resources.yaml

The output signed-resources.yaml contains a QHxSignedResource with signature.

Copy signed-resources.yaml to target environment:

  • USB drive for air-gapped sites
  • Secure file transfer
  • Configuration management system
Terminal window
kubectl apply -f signed-resources.yaml

Check the QHxSignedResource status:

Terminal window
kubectl get qhxsignedresource signed-policy-update -o yaml

Status indicates success or failure:

status:
applied: true
appliedAt: "2026-02-02T10:05:00Z"
message: "Successfully applied 2 resources"

Or on failure:

status:
applied: false
message: "Signature verification failed: untrusted key"

Control what can be created without signatures.

Seal a namespace to require signed creation of specific resource types:

apiVersion: v1
kind: Namespace
metadata:
name: production
annotations:
qhx.dev/sealedForKinds: "Pod,Deployment,StatefulSet"

Now pods, deployments, and statefulsets in this namespace can only be created via signed resources.

Use * to require signatures for all resource types:

annotations:
qhx.dev/sealedForKinds: "*"

Require signed namespace creation:

apiVersion: qhx.dev/v1
kind: QHxClusterPolicy
metadata:
name: qhx-cluster-policy
spec:
requireSignedNamespaces: true

With this setting, new namespaces can only be created via signed resources.

Combined effect:

# In QHxClusterPolicy
requireSignedNamespaces: true
# In every namespace
annotations:
qhx.dev/sealedForKinds: "*"

Result: All resource creation requires signatures.

Delete resources using signed deletion directives:

apiVersion: qhx.dev/v1
kind: QHxSignedResource
metadata:
name: delete-old-policy
spec:
deletedResources:
- apiVersion: qhx.dev/v1
kind: QHxClusterPolicy
metadata:
name: old-policy
namespace: qhx-system
resourceVersion: "2026-02-03T10:00:00Z"
resourceSignature: "..."

This deletes the specified resource after signature verification.

Deploy cluster with sealed policy and trusted signing key:

apiVersion: qhx.dev/v1
kind: QHxClusterPolicy
metadata:
name: qhx-cluster-policy
annotations:
qhx.dev/sealed: "true"
qhx.dev/signedResourceVersion: "2026-01-01T00:00:00Z"
spec:
requireSignedNamespaces: true
restrictSealing: true
trustedSigners:
- kid: "authority-2026"
kty: "EC"
crv: "P-256"
x: "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4"
y: "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM"

Seal existing namespaces:

Terminal window
for ns in default kube-system; do
kubectl annotate namespace $ns qhx.dev/sealedForKinds="*" --overwrite
done

Authority creates signed application bundle:

app-bundle.yaml
apiVersion: v1
kind: Namespace
metadata:
name: trusted-app
annotations:
qhx.dev/sealedForKinds: "*"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
namespace: trusted-app
labels:
mls.qhx.dev/level: "us:s"
spec:
selector:
matchLabels:
app: api-server
template:
metadata:
labels:
app: api-server
mls.qhx.dev/level: "us:s"
spec:
containers:
- name: api
image: registry.example.mil/api-server@sha256:abc123

Sign bundle:

Terminal window
qhx sign create \
--resources app-bundle.yaml \
--private-key authority-key.pem \
--version "2026-02-02T12:00:00Z" \
--output signed-app.yaml

Distribute signed-app.yaml via USB drive to remote site.

Field operator applies:

Terminal window
kubectl apply -f signed-app.yaml

Result: Application deploys successfully because it’s signed by trusted key.

Operator tries to deploy unsigned application:

Terminal window
kubectl create namespace unauthorized-app
# Error: requireSignedNamespaces is true

Operator tries to modify sealed resource:

Terminal window
kubectl annotate namespace default foo=bar
# Error: namespace is sealed

Operator cannot circumvent restrictions without the signing key.

When QHxClusterPolicy is sealed or has signing restrictions, QHx Manager automatically enables self-protection.

Self-protection prevents:

  • Deletion of QHx CRDs
  • Modification of webhook configurations
  • Deletion of qhx-manager deployment
  • Modification of sealed QHxClusterPolicy

Protected resources:

  • qhx.dev CRDs
  • MutatingWebhookConfiguration (qhx-related)
  • ValidatingWebhookConfiguration (qhx-related)
  • Deployment/qhx-manager in qhx-system namespace
  • Service/qhx-manager in qhx-system namespace

Attempts to modify protected resources are rejected:

Terminal window
kubectl delete crd qhxclusterpolicies.qhx.dev
# Error: resource is protected by QHx self-protection

QHx updates must be delivered as signed resources:

Terminal window
# Authority signs QHx upgrade
qhx sign create \
--resources qhx-upgrade-v0.7.0.yaml \
--private-key authority-key.pem \
--version "2026-02-05T10:00:00Z" \
--output signed-upgrade.yaml
# Apply upgrade
kubectl apply -f signed-upgrade.yaml

Self-protection can only be disabled by:

  1. Applying a signed resource that unseals the QHxClusterPolicy
  2. Gaining root access to control plane nodes and manually editing etcd
  3. Modifying Kubernetes API server configuration to disable webhooks

Option 1 is the intended path. Options 2-3 require physical/root access and should be restricted.

System processes are automatically exempted from restrictions:

  • Kubernetes controllers creating pods for deployments
  • System namespaces (kube-system) are not subject to requireSignedNamespaces
  • Status updates by controllers are allowed even on sealed resources

This ensures normal cluster operation continues while enforcing restrictions on user actions.

Check trust anchors in QHxClusterPolicy:

Terminal window
kubectl get qhxclusterpolicy qhx-cluster-policy -o jsonpath='{.spec.trustedSigners}'

Verify the signing key matches one of the trusted keys:

Terminal window
# Extract kid from signed resource
kubectl get qhxsignedresource signed-policy-update -o jsonpath='{.spec.resourceSignature}' | \
base64 -d | jq -r '.kid'
# Check if kid is in trusted signers
kubectl get qhxclusterpolicy qhx-cluster-policy -o json | \
jq '.spec.trustedSigners[] | select(.kid=="authority-key-2026")'

Current version is newer than attempted version:

Terminal window
# Check current version on resource
kubectl get qhxclusterpolicy qhx-cluster-policy \
-o jsonpath='{.metadata.annotations.qhx\.dev/signedResourceVersion}'
# Attempting to apply earlier version will be rejected

Solution: Create a new signed resource with a later version.

Verify resource is sealed:

Terminal window
kubectl get <resource-type> <resource-name> \
-o jsonpath='{.metadata.annotations.qhx\.dev/sealed}'

If output is "true", resource can only be updated via signed resource.

Create signed update:

Terminal window
qhx sign create \
--resources updated-resource.yaml \
--private-key authority-key.pem \
--version "2026-02-06T10:00:00Z" \
--output signed-update.yaml
kubectl apply -f signed-update.yaml

Check qhx-manager logs:

Terminal window
kubectl logs -n qhx-system -l app=qhx-manager | grep "signed-resource"

Common issues:

  • Signature verification in progress
  • Resource conflicts (e.g., namespace already exists with different config)
  • Invalid resource definitions

If a resource is accidentally sealed and you cannot unseal it with a signed resource:

Recovery requires root access to control plane nodes:

  1. SSH to control plane node
  2. Edit etcd directly or modify API server configuration to disable webhooks
  3. Remove sealed annotation
  4. Re-enable webhooks

Prevention: Use restrictSealing: true to prevent ad-hoc sealing.

The signing key is the root of trust:

  • Store offline in Hardware Security Module (HSM) or air-gapped system
  • Use key ceremony procedures for key generation
  • Maintain backup keys in separate physical locations
  • Rotate keys periodically (update trustedSigners with new key, sign rotation with old key)

QHx supports EC keys (NIST P-256, P-384) and RSA keys (2048-bit minimum):

Terminal window
# EC P-256 (recommended)
openssl ecparam -name prime256v1 -genkey -noout -out key.pem
# EC P-384 (higher security)
openssl ecparam -name secp384r1 -genkey -noout -out key.pem
# RSA 4096 (compatibility)
openssl genrsa -out key.pem 4096

EC P-256 provides strong security with smaller signatures.

Initial trust anchor must be established securely:

  • Include in base cluster deployment
  • Physically verify during cluster installation
  • Use out-of-band verification (phone call, in-person verification)

Rotation requires signing the update with an existing trusted key.

All signed resource operations are logged:

Terminal window
kubectl logs -n qhx-system -l app=qhx-manager | grep "signed-resource"

Export logs for compliance:

Terminal window
kubectl logs -n qhx-system -l app=qhx-manager --since=24h | \
grep "signed-resource" > signed-resource-audit.log