feat(storage): migrate all sensitive services to proxmox-lvm-encrypted

Reconcile Terraform with cluster state after manual encrypted PVC migrations
and complete the remaining unfinished migrations. All services storing
sensitive data now use LUKS2-encrypted block storage via the Proxmox CSI
plugin.

## Context

Only Technitium DNS was using encrypted storage in Terraform. Many services
had been manually migrated to encrypted PVCs in the cluster, but Terraform
was never updated — creating dangerous state drift where a `tg apply` could
recreate unencrypted PVCs.

## This change

Phase 0 — Infrastructure:
- Add `proxmox-lvm-encrypted` StorageClass to Helm values (extraParameters)
- Add ExternalSecret for LUKS encryption passphrase to Terraform
- Fix CSI node plugin memory: `node.plugin.resources` (not `node.resources`)
  with 1280Mi limit for LUKS2 Argon2id key derivation

Phase 1 — TF state reconciliation (zero downtime):
- Health, Matrix, N8N, Forgejo, Vaultwarden, Mailserver: state rm + import
- Redis, DBAAS MySQL, DBAAS PostgreSQL: Helm/CNPG value updates

Phase 2 — Data migration (encrypted PVCs existed but unused):
- Headscale, Frigate, MeshCentral: rsync + switchover
- Nextcloud (20Gi): rsync + chart_values update

Phase 3 — New encrypted PVCs:
- Roundcube HTML, HackMD, Affine, DBAAS pgadmin: create + rsync + switchover

Phase 4 — Cleanup:
- Deleted 5 orphaned unencrypted PVCs

## Services migrated (18 PVCs across 14 namespaces)

```
vaultwarden     → vaultwarden-data-encrypted
dbaas           → datadir-mysql-cluster-0, pg-cluster-{1,2}, dbaas-pgadmin-encrypted
mailserver      → mailserver-data-encrypted, roundcubemail-{enigma,html}-encrypted
nextcloud       → nextcloud-data-encrypted
forgejo         → forgejo-data-encrypted
matrix          → matrix-data-encrypted
n8n             → n8n-data-encrypted
affine          → affine-data-encrypted
health          → health-uploads-encrypted
hackmd          → hackmd-data-encrypted
redis           → redis-data-redis-node-{0,1}
headscale       → headscale-data-encrypted
frigate         → frigate-config-encrypted
meshcentral     → meshcentral-{data,files}-encrypted
```

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Viktor Barzin 2026-04-15 20:15:30 +00:00
parent aafb7eea34
commit 8b004c4c94
16 changed files with 151 additions and 96 deletions

View file

@ -117,10 +117,10 @@ resource "kubernetes_config_map" "mailserver_config" {
}
EOF
# Increase max IMAP connections per user+IP - all Roundcube connections come from same pod IP
"dovecot.cf" = <<-EOF
"dovecot.cf" = <<-EOF
mail_max_userip_connections = 50
EOF
fail2ban_conf = <<-EOF
fail2ban_conf = <<-EOF
[DEFAULT]
#logtarget = /var/log/fail2ban.log
@ -167,10 +167,10 @@ resource "kubernetes_secret" "opendkim_key" {
}
resource "kubernetes_persistent_volume_claim" "data_proxmox" {
resource "kubernetes_persistent_volume_claim" "data_encrypted" {
wait_until_bound = false
metadata {
name = "mailserver-data-proxmox"
name = "mailserver-data-encrypted"
namespace = kubernetes_namespace.mailserver.metadata[0].name
annotations = {
"resize.topolvm.io/threshold" = "80%"
@ -180,7 +180,7 @@ resource "kubernetes_persistent_volume_claim" "data_proxmox" {
}
spec {
access_modes = ["ReadWriteOnce"]
storage_class_name = "proxmox-lvm"
storage_class_name = "proxmox-lvm-encrypted"
resources {
requests = {
storage = "2Gi"
@ -447,7 +447,7 @@ resource "kubernetes_deployment" "mailserver" {
volume {
name = "data"
persistent_volume_claim {
claim_name = kubernetes_persistent_volume_claim.data_proxmox.metadata[0].name
claim_name = kubernetes_persistent_volume_claim.data_encrypted.metadata[0].name
}
# iscsi {
# target_portal = "iscsi.viktorbarzin.lan:3260"

View file

@ -40,10 +40,10 @@ resource "kubernetes_config_map" "roundcubemail_config" {
}
resource "kubernetes_persistent_volume_claim" "roundcube_html_proxmox" {
resource "kubernetes_persistent_volume_claim" "roundcube_html_encrypted" {
wait_until_bound = false
metadata {
name = "roundcubemail-html-proxmox"
name = "roundcubemail-html-encrypted"
namespace = kubernetes_namespace.mailserver.metadata[0].name
annotations = {
"resize.topolvm.io/threshold" = "80%"
@ -53,7 +53,7 @@ resource "kubernetes_persistent_volume_claim" "roundcube_html_proxmox" {
}
spec {
access_modes = ["ReadWriteOnce"]
storage_class_name = "proxmox-lvm"
storage_class_name = "proxmox-lvm-encrypted"
resources {
requests = {
storage = "1Gi"
@ -62,10 +62,10 @@ resource "kubernetes_persistent_volume_claim" "roundcube_html_proxmox" {
}
}
resource "kubernetes_persistent_volume_claim" "roundcube_enigma_proxmox" {
resource "kubernetes_persistent_volume_claim" "roundcube_enigma_encrypted" {
wait_until_bound = false
metadata {
name = "roundcubemail-enigma-proxmox"
name = "roundcubemail-enigma-encrypted"
namespace = kubernetes_namespace.mailserver.metadata[0].name
annotations = {
"resize.topolvm.io/threshold" = "80%"
@ -75,7 +75,7 @@ resource "kubernetes_persistent_volume_claim" "roundcube_enigma_proxmox" {
}
spec {
access_modes = ["ReadWriteOnce"]
storage_class_name = "proxmox-lvm"
storage_class_name = "proxmox-lvm-encrypted"
resources {
requests = {
storage = "1Gi"
@ -213,13 +213,13 @@ resource "kubernetes_deployment" "roundcubemail" {
volume {
name = "html"
persistent_volume_claim {
claim_name = kubernetes_persistent_volume_claim.roundcube_html_proxmox.metadata[0].name
claim_name = kubernetes_persistent_volume_claim.roundcube_html_encrypted.metadata[0].name
}
}
volume {
name = "enigma"
persistent_volume_claim {
claim_name = kubernetes_persistent_volume_claim.roundcube_enigma_proxmox.metadata[0].name
claim_name = kubernetes_persistent_volume_claim.roundcube_enigma_encrypted.metadata[0].name
}
}
dns_config {