From 4c962a11142bf6e5353b1d3e2c76c41539d82bee Mon Sep 17 00:00:00 2001 From: Zack Date: Tue, 21 Jul 2026 23:35:11 +0100 Subject: [PATCH] fix(1password): retire the legacy OpenTofu and Github vaults MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OpenTofu held one hand-made discord webhook read by the cloudflare R2 billing alerts. Create that webhook in the discord/community module instead and consume its url via remote state, the same way grafana already does — no manual secret at all. cloudflare_notification_policy_webhooks.secret is optional and Cloudflare formats the payload for discord urls, so it's dropped. Github held only push-o-matic-app, an SSH-key item duplicating credentials the github-app module already manages as GITHUB_APP_IMMICH_PUSH_O_MATIC in tf. The provider can't create SSH-key items, so rather than copy it, point the four PUSH_O_MATIC_* org secrets at the existing tf item. Its pkcs8 field is a like-for-like swap for the old .private_key, which is also PKCS#8. Adds the missing client_id to the github-app module (appended last so the positional field indices in convert_certificate/converted/certificates stay valid). Also drops two dead 'OpenTofu' vault data sources that nothing referenced. --- .../modules/scoped/discord/community/webhooks.tf | 13 +++++++++++++ .../1password/account/github-apps-shared.tf | 4 ++++ .../shared/cloudflare/account/1password.tf | 4 ---- .../modules/shared/cloudflare/account/alerts.tf | 16 +++++----------- .../shared/cloudflare/account/remote-state.tf | 9 +++++++++ .../shared/cloudflare/account/terragrunt.hcl | 2 +- .../shared/cloudflare/api-keys/1password.tf | 4 ---- .../shared/github/secrets/remote-state.tf | 4 ---- .../modules/shared/github/secrets/secrets.tf | 12 +++++++++--- tf/shared/modules/secrets/github-app/outputs.tf | 1 + tf/shared/modules/secrets/github-app/secrets.tf | 10 ++++++++++ 11 files changed, 52 insertions(+), 27 deletions(-) diff --git a/tf/deployment/modules/scoped/discord/community/webhooks.tf b/tf/deployment/modules/scoped/discord/community/webhooks.tf index 5bb91ace..a510b7a0 100644 --- a/tf/deployment/modules/scoped/discord/community/webhooks.tf +++ b/tf/deployment/modules/scoped/discord/community/webhooks.tf @@ -9,6 +9,19 @@ output "leadership_alerts_grafana_webhook_url" { sensitive = true } +// Consumed by cloudflare/account/alerts.tf for the R2 billing notification +// policies. Previously a hand-created Discord webhook stored in the legacy +// "OpenTofu" 1Password vault; terraform owns it now so there's no manual secret. +resource "discord_webhook" "leadership_alerts_cloudflare" { + name = "Cloudflare" + channel_id = discord_text_channel.leadership_alerts.id +} + +output "leadership_alerts_cloudflare_webhook_url" { + value = discord_webhook.leadership_alerts_cloudflare.url + sensitive = true +} + resource "discord_webhook" "team_alerts_grafana" { name = "Grafana" channel_id = discord_text_channel.team_alerts.id diff --git a/tf/deployment/modules/shared/1password/account/github-apps-shared.tf b/tf/deployment/modules/shared/1password/account/github-apps-shared.tf index 8d7156b5..7b16470b 100644 --- a/tf/deployment/modules/shared/1password/account/github-apps-shared.tf +++ b/tf/deployment/modules/shared/1password/account/github-apps-shared.tf @@ -43,5 +43,9 @@ resource "onepassword_item" "github_app_shared" { label = "owner" value = module.github-apps.certificates[each.key].owner } + field { + label = "client_id" + value = module.github-apps.certificates[each.key].client_id + } } } diff --git a/tf/deployment/modules/shared/cloudflare/account/1password.tf b/tf/deployment/modules/shared/cloudflare/account/1password.tf index d46f350f..bf708a3b 100644 --- a/tf/deployment/modules/shared/cloudflare/account/1password.tf +++ b/tf/deployment/modules/shared/cloudflare/account/1password.tf @@ -1,7 +1,3 @@ -data "onepassword_vault" "opentofu" { - name = "OpenTofu" -} - data "onepassword_vault" "kubernetes" { name = "Kubernetes" } diff --git a/tf/deployment/modules/shared/cloudflare/account/alerts.tf b/tf/deployment/modules/shared/cloudflare/account/alerts.tf index 331338fd..1f312653 100644 --- a/tf/deployment/modules/shared/cloudflare/account/alerts.tf +++ b/tf/deployment/modules/shared/cloudflare/account/alerts.tf @@ -1,12 +1,3 @@ -data "onepassword_vault" "opentofu_vault" { - name = "OpenTofu" -} - -data "onepassword_item" "discord_leadership_alerts" { - title = "discord-leadership-alerts-webhook" - vault = data.onepassword_vault.opentofu_vault.name -} - locals { alerts_email = "alerts@immich.app" } @@ -77,9 +68,12 @@ resource "cloudflare_notification_policy" "r2_class_b_operations_alert" { } } +// The webhook is created by the discord/community module rather than being a +// hand-made webhook stashed in 1Password. `secret` is omitted deliberately: +// it's optional, and Cloudflare detects the discord URL and formats the payload +// for it, so the cf-webhook-auth header served no purpose here. resource "cloudflare_notification_policy_webhooks" "discord_leadership_alert" { account_id = var.cloudflare_account_id name = "Discord Leadership Alerts" - url = data.onepassword_item.discord_leadership_alerts.hostname - secret = data.onepassword_item.discord_leadership_alerts.credential + url = data.terraform_remote_state.discord_community.outputs.leadership_alerts_cloudflare_webhook_url } diff --git a/tf/deployment/modules/shared/cloudflare/account/remote-state.tf b/tf/deployment/modules/shared/cloudflare/account/remote-state.tf index 99df6e63..694d1c41 100644 --- a/tf/deployment/modules/shared/cloudflare/account/remote-state.tf +++ b/tf/deployment/modules/shared/cloudflare/account/remote-state.tf @@ -6,3 +6,12 @@ data "terraform_remote_state" "api_keys_state" { schema_name = "prod_cloudflare_api_keys" } } + +data "terraform_remote_state" "discord_community" { + backend = "pg" + + config = { + conn_str = var.tf_state_postgres_conn_str + schema_name = "prod_discord_community" + } +} diff --git a/tf/deployment/modules/shared/cloudflare/account/terragrunt.hcl b/tf/deployment/modules/shared/cloudflare/account/terragrunt.hcl index ca18b9af..6c18f131 100644 --- a/tf/deployment/modules/shared/cloudflare/account/terragrunt.hcl +++ b/tf/deployment/modules/shared/cloudflare/account/terragrunt.hcl @@ -11,5 +11,5 @@ include "root" { } dependencies { - paths = ["../api-keys"] + paths = ["../api-keys", "../../../scoped/discord/community"] } diff --git a/tf/deployment/modules/shared/cloudflare/api-keys/1password.tf b/tf/deployment/modules/shared/cloudflare/api-keys/1password.tf index df10b8f4..6b73538c 100644 --- a/tf/deployment/modules/shared/cloudflare/api-keys/1password.tf +++ b/tf/deployment/modules/shared/cloudflare/api-keys/1password.tf @@ -1,7 +1,3 @@ -data "onepassword_vault" "opentofu" { - name = "OpenTofu" -} - data "onepassword_vault" "kubernetes" { name = "Kubernetes" } diff --git a/tf/deployment/modules/shared/github/secrets/remote-state.tf b/tf/deployment/modules/shared/github/secrets/remote-state.tf index 93ff2ed4..3a8d3353 100644 --- a/tf/deployment/modules/shared/github/secrets/remote-state.tf +++ b/tf/deployment/modules/shared/github/secrets/remote-state.tf @@ -25,10 +25,6 @@ data "terraform_remote_state" "cloudflare_account" { } } -data "onepassword_vault" "github" { - name = "Github" -} - data "onepassword_vault" "tf" { name = "tf" } diff --git a/tf/deployment/modules/shared/github/secrets/secrets.tf b/tf/deployment/modules/shared/github/secrets/secrets.tf index c31df589..0022a439 100644 --- a/tf/deployment/modules/shared/github/secrets/secrets.tf +++ b/tf/deployment/modules/shared/github/secrets/secrets.tf @@ -42,9 +42,14 @@ import { id = "CLOUDFLARE_TILES_R2_KV_TOKEN_HASHED_VALUE" } +// Sourced from the github-app module's converted item in the tf vault, which +// already holds this app's credentials. This replaces the duplicate +// "push-o-matic-app" SSH-key item that lived in the single-item "Github" vault +// (both now retired). pkcs8 is a like-for-like swap for the old item's +// .private_key attribute, which the provider also returns as PKCS#8. data "onepassword_item" "push_o_matic_app" { - title = "push-o-matic-app" - vault = data.onepassword_vault.github.name + title = "GITHUB_APP_IMMICH_PUSH_O_MATIC" + vault = data.onepassword_vault.tf.name } locals { @@ -52,6 +57,7 @@ locals { app_id = [for field in data.onepassword_item.push_o_matic_app.section[0].field : field.value if field.label == "app_id"][0] client_id = [for field in data.onepassword_item.push_o_matic_app.section[0].field : field.value if field.label == "client_id"][0] installation_id = [for field in data.onepassword_item.push_o_matic_app.section[0].field : field.value if field.label == "installation_id"][0] + pkcs8 = [for field in data.onepassword_item.push_o_matic_app.section[0].field : field.value if field.label == "pkcs8"][0] } } @@ -79,7 +85,7 @@ import { resource "github_actions_organization_secret" "push_o_matic_app_key" { secret_name = "PUSH_O_MATIC_APP_KEY" - plaintext_value = data.onepassword_item.push_o_matic_app.private_key + plaintext_value = local.push_o_matic_fields.pkcs8 visibility = "all" } diff --git a/tf/shared/modules/secrets/github-app/outputs.tf b/tf/shared/modules/secrets/github-app/outputs.tf index dd64fba6..2f9cc1ea 100644 --- a/tf/shared/modules/secrets/github-app/outputs.tf +++ b/tf/shared/modules/secrets/github-app/outputs.tf @@ -18,6 +18,7 @@ output "certificates" { app_id = item.section[0].field[2].value installation_id = item.section[0].field[3].value owner = item.section[0].field[4].value + client_id = item.section[0].field[5].value } } sensitive = true diff --git a/tf/shared/modules/secrets/github-app/secrets.tf b/tf/shared/modules/secrets/github-app/secrets.tf index 9401ec37..bc267937 100644 --- a/tf/shared/modules/secrets/github-app/secrets.tf +++ b/tf/shared/modules/secrets/github-app/secrets.tf @@ -34,6 +34,12 @@ resource "onepassword_item" "manual" { label = "owner" value = "CHANGE_ME" } + // Appended last on purpose: convert_certificate and the converted item below + // index this section positionally, so existing fields must keep their index. + field { + label = "client_id" + value = "CHANGE_ME" + } } lifecycle { @@ -85,6 +91,10 @@ resource "onepassword_item" "converted" { label = "owner" value = each.value.section[0].field[3].value } + field { + label = "client_id" + value = each.value.section[0].field[4].value + } } depends_on = [