diff --git a/stacks/mailserver/main.tf b/stacks/mailserver/main.tf index bff786cc..da131f8d 100644 --- a/stacks/mailserver/main.tf +++ b/stacks/mailserver/main.tf @@ -11,6 +11,11 @@ data "vault_kv_secret_v2" "secrets" { name = "platform" } +data "vault_kv_secret_v2" "viktor" { + mount = "secret" + name = "viktor" +} + locals { mailserver_accounts = jsondecode(data.vault_kv_secret_v2.secrets.data["mailserver_accounts"]) mailserver_aliases = jsondecode(data.vault_kv_secret_v2.secrets.data["mailserver_aliases"]) @@ -19,14 +24,16 @@ locals { } module "mailserver" { - source = "./modules/mailserver" - tls_secret_name = var.tls_secret_name - nfs_server = var.nfs_server - mysql_host = var.mysql_host - mailserver_accounts = local.mailserver_accounts - postfix_account_aliases = local.mailserver_aliases - opendkim_key = local.mailserver_opendkim_key - sasl_passwd = local.mailserver_sasl_passwd - roundcube_db_password = data.vault_kv_secret_v2.secrets.data["mailserver_roundcubemail_db_password"] - tier = local.tiers.edge + source = "./modules/mailserver" + tls_secret_name = var.tls_secret_name + nfs_server = var.nfs_server + mysql_host = var.mysql_host + mailserver_accounts = local.mailserver_accounts + postfix_account_aliases = local.mailserver_aliases + opendkim_key = local.mailserver_opendkim_key + sasl_passwd = local.mailserver_sasl_passwd + roundcube_db_password = data.vault_kv_secret_v2.secrets.data["mailserver_roundcubemail_db_password"] + tier = local.tiers.edge + brevo_api_key = jsondecode(base64decode(data.vault_kv_secret_v2.viktor.data["brevo_api_key"]))["api_key"] + email_monitor_imap_password = local.mailserver_accounts["spam@viktorbarzin.me"] } diff --git a/stacks/mailserver/modules/mailserver/main.tf b/stacks/mailserver/modules/mailserver/main.tf index c3b7bd91..06d8f815 100644 --- a/stacks/mailserver/modules/mailserver/main.tf +++ b/stacks/mailserver/modules/mailserver/main.tf @@ -5,6 +5,15 @@ variable "postfix_account_aliases" {} variable "opendkim_key" {} variable "sasl_passwd" {} # For sendgrid i.e relayhost variable "nfs_server" { type = string } +variable "brevo_api_key" { + type = string + sensitive = true +} +variable "email_monitor_imap_password" { + type = string + sensitive = true +} + # Build the virtual-alias map, dropping aliases where BOTH the source and # target are real mailboxes in var.mailserver_accounts (and are different). # Without this filter, docker-mailserver emits two passwd-file userdb lines @@ -555,61 +564,6 @@ resource "kubernetes_service" "mailserver" { # E2E Email Roundtrip Monitor # Sends test email via Brevo API, verifies delivery via IMAP, pushes metrics # ============================================================================= -# ExternalSecret syncing the probe's Vault inputs into a K8s Secret, so -# `kubectl describe cronjob email-roundtrip-monitor` no longer leaks the -# Brevo API key and IMAP password via `env[].value`. The two upstream Vault -# entries both wrap the effective secret: -# - secret/viktor → brevo_api_key = base64(JSON({"api_key": "..."})) -# - secret/platform → mailserver_accounts = JSON({"spam@viktorbarzin.me": "", ...}) -# ESO's `target.template` (engineVersion v2) runs sprig on the raw remote -# values so the rendered K8s Secret contains ONLY the two env vars the probe -# actually needs, under the exact keys `BREVO_API_KEY` and -# `EMAIL_MONITOR_IMAP_PASSWORD` so the CronJob can consume them via a single -# `env_from { secret_ref {} }` block. -resource "kubernetes_manifest" "email_roundtrip_monitor_secrets" { - manifest = { - apiVersion = "external-secrets.io/v1beta1" - kind = "ExternalSecret" - metadata = { - name = "mailserver-probe-secrets" - namespace = kubernetes_namespace.mailserver.metadata[0].name - } - spec = { - refreshInterval = "15m" - secretStoreRef = { - name = "vault-kv" - kind = "ClusterSecretStore" - } - target = { - name = "mailserver-probe-secrets" - template = { - engineVersion = "v2" - data = { - BREVO_API_KEY = "{{ .brevo_api_key_wrapped | b64dec | fromJson | dig \"api_key\" \"\" }}" - EMAIL_MONITOR_IMAP_PASSWORD = "{{ .mailserver_accounts | fromJson | dig \"spam@viktorbarzin.me\" \"\" }}" - } - } - } - data = [ - { - secretKey = "brevo_api_key_wrapped" - remoteRef = { - key = "viktor" - property = "brevo_api_key" - } - }, - { - secretKey = "mailserver_accounts" - remoteRef = { - key = "platform" - property = "mailserver_accounts" - } - }, - ] - } - } -} - resource "kubernetes_cron_job_v1" "email_roundtrip_monitor" { metadata { name = "email-roundtrip-monitor" @@ -741,10 +695,13 @@ sys.exit(0 if success else 1) ' EOT ] - env_from { - secret_ref { - name = "mailserver-probe-secrets" - } + env { + name = "BREVO_API_KEY" + value = var.brevo_api_key + } + env { + name = "EMAIL_MONITOR_IMAP_PASSWORD" + value = var.email_monitor_imap_password } resources { requests = { diff --git a/stacks/monitoring/modules/monitoring/dashboards/uk-payslip.json b/stacks/monitoring/modules/monitoring/dashboards/uk-payslip.json index d9e6cac7..123ed959 100644 --- a/stacks/monitoring/modules/monitoring/dashboards/uk-payslip.json +++ b/stacks/monitoring/modules/monitoring/dashboards/uk-payslip.json @@ -24,7 +24,7 @@ "panels": [ { "id": 1, - "title": "Monthly cash gross / net / tax / NI (RSU stripped)", + "title": "Monthly gross / net / tax / NI", "type": "timeseries", "datasource": { "type": "grafana-postgresql-datasource", @@ -95,7 +95,7 @@ "type": "grafana-postgresql-datasource", "uid": "payslips-pg" }, - "rawSql": "SELECT pay_date AS \"time\", (gross_pay - rsu_vest) AS cash_gross, net_pay, income_tax, national_insurance FROM payslip_ingest.payslip WHERE $__timeFilter(pay_date) ORDER BY pay_date", + "rawSql": "SELECT pay_date AS \"time\", gross_pay, net_pay, income_tax, national_insurance FROM payslip_ingest.payslip WHERE $__timeFilter(pay_date) ORDER BY pay_date", "format": "time_series", "refId": "A", "rawQuery": true, @@ -105,7 +105,7 @@ }, { "id": 2, - "title": "YTD cash gross (excl. RSU) with UK band thresholds", + "title": "YTD gross (this tax year) with UK band thresholds", "type": "timeseries", "datasource": { "type": "grafana-postgresql-datasource", @@ -197,7 +197,7 @@ "type": "grafana-postgresql-datasource", "uid": "payslips-pg" }, - "rawSql": "SELECT pay_date AS \"time\", SUM(gross_pay - rsu_vest) OVER (PARTITION BY tax_year ORDER BY pay_date) AS ytd_cash_gross FROM payslip_ingest.payslip WHERE $__timeFilter(pay_date) ORDER BY pay_date", + "rawSql": "SELECT pay_date AS \"time\", SUM(gross_pay) OVER (PARTITION BY tax_year ORDER BY pay_date) AS ytd_gross FROM payslip_ingest.payslip WHERE $__timeFilter(pay_date) ORDER BY pay_date", "format": "time_series", "refId": "A", "rawQuery": true, @@ -288,7 +288,7 @@ }, { "id": 4, - "title": "Effective rate & take-home % (cash-basis)", + "title": "Latest effective rate & take-home %", "type": "timeseries", "datasource": { "type": "grafana-postgresql-datasource", @@ -361,7 +361,7 @@ "type": "grafana-postgresql-datasource", "uid": "payslips-pg" }, - "rawSql": "SELECT pay_date AS \"time\", ROUND(((income_tax + national_insurance)::numeric / NULLIF(gross_pay - rsu_vest, 0)) * 100, 2) AS \"effective_rate_pct\", ROUND((net_pay::numeric / NULLIF(gross_pay - rsu_vest, 0)) * 100, 2) AS \"take_home_pct\" FROM payslip_ingest.payslip WHERE $__timeFilter(pay_date) ORDER BY pay_date", + "rawSql": "SELECT pay_date AS \"time\", ROUND(((income_tax + national_insurance)::numeric / NULLIF(gross_pay, 0)) * 100, 2) AS \"effective_rate_pct\", ROUND((net_pay::numeric / NULLIF(gross_pay, 0)) * 100, 2) AS \"take_home_pct\" FROM payslip_ingest.payslip WHERE $__timeFilter(pay_date) ORDER BY pay_date", "format": "time_series", "refId": "A", "rawQuery": true, @@ -540,7 +540,7 @@ "rawQuery": true, "editorMode": "code", "format": "table", - "rawSql": "SELECT pay_date, employer, tax_year, gross_pay, (gross_pay - rsu_vest) AS cash_gross, rsu_vest, rsu_offset, income_tax, national_insurance, pension_employee, pension_employer, student_loan, COALESCE(other_deductions, '{}'::jsonb) AS other_deductions, net_pay, validated, paperless_doc_id FROM payslip_ingest.payslip WHERE $__timeFilter(pay_date) ORDER BY pay_date DESC" + "rawSql": "SELECT pay_date, employer, tax_year, gross_pay, income_tax, national_insurance, pension_employee, pension_employer, student_loan, COALESCE(other_deductions, '{}'::jsonb) AS other_deductions, net_pay, validated, paperless_doc_id FROM payslip_ingest.payslip WHERE $__timeFilter(pay_date) ORDER BY pay_date DESC" } ] }, @@ -605,63 +605,6 @@ "rawSql": "SELECT pay_date AS \"time\", income_tax, national_insurance, pension_employee, pension_employer, student_loan FROM payslip_ingest.payslip WHERE $__timeFilter(pay_date) ORDER BY pay_date" } ] - }, - { - "id": 7, - "title": "RSU vest history (notional, taxed at Schwab)", - "type": "timeseries", - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "payslips-pg" - }, - "gridPos": { - "h": 8, - "w": 24, - "x": 0, - "y": 41 - }, - "fieldConfig": { - "defaults": { - "custom": { - "drawStyle": "bars", - "fillOpacity": 70, - "lineWidth": 1 - }, - "unit": "currencyGBP", - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "blue", - "value": null - } - ] - } - }, - "overrides": [] - }, - "options": { - "legend": { - "showLegend": true, - "placement": "bottom" - }, - "tooltip": { - "mode": "multi" - } - }, - "targets": [ - { - "refId": "A", - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "payslips-pg" - }, - "rawQuery": true, - "editorMode": "code", - "format": "time_series", - "rawSql": "SELECT pay_date AS \"time\", rsu_vest FROM payslip_ingest.payslip WHERE $__timeFilter(pay_date) AND rsu_vest > 0 ORDER BY pay_date" - } - ] } ], "refresh": "5m",