📖 Configuration Reference
Every knob the driver exposes, in one place: the environment the binary reads, the Helm values that set it, and the volume context the controller hands to the node.
📜 Table of Contents
- Driver environment variables
- Command-line flags
- Helm values
- StorageClass parameters
- Volume context keys
- Deployed objects
- Sidecar versions
🌱 Driver environment variables
The binary is configured entirely through the environment. There is no config file.
| Variable | Default | Role | Purpose |
|---|---|---|---|
DRIVER_ROLE | controller | both | controller or node. Anything else is a startup error: invalid driver role "..." |
CSI_ENDPOINT | unix:///csi/csi.sock | both | gRPC listen address. The chart overrides it to unix:///var/lib/csi/sockets/pluginproxy/csi.sock for the controller |
LINODE_TOKEN | none | controller | Linode API token. Required for the controller; the process exits with ErrTokenRequired without it. The node plugin ignores it |
KUBECONFIG | unset | controller | Optional kubeconfig file for controller Kubernetes metadata. The chart passes the same file to controller sidecars. Unset keeps in-cluster configuration; an empty or invalid configured file fails startup without falling back. The node role ignores it. |
NFS_ACCESS_POLICY_MODE | space | controller | space disables CSI attachment and relies on the Space access policy; node enables publish/unpublish and maintains each filesystem’s Linode ACL. Any other value is a startup error |
NODE_NAME | none | node | The node’s Kubernetes name, injected from spec.nodeName. Metadata resolution fails without it |
TIMEOUT | 10s | controller | Per-request Linode API client timeout, as a Go duration. An unparseable value logs a warning and falls back to 10s |
LINODE_URL | https://api.linode.com | controller | Linode API base URL. Read by linodego, not by the driver’s own config loading |
LINODE_API_VERSION | v4beta | controller | Linode API version. Also read by linodego |
LINODE_CA | none | controller | Path to a root CA certificate for the Linode API client. Useful only against a non-public endpoint |
main.loadConfig reads LINODE_TOKEN, TIMEOUT, LINODE_CA, DRIVER_ROLE, CSI_ENDPOINT, NODE_NAME, NFS_ACCESS_POLICY_MODE, and KUBECONFIG at startup and passes configuration explicitly into driver setup. The node role ignores KUBECONFIG. LINODE_URL and LINODE_API_VERSION are consumed by linodego directly from the environment.
TIMEOUT bounds a single API call, not an operation. Operations that wait for a resource to become active are bounded separately by a fixed 5-minute internal deadline, which is not configurable.
🚩 Command-line flags
The driver defines no flags of its own. It registers klog’s flags and forces -logtostderr=true, so the only useful one is verbosity, -v=<level>.
klog flags are command-line arguments, and the chart exposes no args value for the plugin container, so raising verbosity means patching the workload:
kubectl -n kube-system patch deploy csi-linode-nfs-controller --type=json \
-p='[{"op":"add","path":"/spec/template/spec/containers/0/args","value":["-v=4"]}]'
The levels the driver actually uses:
| Level | What appears |
|---|---|
| default | Errors and warnings |
-v=2 | Startup, role, endpoint, driver configuration |
-v=4 | Every RPC entry (handling controller rpc), mount and unmount decisions, metadata fallbacks |
-v=5 | Idempotent no-op paths, such as an unstage on an already-unmounted path |
-v=4 is the level worth reaching for when debugging; see Troubleshooting.
⛭ Helm values
Top level
| Value | Default | Description |
|---|---|---|
apiToken | "" | Linode API token. Required unless secretRef is set. The chart puts it in a Secret named linode-api-token |
namespace | kube-system | Namespace for every object the chart creates |
kubeletPath | /var/lib/kubelet | Host kubelet root. Controls the plugin socket and registration hostPaths |
linodeURL | https://api.linode.com | Sets LINODE_URL |
linodeAPIVersion | v4beta | Sets LINODE_API_VERSION |
secretRef.name | unset | Use an existing Secret instead of creating one |
secretRef.apiTokenRef | token | Key within that Secret |
driver.name | linodenfs.csi.linode.com | The CSI driver name. Changing it changes the CSIDriver object, the socket path, and the provisioner every StorageClass must use |
accessPolicy.mode | space | Authorization model. space uses the Space access policy without CSI attachment. node maintains per-filesystem Linode ACLs and requires CSI attachment, but is not currently recommended because its asynchronous per-node policy updates cause severe scheduling and cleanup delays |
podAnnotations | {} | Applied to controller and node pods |
podLabels | {} | Applied to controller and node pods |
Changing driver.name on an existing installation orphans every PV provisioned under the old name. Kubernetes routes by driver name, so those PVs become unmanageable. Do not.
Driver image
| Value | Default | Description |
|---|---|---|
image.repository | docker.io/linode/linode-filestorage-csi-driver | Driver image |
image.tag | "" | Defaults to Chart.appVersion when empty |
image.pullPolicy | IfNotPresent | |
image.podsMountDir | /var/lib/kubelet | Host directory bind-mounted Bidirectional into the node plugin. Must track kubeletPath |
image.env | [] | Extra env vars appended to the plugin container in both workloads |
image.volumes | [] | Extra volumes on both pods |
image.volumeMounts | [] | Extra mounts on the plugin container in both workloads |
image.resources | cpu: 100m, memory: 128Mi requests | Plugin container resources, applied to both roles |
image.env, image.volumes, and image.volumeMounts are shared by the controller and the node. There is no per-role override, so anything you add here lands in both.
Controller
| Value | Default | Description |
|---|---|---|
controller.enabled | true | Deploy the controller Deployment |
controller.replicaCount | 1 | Replicas. See the note below |
controller.hostNetwork | false | Host networking for the controller pod |
controller.dnsPolicy | Default | Set to ClusterFirstWithHostNet if you enable hostNetwork |
controller.kubeconfig.* | "" | mountDir and secretKey select the kubeconfig path; secretName additionally mounts an existing Secret |
controller.serviceAccount.enabled | true | Create the ServiceAccount and set serviceAccountName on the Deployment. Leave it on; see the note below |
controller.serviceAccount.name | csi-linode-nfs-controller | |
controller.rbac.enabled | true | Create the ClusterRole and ClusterRoleBinding |
controller.nodeSelector | {} | |
controller.affinity | {} | |
controller.tolerations | CriticalAddonsOnly + NoExecute | Lets the controller schedule on control-plane-ish nodes |
On controller.kubeconfig: setting mountDir and secretKey selects mountDir/secretKey for the controller plugin and sidecars. Setting secretName as well mounts that Secret at mountDir; without it, you must make the file available to each container that uses it. If the path fields are unset, they use in-cluster configuration. The node plugin ignores this setting and keeps its workload-cluster in-cluster identity. The kubeconfig identity must have the needed permissions in its target cluster; controller RBAC in the Helm release cluster does not grant remote permissions.
On serviceAccount.enabled: this flag creates the ServiceAccount and sets serviceAccountName on the pod. Setting it to false leaves the workload on the namespace’s default ServiceAccount; it does not select a user-managed ServiceAccount. Keep it enabled for the normal in-cluster installation.
On replicaCount: raising it above 1 gives you multiple driver processes, and the sidecars lease-elect among themselves so only one is active. The driver’s own in-process locks (which serialize concurrent operations on the same volume) are per-process and do not coordinate across replicas, so leader election is what keeps that safe. Leave it at 1 unless you have a reason.
Node
| Value | Default | Description |
|---|---|---|
node.enabled | true | Deploy the node DaemonSet |
node.pluginSecurityContext | privileged: true, runAsUser: 0, runAsGroup: 0 | Required. Root is needed to create the socket on the kubelet hostPath and to mount in the host namespace |
node.serviceAccount.enabled | true | Create the ServiceAccount and set serviceAccountName on the DaemonSet. Leave it on; see the note under Controller |
node.serviceAccount.name | csi-linode-nfs-node | |
node.rbac.enabled | true | Create the node ClusterRole and binding (get, list, watch on nodes) |
node.registrar.* | see below | csi-node-driver-registrar image, pull policy, resources, plus env and volumeMounts extension points |
node.livenessProbe.* | see below | livenessprobe image, pull policy, resources |
The node DaemonSet always uses hostNetwork: true; it is not a value. It has no tolerations either, so it will not schedule onto tainted nodes without help.
Sidecars
| Value | Default | Enabled by default |
|---|---|---|
sidecars.provisioner.* | registry.k8s.io/sig-storage/csi-provisioner:v6.2.0 | ✅ always |
sidecars.attacher.* | registry.k8s.io/sig-storage/csi-attacher:v4.12.0 | ✅ always |
sidecars.resizer.enabled | true | ✅ enabled by default for PVC capacity increases |
sidecars.resizer.* | registry.k8s.io/sig-storage/csi-resizer:v2.1.0 | |
sidecars.snapshotter.enabled | false | ❌ leave off; snapshots are not yet available |
sidecars.snapshotter.* | registry.k8s.io/sig-storage/csi-snapshotter:v8.2.0 | |
sidecars.livenessProbe.* | registry.k8s.io/sig-storage/livenessprobe:v2.15.0 | ✅ always |
Each block takes repository, tag, pullPolicy, and resources.
🎚 StorageClass parameters
Summarized here; the full rules, error messages, and idempotency implications are on StorageClass parameters.
| Parameter | Required | Values |
|---|---|---|
linodenfs.csi.linode.com/space-id | one of the two | positive integer as a string |
linodenfs.csi.linode.com/space-label | one of the two | exact label of an existing space |
linodenfs.csi.linode.com/tags | no | comma-separated |
linodenfs.csi.linode.com/filesystem-root-squash | no | none, root_squash, all_squash |
🧾 Volume context keys
CreateVolume returns these in the CSI volume context. Kubernetes stores them as the PV’s spec.csi.volumeAttributes and passes them back to the node plugin on every stage and publish. They are the controller’s only channel to the node, which holds no API token and cannot look anything up.
| Key | Example | Consumed by |
|---|---|---|
space-id | 42 | informational |
filesystem-id | 1337 | informational |
mount-target | fs-1337.us-ord.nfs.linode.com:/export | NodeStageVolume: preferred FQDN mount source, or an empty string when absent |
mount-target-ips | [2001:db8::1]:/export,[2001:db8::2]:/export | NodeStageVolume: comma-separated IP mount sources, in API order |
region | us-ord | informational |
The controller passes both target forms after the filesystem becomes active. It returns FailedPrecondition if neither a non-empty FQDN source nor a usable IPv6 source is available. The node prefers mount-target; otherwise, it uses the first usable IPv6 source from mount-target-ips. IPv4 and IPv4-mapped IPv6 addresses are ignored. A node request without either usable target form fails with InvalidArgument.
Existing FQDN-only PVs remain supported. During a rolling upgrade, update all node plugins before provisioning IP-only volumes. Older node plugins require a non-empty mount-target and cannot stage IP-only volumes. Rolling node plugins back has the same restriction.
The values in volumeAttributes are a snapshot from provisioning; the driver never refreshes them. Kubernetes does not allow changes to these fields on an existing PV. See missing mount target recovery to replace a PV while keeping its filesystem.
📦 Deployed objects
What a default install creates, so you know what to look for:
| Kind | Name | Notes |
|---|---|---|
CSIDriver | linodenfs.csi.linode.com | attachRequired: false in space mode or true in node mode; podInfoOnMount: false, fsGroupPolicy: File |
Deployment | csi-linode-nfs-controller | 4 containers by default |
DaemonSet | csi-linode-nfs-node | 3 containers, hostNetwork |
Secret | linode-api-token | Only when secretRef is unset |
ServiceAccount | csi-linode-nfs-controller, csi-linode-nfs-node | |
ClusterRole / ClusterRoleBinding | one pair per ServiceAccount |
Everything carries the label role: csi-linode, which is the handle for finding it all:
kubectl -n kube-system get pods -l role=csi-linode
No StorageClass and no VolumeSnapshotClass are created. Both are yours to write.
🏷 Sidecar versions
For reference when matching against your cluster’s Kubernetes version:
| Sidecar | Version | Purpose |
|---|---|---|
csi-provisioner | v6.2.0 | Watches PVCs, calls CreateVolume and DeleteVolume |
csi-attacher | v4.12.0 | Watches VolumeAttachments and calls publish/unpublish in node mode. It remains deployed but has no attachments to process in space mode |
csi-snapshotter | v8.2.0 | Watches VolumeSnapshotContents, calls the snapshot RPCs. Disabled by default; snapshots are not yet available |
csi-resizer | v2.1.0 | Handles PVC capacity increases through ControllerExpandVolume. Enabled by default |
csi-node-driver-registrar | v2.16.0 | Registers the plugin with the kubelet |
livenessprobe | v2.15.0 | Probes the driver’s own Probe RPC. Runs in both workloads |
The versions above are the chart’s defaults at the time of writing. charts/linode-nfs-csi-driver/values.yaml is the source of truth; check it before relying on a specific version.