diff --git a/CLAUDE.md b/CLAUDE.md index f9aaee4..71a81b4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,6 +19,7 @@ Monorepo: | `frontend/` | Angular-21-App (Hauptfokus, siehe unten) | | `backend/` | Spring-Boot-Service (Java 21, wird parallel gebaut) | | `docs/` | Design-Brief, Screens, Pencil-Quelle | +| `deploy/` | GitOps-Deployment: Helm-Chart + ArgoCD (siehe §5) | Maßgebliche fachliche Referenz: `docs/design/policy-hub-design.md` + `docs/design/screens/`. (Die Root-`README.md` und `docs/README.md` sind aktuell nur Platzhalter.) @@ -193,19 +194,64 @@ Kompakter Überblick; Details in `backend/README.md`. --- -## 5. Beitrag & Git-Konventionen +## 5. CI/CD, Docker & Deployment + +> ⚠️ Staging-Infrastruktur, **nicht** produktiv. Details in `deploy/README.md`. + +### Docker (lokal) +- **Root `docker-compose.yml`** — voller Stack **ohne** Mocks (Postgres + Backend + Frontend): + `docker compose up --build`. Frontend `:4200` (Dev-Image mit Live-Mount), Backend `:8080`, + Postgres `:5432`. Aus `frontend/` auch via `npm run docker:dev`. +- **Dockerfiles:** Backend `backend/Dockerfile`; Frontend `frontend/cicd/docker/Dockerfile.development` + (Dev) und `Dockerfile.production` (nginx, SPA-Fallback, Port 8080, `nginx.conf` daneben). + +### GitHub Actions (`.github/workflows/`) +- **`test.yml`** — CI-Gate: `backend-test` (`./gradlew test`, JUnit 5 + Testcontainers/Docker), + `frontend-unit-test` (`npm test`, Vitest), `frontend-e2e-test` (`npm run e2e`, Cypress gegen + Mock-Modus). Trigger: PRs gegen `main`, Push auf `main` (ignoriert `deploy/**` und `**/*.md`), + `workflow_dispatch`. +- **`build-and-publish.yml`** — baut Backend- & Frontend-Images, pusht nach + `ghcr.io/project-construct-x/policy-hub-{backend,frontend}` (Tags `` + `main`) und + **committet die neuen Image-Tags zurück** in `deploy/helm/policy-hub/values.yaml` + (Commit `ci: … [skip ci]`). Trigger: Push auf `main` (ohne `deploy/**`, `**/*.md`). + +### Deployment (GitOps) +- **ArgoCD** (auto-sync, self-heal) rendert das Umbrella-Helm-Chart `deploy/helm/policy-hub/` + in Namespace `policyhub`: Postgres-StatefulSet, Backend- & Frontend-Deployment + Services, Ingress + `policy-hub.staging.construct-x.net` (`/` → Frontend, `/api` → Backend). +- **Flow:** Push auf `main` → Images gebaut/gepusht → Tags in `values.yaml` gebumpt → ArgoCD rollt aus. +- Layout: `helm/policy-hub/` (Chart), `argocd/{project,application}.yaml`, `secrets/` (Doku zum + Secret-Handling, siehe `secrets/README.md`). +- **Secrets (⚠️ nicht production-ready — K8s-Secrets sind nur base64-kodiert, nicht verschlüsselt):** + - **DB-Passwort** erzeugt das Chart selbst (`templates/secrets.yaml`, `randAlphaNum 32`) — **einmalig + beim ersten Install, keine automatische Rotation**. Zwei Guards halten den Wert stabil und sind + beide zwingend: der Helm-`lookup` im Template (deckt `helm upgrade` ab) **und** + `ignoreDifferences` auf `/data/db-password` + `RespectIgnoreDifferences=true` in + `argocd/application.yaml` (deckt ArgoCD ab, dessen repo-server ohne Cluster-Zugriff rendert). + Fällt einer weg, rotiert das Passwort bei jedem Reconcile und das Backend bricht gegen das + bestehende Postgres-PVC. + - **HTTP Basic Auth ist ein Provisorium** bis das echte Construct-X-Auth-Verfahren feststeht. + Bewusst isoliert in `templates/secret-basic-auth.yaml` + `auth`-Block in `values.yaml` + (Passwort **absichtlich im Klartext in git**), damit es in einem Zug entfernt werden kann — die + Ausbau-Anleitung steht im Kopf-Kommentar des Templates. + - Nur `ghcr-creds` wird noch manuell per `kubectl` angelegt. +- `argocd/application.yaml` wird per `kubectl apply` gebootstrappt, ist also **nicht** selbst + GitOps-verwaltet — nach Änderungen daran erneut applyen. + +## 6. Beitrag & Git-Konventionen - PRs gegen `main`; jeden PR möglichst an ein Issue verlinken (Templates unter `.github/`). - **Conventional Commits** (bevorzugt), **signierte Commits** (bevorzugt), **License-Header** pro Datei. - Branch-Namen: `feature/*`, `fix/*`. - Dual-License: Apache-2.0 (Code) / CC-BY-4.0 (Non-Code). -## 6. Dokumentation +## 7. Dokumentation - UI-Referenz: `docs/design/policy-hub-design.md` + `docs/design/screens/`. - Barrierefreiheit: `docs/accessibility.md` (WCAG-2.2-AA-Stand + bekannte Restrisiken). - Design-Quelle: `docs/design/Policy_hub.pen` (Pencil). - Backend-Details: `backend/README.md`. Frontend-Details: `frontend/README.md`. + Deployment-Details: `deploy/README.md`. --- diff --git a/deploy/README.md b/deploy/README.md index c86b93a..c213e2b 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -31,13 +31,15 @@ ArgoCD (auto-sync, self-heal) → namespace "policyhub" | `helm/policy-hub/` | Umbrella Helm chart (backend, frontend, in-cluster postgres, ingress) | | `argocd/project.yaml` | ArgoCD `AppProject` `policyhub` | | `argocd/application.yaml` | Workload `Application` → `helm/policy-hub` | -| `secrets/` | Manual `kubectl` secret creation (see `secrets/README.md`) | +| `secrets/` | Secret handling — what the chart generates, what you create (see `secrets/README.md`) | ## Prerequisites in the cluster - **ingress-nginx** controller and **cert-manager** with a `ClusterIssuer` (set `ingress.clusterIssuer` in `values.yaml` — currently `letsencrypt-staging`). -- The two secrets (`policy-hub-secrets`, `ghcr-creds`) created in the namespace +- The image pull secret `ghcr-creds` created in the namespace — the only secret + still created by hand. The database password is generated by the chart on the + first install and the Basic-Auth password comes from `values.yaml` (see `secrets/README.md`). - ArgoCD has read access to `github.com/project-construct-x/policy-hub` (add a repo credential in ArgoCD if the repo is private). @@ -45,11 +47,8 @@ ArgoCD (auto-sync, self-heal) → namespace "policyhub" ## First-time bootstrap ```sh -# 1. Namespace + secrets (once, out-of-band — see deploy/secrets/README.md). +# 1. Namespace + image pull secret (once, out-of-band — see deploy/secrets/README.md). kubectl create namespace policyhub -kubectl create secret generic policy-hub-secrets -n policyhub \ - --from-literal=db-password='' \ - --from-literal=security-user-password='' kubectl create secret docker-registry ghcr-creds -n policyhub \ --docker-server=ghcr.io --docker-username= --docker-password= @@ -58,6 +57,10 @@ kubectl apply -f deploy/argocd/project.yaml kubectl apply -f deploy/argocd/application.yaml ``` +> `deploy/argocd/application.yaml` is bootstrapped with `kubectl`, so it is not +> GitOps-managed: re-apply it whenever that file changes (e.g. after the +> `ignoreDifferences` block was added). + ArgoCD then syncs everything. Subsequent deploys are fully automatic: a push to `main` publishes new images and bumps the tags, and ArgoCD rolls them out. @@ -83,6 +86,14 @@ stack (`docker compose up --build`). `postgres:16-alpine` image (not the Bitnami subchart — Bitnami moved its free images to `bitnamilegacy` in 2025). Switch to an external DB by setting `postgresql.enabled: false` and pointing `database.*` at it. +- **DB password**: generated once by the chart, then kept stable by a Helm + `lookup` plus the `ignoreDifferences` / `RespectIgnoreDifferences=true` pair + in `argocd/application.yaml`. Both guards are load-bearing — see + `secrets/README.md` before touching either. +- **Basic Auth is temporary and not production-ready.** The backend credential + lives in plaintext in `values.yaml` on purpose; the whole block is scoped to + `templates/secret-basic-auth.yaml` so it can be deleted in one step once the + real Construct-X auth mechanism is decided. - **Recommended next step:** add a test gate to CI before publishing — frontend `npm run lint` + `npm test`, backend `./gradlew build` (Testcontainers work on GitHub-hosted runners). diff --git a/deploy/argocd/application.yaml b/deploy/argocd/application.yaml index b2cb4aa..b87c68e 100644 --- a/deploy/argocd/application.yaml +++ b/deploy/argocd/application.yaml @@ -19,9 +19,22 @@ spec: destination: server: https://kubernetes.default.svc namespace: policyhub + # The database password is generated by the chart on the FIRST install only. + # ArgoCD's repo-server renders without cluster access, so the Helm `lookup` + # in templates/secrets.yaml cannot see the existing Secret and produces a + # fresh random value on every render. Ignoring that field — and respecting + # the ignore during sync — is what keeps the password stable instead of + # rotating (and breaking the Postgres PVC) on every reconcile. + ignoreDifferences: + - group: "" + kind: Secret + name: policy-hub-secrets + jsonPointers: + - /data/db-password syncPolicy: automated: prune: true selfHeal: true syncOptions: - CreateNamespace=true + - RespectIgnoreDifferences=true diff --git a/deploy/helm/policy-hub/templates/NOTES.txt b/deploy/helm/policy-hub/templates/NOTES.txt index 06a3344..50d43da 100644 --- a/deploy/helm/policy-hub/templates/NOTES.txt +++ b/deploy/helm/policy-hub/templates/NOTES.txt @@ -16,5 +16,15 @@ The app will be reachable at: https://{{ .Values.ingress.host }}/api/... (backend) {{- end }} -Prerequisite: the Secret "{{ .Values.secret.name }}" (keys: {{ .Values.secret.keys.dbPassword }}, {{ .Values.secret.keys.authPassword }}) -and the image pull secret must exist in the namespace. See deploy/secrets/README.md. +Prerequisite: the image pull secret must exist in the namespace — it is the only +one created out-of-band. See deploy/secrets/README.md. + +The database password in "{{ .Values.secret.name }}" is generated on the first install and +re-used afterwards (no automatic rotation). Read it with: + kubectl -n {{ .Release.Namespace }} get secret {{ .Values.secret.name }} -o jsonpath='{.data.{{ .Values.secret.keys.dbPassword }}}' | base64 -d +{{- if .Values.auth.enabled }} + +TEMPORARY: the backend is behind HTTP Basic Auth ({{ .Values.auth.username }} / see values.yaml -> +auth.password, plaintext in git). Placeholder until the real Construct-X auth +mechanism lands — see templates/secret-basic-auth.yaml. +{{- end }} diff --git a/deploy/helm/policy-hub/templates/backend-deployment.yaml b/deploy/helm/policy-hub/templates/backend-deployment.yaml index dbf61b2..29e21b7 100644 --- a/deploy/helm/policy-hub/templates/backend-deployment.yaml +++ b/deploy/helm/policy-hub/templates/backend-deployment.yaml @@ -46,13 +46,18 @@ spec: secretKeyRef: name: {{ .Values.secret.name }} key: {{ .Values.secret.keys.dbPassword }} + # --- TEMPORARY: HTTP Basic Auth, remove together with ----------- + # --- templates/secret-basic-auth.yaml (see the notes there). ---- + {{- if .Values.auth.enabled }} - name: SPRING_SECURITY_USER_NAME value: {{ .Values.auth.username | quote }} - name: SPRING_SECURITY_USER_PASSWORD valueFrom: secretKeyRef: - name: {{ .Values.secret.name }} - key: {{ .Values.secret.keys.authPassword }} + name: {{ .Values.auth.secretName }} + key: {{ .Values.auth.secretKey }} + {{- end }} + # --- end TEMPORARY ---------------------------------------------- # Startup probe gives the app (Flyway migration + boot) time before # the liveness probe can restart it. startupProbe: diff --git a/deploy/helm/policy-hub/templates/ingress.yaml b/deploy/helm/policy-hub/templates/ingress.yaml index c16f5a6..7eaa713 100644 --- a/deploy/helm/policy-hub/templates/ingress.yaml +++ b/deploy/helm/policy-hub/templates/ingress.yaml @@ -43,13 +43,6 @@ spec: name: {{ include "policy-hub.fullname" . }}-backend port: number: {{ .Values.backend.service.port }} - - path: /swagger-ui - pathType: Prefix - backend: - service: - name: {{ include "policy-hub.fullname" . }}-backend - port: - number: {{ .Values.backend.service.port }} - path: /v3/api-docs pathType: Prefix backend: diff --git a/deploy/helm/policy-hub/templates/secret-basic-auth.yaml b/deploy/helm/policy-hub/templates/secret-basic-auth.yaml new file mode 100644 index 0000000..b5a3955 --- /dev/null +++ b/deploy/helm/policy-hub/templates/secret-basic-auth.yaml @@ -0,0 +1,40 @@ +{{- /* + ========================================================================== + TEMPORARY — HTTP Basic Auth for the backend. DO NOT BUILD ON THIS. + ========================================================================== + The backend's Basic Auth (backend/src/main/java/org/constructx/policyhub/ + core/security/SecurityConfig.java + application-prod.yaml) is a placeholder + until the real Construct-X authentication mechanism is decided. Which one + that will be is still open, so this is kept as small and as isolated as + possible — one file, one values block, one env block. + + It is deliberately NOT production-ready: + - the password is committed to git IN PLAINTEXT (values.yaml -> auth.password) + because it is a throwaway staging credential; anyone with read access to + the repository can see it; + - Kubernetes Secrets are only base64-encoded, not encrypted. + + Removing this once the real mechanism lands is self-contained: + 1. delete this file, + 2. delete the `auth:` block in values.yaml, + 3. delete the SPRING_SECURITY_USER_* env entries in backend-deployment.yaml, + 4. drop the basic-auth notes in deploy/secrets/README.md, deploy/README.md + and templates/NOTES.txt. + + Note: application-prod.yaml has no defaults for SPRING_SECURITY_USER_NAME / + _PASSWORD, so the backend fails to start when `auth.enabled` is false. The + flag exists to prepare step 3 above, not as a runtime switch — keep it true + until the backend itself drops Basic Auth. + ========================================================================== +*/ -}} +{{- if .Values.auth.enabled }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Values.auth.secretName }} + labels: + {{- include "policy-hub.labels" . | nindent 4 }} +type: Opaque +stringData: + {{ .Values.auth.secretKey }}: {{ .Values.auth.password | quote }} +{{- end }} diff --git a/deploy/helm/policy-hub/templates/secrets.yaml b/deploy/helm/policy-hub/templates/secrets.yaml index 7129007..8ea6aa1 100644 --- a/deploy/helm/policy-hub/templates/secrets.yaml +++ b/deploy/helm/policy-hub/templates/secrets.yaml @@ -1,14 +1,52 @@ +{{- /* + Application secret — database password. + + NOT PRODUCTION-READY: Kubernetes Secrets are only base64-encoded, never + encrypted. Anyone with read access to the namespace — or to an etcd backup — + can decode this value. Replace this with a real secrets manager (Sealed + Secrets, External Secrets Operator, Vault, …) before this leaves staging. + + NO AUTOMATIC ROTATION: the password is generated exactly once, on the first + install, and re-used from then on. `randAlphaNum` is re-evaluated on every + render, so two independent guards keep the value stable: + 1. the `lookup` below — covers `helm upgrade` with cluster access; + 2. `ignoreDifferences` + `RespectIgnoreDifferences=true` in + deploy/argocd/application.yaml — covers ArgoCD, whose repo-server + renders without cluster access and therefore always gets nil here. + Losing either guard means the password rotates on every reconcile. + + Rotating on purpose also requires re-initialising the database: Postgres + only honours POSTGRES_PASSWORD on the first initdb, so an existing PVC keeps + the old password. See deploy/secrets/README.md. + + Precedence: explicit .Values.secret.dbPassword > value already in the + cluster > freshly generated. +*/ -}} {{- if .Values.secret.create }} +{{- $dbKey := .Values.secret.keys.dbPassword }} +{{- $dbPassword := randAlphaNum 32 | b64enc }} +{{- $existing := lookup "v1" "Secret" .Release.Namespace .Values.secret.name }} +{{- if $existing }} +{{- $found := index (default (dict) $existing.data) $dbKey }} +{{- if $found }}{{ $dbPassword = $found }}{{ end }} +{{- end }} +{{- if .Values.secret.dbPassword }} +{{- $dbPassword = .Values.secret.dbPassword | b64enc }} +{{- end }} apiVersion: v1 kind: Secret metadata: name: {{ .Values.secret.name }} labels: {{- include "policy-hub.labels" . | nindent 4 }} + annotations: + # Never prune or delete this Secret: it holds the only copy of the database + # password, and the Postgres PVC cannot be re-keyed without a re-init. + helm.sh/resource-policy: keep + argocd.argoproj.io/sync-options: Prune=false type: Opaque -stringData: - {{ .Values.secret.keys.dbPassword }}: {{ .Values.secret.dbPassword | quote }} - {{ .Values.secret.keys.authPassword }}: {{ .Values.secret.authPassword | quote }} +data: + {{ $dbKey }}: {{ $dbPassword | quote }} {{- end }} --- {{- if .Values.ghcrCreds.create }} diff --git a/deploy/helm/policy-hub/values.yaml b/deploy/helm/policy-hub/values.yaml index d03cf2f..8e9fb11 100644 --- a/deploy/helm/policy-hub/values.yaml +++ b/deploy/helm/policy-hub/values.yaml @@ -64,22 +64,32 @@ frontend: database: name: policyhub username: policyhub -# Basic-auth user for the backend (non-secret parts). Password from the Secret. +# --------------------------------------------------------------------------- +# TEMPORARY — HTTP Basic Auth for the backend. NOT PRODUCTION-READY. +# Placeholder until the real Construct-X auth mechanism is decided. The +# password is plaintext in git ON PURPOSE: it is a throwaway staging +# credential, and the whole block is meant to be deleted rather than secured. +# templates/secret-basic-auth.yaml documents how to remove this in one go. +# --------------------------------------------------------------------------- auth: + enabled: true username: admin -# Name + keys of the Secret holding the sensitive values. Creates the Secret -# from dbPassword/authPassword below when `create: true`. Leave create: false -# (default) and set those two as ArgoCD Application parameter overrides -# (Application -> Parameters in the UI) instead of committing them here — -# see deploy/secrets/README.md. + password: ph-staging-8Qk2vTn4 + secretName: policy-hub-basic-auth + secretKey: security-user-password +# Secret holding the database password. The chart owns and creates it: on the +# first install the password is generated (randAlphaNum 32), every later render +# re-uses the value already in the cluster — there is no automatic rotation. +# See templates/secrets.yaml and deploy/secrets/README.md. +# Set `dbPassword` only to force a specific password (restore / disaster +# recovery), e.g. as an ArgoCD Application parameter override; leave it empty +# otherwise. NOT PRODUCTION-READY: Secrets are base64-encoded, not encrypted. secret: name: policy-hub-secrets - create: false + create: true dbPassword: "" - authPassword: "" keys: dbPassword: db-password - authPassword: security-user-password # --------------------------------------------------------------------------- # In-cluster PostgreSQL (self-contained StatefulSet, official image). # Set enabled: false to use an external/managed database instead — then point diff --git a/deploy/secrets/README.md b/deploy/secrets/README.md index 9ce03e8..f553d75 100644 --- a/deploy/secrets/README.md +++ b/deploy/secrets/README.md @@ -1,30 +1,84 @@ # Secrets -Two secrets must exist in the `policyhub` namespace before the pods start. They -are created **once, out-of-band, with `kubectl`** — never committed to git. -Secrets are the standard exception to GitOps; ArgoCD manages everything else. +> ⚠️ **None of this is production-ready.** Kubernetes Secrets are only +> base64-encoded, never encrypted — anyone with read access to the namespace, +> or to an etcd backup, can decode every value described here. Before this +> setup leaves staging it needs a real secrets manager (Sealed Secrets, +> External Secrets Operator, Vault, …). -The Helm chart only references them by name (`values.yaml → secret.name` and -`imagePullSecrets`); it never creates them. +Three secrets are involved. Only one of them still needs a manual `kubectl` +step: -## 1. Application secret — DB + basic-auth passwords +| Secret | Created by | Status | +|--------|-----------|--------| +| `policy-hub-secrets` (DB password) | the Helm chart, generated on first install | nothing to do | +| `policy-hub-basic-auth` (backend Basic Auth) | the Helm chart, plaintext from `values.yaml` | **temporary**, see below | +| `ghcr-creds` (image pull) | you, once, with `kubectl` | manual | -Keys must match `values.yaml → secret.keys` (`db-password`, `security-user-password`): +## 1. Database password — generated by the chart + +`templates/secrets.yaml` creates `policy-hub-secrets` (key `db-password`) with +`randAlphaNum 32` on the **first** install. It is used by the postgres +StatefulSet (`POSTGRES_PASSWORD`) and the backend (`DB_PASSWORD`). + +**There is no automatic rotation.** `randAlphaNum` would produce a new value on +every render, so two independent guards keep it stable: + +1. a Helm `lookup` in `templates/secrets.yaml` re-uses the value already stored + in the cluster — this covers `helm upgrade` with a cluster context; +2. `ignoreDifferences` on `/data/db-password` plus the + `RespectIgnoreDifferences=true` sync option in `deploy/argocd/application.yaml` + — this covers ArgoCD, whose repo-server renders **without** cluster access + and therefore always falls through to a fresh random value. + +Both are required. Removing either one makes the password rotate on every +reconcile, which breaks the backend against the existing Postgres volume. + +Read the current password: ```sh -kubectl create secret generic policy-hub-secrets \ - --namespace policyhub \ - --from-literal=db-password='' \ - --from-literal=security-user-password='' +kubectl -n policyhub get secret policy-hub-secrets \ + -o jsonpath='{.data.db-password}' | base64 -d; echo ``` -Used by the postgres StatefulSet (`POSTGRES_PASSWORD`) and the backend -(`DB_PASSWORD` + `SPRING_SECURITY_USER_PASSWORD`). +### Forcing a specific password (restore / disaster recovery) + +Set `secret.dbPassword` — as an ArgoCD Application parameter override, not in +git. It takes precedence over both the stored and the generated value. + +### Rotating on purpose + +Postgres only honours `POSTGRES_PASSWORD` during the first `initdb`, so +changing the Secret alone is **not** enough — the running database keeps the +old password and the backend starts failing with +`password authentication failed`. A real rotation is either + +- `ALTER USER policyhub WITH PASSWORD '…'` inside the running database, then + update the Secret to the same value and `kubectl rollout restart deploy -n policyhub`, or +- delete the Secret **and** the Postgres PVC, then let ArgoCD re-create both + from scratch (destroys all data). + +## 2. Backend Basic Auth — temporary + +> ⚠️ **Temporary placeholder, not production-ready.** The backend's HTTP Basic +> Auth (`backend/.../core/security/SecurityConfig.java`) exists only until the +> real Construct-X authentication mechanism is decided. Which one that will be +> is still open. -## 2. Image pull secret — private ghcr.io images +`templates/secret-basic-auth.yaml` creates `policy-hub-basic-auth` (key +`security-user-password`) from `values.yaml → auth.password`. That password is +**committed to git in plaintext, on purpose** — it is a throwaway staging +credential, and the whole block is meant to be deleted rather than secured. Do +not reuse it anywhere else, and do not put anything valuable behind it. -Name must match `values.yaml → imagePullSecrets` (`ghcr-creds`). Use a GitHub -token (classic PAT or fine-grained) with `read:packages`: +The template's header comment lists the four steps needed to remove Basic Auth +once the real mechanism lands. + +## 3. Image pull secret — private ghcr.io images + +The only secret still created out-of-band. Name must match +`values.yaml → imagePullSecrets` (`ghcr-creds`). Use a GitHub token (classic +PAT or fine-grained) with `read:packages`: ```sh kubectl create secret docker-registry ghcr-creds \ @@ -34,41 +88,57 @@ kubectl create secret docker-registry ghcr-creds \ --docker-password= ``` -## Notes - -- Run these once per namespace, after it exists (`kubectl create namespace policyhub`, - or let ArgoCD create it and add the secrets right after — pods stay in - `CreateContainerConfigError` / `ImagePullBackOff` until the secrets exist, then - recover on their own). -- **Rotation**: re-create with - `kubectl create secret ... --dry-run=client -o yaml | kubectl apply -f -`, then - restart the affected workloads (`kubectl rollout restart deploy -n policyhub`). -- **Why not GitHub Actions secrets?** They are write-only — no API returns the - plaintext value, so nothing outside a running workflow (neither ArgoCD nor the - cluster) can read them. Using them would require the CI to push secrets into the - cluster, which needs cluster credentials in CI and a reachable cluster API. - -## Alternative: creating the Secrets via ArgoCD (no kubectl needed) +Rotate by re-creating it: -Not finalized — an interim option for anyone without direct cluster/kubectl -access but with ArgoCD UI access. The chart can create both Secrets itself -(`templates/secrets.yaml`), gated behind `create: false` defaults so nothing -changes for the values already committed to `values.yaml`. +```sh +kubectl create secret docker-registry ghcr-creds --namespace policyhub \ + --docker-server=ghcr.io --docker-username= --docker-password= \ + --dry-run=client -o yaml | kubectl apply -f - +``` -In the ArgoCD UI, open the `policy-hub` Application -> App Details -> -Parameters, and add overrides (do **not** commit these to `values.yaml`): +Alternatively the chart can create it too — set `ghcrCreds.create=true` plus +`ghcrCreds.username` / `ghcrCreds.password` as ArgoCD Application parameter +overrides (Application → App Details → Parameters). Those parameters live in +the Application object in-cluster, in plaintext, visible to anyone with ArgoCD +read access. -``` -secret.create=true -secret.dbPassword= -secret.authPassword= -ghcrCreds.create=true -ghcrCreds.username= -ghcrCreds.password= -``` +## Notes -Caveat: these parameters live in the Application object in-cluster, not in -git — but they're plaintext there too, visible to anyone with read access to -the Application in ArgoCD. This is not a substitute for a real secrets -manager (Sealed Secrets, External Secrets Operator, …), just a way to avoid -needing a direct kubectl context for the one-time setup. +- Run the `ghcr-creds` command once per namespace, after it exists (ArgoCD + creates `policyhub` via `CreateNamespace=true`). Pods stay in + `ImagePullBackOff` until the secret exists, then recover on their own. +- **Why not GitHub Actions secrets?** They are write-only — no API returns the + plaintext value, so nothing outside a running workflow (neither ArgoCD nor + the cluster) can read them. Using them would require CI to push secrets into + the cluster, which needs cluster credentials in CI and a reachable API server. +## Migrating the existing staging deployment + +As of this change the live `policy-hub-secrets` is already ArgoCD-managed and +was rendered from Application **parameter overrides** — not from git and not by +hand. It currently holds both `db-password` and `security-user-password`. + +1. **Remove the stale parameter overrides** in the ArgoCD UI (Application → App + Details → Parameters): + - `secret.authPassword` — the value no longer exists in the chart; the + Basic-Auth password now comes from `values.yaml → auth.password`. + - `secret.dbPassword` — as long as this is set it takes precedence over both + the stored and the generated password, so the generation never kicks in. + - `secret.create` can go too; `values.yaml` now defaults it to `true`. +2. **The database keeps working either way.** `ignoreDifferences` on + `/data/db-password` means ArgoCD leaves the live value alone, so the existing + Postgres volume stays readable. Note the flip side: the *current* staging + password survives the migration — the freshly generated one only applies to a + genuinely new install. To move the running environment onto a generated + password, follow "Rotating on purpose" above. +3. **Drop the now-unused key** from the Secret: + ```sh + kubectl patch secret policy-hub-secrets -n policyhub --type=json \ + -p '[{"op":"remove","path":"/data/security-user-password"}]' + ``` +4. **Re-apply the Application** so `ignoreDifferences` and + `RespectIgnoreDifferences=true` take effect — without them the password + rotates on the next reconcile: + ```sh + kubectl apply -f deploy/argocd/application.yaml + ``` + Do this *before* the chart change reaches `main`.