Enable Transport Security¶
This guide shows you how to encrypt volume traffic in flight: NVMe-TCP block volumes with per-volume pre-shared keys, and NFS filesystem volumes with mutual TLS. Once enabled, volumes provisioned from a TLS-enabled StorageClass fail closed rather than falling back to plaintext.
Before you rely on this, understand what it proves. Mutual TLS gives you machine identity and confidentiality, not per-node authorization — the kernel validates client certificates only as CA membership. Read Transport Security, particularly the security boundary section, before designing around it.
Transport security is independent of encryption at rest. You can enable either, both, or neither.
Prerequisites¶
Before you begin, verify that you have the following:
- Kubernetes v1.36 or later with the
PodCertificateRequestfeature gate enabled on the API server, controller manager and kubelet. Node client certificates are delivered exclusively through PodCertificate projection; without the gate, no node can obtain a credential and TLS volumes will never become ready. See Version Compatibility. - Consumer and storage nodes running a kernel with
xprtsecsupport for NFS and TLS support in thenvme-tcpinitiator. - The kernel modules loaded on the relevant nodes. The chart's init containers handle this:
consumer nodes load
nvme-fabrics,nvme-tcpandnvme-keyring; storage nodes additionally load the target-side modules. Consumer nodes must never loadnvmet. - Permission to install or upgrade the zfs-csi Helm release, and to create the signing namespace.
Step 1: Enable TLS in the Chart¶
Transport TLS is controlled by the network.tls block. It is enabled by default:
Enabling it deploys three things: the certificate signer, the certificate authority material, and
privileged hostNetwork tlshd sidecars in the node and storage DaemonSets that answer the
kernel's handshake upcalls.
The tlshd sidecars start only when both the node and storage workloads are enabled. If you are
running a partial installation, TLS volumes will not become ready.
Step 2: Choose the Signing Namespace¶
The signing CA holds a private key and lives outside the driver namespace. This is deliberate: runtime node and storage identities need broad Secret read permissions in the driver namespace for dynamic NVMe pre-shared keys, and the CA signing key must not be reachable from that blast radius.
Leave signingNamespace empty to use the default — the driver namespace suffixed with
-signing — or set it explicitly:
The chart creates this namespace when it differs from the driver namespace. Storage agents consume only the CA public material and their own server leaf certificates; they never see the signing key.
Step 3: Verify the Signer Is Running¶
Install or upgrade the release, then confirm the signer is healthy:
kubectl -n <signing-namespace> get deploy
kubectl -n <signing-namespace> logs deploy/zfs-csi-tls-signer
Then confirm nodes are actually receiving certificates. Each node plugin pod should have a
projected certificate; if the PodCertificateRequest feature gate is missing, the pod will not
start cleanly and the events will say why:
kubectl -n <driver-namespace> get pods -l app.kubernetes.io/component=node
kubectl -n <driver-namespace> describe pod <node-plugin-pod>
Step 4: Enable TLS StorageClasses¶
Transport security is requested per volume through StorageClass parameters — nfsTLS for
filesystem volumes, nvmeTLS for block volumes. The chart ships TLS variants:
storageClasses:
tankNFSTLS:
enabled: true
name: zfs-tank-nfs-tls
pool: tank
nfsExportCIDRs: ["10.0.0.0/24"]
reclaimPolicy: Delete
tankNVMeTLS:
enabled: true
name: zfs-tank-nvme-tls
pool: tank
fsType: xfs
blocksize: 16k
reclaimPolicy: Delete
Set nfsExportCIDRs to your consumer-node network. This is not optional hardening — because
certificate identity does not authorize individual clients, the export CIDR list is doing real
access-control work. The chart default is a placeholder and will not admit your nodes.
nvmeTLS requires type=block with transport=nvme-tcp; nfsTLS requires type=filesystem.
The driver rejects mismatched combinations at provisioning time.
These StorageClasses are never marked as the cluster default. The chart deliberately avoids affecting PVCs belonging to other drivers.
Step 5: Verify a Volume¶
Provision a PVC from a TLS StorageClass and confirm it binds and mounts:
For a filesystem volume, confirm the mount actually negotiated TLS on the consumer node — the
mount options should include xprtsec=mtls:
kubectl -n <driver-namespace> exec <node-plugin-pod> -c <container> -- \
grep <volume-id> /proc/mounts
If the volume stays Pending or the pod cannot mount, that is the fail-closed behaviour working.
Check the tlshd sidecar logs on both the consumer and storage side.
Troubleshooting¶
Volume never becomes ready. A volume requesting TLS will not go ready until the runtime is genuinely capable of serving it — the storage side needs a valid endpoint-bound server certificate. Check the storage agent and signer logs.
Handshake fails with no obvious error. Confirm tlshd is running as a hostNetwork
privileged sidecar on both ends. The kernel's handshake upcall is delivered over netlink in the
host network namespace; a tlshd in a different namespace will never see it.
NVMe attach fails on a node that previously worked. Confirm the nvme-keyring module is
loaded. Pre-shared keys live in the kernel .nvme keyring, and keys are tagged with the network
namespace of the process that inserted them — inserter and consumer must share the host namespace.
Mount is rejected immediately. Check the export CIDRs on the StorageClass actually cover the consumer node's address. Fail-closed rejection is the correct response to an unauthorized client.
Further Reading¶
- Transport Security (explanation)
- Transport (explanation)
- Enable Per-Volume Encryption (how-to)
- Version Compatibility (reference)
- Helm Values Reference (reference)
Last Updated: July 2026