etcd backup & restore¶
etcd holds all your cluster state — every object, every Secret. Lose it and the cluster is gone. CAPVCO gives you encrypted, WORM-protected etcd snapshots on a schedule, and — just as important — a restore procedure that has actually been exercised, not just written down.
Turn it on¶
Use cluster-template-backup.yaml, or add spec.backup to a VCOCluster. You need a BYO S3
bucket with Object Lock + versioning enabled (see the prerequisite below), and an S3 credentials
Secret.
kubectl create secret generic my-cluster-backup-s3 \
--from-literal=accessKeyID=<key> \
--from-literal=secretAccessKey=<secret>
VCO_LOCATION=<loc> \
VCO_IMAGE_ID=<prebaked-image-id-in-that-location> \
VCO_BACKUP_S3_BUCKET=my-etcd-backups \
VCO_BACKUP_S3_ENDPOINT=<s3-endpoint> \
clusterctl generate cluster my-cluster \
--kubernetes-version v1.36.4 \
--infrastructure vco --flavor backup \
--worker-machine-count 1 > my-cluster.yaml
kubectl apply -f my-cluster.yaml
Create the bucket with Object Lock at creation time
S3 Object Lock can only be enabled when a bucket is created, never added later. Create the
bucket up front with Object Lock + versioning on. CAPVCO validates this at reconcile and
raises BackupReady=False rather than silently running without WORM protection — it only ever
writes, lists, and (when retention expires) deletes.
The spec.backup fields¶
spec:
backup:
s3:
endpoint: <s3-endpoint> # empty = AWS S3
region: us-east-1
bucket: my-etcd-backups # required
prefix: clusters
forcePathStyle: true # required by most non-AWS S3
credentialsSecretRef: my-cluster-backup-s3 # keys: accessKeyID, secretAccessKey
schedule: "0 * * * *" # hourly (finest tier cadence)
retention: {hourly: 24, daily: 7, weekly: 4, monthly: 6}
objectLock: {enabled: true, mode: compliance} # or governance
encryption: {mode: age, recipient: ""} # empty = provider-generated key
Pin the backup image for a hardened deployment
The snapshot CronJob runs as root on a control-plane node — kubeadm writes
the etcd key 0600 root:root, so the snapshot can't read it otherwise — with
the host etcd PKI mounted read-only and the S3 credentials in its env. That
blast radius is deliberately tightened (the pod drops all Linux capabilities,
disables privilege escalation, and runs the default seccomp profile), but the
image it runs is the provider's own manager image, which defaults to the
mutable :latest tag. For a reproducible / hardened deployment, set
BACKUP_IMAGE on the manager to a digest-pinned reference
(…/cluster-api-provider-vco@sha256:…) so a tag re-point can't change what
runs with that access on your next snapshot.
Grandfather-father-son retention¶
The snapshot job runs at the finest cadence (hourly). At upload time each snapshot is classified into the tiers it "opens" — if there's no snapshot yet for the current hour / day / ISO-week / month, this one becomes that tier's keeper (one snapshot can open several tiers at once). You keep the last N of each tier:
| Tier | Default keep | Variable |
|---|---|---|
| hourly | 24 | VCO_BACKUP_KEEP_HOURLY |
| daily | 7 | VCO_BACKUP_KEEP_DAILY |
| weekly | 4 | VCO_BACKUP_KEEP_WEEKLY |
| monthly | 6 | VCO_BACKUP_KEEP_MONTHLY |
A count of 0 disables a tier. This tier-classification approach (rather than "keep last N objects")
is deliberate — it plays correctly with Object Lock, where you can't delete early.
Object Lock (WORM)¶
With objectLock.enabled: true, each snapshot is written with a retain-until date equal to the
lifetime of the longest tier it belongs to, so it can't be deleted (or ransomware-encrypted) before
it legitimately expires:
compliance(default) — cannot be shortened or bypassed by anyone, including the account root. Ransomware-grade. Use this.governance— allows privileged bypass. The weaker option.
Encryption (age)¶
etcd contains every Secret, so snapshots are client-side encrypted with age before they leave the node — S3 never sees plaintext.
- BYO recipient (recommended): set
encryption.recipientto your age public key (age1…). CAPVCO encrypts to it and never holds a private key — you own the identity, off cluster. - Provider-generated (fallback): leave
recipientempty and CAPVCO generates a keypair, storing the identity as a management-cluster Secret<cluster>-etcd-backup-age. TheKeyEscrowedcondition stays amber until you copy that identity somewhere safe and acknowledge it:
# export and store the private identity OFF the management cluster:
kubectl get secret <cluster>-etcd-backup-age -o jsonpath='{.data.identity}' | base64 -d > backup-identity.txt
# then acknowledge escrow:
kubectl annotate vcocluster <cluster> \
capvco.infrastructure.cluster.x-k8s.io/backup-key-escrowed=true --overwrite
No identity, no restore
The private age identity must outlive the workload cluster. If you lose it, your backups are unrecoverable ciphertext. BYO-recipient is the safest posture because the key never touches the cluster at all.
Restore¶
There is no push-button restore in this release — deliberately. Restoring etcd is destructive and
situational, so it's a guided runbook backed by safety-gated helper commands
(capvco-backup fetch, and the restore-datadir helper). The full step-by-step, with the safety
gates and the geo specifics, is the etcd restore runbook.
The shape of it:
- Confirm the cluster is genuinely unrecoverable (not just slow or partitioned).
- Pause Cluster API so KCP doesn't remediate into your restore:
- Pick a snapshot (newest good, or the last one before corruption).
capvco-backup fetch— download + age-decrypt + verify checksum.- Restore into a staged dir and atomically swap it into
/var/lib/etcdon one fresh control-plane node;kubeadm initagainst it. - Re-point the endpoint; bring the other control-plane nodes up empty to join fresh (never restore multiple members in parallel).
- Verify quorum + workloads, then un-pause CAPI.
The helpers refuse to touch a data-dir unless etcd is provably dead (or you pass --force), verify
the checksum before restoring, and always stage + atomic-swap rather than overwrite in place.
Practise it
A restore path you've never run is a wish, not a plan. Do a dry run against a throwaway cluster
so the runbook is muscle memory before you need it for real. The fetch → decrypt →
etcdctl snapshot status path has been validated against real snapshots.
Next: make nodes boot fast and offline → Pre-baked images.