πŸš€ Installation

πŸ“œ Table of Contents

  1. Requirements
  2. Secure a Linode API access token
  3. Check your cluster first
  4. Create an NFS Storage Space
  5. Deployment methods
  6. Verify the installation
  7. Upgrading the driver
  8. Uninstalling the driver
  9. Optional configuration

πŸ”§ Requirements

Requirement Detail
Kubernetes A cluster with the CSI feature set enabled
Linode Cloud Controller Manager Required. The driver reads spec.providerID (linode://12345) off node objects to resolve Linode IDs
Region topology labels Nodes must carry topology.kubernetes.io/region (or the beta label). The CCM sets these
A VPC Every node must be attached to a VPC. The driver requires VPC-backed IPv6 connectivity to reach mount targets
A single region All workload nodes must be in one region. The controller discovers it from one Node on first provisioning and caches it for the process lifetime
An NFS Storage Space Pre-created. The driver provisions filesystems inside it but never creates the space
NFSv4 kernel support The node kernel must support NFSv4. By default the driver mounts with vers=4.1,proto=tcp6; mountOptions on the StorageClass replace those defaults. The NFS client tools ship in the plugin image, so nothing needs installing on the host
LINODE_TOKEN A Personal Access Token with read/write access to NFS resources

VPC-backed IPv6 connectivity and a correct Space access policy are required for mounts. The driver does not discover a VPC or enforce customer Space ACLs during provisioning. See Access and Networking.

πŸ” Secure a Linode API access token

Generate a Personal Access Token (PAT) in the Linode Cloud Manager. The controller uses it for every Linode API call, so scope it to what the driver actually touches:

  • NFS / file storage: read/write, to create and delete filesystems and update filesystem access policies. The controller only reads Storage Space access policies

The driver makes no Linode instance, interface, or VPC discovery calls.

Give it an expiry long enough for continued use, and plan to rotate it. The token is injected as the LINODE_TOKEN environment variable, so a rotation requires restarting the controller pod.

The node DaemonSet does not receive the token and does not call the Linode API.

🩺 Check your cluster first

Confirm that workload nodes have the provider IDs and region labels used by node identity and controller region discovery. Check their VPC and subnet assignments separately for mount reachability (see Requirements).

kubectl get nodes -o custom-columns=\
NAME:.metadata.name,\
PROVIDER:.spec.providerID,\
REGION:'.metadata.labels.topology\.kubernetes\.io/region'

Every node should show a linode:// provider ID and the same region. The controller requests one workload Node and caches its region after the first successful lookup; it does not compare all nodes. No region installation value is required, and the controller can start before workers exist. Failed discovery can retry on a later provisioning request.

Find the workload VPC ID in Cloud Manager and give it to the Space owner for access-policy configuration. The driver does not perform that lookup.

πŸ“‚ Create an NFS Storage Space

The driver never creates a Storage Space. Create one for your cluster, then note either its numeric ID or its label; every StorageClass must name one or the other.

Before creating a PVC, ask the Space owner to enable the Space access policy and add the cluster VPC to its vpc_acl. The driver neither validates nor changes this policy. If it is wrong, PVCs still bind, but pods stay ContainerCreating with mount.nfs4 errors. See Space access policy.

Create it in the Cloud Manager, or against the API directly:

curl -sS -X POST \
  -H "Authorization: Bearer ${LINODE_API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"label": "my-cluster-space"}' \
  https://api.linode.com/v4beta/nfs/spaces

The response is the new Space, and its id is the Space ID. A response looks like this (the values are examples):

{
  "id": 42,
  "label": "my-cluster-space",
  "description": null,
  "status": "active",
  "created": "2026-05-01T12:00:00Z",
  "updated": "2026-05-01T12:00:00Z",
  "tags": []
}

Creating a Space does not provision backend resources right away. They are provisioned when the first filesystem is created in a region, so the first volume you provision there can take longer than later ones.

To find the ID of an existing Space, list the Spaces on the account:

curl -sS \
  -H "Authorization: Bearer ${LINODE_API_TOKEN}" \
  https://api.linode.com/v4beta/nfs/spaces

You will reference it from a StorageClass as either:

parameters:
  linodenfs.csi.linode.com/space-id: "42"
  # or
  linodenfs.csi.linode.com/space-label: "my-cluster-space"

The two are mutually exclusive, and one of them is required. See StorageClass parameters.

πŸ”“ Admit the cluster VPC on the Space

The driver reads the Space access policy for its mTLS mode and never changes or validates its ACL. A PVC can bind while the policy is disabled or does not admit the workload VPC; the failure appears when a pod mounts the volume. If you own the Space, configure it below. Otherwise, send the Space owner these steps and the workload VPC ID.

Read the current policy first. A new policy is disabled by default.

curl -sS \
  -H "Authorization: Bearer ${LINODE_API_TOKEN}" \
  https://api.linode.com/v4beta/nfs/spaces/${SPACE_ID}/access-policy

Then enable it and add the VPC ID from the cluster check. You can also do this in Cloud Manager; the curl commands call the API directly.

curl -sS -X PUT \
  -H "Authorization: Bearer ${LINODE_API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d "{\"enabled\": true, \"vpcs\": [{\"id\": ${VPC_ID}}]}" \
  https://api.linode.com/v4beta/nfs/spaces/${SPACE_ID}/access-policy

The PUT replaces the whole VPC list. If vpc_acl in the earlier response already has entries, include them in vpcs too, with their subnet IDs if they have any. Leaving subnets out admits every subnet in the VPC.

The Linode API applies the update asynchronously. The PUT response can already list the VPC while the policy’s status is updating. Run the GET again to confirm it is active before mounting. The driver does not wait for the Space policy during provisioning.

🧰 Deployment methods

There are two ways to deploy the driver:

  1. Using Helm (recommended)
  2. Using kubectl with the released Kustomize manifest

The Helm chart in charts/linode-nfs-csi-driver is the source of truth. deploy/kubernetes/base is rendered from it by mise run update-kustomize, and CI fails if the two drift apart, so both paths deploy the same objects.

1. Using Helm

πŸ”„ Add the linode-nfs-csi repo

helm repo add linode-nfs-csi https://linode.github.io/linode-filestorage-csi-driver/
helm repo update linode-nfs-csi

Every tagged release publishes a versioned chart package and repository index to that site, so this is the only step that needs repeating when a new version lands.

πŸš€ Deploy the CSI driver

export LINODE_API_TOKEN="...your Linode API token..."

helm install linode-nfs-csi-driver \
  --namespace kube-system \
  --set apiToken="${LINODE_API_TOKEN}" \
  linode-nfs-csi/linode-nfs-csi-driver

Pin a version with --version, and use helm upgrade --install if you want the same command to work whether or not the release already exists.

Or install from a checkout of the repository:

export LINODE_TOKEN="...your Linode API token..."
mise run helm-install

mise run helm-install wraps helm upgrade --install against charts/linode-nfs-csi-driver, targeting the kube-system namespace and setting the image repository and tag from IMAGE_REPO and IMAGE_VERSION. It refuses to run without LINODE_TOKEN set.

See helm install for command documentation.

πŸ”‘ Bring your own Secret

When secretRef is unset and apiToken is provided, the chart creates a Secret named linode-api-token in the release namespace with the token under the key token.

To point at a Secret you manage instead, set secretRef and leave apiToken empty:

# values.yaml
secretRef:
  name: my-linode-secret
  apiTokenRef: token   # defaults to "token" when omitted

The chart then skips creating a Secret and wires LINODE_TOKEN to my-linode-secret’s token key.

πŸ”© Configuring the chart

Override values with --set var=value, or with a custom values file:

helm upgrade --install linode-nfs-csi-driver \
  --namespace kube-system \
  -f custom-values.yaml \
  charts/linode-nfs-csi-driver

Using a values file is the recommended approach; --set on deeply nested keys is easy to get subtly wrong. For the full list of values, see the Configuration Reference or charts/linode-nfs-csi-driver/values.yaml.

2. Using kubectl

πŸ”‘ Create the Secret first

The raw manifests do not create the API token Secret. Create it before applying them:

apiVersion: v1
kind: Secret
metadata:
  name: linode-api-token
  namespace: kube-system
stringData:
  token: "your linode api token"
type: Opaque
kubectl apply -f secret.yaml
kubectl get secret linode-api-token -n kube-system

The Secret name and key are not configurable in the raw manifests: the controller expects linode-api-token in kube-system with a token key. Use Helm if you need different names.

πŸš€ Apply the manifest

Apply the manifest attached to a release. It carries the release image tag:

# Use the latest tag from https://github.com/linode/linode-filestorage-csi-driver/releases
export VERSION="<release tag>"
kubectl apply -f https://github.com/linode/linode-filestorage-csi-driver/releases/download/${VERSION}/linode-filestorage-csi-driver-${VERSION}.yaml

Development only: kustomize build deploy/kubernetes/base | kubectl apply -f - builds from a checkout, but the base pins the :dev image tag. The release workflow does not push that tag, so the image will not pull unless you build and push your own. Use the released manifest for normal installs.

There are also deploy/kubernetes/overlays/ci and deploy/kubernetes/overlays/dev overlays, both of which currently just include the base.

Do not hand-edit deploy/kubernetes/base. It is generated. Change the Helm chart and run mise run update-kustomize.

βœ… Verify the installation

kubectl -n kube-system get pods -l role=csi-linode
kubectl get csidrivers linodenfs.csi.linode.com

You should see the controller Deployment and one node pod per node:

NAME                                       READY   STATUS    RESTARTS   AGE
csi-linode-nfs-controller-6b8f9c4d7-x2klm  5/5     Running   0          45s
csi-linode-nfs-node-2rq8w                  3/3     Running   0          45s
csi-linode-nfs-node-9fzp4                  3/3     Running   0          45s

The controller has 5 containers by default (plugin, csi-provisioner, csi-attacher, csi-resizer, liveness-probe), plus csi-snapshotter if you enabled it. Each node pod has 3 (plugin, node-driver-registrar, liveness-probe).

Confirm the node plugins registered with their kubelets:

kubectl get csinodes -o custom-columns=NODE:.metadata.name,DRIVERS:.spec.drivers[*].name

Every node should list linodenfs.csi.linode.com. If one does not, see Troubleshooting.

⏫ Upgrading the driver

export LINODE_API_TOKEN="...your Linode API token..."

helm repo update linode-nfs-csi

helm upgrade linode-nfs-csi-driver \
  --install \
  --namespace kube-system \
  --set apiToken="${LINODE_API_TOKEN}" \
  linode-nfs-csi/linode-nfs-csi-driver

helm repo update first, or Helm upgrades to the newest chart it already knows about rather than the newest chart that exists.

Upgrades roll the controller Deployment and the node DaemonSet. Mounted volumes stay mounted across a node plugin restart, because the mounts live in the host mount namespace, not the plugin container. In-flight provisioning retries once the controller is back.

Change the access-policy mode

Set one authorization model for the installation:

# Default: Space access policy only, without CSI attachment.
helm upgrade linode-nfs-csi-driver \
  --namespace kube-system \
  --reuse-values \
  --set accessPolicy.mode=space \
  linode-nfs-csi/linode-nfs-csi-driver

# Optional compatibility mode; not currently recommended because per-node
# policy updates add severe scheduling and cleanup delays.
helm upgrade linode-nfs-csi-driver \
  --namespace kube-system \
  --reuse-values \
  --set accessPolicy.mode=node \
  linode-nfs-csi/linode-nfs-csi-driver

Use space mode unless per-node Linode ACLs are a hard requirement. node mode waits for an asynchronous filesystem policy update on every CSI publish and unpublish operation, and these updates serialize for a volume. The delay grows sharply when an RWX volume is used across many nodes.

Kubernetes treats CSIDriver.spec.attachRequired as immutable. To change modes, first stop workloads using this driver, delete the linodenfs.csi.linode.com CSIDriver, then run the Helm upgrade so it is recreated with the matching value.

When changing from node to space, existing filesystems can retain an enabled Linode ACL. Recreate or explicitly migrate those filesystem policies before restarting workloads. Changing from space to node needs no policy migration: the first publish enables the filesystem policy and adds its node.

See helm upgrade for command documentation.

🧹 Uninstalling the driver

Delete workloads and PVCs before removing the driver. Once the controller is gone, nothing can process DeleteVolume, and PVs with a Delete reclaim policy will hang in Terminating with their finalizers unsatisfied.

kubectl delete pvc --all -n <your-namespace>
helm uninstall linode-nfs-csi-driver -n kube-system

Uninstalling does not remove:

  • The NFS Storage Space, which the driver never owned.
  • The Space access policy, which the driver never validates or changes.
  • Filesystems whose PVs used the Retain reclaim policy.

See helm uninstall for command documentation.

🧩 Optional configuration

PVC expansion

sidecars:
  resizer:
    enabled: true

The resizer is enabled by default and handles PVC capacity increases. Set allowVolumeExpansion: true in the StorageClass as well. The driver rejects reductions; the Linode API enforces its maximum customer-manageable capacity and returns any rejection reason to Kubernetes.

ServiceAccount and RBAC toggles

The chart creates both ServiceAccounts and their ClusterRoles/ClusterRoleBindings by default, and that is the arrangement the driver is meant to run in:

controller:
  serviceAccount:
    enabled: true
    name: csi-linode-nfs-controller
  rbac:
    enabled: true          # false: skip the ClusterRole and ClusterRoleBinding

node:
  serviceAccount:
    enabled: true
    name: csi-linode-nfs-node
  rbac:
    enabled: true

Leave serviceAccount.enabled at true. Setting it to false does not point the workload at a ServiceAccount you manage; the chart omits serviceAccountName from the pod spec entirely, so the pod runs as the namespace’s default ServiceAccount and the RBAC the driver needs is not bound to it.

rbac.enabled: false is the supported toggle if you manage roles and bindings yourself. Bind them to the ServiceAccount names above.

The controller role needs PVs, PVCs, nodes, events, secrets, CSINodes, VolumeAttachments, StorageClasses, leases, and the snapshot API group. The node role needs only get, list, and watch on nodes, which is how the node plugin resolves its own Linode ID when instance metadata is unavailable.

Alternate kubelet path

Distributions that do not use /var/lib/kubelet need both values changed together:

kubeletPath: /var/lib/k0s/kubelet
image:
  podsMountDir: /var/lib/k0s/kubelet

kubeletPath controls the plugin socket and registration directories; podsMountDir is the bidirectional host mount where pod volume directories live. Leaving them inconsistent produces a node plugin that registers but cannot publish.

Non-default Linode API endpoint

linodeURL: https://api.linode.com
linodeAPIVersion: v4beta

Both are read by linodego from the environment (LINODE_URL, LINODE_API_VERSION). The default API version is v4beta because the managed NFS endpoints are still beta.


This site uses Just the Docs, a documentation theme for Jekyll.