Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

  • rclone on the operator machine. just ops setup installs 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:

SecretValueShape
STORAGEBOX_HOSTthe subaccount hostname, not the box’suXXXXX-subN.your-storagebox.de
STORAGEBOX_USERthe subaccount usernameuXXXXX-subN
STORAGEBOX_KEY_PEMthe private key file, verbatimthe file ssh-keygen wrote, -----BEGIN OPENSSH PRIVATE KEY----- through the matching END line
STORAGEBOX_CRYPT_PASSWORDrclone obscure outputhV3d1QoZ8kFy…, 80 characters of URL-safe base64
STORAGEBOX_CRYPT_PASSWORD2rclone obscure outputas above, a different value
GDRIVE_CLIENT_IDthe OAuth client from the Google consoleXXXXXXXXXXXX-XXXXXX….apps.googleusercontent.com
GDRIVE_CLIENT_SECRETthe same clientGOCSPX-…, 35 characters
GDRIVE_ROOT_FOLDER_IDthe folder id rclone lsf reports1AbCdEfGh…, 33 opaque characters
GDRIVE_TOKENthe 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_PASSWORDrclone obscure outputas STORAGEBOX_CRYPT_PASSWORD, a different value
GDRIVE_CRYPT_PASSWORD2rclone obscure outputas 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.

  1. Create Storage Box. Pick a location and a size.

  2. 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 the user and the host in the INI. The box’s own uXXXXX hostname is not used anywhere in this procedure.

  3. 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.

  4. 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
    
  5. 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-key
    

    The manual path is fiddlier than it looks, which is why install-ssh-key is the one to use: port 23 accepts only one-line OpenSSH keys and port 22 only RFC4716, so hand-uploading an authorized_keys means converting the key for whichever port you skipped.

  6. Check it. No password prompt:

    sftp -P 23 -i ~/.ssh/futhark-storagebox uXXXXX-subN@uXXXXX-subN.your-storagebox.de
    
  7. Collect the three values. Nothing here needs rclone. STORAGEBOX_HOST and STORAGEBOX_USER are the subaccount’s hostname and username from step 2, both uXXXXX-subN. The port is not a secret and is fixed at 23 in the manifest. STORAGEBOX_KEY_PEM is the contents of ~/.ssh/futhark-storagebox:

    cat ~/.ssh/futhark-storagebox
    

    The config carries the key itself rather than a path to it. The CSI driver mounts the config Secret and nothing else, so a key_file path 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 in Testing is 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, not drive. 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. drive is 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.

  1. At console.cloud.google.com, create a project. It exists only to hold this client.
  2. APIs & Services → Enable APIs and services, search for Google Drive API, enable it. Without this the token mints fine and every call 403s.
  3. 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.
  4. Data Access → Add or remove scopes. Add https://www.googleapis.com/auth/drive.file and nothing else. rclone’s own documentation tells you to add drive, docs and drive.metadata.readonly. Do not. drive is restricted, and asking for it puts the app into a verification review it will not pass for personal use.
  5. Audience → Add users. Add yourself. You need to be a test user to authorize before the app is published.
  6. 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.
  7. 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.