The rclone remotes
Fill Infisical /infra/csi-rclone with the eleven values the two rclone-backed StorageClasses
need, so they provision and mount. At the end, a PVC against each class binds and a pod mounts
it.
These are the only Infisical secrets in Cold bootstrap that are neither generated by a
recipe nor copied from a provider’s console. You collect them from two web consoles, from a key
file you generate, and from rclone output on the operator machine.
Prerequisites
rcloneon the operator machine.just ops setupinstalls it at bootstrap step 3. You use it three times: to obscure the four crypt passwords, to mint the Google Drive token, and to create the Drive folder and read its id. You never write a config file for the cluster.- A Hetzner account, for the Storage Box.
- A Google account and a GCP project you can create an OAuth client in.
- Access to write Infisical
/infra/csi-rclone, and to the Proton Pass vault.
Expect an hour, most of it in two web consoles.
The artifact
The rclone INI is not a file you own. It is committed in infra/storage/app/secret.yaml, with the
credentials as {{ .KEY.Value }} placeholders, and the Infisical operator renders it into the
configData key csi-driver-rclone reads. That manifest is the authority on the config’s shape.
This page is the authority on where the values in it come from.
What is committed there is one INI with four sections: a backend, and a crypt wrapping it, for
each of the two StorageClasses in infra/storage/app/. The crypt section headers are fixed. Each
must match the remote: parameter of the class naming it (storageclass-storagebox.yaml,
storageclass-gdrive.yaml), or that class provisions nothing.
What you supply is eleven secrets under /infra/csi-rclone:
| Secret | Value | Shape |
|---|---|---|
STORAGEBOX_HOST | the subaccount hostname, not the box’s | uXXXXX-subN.your-storagebox.de |
STORAGEBOX_USER | the subaccount username | uXXXXX-subN |
STORAGEBOX_KEY_PEM | the private key file, verbatim | the file ssh-keygen wrote, -----BEGIN OPENSSH PRIVATE KEY----- through the matching END line |
STORAGEBOX_CRYPT_PASSWORD | rclone obscure output | hV3d1QoZ8kFy…, 80 characters of URL-safe base64 |
STORAGEBOX_CRYPT_PASSWORD2 | rclone obscure output | as above, a different value |
GDRIVE_CLIENT_ID | the OAuth client from the Google console | XXXXXXXXXXXX-XXXXXX….apps.googleusercontent.com |
GDRIVE_CLIENT_SECRET | the same client | GOCSPX-…, 35 characters |
GDRIVE_ROOT_FOLDER_ID | the folder id rclone lsf reports | 1AbCdEfGh…, 33 opaque characters |
GDRIVE_TOKEN | the whole {"access_token":…} JSON rclone writes, one line | {"access_token":"ya29.…","token_type":"Bearer","refresh_token":"1//…","expiry":"2026-01-01T00:00:00Z"} |
GDRIVE_CRYPT_PASSWORD | rclone obscure output | as STORAGEBOX_CRYPT_PASSWORD, a different value |
GDRIVE_CRYPT_PASSWORD2 | rclone obscure output | as above, a different value |
The shapes are illustrative, truncated with …, and no value in that column is real. Each rotates
on its own. The rotation procedures are in
Credential rotation.
Every value is a single line except STORAGEBOX_KEY_PEM, which is stored as the multi-line file it
is. rclone reads key_pem as a Go string literal, so the INI needs the newlines escaped as \n;
the manifest does that at render time with {{ .STORAGEBOX_KEY_PEM.Value | replace "\n" "\\n" }}.
Paste the key as it comes out of ssh-keygen and change nothing about it.
The Storage Box
Everything here is in the Hetzner Console, under Storage Boxes.
-
Create Storage Box. Pick a location and a size.
-
Create a subaccount for the cluster, so the credential the cluster holds is not the box’s main account. It gets its own home directory, and both a username and a hostname of the form
uXXXXX-subN. Those are theuserand thehostin the INI. The box’s ownuXXXXXhostname is not used anywhere in this procedure. -
Enable SSH support and External Reachability on the subaccount, and leave both off on the box itself. Port 22 is always active but carries SCP and SFTP only; port 23 is the one you enable, and it is the port rclone uses. External Reachability is not optional here, because the cluster is not inside Hetzner’s network. Both settings take a few minutes to apply.
Both toggles exist at the box level too, and setting them there is what the Hetzner documentation describes. Do not. The box-level toggles reach the main account, whose directory is the parent of every subaccount, and every subaccount you add later. Scoped to this subaccount, the port 23 that is exposed to the internet reaches one directory, and a subaccount added later for something else starts with no external access at all.
-
Generate a dedicated key pair. Not your admin identity. This one ends up in Infisical and in every node’s driver container.
ssh-keygen -t ed25519 -f ~/.ssh/futhark-storagebox -N '' -C futhark-csi -
Install the public half. One command, and it handles both ports:
cat ~/.ssh/futhark-storagebox.pub | ssh -p23 uXXXXX-subN@uXXXXX-subN.your-storagebox.de install-ssh-keyThe manual path is fiddlier than it looks, which is why
install-ssh-keyis the one to use: port 23 accepts only one-line OpenSSH keys and port 22 only RFC4716, so hand-uploading anauthorized_keysmeans converting the key for whichever port you skipped. -
Check it. No password prompt:
sftp -P 23 -i ~/.ssh/futhark-storagebox uXXXXX-subN@uXXXXX-subN.your-storagebox.de -
Collect the three values. Nothing here needs rclone.
STORAGEBOX_HOSTandSTORAGEBOX_USERare the subaccount’s hostname and username from step 2, bothuXXXXX-subN. The port is not a secret and is fixed at23in the manifest.STORAGEBOX_KEY_PEMis the contents of~/.ssh/futhark-storagebox:cat ~/.ssh/futhark-storageboxThe config carries the key itself rather than a path to it. The CSI driver mounts the config Secret and nothing else, so a
key_filepath would not exist inside the driver container and every mount would fail at authentication.
One option you may need and should not set pre-emptively. Port 23 gives a restricted shell, and
rclone probes it for a hash command on first use. Whether that probe succeeds against a Storage
Box is unverified here. If a mount logs errors about md5sum, add disable_hashcheck = true to
[storagebox] in infra/storage/app/secret.yaml. It is a setting, not a credential, so it belongs
in the manifest rather than in Infisical. Crypt does not use the remote’s hashes anyway.
The Google Drive credential
[gdrive] needs its own GCP project and OAuth client. rclone’s built-in client ID is shared,
heavily rate limited, and being retired during 2026.
Two settings on that client decide whether the mount survives:
- Publishing status must be
In production. An external app left inTestingis issued refresh tokens that expire after 7 days. The CSI driver mounts the config Secret read-only, so rclone cannot write a rotated token back. A stable refresh token is a hard requirement, and the failure is silent until the eighth day. See what cannot be rotated. - Scope
drive.file, notdrive. It grants access only to files the app itself created, which is the folder boundary the cluster is supposed to have, enforced by Google rather than by convention. It is also not a sensitive scope, so publishing needs no verification review.driveis restricted and triggers one.
A personal Google account rules out a service account, which is the usual answer for unattended access: a service account has no Drive quota of its own, and files it creates in a shared folder fail. So this is a user OAuth token, minted once, interactively.
In the Google Cloud console
The pages moved. What used to be “APIs & Services → OAuth consent screen” is now Google Auth Platform, split across Branding, Audience, Data Access and Clients. Instructions written against the old UI, including rclone’s own, no longer match.
- At console.cloud.google.com, create a project. It exists only to hold this client.
- APIs & Services → Enable APIs and services, search for Google Drive API, enable it. Without this the token mints fine and every call 403s.
- Google Auth Platform → Get started. Fill in Branding (an app name and a support email), then set the audience to External and give a contact email.
- Data Access → Add or remove scopes. Add
https://www.googleapis.com/auth/drive.fileand nothing else. rclone’s own documentation tells you to adddrive,docsanddrive.metadata.readonly. Do not.driveis restricted, and asking for it puts the app into a verification review it will not pass for personal use. - Audience → Add users. Add yourself. You need to be a test user to authorize before the app is published.
- Clients → Create client. Application type Desktop app, even though the operator machine may be headless. The headless flow below still uses a desktop client. Record the client ID and secret.
- Audience → Publish app. Confirm the status now reads
In production. This is the step whose omission is invisible for a week.
Minting the token
This is the one place a local rclone remote is unavoidable: GDRIVE_TOKEN only exists once rclone
has completed the OAuth flow, and it writes the result to its own config file. The remote you make
here is a means of producing GDRIVE_TOKEN and GDRIVE_ROOT_FOLDER_ID, not a draft of the
cluster’s config.
Run rclone config: n, name it gdrive, storage type drive, then the client ID and
secret from step 6. At the scope prompt, answer drive.file by value rather than by menu number,
because the numbering shifts between rclone versions. Leave service_account_file blank. Say yes to
the advanced config so you can reach root_folder_id, but leave it blank on this pass; you do not
have the id yet. Answer n to the Shared Drive question.
Then the browser. On a machine with one, take the default and let rclone open it. Headless, answer
n to “Use web browser to automatically authenticate rclone with remote?”, run the
rclone authorize "drive" "…" command it prints on a machine that has a browser, and paste the
token back. Either way Google shows an unverified-app warning. That is expected for a personal
client, and you continue past it.
The folder
With drive.file, rclone sees only what rclone created. A folder made in the Drive web UI is
invisible to the cluster no matter what you name it, so create it here:
rclone mkdir gdrive:futhark
rclone lsf --dirs-only --format pi --separator ' ' gdrive:
That id is GDRIVE_ROOT_FOLDER_ID. Every path is then relative to that folder, which is what puts
the CSI driver’s <namespace>/<pvc> inside it rather than at the root of your Drive.
The crypt passwords
The four crypt passwords are the only values with no console and no file behind them. You generate them. They are not plaintext: rclone stores them obscured, and a plaintext value fails at mount time on the base64 decode rather than being read as the password. Generate each with:
rclone obscure "$(openssl rand -base64 32)"
Four values, all distinct. The two crypts do not share a password, so a leak of one does not read the other’s data. Copy all four into the Proton Pass vault before they go into Infisical, for the same reason as the restic repository password in Backup and recovery: lose one and its data is ciphertext forever, and Infisical is not a backup of itself. These are among the values that cannot be rotated.
Verifying locally first
Optional. Checking it worked catches the same failures a sync cycle later. Do this if you would rather find a bad credential on the operator machine than in a driver log.
The local config already has a working gdrive remote. Add the two crypt sections and a
storagebox section to it, taking the section headers and remote = values from
infra/storage/app/secret.yaml. Use key_file = ~/.ssh/futhark-storagebox in the local
[storagebox], not key_pem: an INI value cannot span lines, which is exactly why the cluster’s
config escapes the key instead.
Both crypts must then resolve. An empty listing is a pass, an error is not:
rclone lsd storagebox-crypt:
rclone lsd gdrive-crypt:
This exercises the credentials, the crypt passwords and the Drive folder id. It does not exercise the escaping, which exists only in the rendered config, so check that after the sync.
Upload and clean up
Put the eleven values into Infisical /infra/csi-rclone. Nine come from the two consoles, from
ssh-keygen and from rclone obscure above. The remaining two, GDRIVE_TOKEN and
GDRIVE_ROOT_FOLDER_ID, are read out of the local rclone config; rclone config file prints its
path.
Then destroy the local copies. They hold the Drive refresh token and the Storage Box private key in the clear, and nothing on the operator machine reads them again:
shred -u "$(rclone config file | tail -1)" ~/.ssh/futhark-storagebox
Keep ~/.ssh/futhark-storagebox.pub if you want to recognise the key on the box later; the
private half now lives only in Infisical and Proton Pass.
Checking it worked
Confirm the operator rendered the INI and that no placeholder was left unfilled. A missing Infisical
secret does not fail the sync; it renders as <no value>:
kubectl -n csi-rclone get secret storagebox-secret -o jsonpath='{.data.configData}' | base64 -d
key_pem is the exception, because it is the one value the template passes through a function. A
missing STORAGEBOX_KEY_PEM fails the render with invalid value; expected string, and the
InfisicalStaticSecret reports it:
kubectl -n csi-rclone describe infisicalstaticsecret storagebox-secret
In the rendered INI, key_pem is one line, and its \n are two characters each. If you see the
key spanning several lines, the value in Infisical was escaped by hand before it was stored. Store
the file verbatim and let the manifest escape it.
The end-to-end test is a PVC. Create one against each class, watch it bind, and delete it:
kubectl -n default create -f - <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: rclone-smoke
spec:
accessModes: [ReadWriteOnce]
storageClassName: storagebox-crypt
resources:
requests:
storage: 1Gi
EOF
kubectl -n default get pvc rclone-smoke -w
Binding proves provisioning, not mounting. The mount happens when a pod attaches. If a pod stays
in ContainerCreating, the reason is in the node driver’s rclone container:
kubectl -n csi-rclone get pods -o wide
kubectl -n csi-rclone logs <the node pod on the pod's node> -c rclone --tail=50
The node DaemonSet is what mounts, so the pod you want is the one on the same node as the stuck
workload. That is what -o wide is for.
Both classes are reclaimPolicy: Retain, so deleting the PVC leaves the directory on the remote.
Remove it with rclone purge if you do not want the smoke test’s leftovers.
Reading the remotes afterwards
Once both classes work, files.$SUB_INTERNAL.$DOMAIN browses each remote at its root, decrypted,
in a browser. That is the faster check on any later day: a directory listing there exercises the
credentials, the crypt passwords and the mount in one page load. It is also a full read-write view
of everything on the Storage Box, so read
Copyparty before using it.