diff --git a/platform/keycloak/keycloak-admin-credentials.sops.yaml b/platform/keycloak/keycloak-admin-credentials.sops.yaml new file mode 100644 index 0000000..70c86cc --- /dev/null +++ b/platform/keycloak/keycloak-admin-credentials.sops.yaml @@ -0,0 +1,53 @@ +# ============================================================================= +# TEMPLATE — NOT APPLIED, NOT ENCRYPTED (.yaml.example is ignored by kustomize +# and never auto-encrypted by SOPS). Reference scaffold only. +# +# (a) Copy to: platform/keycloak/keycloak-admin-credentials.sops.yaml +# (b) kustomization: add this line under `resources:` in +# platform/keycloak/kustomization.yaml +# - keycloak-admin-credentials.sops.yaml +# (c) Encrypt: sops -e -i platform/keycloak/keycloak-admin-credentials.sops.yaml +# (d) Consumed by: Keycloak Helm chart -> auth.existingSecret. [Phase 6] +# +# !! VERIFY KEY NAMES BEFORE ENCRYPTING !! +# The exact stringData key names depend on the pinned Keycloak chart version. +# Bitnami Keycloak commonly reads `admin-password`; some versions also read +# `admin-user` (and older/other variants use `adminPassword`). Check the pinned +# chart's values (auth.existingSecret / auth.passwordSecretKey) and adjust the +# keys below to match before running `sops -e -i`. +# +# !! SEPARATE DB SECRET LIKELY NEEDED !! +# The Keycloak helmrelease references "Admin + DB credentials". Keycloak needs a +# PostgreSQL DB credential in addition to these admin creds. A separate DB secret +# (or the chart's postgresql.auth.existingSecret) will likely be required at +# Phase 6 — flag and provision it then. +# ============================================================================= +apiVersion: v1 +kind: Secret +metadata: + name: keycloak-admin-credentials + namespace: keycloak +type: Opaque +stringData: + admin-user: ENC[AES256_GCM,data:JQY/SY8=,iv:6L/zbQP0HWIenMLA9qhb7IhGEF12+lOEcDZOy+2Yi5Q=,tag:Y5mCE3jRUwB8Sd8/ZPK4dg==,type:str] + admin-password: ENC[AES256_GCM,data:0DOPDe3zWFlc+BPTBqAY,iv:FnQc6F0y0gsD1S47u13oRx9lUee7zLvh3d5QAwK2FyY=,tag:pwO3PUzEf32iqBYF0uIRwg==,type:str] +sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] + age: + - recipient: age175l8mdpdfu7hq0aml3exs3gykarwjmuvr23sq56s3wjejnyk44wsp4mfzg + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXd2JoSE9Cd1pvN2x0djBs + bW16TGZMZ0JDMmIzZ3JqTFNrWFJGV0Zwc25jClpxNDh6VHNwNlVmMy9PcFh2VXp2 + bm5mc1lsVFZLNDNBVllJWVc1a24yb28KLS0tIHUvSnRtQTJaMzRremFWMkxaWkxB + N3I2c3cyYXRMQlF2TFFKcVNFTlFKT3cK6zMk9NYrfEiZUtdmYYsoaOhI6qXNkGWj + k/kl8WwBv2pELusvc5mlAJTmwcdOUA36QtLcwHK58tAMPpyel0dohQ== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2026-08-22T02:48:22Z" + mac: ENC[AES256_GCM,data:SmREeL7sleG6LVvjzNO4xnoruGsexDUHsblxG1L7YaVDPp/5KURptAc3MfbZKXweYhA5D+eoJVRk4x8CJ8AVxB5rHeon1WX51kamK4zepKSBFItK7beSvHg4sOnn595IY76e8gl8PabwsRl2tvFLr0Hc4DKnPQQl4UjbMcpDfjo=,iv:us2wMyI7BTNlgPPXYS9KZnKVu8xh7K9rOpwXHLJ7njY=,tag:oyp0g8g3Z+mOeMwqcugiOQ==,type:str] + pgp: [] + encrypted_regex: ^(data|stringData)$ + version: 3.9.4 diff --git a/platform/kustomization.yaml b/platform/kustomization.yaml index 5dda723..5ff0078 100644 --- a/platform/kustomization.yaml +++ b/platform/kustomization.yaml @@ -17,8 +17,9 @@ resources: - cert-manager # Let's Encrypt DNS-01 wildcard — needs SOPS `cloudflare-api-token` - externaldns # auto DNS records — needs SOPS `cloudflare-api-token` - gateway # Gateway API listeners (ingress; replaces Traefik) - # ---- Later phases: enabled when their phase opens (creds already provisioned via SOPS) ---- - # - monitoring # Phase 4 (Observability): kube-prometheus-stack + Loki + Alloy + Alertmanager + # ---- Phase 4 — Observability (creds provisioned via SOPS: grafana-admin, alertmanager-smtp) ---- + - monitoring # kube-prometheus-stack + Loki + Alloy + Alertmanager + # ---- Later phases: enabled when their phase opens ---- # - velero # Phase 4 (Backup/DR): BLOCKED on OQ-BKP (off-cluster backup target decision) # - image-automation # Phase 4 (Automated updates): Flux image-automation + Renovate # - kubevirt # Phase 5 (IaaS proof): KubeVirt + CDI diff --git a/platform/monitoring/alertmanager-smtp.sops.yaml b/platform/monitoring/alertmanager-smtp.sops.yaml new file mode 100644 index 0000000..dd1a59e --- /dev/null +++ b/platform/monitoring/alertmanager-smtp.sops.yaml @@ -0,0 +1,46 @@ +# ============================================================================= +# TEMPLATE — NOT APPLIED, NOT ENCRYPTED (.yaml.example is ignored by kustomize +# and never auto-encrypted by SOPS). Reference scaffold only. +# +# (a) Copy to: platform/monitoring/alertmanager-smtp.sops.yaml +# (b) kustomization: add this line under `resources:` in +# platform/monitoring/kustomization.yaml +# - alertmanager-smtp.sops.yaml +# (c) Encrypt: sops -e -i platform/monitoring/alertmanager-smtp.sops.yaml +# (d) Consumed by: Alertmanager (kube-prometheus-stack). Only the SMTP +# PASSWORD lives here. It is mounted at +# /etc/alertmanager/secrets/alertmanager-smtp/smtp-password. [Phase 4] +# +# NOTE: The non-secret SMTP endpoint fields — smtp_smarthost, smtp_from, +# smtp_auth_username, and the receiver `to:` addresses — are configured in +# platform/monitoring/kube-prometheus-stack.yaml (currently REPLACE-ME +# placeholders there). Only the password is this secret. +# ============================================================================= +apiVersion: v1 +kind: Secret +metadata: + name: alertmanager-smtp + namespace: monitoring +type: Opaque +stringData: + smtp-password: ENC[AES256_GCM,data:MDBdjOjZXSoieAwO0GJVN7RhTQ==,iv:40Kwqv4fO5QjehYXf0fNGPA+asInXfzUDChuQv7HYrM=,tag:HgMu9y2+gqufSAQ9MqKiOA==,type:str] +sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] + age: + - recipient: age175l8mdpdfu7hq0aml3exs3gykarwjmuvr23sq56s3wjejnyk44wsp4mfzg + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBiamFxdXVUR0VxMlNkeUFL + OXZqc3d5V1RGQVhHMW15dmllakNZeDVUQzJVCnVtRHBCOWJOSmsxVVRFSEZBTkFi + V3FkSzVDMkU5aWNxR0lVMGpheEk1OUkKLS0tIE5hQ3A4eDV4eklWVFB6NjZkaUFT + UHhPc0FDbDNuT2ZERHJwYllCY1U2NmsKF2Ct1RzFMHYW5JNy8v0kS8oV96eecH9b + nkG3ve8Ku1Ttw4O8htQcJ0qs6rE4+rXXNrhszmFOWDHZEKBd5CaWiw== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2026-08-22T02:35:27Z" + mac: ENC[AES256_GCM,data:3/KTeIFvRZKDQFH7WvkJLPH2eyTl8g5LPLKBvV7Dg0u+ZfSkNU1zql3LzLbApiqTXcP2393pZyO+11AO5TK4ObqfFVYxqyTFega1CCnkgrHygFGmt70jwlgCXbWqkC+AGG8XOUb96M0tCrRbJjz/C3eoFjEmqTZLNfqn+smNO3U=,iv:KgC0NsxHWCbeidHa8Yt+F9Qh1YslHvKtdYr+4tM/IB8=,tag:GtDE/7fuTbPZMVE6C/oiLQ==,type:str] + pgp: [] + encrypted_regex: ^(data|stringData)$ + version: 3.9.4 diff --git a/platform/monitoring/grafana-admin-credentials.sops.yaml b/platform/monitoring/grafana-admin-credentials.sops.yaml new file mode 100644 index 0000000..0a3a7a1 --- /dev/null +++ b/platform/monitoring/grafana-admin-credentials.sops.yaml @@ -0,0 +1,41 @@ +# ============================================================================= +# TEMPLATE — NOT APPLIED, NOT ENCRYPTED (.yaml.example is ignored by kustomize +# and never auto-encrypted by SOPS). Reference scaffold only. +# +# (a) Copy to: platform/monitoring/grafana-admin-credentials.sops.yaml +# (b) kustomization: add this line under `resources:` in +# platform/monitoring/kustomization.yaml +# - grafana-admin-credentials.sops.yaml +# (c) Encrypt: sops -e -i platform/monitoring/grafana-admin-credentials.sops.yaml +# (d) Consumed by: kube-prometheus-stack -> grafana.admin.existingSecret +# (userKey: admin-user, passwordKey: admin-password). [Phase 4] +# ============================================================================= +apiVersion: v1 +kind: Secret +metadata: + name: grafana-admin-credentials + namespace: monitoring +type: Opaque +stringData: + admin-user: ENC[AES256_GCM,data:lBUukEo=,iv:hbLPBZujuBp44qhqGAO+ZrNtBStW5RdblWQmgLv5fbI=,tag:c4AyEyeyYMGYJL80ipMMcQ==,type:str] + admin-password: ENC[AES256_GCM,data:n53uTxnmIpbKomR//0dm,iv:cx5v29rYPtO0Z2iNmv0ahj/al1Ye/7Mu5MyK0quXpMI=,tag:xAnKhR88oRUwzgL8tuZOJA==,type:str] +sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] + age: + - recipient: age175l8mdpdfu7hq0aml3exs3gykarwjmuvr23sq56s3wjejnyk44wsp4mfzg + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4U3V5TUNqcDhEK2NHK1F0 + VkV4YmQ2bHVYM1ExQytzNDNmRi9QeFNCMjJzClp4SnlVYmlXTER6N05wQldDRzZv + enFZcng4UTExbkF5S2ltWTV5UUkyWTQKLS0tIE12NEhZRk1Ya3JTTjJFQlgwN1Jr + cHhySmRuZ2xpTXMyRVJ3ZThGVm5OSk0K32w53bZMjE8E1kKtHxczDbI1DECgm4Nd + APFB2qmf7PP5vRwfiZEsypy19a+sg02oq4WZD9OcwHUElPdOs1Am+w== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2026-08-22T02:29:14Z" + mac: ENC[AES256_GCM,data:o/Z26bcQRG8rHIoHOWHPAr7Bbdn1+FQDdPizBYsl0u167Gn4DGV4J6HxC9loPnRUNVT/0wyy7PbPDfkyxosjVt9amBGc1t7iC7K/RK2nP2IQffGUbfWiCZqXrt0SVHn+wlvtADQxieAzHLMHD1aZhVei3o5WxpzQYHb9iofDIuU=,iv:0IVvtrI9klknXLrtHIJYYEuziTTBu4GUUpqWIVWJp+g=,tag:6MxmKMY7sQLxG0D0qqyNEA==,type:str] + pgp: [] + encrypted_regex: ^(data|stringData)$ + version: 3.9.4 diff --git a/platform/monitoring/kube-prometheus-stack.yaml b/platform/monitoring/kube-prometheus-stack.yaml index b12d911..199dfc2 100644 --- a/platform/monitoring/kube-prometheus-stack.yaml +++ b/platform/monitoring/kube-prometheus-stack.yaml @@ -91,9 +91,9 @@ spec: config: global: resolve_timeout: 5m - smtp_smarthost: "REPLACE-ME:587" # operator hand-off (e.g. smtp.gmail.com:587) - smtp_from: "REPLACE-ME" # operator hand-off (From: address) - smtp_auth_username: "REPLACE-ME" # operator hand-off (SMTP login) + smtp_smarthost: "smtp.gmail.com:587" # operator hand-off (e.g. smtp.gmail.com:587) + smtp_from: "alertmanager@keithjackson.dev" # operator hand-off (From: address) + smtp_auth_username: "keithallenjackson@gmail.com" # operator hand-off (SMTP login) smtp_auth_password_file: /etc/alertmanager/secrets/alertmanager-smtp/smtp-password smtp_require_tls: true route: @@ -111,13 +111,13 @@ spec: receivers: - name: email-warning email_configs: - - to: "REPLACE-ME" # operator hand-off (alert inbox) + - to: "keithallenjackson@gmail.com" # alert inbox (operator: change if a different inbox is wanted) send_resolved: true headers: subject: '[WARNING] {{ .CommonLabels.alertname }} ({{ .CommonLabels.namespace }})' - name: email-critical email_configs: - - to: "REPLACE-ME" # operator hand-off (alert inbox) + - to: "keithallenjackson@gmail.com" # alert inbox (operator: change if a different inbox is wanted) send_resolved: true headers: subject: '[CRITICAL] {{ .CommonLabels.alertname }} ({{ .CommonLabels.namespace }})' diff --git a/platform/monitoring/kustomization.yaml b/platform/monitoring/kustomization.yaml index e051fa0..11ec3ce 100644 --- a/platform/monitoring/kustomization.yaml +++ b/platform/monitoring/kustomization.yaml @@ -8,3 +8,5 @@ resources: - loki.yaml - alloy.yaml - alerts.yaml + - grafana-admin-credentials.sops.yaml + - alertmanager-smtp.sops.yaml diff --git a/platform/oauth2-proxy/oauth2-proxy-credentials.sops.yaml b/platform/oauth2-proxy/oauth2-proxy-credentials.sops.yaml new file mode 100644 index 0000000..a0f6ac3 --- /dev/null +++ b/platform/oauth2-proxy/oauth2-proxy-credentials.sops.yaml @@ -0,0 +1,32 @@ +# ============================================================================= +# TEMPLATE — NOT APPLIED, NOT ENCRYPTED (.yaml.example is ignored by kustomize +# and never auto-encrypted by SOPS). Reference scaffold only. +# +# (a) Copy to: platform/oauth2-proxy/oauth2-proxy-credentials.sops.yaml +# (b) kustomization: add this line under `resources:` in +# platform/oauth2-proxy/kustomization.yaml +# - oauth2-proxy-credentials.sops.yaml +# (c) Encrypt: sops -e -i platform/oauth2-proxy/oauth2-proxy-credentials.sops.yaml +# (d) Consumed by: oauth2-proxy Helm chart -> existingSecret. [Phase 6] +# +# VALUE NOTES: +# - client-id / client-secret: come from the Keycloak OIDC client created for +# oauth2-proxy (Phase 6, after Keycloak is up). +# - cookie-secret: must be a 16, 24, or 32-byte value, base64-encoded. +# Generate with: openssl rand -base64 32 +# +# !! VERIFY KEY NAMES BEFORE ENCRYPTING !! +# Confirm the exact stringData key names against the pinned oauth2-proxy chart's +# existingSecret contract (common keys: client-id, client-secret, cookie-secret, +# but chart versions vary). Adjust below to match before running `sops -e -i`. +# ============================================================================= +apiVersion: v1 +kind: Secret +metadata: + name: oauth2-proxy-credentials + namespace: oauth2-proxy +type: Opaque +stringData: + client-id: REPLACE-ME + client-secret: REPLACE-ME + cookie-secret: REPLACE-ME