feat(tripit): encrypted personal-document vault PVC + DOCUMENT_ENCRYPTION_KEY
Add a proxmox-lvm-encrypted RWO PVC (tripit-personal-documents) mounted at /data/personal-documents on the app container, PERSONAL_STORAGE_DIR env, and the DOCUMENT_ENCRYPTION_KEY ExternalSecret entry (seeded in Vault secret/tripit). A root chown init-container makes the block volume writable by the non-root app without touching the NFS doc vault. Backs the new owner-only encrypted personal document vault in the tripit app. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
27989cd9f1
commit
deb031cc2c
2 changed files with 87 additions and 20 deletions
|
|
@ -115,7 +115,7 @@
|
||||||
| priority-pass | Boarding pass color transformer | priority-pass |
|
| priority-pass | Boarding pass color transformer | priority-pass |
|
||||||
| status-page | Status page | status-page |
|
| status-page | Status page | status-page |
|
||||||
| plotting-book | Book plotting/world-building app | plotting-book |
|
| plotting-book | Book plotting/world-building app | plotting-book |
|
||||||
| tripit | Self-hosted TripIt-clone travel-itinerary PWA (FastAPI + SvelteKit SPA, same-origin). CNPG (`tripit` db, Vault static role `pg-tripit`) + RWX NFS doc vault (`/srv/nfs/tripit-documents`). `auth=required` (Authentik forward-auth, reads `X-authentik-email`); second `auth=none` ingress on `/api/calendar` for HMAC-token-gated `.ics` feed. Email-ingest CronJob `tripit-ingest-mail` (`*/30`) parses me@viktorbarzin.me via read-only IMAP with local LLM (`qwen3vl-4b`); plus `tripit-poll-flights` + `tripit-run-reminders`. App secrets in Vault `secret/tripit`. | tripit |
|
| tripit | Self-hosted TripIt-clone travel-itinerary PWA (FastAPI + SvelteKit SPA, same-origin). CNPG (`tripit` db, Vault static role `pg-tripit`) + RWX NFS trip-doc vault (`/srv/nfs/tripit-documents`) + RWO `proxmox-lvm-encrypted` personal-document vault `tripit-personal-documents` (passports/IDs — AES-256-GCM app-layer envelope, master key `DOCUMENT_ENCRYPTION_KEY` in `secret/tripit`). `auth=required` (Authentik forward-auth, reads `X-authentik-email`); second `auth=none` ingress on `/api/calendar` for HMAC-token-gated `.ics` feed. Email-ingest CronJob `tripit-ingest-mail` (`*/30`) parses me@viktorbarzin.me via read-only IMAP with local LLM (`qwen3vl-4b`); plus `tripit-poll-flights` + `tripit-run-reminders`. App secrets in Vault `secret/tripit`. | tripit |
|
||||||
|
|
||||||
## Cloudflare Domains
|
## Cloudflare Domains
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,14 +29,15 @@ locals {
|
||||||
# at the RWX NFS PVC — the app's default ./var is not writable by the
|
# at the RWX NFS PVC — the app's default ./var is not writable by the
|
||||||
# non-root user.
|
# non-root user.
|
||||||
app_env = {
|
app_env = {
|
||||||
AUTH_MODE = "forwardauth"
|
AUTH_MODE = "forwardauth"
|
||||||
SERVE_FRONTEND_DIR = "/app/frontend_build"
|
SERVE_FRONTEND_DIR = "/app/frontend_build"
|
||||||
STORAGE_DIR = "/data/documents"
|
STORAGE_DIR = "/data/documents"
|
||||||
FLIGHT_PROVIDER = "fake"
|
PERSONAL_STORAGE_DIR = "/data/personal-documents"
|
||||||
WEATHER_PROVIDER = "openmeteo"
|
FLIGHT_PROVIDER = "fake"
|
||||||
PUSH_PROVIDER = "webpush"
|
WEATHER_PROVIDER = "openmeteo"
|
||||||
LLM_MODE = "fake"
|
PUSH_PROVIDER = "webpush"
|
||||||
MAIL_INGEST_ENABLED = "false"
|
LLM_MODE = "fake"
|
||||||
|
MAIL_INGEST_ENABLED = "false"
|
||||||
# Outbound mail for linked-email verification — submitted via the cluster
|
# Outbound mail for linked-email verification — submitted via the cluster
|
||||||
# mailserver as spam@ (which relays out via Brevo). SMTP_PASSWORD comes from
|
# mailserver as spam@ (which relays out via Brevo). SMTP_PASSWORD comes from
|
||||||
# tripit-secrets (mapped to the existing PLANS_IMAP_PASSWORD). PUBLIC_BASE_URL
|
# tripit-secrets (mapped to the existing PLANS_IMAP_PASSWORD). PUBLIC_BASE_URL
|
||||||
|
|
@ -106,6 +107,7 @@ resource "kubernetes_manifest" "external_secret" {
|
||||||
{ secretKey = "VAPID_PRIVATE_KEY", remoteRef = { key = "tripit", property = "VAPID_PRIVATE_KEY" } },
|
{ secretKey = "VAPID_PRIVATE_KEY", remoteRef = { key = "tripit", property = "VAPID_PRIVATE_KEY" } },
|
||||||
{ secretKey = "VAPID_SUBJECT", remoteRef = { key = "tripit", property = "VAPID_SUBJECT" } },
|
{ secretKey = "VAPID_SUBJECT", remoteRef = { key = "tripit", property = "VAPID_SUBJECT" } },
|
||||||
{ secretKey = "CALENDAR_TOKEN_SECRET", remoteRef = { key = "tripit", property = "CALENDAR_TOKEN_SECRET" } },
|
{ secretKey = "CALENDAR_TOKEN_SECRET", remoteRef = { key = "tripit", property = "CALENDAR_TOKEN_SECRET" } },
|
||||||
|
{ secretKey = "DOCUMENT_ENCRYPTION_KEY", remoteRef = { key = "tripit", property = "DOCUMENT_ENCRYPTION_KEY" } },
|
||||||
{ secretKey = "IMAP_PASSWORD", remoteRef = { key = "tripit", property = "IMAP_PASSWORD" } },
|
{ secretKey = "IMAP_PASSWORD", remoteRef = { key = "tripit", property = "IMAP_PASSWORD" } },
|
||||||
# spam@viktorbarzin.me password — used only by the ingest-plans CronJob
|
# spam@viktorbarzin.me password — used only by the ingest-plans CronJob
|
||||||
# (forward-to-parse via the @viktorbarzin.me -> spam@ catch-all).
|
# (forward-to-parse via the @viktorbarzin.me -> spam@ catch-all).
|
||||||
|
|
@ -183,6 +185,37 @@ module "documents_nfs" {
|
||||||
access_modes = ["ReadWriteMany"]
|
access_modes = ["ReadWriteMany"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# RWO encrypted PVC for the PERSONAL document vault (passports, IDs). Separate
|
||||||
|
# from the RWX NFS trip-doc store: owner-private identity docs get LUKS2 at-rest
|
||||||
|
# (proxmox-lvm-encrypted) UNDER the app-layer AES-256-GCM ciphertext (defense in
|
||||||
|
# depth). RWO is safe because the Deployment is replicas=1 + Recreate (single
|
||||||
|
# writer); only the app container mounts it, not the worker CronJobs.
|
||||||
|
resource "kubernetes_persistent_volume_claim" "personal_documents" {
|
||||||
|
wait_until_bound = false
|
||||||
|
metadata {
|
||||||
|
name = "tripit-personal-documents"
|
||||||
|
namespace = kubernetes_namespace.tripit.metadata[0].name
|
||||||
|
annotations = {
|
||||||
|
"resize.topolvm.io/threshold" = "10%"
|
||||||
|
"resize.topolvm.io/increase" = "100%"
|
||||||
|
"resize.topolvm.io/storage_limit" = "5Gi"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
spec {
|
||||||
|
access_modes = ["ReadWriteOnce"]
|
||||||
|
storage_class_name = "proxmox-lvm-encrypted"
|
||||||
|
resources {
|
||||||
|
requests = {
|
||||||
|
storage = "2Gi"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lifecycle {
|
||||||
|
# Autoresizer grows requests.storage up to storage_limit; PVCs can't shrink.
|
||||||
|
ignore_changes = [spec[0].resources[0].requests]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
resource "kubernetes_deployment" "tripit" {
|
resource "kubernetes_deployment" "tripit" {
|
||||||
metadata {
|
metadata {
|
||||||
name = "tripit"
|
name = "tripit"
|
||||||
|
|
@ -235,6 +268,28 @@ resource "kubernetes_deployment" "tripit" {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# The proxmox-lvm-encrypted block PVC mounts root-owned; the app runs as
|
||||||
|
# uid 10001. chown it so the non-root app can write. Scoped to THIS block
|
||||||
|
# volume only (a pod-level fsGroup would also recursively chown the NFS
|
||||||
|
# doc vault, whose CSI fsGroupPolicy=File — risky on a root-squashed
|
||||||
|
# export). The NFS vault handles its own perms and is left untouched.
|
||||||
|
init_container {
|
||||||
|
name = "chown-personal-documents"
|
||||||
|
image = "busybox:1.37"
|
||||||
|
command = ["sh", "-c", "chown -R 10001:999 /data/personal-documents"]
|
||||||
|
security_context {
|
||||||
|
run_as_user = 0
|
||||||
|
}
|
||||||
|
volume_mount {
|
||||||
|
name = "personal-documents"
|
||||||
|
mount_path = "/data/personal-documents"
|
||||||
|
}
|
||||||
|
resources {
|
||||||
|
requests = { cpu = "10m", memory = "16Mi" }
|
||||||
|
limits = { memory = "32Mi" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
container {
|
container {
|
||||||
name = "tripit"
|
name = "tripit"
|
||||||
image = local.image
|
image = local.image
|
||||||
|
|
@ -263,6 +318,11 @@ resource "kubernetes_deployment" "tripit" {
|
||||||
mount_path = "/data/documents"
|
mount_path = "/data/documents"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
volume_mount {
|
||||||
|
name = "personal-documents"
|
||||||
|
mount_path = "/data/personal-documents"
|
||||||
|
}
|
||||||
|
|
||||||
readiness_probe {
|
readiness_probe {
|
||||||
http_get {
|
http_get {
|
||||||
path = "/healthz"
|
path = "/healthz"
|
||||||
|
|
@ -292,6 +352,13 @@ resource "kubernetes_deployment" "tripit" {
|
||||||
claim_name = module.documents_nfs.claim_name
|
claim_name = module.documents_nfs.claim_name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
volume {
|
||||||
|
name = "personal-documents"
|
||||||
|
persistent_volume_claim {
|
||||||
|
claim_name = kubernetes_persistent_volume_claim.personal_documents.metadata[0].name
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -373,9 +440,9 @@ locals {
|
||||||
# spam@'s password via imap_password_key (secret/tripit PLANS_IMAP_PASSWORD),
|
# spam@'s password via imap_password_key (secret/tripit PLANS_IMAP_PASSWORD),
|
||||||
# because env_from otherwise injects the Gmail app-password.
|
# because env_from otherwise injects the Gmail app-password.
|
||||||
ingest-plans = {
|
ingest-plans = {
|
||||||
schedule = "*/15 * * * *"
|
schedule = "*/15 * * * *"
|
||||||
command = ["python", "-m", "tripit_api", "ingest-mail"]
|
command = ["python", "-m", "tripit_api", "ingest-mail"]
|
||||||
suspend = false
|
suspend = false
|
||||||
imap_pw_secret_key = "PLANS_IMAP_PASSWORD"
|
imap_pw_secret_key = "PLANS_IMAP_PASSWORD"
|
||||||
extra_env = {
|
extra_env = {
|
||||||
LLM_MODE = "llamacpp"
|
LLM_MODE = "llamacpp"
|
||||||
|
|
@ -401,10 +468,10 @@ locals {
|
||||||
# authorization 403s the in-cluster *.svc Host header, so we reach it through
|
# authorization 403s the in-cluster *.svc Host header, so we reach it through
|
||||||
# the ingress (auth=none, api_key-gated) instead.
|
# the ingress (auth=none, api_key-gated) instead.
|
||||||
transport-nudge = {
|
transport-nudge = {
|
||||||
schedule = "0 8 * * *"
|
schedule = "0 8 * * *"
|
||||||
timezone = "Europe/London"
|
timezone = "Europe/London"
|
||||||
command = ["python", "-m", "tripit_api", "run-transport-nudge"]
|
command = ["python", "-m", "tripit_api", "run-transport-nudge"]
|
||||||
suspend = false
|
suspend = false
|
||||||
extra_env = {
|
extra_env = {
|
||||||
NUDGES_ENABLED = "true"
|
NUDGES_ENABLED = "true"
|
||||||
SLACK_PROVIDER = "slack"
|
SLACK_PROVIDER = "slack"
|
||||||
|
|
@ -413,10 +480,10 @@ locals {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
weather-brief = {
|
weather-brief = {
|
||||||
schedule = "0 21 * * *"
|
schedule = "0 21 * * *"
|
||||||
timezone = "Europe/London"
|
timezone = "Europe/London"
|
||||||
command = ["python", "-m", "tripit_api", "run-weather-brief"]
|
command = ["python", "-m", "tripit_api", "run-weather-brief"]
|
||||||
suspend = false
|
suspend = false
|
||||||
extra_env = {
|
extra_env = {
|
||||||
NUDGES_ENABLED = "true"
|
NUDGES_ENABLED = "true"
|
||||||
SLACK_PROVIDER = "slack"
|
SLACK_PROVIDER = "slack"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue