From 4daf28d4bbbd045ff8caf7fdbe0015204f1bcbe6 Mon Sep 17 00:00:00 2001 From: 3uzbcqje <3uzbcqje@addy.to> Date: Wed, 9 Sep 2026 11:10:08 -0700 Subject: [PATCH] Track the ClusterIssuer instead of gitignoring it clusterissuer.yaml has been gitignored since 2024-04-14, so the root of every TLS certificate on this cluster had no history, no review and no drift detection. If it were edited or deleted, nothing would notice or restore it. The gitignore was not protecting anything. Git history shows the file was committed for a week first -- ea73fb3 added it on 2024-04-07, and 6d575ac "Prod clusterissuer" deleted it on 2024-04-14 in the same commit that turned it into a template. The deleted version contained the same subscription ID, client ID and email address the ignore now appears to guard, in plaintext. None of those values are sensitive by this repo's own convention: subscription ID 86f3145a-... already in applications/external-dns/values.yaml resource group equalvote 9 tracked files prod.equal.vote 9 tracked files gmail@evanstucker.com 3 tracked files client ID ebafbf2e-... the only one absent -- but external-dns's 747dcea4-... and loki's 098d7377-... are both committed in plaintext A managed-identity client ID is an identifier, not a credential. Rendering the template with the live values reproduces the running ClusterIssuer exactly, so there is no hidden per-environment variance to preserve. The tracked manifest is byte-identical to what is deployed. It lives in secrets/ because that is the only directory an auto-syncing app applies raw manifests from -- secrets/kustomization.yaml already carries namespaces.yaml the same way. The name is a poor fit for a ClusterIssuer; moving it later would mean changing bootstrap-secrets' path in application.yaml, which needs a manual kubectl apply. The .gitignore entry is now anchored to /clusterissuer.yaml. Unanchored it matched at any depth, which would have silently ignored secrets/clusterissuer.yaml too. utils/clusterissuer.template.txt is kept as the record of where the five values come from, with a note that rendering it no longer deploys anything. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XTsEHfxxGsxGkKWfyQCYmr --- .gitignore | 5 ++++- AGENTS.md | 19 ++++++++++------ README.md | 14 ++++++++---- secrets/clusterissuer.yaml | 37 ++++++++++++++++++++++++++++++++ secrets/kustomization.yaml | 1 + utils/clusterissuer.template.txt | 5 +++++ 6 files changed, 69 insertions(+), 12 deletions(-) create mode 100644 secrets/clusterissuer.yaml diff --git a/.gitignore b/.gitignore index be51d26..b283a20 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ -clusterissuer.yaml +# Only the root-level render from utils/clusterissuer.template.txt. The +# real manifest is tracked at secrets/clusterissuer.yaml -- an unanchored +# pattern would match that too. +/clusterissuer.yaml local/** diff --git a/AGENTS.md b/AGENTS.md index 4008a99..d8ad371 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,13 +53,17 @@ sops --encrypt secrets/secrets.enc.yaml > secrets/secrets.enc.yaml ## ClusterIssuer -`clusterissuer.yaml` is **gitignored**. Generate it from template: +Tracked at `secrets/clusterissuer.yaml` and applied by the `bootstrap-secrets` +app through `secrets/kustomization.yaml`. Edit it and push; no manual apply. -```sh -export EMAIL_ADDRESS=gmail@evanstucker.com -envsubst < utils/clusterissuer.template.txt > clusterissuer.yaml -kubectl apply -f clusterissuer.yaml -``` +`utils/clusterissuer.template.txt` is kept as the record of where the five values +come from when standing up a new environment. Rendering it writes to +`/clusterissuer.yaml`, which stays gitignored — if you use it, copy the result +into `secrets/clusterissuer.yaml` rather than applying it directly. + +Nothing in it is secret: the subscription ID is already in +`applications/external-dns/values.yaml`, and managed-identity client IDs are +committed in plaintext elsewhere in this repo. ## Azure resource names @@ -96,4 +100,5 @@ Defined once in the ApplicationSet template at `applications/applicationset.yaml and its CRDs have to exist before the `core` / `post` apps whose Ingresses trigger ingress-shim. It also carries its own CRDs (`crds.enabled: true`). - `applications-disabled/matomo` is the only disabled app -- `local/` and `clusterissuer.yaml` are gitignored +- `local/` and a root-level `/clusterissuer.yaml` render are gitignored; the + real manifest lives at `secrets/clusterissuer.yaml` diff --git a/README.md b/README.md index a82c1c6..44bf3ec 100644 --- a/README.md +++ b/README.md @@ -69,13 +69,20 @@ az identity federated-credential create \ --subject "system:serviceaccount:${SERVICE_ACCOUNT_NAMESPACE}:${SERVICE_ACCOUNT_NAME}" ``` -Create the clusterissuer: +The ClusterIssuer is tracked at `secrets/clusterissuer.yaml` and applied by the +`bootstrap-secrets` app, so there is nothing to apply by hand. It references the +managed identity created above via `managedIdentity.clientID` — update that value +in the manifest when standing up a new environment. + +`utils/clusterissuer.template.txt` records where the values come from: ``` -cat clusterissuer.template.txt | envsubst > clusterissuer.yaml -k apply -f clusterissuer.yaml +cat utils/clusterissuer.template.txt | envsubst > clusterissuer.yaml ``` +That render is gitignored. Copy the result into `secrets/clusterissuer.yaml` and +push, rather than applying it directly. + # Using Loki/Grafana Loki: This is a service for backing up logs @@ -119,4 +126,3 @@ Grafana: This is the web end point for viewing those logs ``` {pod=~"star-server-app-.*"} |~ "status:50.+" != "robots.txt" ``` - diff --git a/secrets/clusterissuer.yaml b/secrets/clusterissuer.yaml new file mode 100644 index 0000000..c3a11b2 --- /dev/null +++ b/secrets/clusterissuer.yaml @@ -0,0 +1,37 @@ +# Tracked here so ArgoCD manages the cluster's ACME issuer like everything else. +# It was gitignored from 2024-04-14 (6d575ac) until now, which left the root of +# all TLS on this cluster with no history, no review and no drift detection. +# +# Nothing here is secret. The subscription ID is already in +# applications/external-dns/values.yaml, the resource group and domain appear in +# nine tracked files, and managed-identity client IDs for external-dns and loki +# are committed in plaintext -- they are identifiers, not credentials. +# +# utils/clusterissuer.template.txt is kept as the record of where these five +# values come from when standing up a new environment. If you regenerate from it, +# update this file rather than applying the output by hand. +apiVersion: "cert-manager.io/v1" +kind: "ClusterIssuer" +metadata: + name: "letsencrypt-prod" +spec: + acme: + server: "https://acme-v02.api.letsencrypt.org/directory" + email: "gmail@evanstucker.com" + privateKeySecretRef: + name: "letsencrypt-prod" + solvers: + - selector: + dnsNames: + - "*.prod.equal.vote" + dns01: + azureDNS: + hostedZoneName: "prod.equal.vote" + resourceGroupName: "equalvote" + subscriptionID: "86f3145a-48cc-4255-8757-dd3104d15e57" + environment: "AzurePublicCloud" + managedIdentity: + clientID: "ebafbf2e-5c81-424f-a11c-eccfe9a2ad8b" + - http01: + ingress: + ingressClassName: "nginx" diff --git a/secrets/kustomization.yaml b/secrets/kustomization.yaml index d410846..caf6a36 100644 --- a/secrets/kustomization.yaml +++ b/secrets/kustomization.yaml @@ -2,5 +2,6 @@ apiVersion: "kustomize.config.k8s.io/v1beta1" kind: "Kustomization" resources: - "./namespaces.yaml" + - "./clusterissuer.yaml" generators: - "./secret-generator.yaml" diff --git a/utils/clusterissuer.template.txt b/utils/clusterissuer.template.txt index aa89e00..9275244 100644 --- a/utils/clusterissuer.template.txt +++ b/utils/clusterissuer.template.txt @@ -1,3 +1,8 @@ +# NOTE: the live manifest is tracked at secrets/clusterissuer.yaml and applied +# by the bootstrap-secrets app. This template is kept only to record where the +# substituted values come from. Rendering it does not deploy anything -- copy +# the result into secrets/clusterissuer.yaml and push. + # cert-managers documentation is getting worse. The letsencrypt-prod ClusterIssuer example is buried here: # https://cert-manager.io/docs/tutorials/acme/pomerium-ingress/#configure-lets-encrypt-issuer