diff --git a/infrastructure/etcd-backup/10-storage.yaml b/infrastructure/etcd-backup/10-storage.yaml new file mode 100644 index 0000000..d7ebb8d --- /dev/null +++ b/infrastructure/etcd-backup/10-storage.yaml @@ -0,0 +1,36 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + name: naviya-etcd-backups-offsite +spec: + capacity: + storage: 10Gi + accessModes: + - ReadWriteMany + persistentVolumeReclaimPolicy: Retain + storageClassName: "" + mountOptions: + - hard + - nfsvers=4.2 + - timeo=600 + - retrans=2 + - nosuid + - nodev + - noexec + nfs: + server: 172.16.2.252 + path: /Volume1/Backup/naviya-kubernetes/etcd +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: naviya-etcd-backups-offsite + namespace: kube-system +spec: + accessModes: + - ReadWriteMany + storageClassName: "" + volumeName: naviya-etcd-backups-offsite + resources: + requests: + storage: 10Gi diff --git a/infrastructure/etcd-backup/20-cronjob.yaml b/infrastructure/etcd-backup/20-cronjob.yaml new file mode 100644 index 0000000..fc7e68c --- /dev/null +++ b/infrastructure/etcd-backup/20-cronjob.yaml @@ -0,0 +1,285 @@ +apiVersion: batch/v1 +kind: CronJob +metadata: + name: naviya-etcd-backup + namespace: kube-system +spec: + schedule: "17 3 * * *" + timeZone: Etc/UTC + concurrencyPolicy: Forbid + startingDeadlineSeconds: 3600 + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 3 + jobTemplate: + spec: + backoffLimit: 1 + activeDeadlineSeconds: 1800 + ttlSecondsAfterFinished: 86400 + template: + metadata: + labels: + app.kubernetes.io/name: naviya-etcd-backup + spec: + automountServiceAccountToken: false + hostNetwork: true + dnsPolicy: ClusterFirstWithHostNet + restartPolicy: Never + nodeSelector: + node-role.kubernetes.io/control-plane: "" + tolerations: + - key: node-role.kubernetes.io/control-plane + operator: Exists + effect: NoSchedule + securityContext: + seccompProfile: + type: RuntimeDefault + initContainers: + - name: snapshot + image: registry.k8s.io/etcd:3.6.8-0 + imagePullPolicy: IfNotPresent + command: + - etcdctl + args: + - --endpoints=https://127.0.0.1:2379 + - --cacert=/etcd-certs/ca.crt + - --cert=/etcd-certs/healthcheck-client.crt + - --key=/etcd-certs/healthcheck-client.key + - snapshot + - save + - /work/snapshot.db + securityContext: + runAsUser: 0 + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + volumeMounts: + - name: etcd-certs + mountPath: /etcd-certs + readOnly: true + - name: work + mountPath: /work + - name: validate + image: registry.k8s.io/etcd:3.6.8-0 + imagePullPolicy: IfNotPresent + command: + - etcdutl + args: + - --write-out=table + - snapshot + - status + - /work/snapshot.db + securityContext: + runAsUser: 0 + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + volumeMounts: + - name: work + mountPath: /work + readOnly: true + containers: + - name: encrypt + image: docker.io/alpine/openssl@sha256:61cec9c1f221755bf995f1f309211b222164599e2d1943b666f878ef644d3a0e + imagePullPolicy: IfNotPresent + command: ["sh", "-ec"] + args: + - | + umask 077 + + stamp="$(date -u +%Y%m%dT%H%M%SZ)" + name="naviya-etcd-${stamp}-${NODE_NAME}.db.enc" + target="/offsite/daily/${name}" + tmp="${target}.tmp.$$" + checksum="${target}.sha256" + checksum_tmp="${checksum}.tmp.$$" + verification="/work/verification.db" + + cleanup() { + rm -f \ + "$tmp" \ + "$checksum_tmp" \ + "$verification" + } + trap cleanup EXIT + + openssl enc \ + -aes-256-cbc \ + -salt \ + -pbkdf2 \ + -iter 600000 \ + -md sha256 \ + -in /work/snapshot.db \ + -out "$tmp" \ + -pass file:/key/passphrase + + test -s "$tmp" + + openssl enc \ + -d \ + -aes-256-cbc \ + -pbkdf2 \ + -iter 600000 \ + -md sha256 \ + -in "$tmp" \ + -out "$verification" \ + -pass file:/key/passphrase + + snapshot_sha="$(sha256sum /work/snapshot.db | + awk '{print $1}')" + + verification_sha="$(sha256sum "$verification" | + awk '{print $1}')" + + test "$snapshot_sha" = "$verification_sha" + + rm -f "$verification" + + echo "Chiffrement et déchiffrement de contrôle : OK" + + mv "$tmp" "$target" + + encrypted_sha="$(sha256sum "$target" | + awk '{print $1}')" + + printf '%s %s\n' \ + "$encrypted_sha" \ + "$name" \ + > "$checksum_tmp" + + mv "$checksum_tmp" "$checksum" + + ( + cd /offsite/daily + sha256sum -c "$(basename "$checksum")" + ) + + echo "Snapshot etcd quotidien validé : $target" + + copy_tmp="" + copy_checksum_tmp="" + + cleanup() { + rm -f \ + "$tmp" \ + "$checksum_tmp" \ + "$verification" + + test -z "$copy_tmp" || + rm -f "$copy_tmp" + + test -z "$copy_checksum_tmp" || + rm -f "$copy_checksum_tmp" + } + + copy_archive() { + tier="$1" + tier_dir="/offsite/${tier}" + tier_target="${tier_dir}/${name}" + copy_tmp="${tier_target}.tmp.$$" + tier_checksum="${tier_target}.sha256" + copy_checksum_tmp="${tier_checksum}.tmp.$$" + + cp "$target" "$copy_tmp" + + copy_sha="$(sha256sum "$copy_tmp" | + awk '{print $1}')" + + test "$copy_sha" = "$encrypted_sha" + mv "$copy_tmp" "$tier_target" + + printf '%s %s\n' \ + "$encrypted_sha" \ + "$name" \ + > "$copy_checksum_tmp" + + mv \ + "$copy_checksum_tmp" \ + "$tier_checksum" + + ( + cd "$tier_dir" + sha256sum -c "$(basename "$tier_checksum")" + ) + + copy_tmp="" + copy_checksum_tmp="" + + echo "Copie ${tier} validée : $tier_target" + } + + if test "$(date -u +%u)" = "7"; then + copy_archive weekly + fi + + if test "$(date -u +%d)" = "01"; then + copy_archive monthly + fi + + find /offsite/daily \ + -type f \ + \( -name '*.db.enc' -o -name '*.db.enc.sha256' \) \ + -mtime +14 \ + -delete + + find /offsite/weekly \ + -type f \ + \( -name '*.db.enc' -o -name '*.db.enc.sha256' \) \ + -mtime +56 \ + -delete + + find /offsite/monthly \ + -type f \ + \( -name '*.db.enc' -o -name '*.db.enc.sha256' \) \ + -mtime +370 \ + -delete + + find /offsite \ + -type f \ + -name '*.tmp.*' \ + -mtime +1 \ + -delete + + trap - EXIT + echo "Sauvegarde etcd chiffrée terminée avec succès" + env: + - name: NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + resources: + requests: + cpu: 25m + memory: 32Mi + limits: + cpu: 500m + memory: 256Mi + securityContext: + runAsUser: 0 + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + volumeMounts: + - name: work + mountPath: /work + - name: offsite + mountPath: /offsite + - name: encryption-key + mountPath: /key + readOnly: true + volumes: + - name: work + emptyDir: {} + - name: etcd-certs + hostPath: + path: /etc/kubernetes/pki/etcd + type: Directory + - name: offsite + persistentVolumeClaim: + claimName: naviya-etcd-backups-offsite + - name: encryption-key + secret: + secretName: naviya-etcd-backup-encryption + defaultMode: 0400 diff --git a/infrastructure/etcd-backup/kustomization.yaml b/infrastructure/etcd-backup/kustomization.yaml new file mode 100644 index 0000000..d8251ac --- /dev/null +++ b/infrastructure/etcd-backup/kustomization.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - 10-storage.yaml + - 20-cronjob.yaml