Skip to content

fix(security): GitOps SSH-sleutel uit image + OPI RBAC inscoping#80

Merged
uittenbroekrobbert merged 12 commits into
mainfrom
fix/opi-image-secrets-rbac
May 22, 2026
Merged

fix(security): GitOps SSH-sleutel uit image + OPI RBAC inscoping#80
uittenbroekrobbert merged 12 commits into
mainfrom
fix/opi-image-secrets-rbac

Conversation

@anneschuth

@anneschuth anneschuth commented May 17, 2026

Copy link
Copy Markdown
Member

Scope en eerlijke verwachtingen

Wat dit oplost (in volgorde van werkelijke impact):

  1. DEBUG: bool = False default in config.py — productie liep met DEBUG=False alleen omdat dev .env dat overschreef; na .env-removal valt-ie zonder fix terug op True. Echte regressie-fix.
  2. Dockerfile-cleanup: keys/ en .env* worden niet meer in image-lagen gebakken. Image-hygiene.
  3. /keys/ in .gitignore + key-files verwijderd uit repo — voorkomt re-introductie. Audit-signaal: "keys zijn weg, niet meer per ongeluk terug te plaatsen".
  4. secrets list/update weg uit namespace-manager ClusterRole in local + sandboxed-local overlays. Beperkt blast-radius op dev/test clusters.
  5. _blueprint-cluster-role.yaml + _blueprint-cluster-binding.yaml in odcn-production overlay — documenteert OPI's gewenste rechten als referentie. Niet in kustomization, niet door tasks toegepast.

Wat dit NIET oplost (eerlijk over scope):

  • De "keys in repo"-zorg: het ging om dev-keys voor de oude lokale Docker-git daemon (host.docker.internal:2222), niet productie-GitOps-keys. Productie gebruikt HTTPS + PAT in operations-manager-env-secrets. De verwijdering is dus hygiene + perceptie, geen acute lekafdichting.
  • Productie-RBAC scoping: op odcn-production beheert ODCN de RBAC via Capsule per-tenant RoleBindings naar de built-in admin ClusterRole — er is geen namespace-manager ClusterRole op productie. De secrets list/update removal raakt productie niet. Strakkere productie-RBAC is een Capsule-tenant-config wijziging — apart issue.
  • Image-tag pinning: blijft :latest. Digest-pin is follow-up Pin OPI image op immutable digest in plaats van :latest #92.
  • Volledige cleanup SSH-URL code-paden: GIT_SERVER_KEY_PATH env-var en de if not is_https branches in argo_manager.py / git_monitor.py blijven staan tot na merge van fix: verwijder onbereikbare git-repo-aanmaakcode (latente RCE op git-host) #72. Aparte cleanup-PR.

Bootstrap-deploy

Productie OPI wordt handmatig uitgerold via:
```bash
task update-operations-manager CLUSTER_TYPE=odcn-production
```
Niet door ArgoCD. Na merge moet die task gedraaid worden om Dockerfile + deployment-yaml wijzigingen toe te passen. Image opnieuw bouwen + pushen voor de Dockerfile-cleanup.

Bestanden

  • `operations-manager/Dockerfile` — geen keys/env COPY
  • `bootstrap/.../operations-manager/base/deployment.yaml` — Secret-volume "git-server-key" verwijderd
  • `bootstrap/.../operations-manager/overlays/local/cluster-role.yaml` — secrets list/update weg
  • `bootstrap/.../operations-manager/overlays/sandboxed-local/cluster-role.yaml` — idem
  • `bootstrap/.../operations-manager/overlays/odcn-production/_blueprint-cluster-role.yaml` — nieuwe blueprint (niet in kustomization)
  • `bootstrap/.../operations-manager/overlays/odcn-production/_blueprint-cluster-binding.yaml` — nieuwe blueprint
  • `keys/git-server-key`, `keys/git-server-key.pub` — verwijderd
  • `operations-manager/python/opi/core/config.py` — DEBUG: bool = False
  • `.gitignore` — /keys/ toegevoegd

Follow-ups op project ZAD

@uittenbroekrobbert

Copy link
Copy Markdown
Contributor

⚠️ Richting goed (sleutel uit image-lagen, secrets list cluster-breed eruit, prod-RBAC declaratief), maar drie blokkers voor merge — anders breekt prod direct na deploy.

Blokker 1: Stale rebase op prod deployment-patch

bootstrap/rig-system/kustomize/operations-manager/overlays/odcn-production/patches/deployment.yaml is gebaseerd op een oude main. Diff vs huidige main verwijdert ongewenst:

  • imagePullSecrets: ghcr-rig-robot-pull-secret (regel 8-9 in main, weg in PR) — zonder dit kan de pod de prod-image niet pullen vanaf rcr.rijksapps.nl.
  • Image-registry wijzigt van rcr.rijksapps.nl/ghcr-rig/minbzk/...:latest naar ghcr.io/minbzk/...:v0.0.0-REPLACE-WITH-DIGEST. Productie pull-t nu uit de interne rcr-mirror; switchen naar publieke ghcr.io kan netwerk/auth-fouten geven (afhankelijk van prod-egress en image-pull-secrets).
  • LOG_ERRORS_TO_FILE=true + LOG_ERRORS_FILE_PATH=/data/logs/errors.log env-vars + error-logs emptyDir-volume + mount (regels 28-31, 36-37, 45-47 in main) — error logging-pad verdwijnt.

Fix: rebase op huidige main, behoud alle drie de blokken, pas alleen de image-regel + nieuwe Secret-volume aan.

Blokker 2: Ontbrekende bootstrap voor git-server-key Secret

base/deployment.yaml:124-137 mount het Secret als verplicht volume (geen optional: true). Zonder dat Secret in de namespace blijft de pod in ContainerCreating. task sandbox:create-sops-age-secret (Taskfile.yaml:2371) is het bestaande patroon voor het sops-age-key-Secret; geen equivalent voor git-server-key.

Fix-opties:

A. Voeg task sandbox:create-git-server-key-secret toe + prod-runbook-regel in de PR-beschrijving.

B. Beter: maak het volume optional: true. Want — verifieerbaar in argo_manager.py:212-218, git_monitor.py:189-192 en alle drie de cluster-configmaps — de SSH-sleutel wordt op runtime nergens gelezen. Alle git-URLs zijn HTTPS. De legacy GIT_SERVER_* keten is dode code (vergelijk #72). Een volgende cleanup-PR kan het volume helemaal verwijderen samen met GIT_SERVER_KEY_PATH.

Mijn voorkeur: B, en in een follow-up de keten compleet opruimen. A als tussenstap is ook valide.

Blokker 3: Image-tag-placeholder niet ingevuld

overlays/odcn-production/patches/deployment.yaml:14: v0.0.0-REPLACE-WITH-DIGEST. PR-tekst noemt dit zelf — moet @sha256:<digest> worden vóór merge.

Klein grut

defaultMode: 0400 + Secret-volume root:root + pod-runAsUser: 1001

base/deployment.yaml:133: met deze combinatie is het Secret-bestand alleen voor root leesbaar; de container draait als UID 1001 en kan het bestand niet lezen. Niet operationeel kritiek omdat de sleutel nergens gelezen wordt (zie Blokker 2), maar als blokker 2 wordt opgelost door de key écht te mounten en te gebruiken: zet defaultMode: 0440 met fsGroup: 1001 op pod-spec, of 0444.

git-server-key.pub in items: ongebruikt

base/deployment.yaml:135-137 mapt ook de .pub-key. Code gebruikt alleen de private key. Harmless, mag weg voor schoonheid.

git rm --cached ontbreekt voor de vier eerder geleakte bestanden

.gitignore is bijgewerkt, maar keys/git-server-key, keys/git-server-key.pub, operations-manager/python/.env, operations-manager/python/.env.production zijn nog steeds tracked. .gitignore werkt niet op already-tracked files. Voeg git rm --cached op die vier toe in deze PR — anders kunnen ze opnieuw aangemaakt worden met git add -f (per ongeluk of expres). Niet-blokkerend voor functioneren maar wel voor de hygiëne-claim van de PR.

Vergelijk huidige prod ClusterRole

overlays/odcn-production/cluster-role.yaml (nieuw) is identiek aan overlays/local/cluster-role.yaml. Eerste keer dat productie-RBAC declaratief in de repo zit. Graag vóór merge kubectl get clusterrole namespace-manager -o yaml op prod afzetten tegen deze YAML — anders trekt ArgoCD-sync mogelijk regels weg die er nu wel staan.

Veilig (niet-blokkerend, geverifieerd)

  • Cluster-brede secrets list weg is veilig. Geen code gebruikt list_namespaced_secret / list_secret_for_all_namespaces; connectors/kubectl.py:317 get_secret is altijd namespaced.
  • Cluster-brede secrets update weg is veilig. Enige niet-get mutatie is kubectl apply (connectors/kubectl.py:302) → triggert patch, niet update.
  • Backwards-compat env via envFrom: operations-manager-env-secrets klopt. config.py:87-101 slaat ontbrekende .env/.env.production netjes over.

Apart, follow-ups (terecht buiten scope)

  • Rotatie van git-server-key: PR doet dit niet en kan dat ook niet (operator-actie). Hoort in eigen issue. Plus: git filter-repo/BFG om de byte-content uit history te halen.
  • Per-namespace Role+RoleBinding voor de overgebleven cluster-brede secrets create/get/patch. Volgende stap naar werkelijk per-tenant RBAC.
  • CI secret-scan (gitleaks/trufflehog) om herintroductie te voorkomen.

@uittenbroekrobbert uittenbroekrobbert force-pushed the fix/opi-image-secrets-rbac branch from e7da687 to a3615b0 Compare May 19, 2026 10:11
@uittenbroekrobbert

Copy link
Copy Markdown
Contributor

Follow-up: PR-omschrijving deels op onjuiste info, scope herzien

Na een diepere audit op de premissen van deze PR is gebleken dat de "kritieke leak"-claim grotendeels niet klopt. Wat overblijft is wel reëel maar minder dramatisch. Branch is gerebased + augmenteerd (`a3615b0c`).

Wat de "geleakte" files werkelijk zijn

Bestand Wat staat erin Reëel risico
`keys/git-server-key` ed25519-key met fingerprint `robbertuittenbroek@Mac` voor `host.docker.internal:2222` Lokale dev-key, niet productie-GitOps. Productie gebruikt HTTPS (cluster_config + #79-audit)
`.env` `CLUSTER_MANAGER=local`, `API_TOKEN` met comment "Unsafe for local dev only", AGE-encrypted git-passwords (geen plaintext) Lokale dev-config, geen prod-secrets
`.env.production` (huidige main) Eén comment-regel "use the configmaps instead" Leeg
`.env.production` (oude history v1.0.0-tag) `OIDC_CLIENT_SECRET=Xx8FxVZsJFxWx88pNjdZO7Ykh6T9EOrI` voor `http://keycloak.kind/realms/rig-platform/...\` Lokale Kind-cluster OIDC client, niet productie. Geverifieerd door maintainer: productie-Keycloak gebruikt een andere secret voor dezelfde client-ID. Geen rotation nodig.

Niet nodig dus (in tegenstelling tot wat PR-omschrijving stelt):

  • ❌ Roteer GitOps SSH-sleutel
  • ❌ Roteer alle `.env.production` secrets (waren er niet)
  • ❌ `git rm --cached` op deze bestanden (lokale dev-files)
  • ❌ Git history scrubben

Wat WEL valide is en behouden blijft

  1. Dockerfile-cleanup (geen keys/env COPY in image-lagen) — hygiëne ✓
  2. `secrets list/update` uit ClusterRoleechte security-winst, was full-cluster-secret-disclosure. Gebleven.
  3. `secrets create/get/patch/delete` cluster-breed — code-analyse bevestigt dat OPI deze nodig heeft (`sops.py`, `keycloak_setup.py`, `project_manager._ensure_sops_secret` rotatie). Per-namespace Roles is een follow-up die app-code vereist.
  4. Productie RBAC declaratief in repo — auditeerbaar in plaats van out-of-band ODCN-config ✓
  5. `git-server-key` Secret-volume optional gemaakt — defensief, voorkomt ContainerCreating-loop. Volume helemaal weghalen is een follow-up.

Augmentation deze ronde (`a3615b0c`)

`nodes/services/endpoints` regel uit alle drie de cluster-roles verwijderd. Code-grep bewijst:

  • Geen K8s nodes-API-calls in `opi/` (alleen Argo's "resource tree nodes" en event-text-strings)
  • Geen calls naar K8s services/endpoints API (alleen interne Service-naam-strings)
  • "Prometheus service discovery" was aspirational; Prometheus heeft eigen SA

Wel behouden:

  • `/metrics` + `/metrics/cadvisor` non-resource — Prometheus-twijfel (mogelijk in dev-cluster onder dezelfde SA gebruikt)
  • `watch`-verbs op events/pods — marginale praktische winst, code gebruikt geen K8s Watch (alleen polling via `kubectl get`). Zou kunnen weg maar niet de moeite.

Rebase-resolutions

Twee conflicten met main, opgelost:

  1. `overlays/odcn-production/patches/deployment.yaml` — main heeft `imagePullSecrets: ghcr-rig-robot-pull-secret` + `LOG_ERRORS_TO_FILE` env + `error-logs` emptyDir + de `rcr.rijksapps.nl` registry. PR had die ongewenst weggehaald (was based op oude main). Allemaal hersteld, alleen TODO-comment voor digest-pin behouden.
  2. Bij rebase ook prod-deployment image-tag: PR had `ghcr.io/.../v0.0.0-REPLACE-WITH-DIGEST` placeholder. Vervangen door huidige prod-waarde `rcr.rijksapps.nl/ghcr-rig/.../operations-manager:latest` + TODO-comment dat digest-pin een aparte release-actie is. Zo kan deze PR mergen zonder dat een operator eerst een digest moet leveren.

ODCN coördinatie nodig

`overlays/odcn-production/cluster-role.yaml` + `cluster-binding.yaml` zijn nu declaratief in repo, maar productie-cluster RBAC wordt door ODCN-team beheerd, niet via onze ArgoCD-sync. Pre-merge actie: stem af met ODCN of zij deze ClusterRole + Binding toepassen.

Test op staging vóór merge

```bash
SOPS_AGE_KEY="$(sed -n '3p' security/key.txt)" kustomize build \
--enable-alpha-plugins --enable-exec --load-restrictor LoadRestrictionsNone \
bootstrap/rig-system/kustomize/operations-manager/overlays/odcn-production/
```

Plus rookproef OPI-pod start zonder `git-server-key` Secret (optional-volume werkt) en de getrimmde RBAC volstaat voor namespace-creatie + secret-rotatie.

Hooks: alle pre-push checks groen (ruff, pyright, pytest 3347, pip-audit). LGTM op deze scope.

@uittenbroekrobbert

Copy link
Copy Markdown
Contributor

Follow-up issues aangemaakt

anneschuth and others added 4 commits May 22, 2026 14:31
…AC in

De operations-manager image bakte de SSH-privésleutel waarmee naar alle drie
de GitOps-repos wordt gepusht plus .env/.env.production in de image-lagen.
Iedereen die de (als :latest gerefereerde) image kan pullen of de repo kan
lezen kreeg daarmee de GitOps-schrijfsleutel en kon willekeurige manifests
committen die ArgoCD vervolgens toepast.

Daarnaast gaf de ClusterRole namespace-manager cluster-breed secrets
create/get/list/patch/update en pods create/delete. Cluster-breed
secrets list betekent dat OPI elke tenant-secret kon lezen.

Wijzigingen:
- Dockerfile: COPY van keys/git-server-key(.pub), .env en .env.production
  verwijderd; de SSH-sleutel komt nu via een read-only Secret-volume op
  /app/keys/git-server-key (GIT_SERVER_KEY_PATH). Env komt al via
  envFrom operations-manager-env-secrets.
- base/deployment.yaml: Secret-volume git-server-key toegevoegd, zelfde
  patroon als de bestaande sops-age-key (operator levert het materiaal).
- local + sandboxed-local cluster-role: cluster-brede secrets list en
  update verwijderd; alleen create/get/patch behouden.
- odcn-production overlay: scoped cluster-role en cluster-binding
  toegevoegd zodat productie-RBAC auditeerbaar is vanuit de repo.
- odcn-production image niet langer :latest maar een te vervangen
  versietag-placeholder (digest-pin vereist voor merge).
- .gitignore: keys/ en .env* uitgesloten.
…ete toe aan productie-RBAC

Twee defecten in de oorspronkelijke fix verholpen.

1. base/deployment.yaml verwees naar een niet-optionele Secret-volume
   "git-server-key" die door geen enkele overlay (local, sandboxed-local,
   odcn-production) wordt aangemaakt. Een niet-optionele secret-volume
   waarvan de Secret ontbreekt houdt de pod permanent in ContainerCreating
   (MountVolume.SetUp failed: secret "git-server-key" not found), waardoor
   OPI na deze wijziging in elke omgeving niet meer zou starten. De SSH-
   sleutel wordt door geen enkele overlay gebruikt: alle pushen naar de
   GitOps-repos gaan via HTTPS met een SOPS-versleutelde PAT
   (GIT_ARGO_APPLICATIONS_URL is https://, GIT_ARGO_APPLICATIONS_KEY is "").
   optional: true toegevoegd, gelijk aan de sops-age-key-referentie die
   ook optional: true is.

2. De nieuwe productie-ClusterRole gaf secrets create/get/patch maar geen
   delete, terwijl project_manager bij rotatie van de SOPS-sleutelparen de
   verouderde sops-age-key Secret in een tenant-namespace verwijdert
   (delete_resource("secret", "sops-age-key", namespace)). delete
   toegevoegd zodat de in-repo ClusterRole overeenkomt met wat de code
   daadwerkelijk uitvoert.
…espace-manager ClusterRole

OPI gebruikt deze resources nergens in de Python-code:
- nodes, nodes/proxy, nodes/metrics: geen K8s nodes-API-calls in opi/
- services, endpoints: alleen interne string-referenties naar Service-namen,
  geen kubectl get services/endpoints

De comment 'Prometheus service discovery' was aspirational; Prometheus draait
met zijn eigen ServiceAccount en doet de discovery zelf, niet via OPI's SA.

Non-resource /metrics en /metrics/cadvisor blijven staan: mogelijke
in-cluster Prometheus-scrape onder dezelfde SA in dev-omgeving (twijfelgeval,
behouden om geen onverwachte Kind-breakage).

Watch-verbs op events en pods blijven ook staan: marginale praktische winst
en code gebruikt geen K8s Watch API (alleen polling via kubectl get).

Geldt voor local, sandboxed-local en odcn-production overlays.
…luster-role als blueprint

Twee aanpassingen:
- nodes/services/endpoints rule terug in productie cluster-role; behoud
  ruimte voor Prometheus service-discovery onder dezelfde SA.
- cluster-role.yaml en cluster-binding.yaml hernoemd naar
  _blueprint-cluster-*.yaml en uit kustomization.yaml gehaald. ODC
  platform-team beheert de werkelijke ClusterRole; deze files documenteren
  de gewenste rechten-inrichting voor parity, ArgoCD past ze niet toe.
@uittenbroekrobbert uittenbroekrobbert force-pushed the fix/opi-image-secrets-rbac branch from a3615b0 to 6c42192 Compare May 22, 2026 12:33
De SSH-key onder keys/git-server-key was van een oude lokale Docker-git-
daemon opzet (host.docker.internal:2222). Productie + sandbox gebruiken
HTTPS naar GitHub met PATs in operations-manager-env-secrets; geen enkele
URL begint met git:// of ssh://. De key werd nergens gelezen.

Verwijderd:
- keys/git-server-key, keys/git-server-key.pub (tracked files)
- /app/keys Secret-volume + mount in base/deployment.yaml

De resterende cleanup (config.py env-vars, dood SSH-URL-pad in
argo_manager/git_monitor) komt na merge van #72 in een aparte PR.
- Dockerfile: /app/keys mkdir + verbose comment-blok weg
- patches/deployment.yaml: TODO-comment over digest-pin weg (zit in follow-up issue #92)
- .gitignore: alleen /keys/ als forward-protection; comments + onnodige patronen weg
…ployed

task update-operations-manager runt kustomize build | kubectl apply op
de odcn-production overlay; cluster-role.yaml + cluster-binding.yaml
horen in resources te staan en worden zo daadwerkelijk toegepast.
Geen blueprint — productie ClusterRole 'namespace-manager' bestaat al
op de cluster (creationTimestamp 2026-05-21) zonder argo-tracking,
consistent met manual-bootstrap pad.
Productie RBAC wordt geleverd via Capsule per-tenant RoleBindings naar
de built-in admin ClusterRole; geen cluster-brede namespace-manager
ClusterRole bestaat op productie (geverifieerd live). De files in
overlays/odcn-production documenteren de gewenste set rechten als
referentie. Hernoemd naar _blueprint-*.yaml en uit kustomization.yaml
zodat task update-operations-manager ze niet probeert toe te passen.
Productie zou na .env-removal in debug-mode draaien want config.py default
was True en het container env zet DEBUG niet expliciet. Sluit defense-in-depth:
expliciet DEBUG=true overschrijven in dev .env is duidelijker dan vertrouwen
op een onveilige default.
Blueprint is minimum-rechten zoals local/sandbox draaien. Productie via
Capsule krijgt feitelijk wijdere rechten per tenant-namespace (built-in
admin) plus tenant-ownership voor namespace-creatie. Header documenteert
het verschil kort.
@uittenbroekrobbert uittenbroekrobbert merged commit 0a46ff6 into main May 22, 2026
19 of 20 checks passed
@uittenbroekrobbert uittenbroekrobbert deleted the fix/opi-image-secrets-rbac branch May 22, 2026 13:56
uittenbroekrobbert added a commit that referenced this pull request May 27, 2026
PR #80 verwijderde al de SSH-key files (keys/git-server-key + .pub) en de
volume mount in deployment.yaml. Hierna is de SSH-key-injectie code-pad
functioneel dood — een open(/app/keys/git-server-key) zou nu crashen.

Restanten:
- keys/authorized_keys (alleen gemount door git-test-server)
- git-test-server/ docker-compose + README (lokale SSH git-daemon)
- functional_tests/ (consumeerde alleen die docker-git server)
- GIT_SERVER_KEY_PATH + GIT_ARGO_APPLICATIONS_KEY in config.py
- SSH-key file-read in argo_manager.py + git_monitor.py
- GIT_ARGO_APPLICATIONS_KEY="" regels uit cluster overlays
- GIT_*_KEY regels uit operations-manager/python/.env(.production)
- functional_tests-referentie uit operations-manager/CLAUDE.md

Per-project SSH-keys (AGE-encrypted via project file -> ArgoCD repo Secret)
blijven werken via een ander code-pad; deze opruiming raakt alleen het
'globale SSH-key path uit settings' patroon dat niemand meer gebruikt.
uittenbroekrobbert added a commit that referenced this pull request Jun 10, 2026
…ls, wizard-fixes, parallelle deploys (#124)

* fix(forms): voorkom corruptie van component-aliases met '=' in YAML-waarden

De alias-editor verstuurt vrije YAML; KeyValueConverter splitste echter
elke regel naar voren op '=' waardoor URL-waarden als
'KEY: "postgres://...?schema=x"' onleesbaar werden opgeslagen.
Daarnaast detecteerde _detect_env_var_format een YAML-regel niet
zodra de waarde een '=' bevatte, zodat de validator dezelfde
fallback maakte.

- _detect_env_var_format herkent nu YAML aan 'KEY:' aan regelbegin,
  ongeacht een '=' verderop in de waarde.
- KeyValueConverter._parse_env_text delegeert naar
  validate_and_parse_env_vars zodat YAML- en KEY=VALUE-invoer beide
  correct geparseerd worden; bestaande KEY=VALUE-flow (user-env-vars)
  blijft ongewijzigd door write_as="string" pad.

* fix(csrf): herstel form-body verlies en ontbrekende tokens op htmx-knoppen

CSRFMiddleware parste de form-body om het token te valideren, maar onder
BaseHTTPMiddleware consumeerde dat de body zonder hem te cachen, waardoor
de downstream handler een lege body kreeg en elk veld als ontbrekend las
('Dit veld is verplicht'). Cache de body nu voor het parsen.

Voeg daarnaast de X-CSRF-Token toe aan de tune-, preset- en refresh-knoppen
(htmx hx-post) en geef 'request' door aan de subdomein-modal render, zodat
deze POSTs niet langer op centrale CSRF-enforcement stuklopen (403/500).

* fix(env-vars): behoud bewust geplaatste quotes in user-env-vars waarden

_decrypt_and_clean_env_vars stripte onvoorwaardelijk omringende quotes van
elke waarde, nadat validate_and_parse_env_vars de quote-semantiek al correct
had afgehandeld. Daardoor werd een waarde die de quotes bewust als inhoud
nodig heeft (bv. cal.com ALLOWED_HOSTNAMES: '"host"' zodat de app deze tot
een JSON-array kan wrappen) stil gecorrumpeerd tot de kale waarde -- geen
enkele quote-vorm in het project-bestand overleefde dit.

Verwijder de dubbele strip; quote-afhandeling hoort op een plek thuis
(validate_and_parse_env_vars). Met regressietests.

* fix(netpol): sta ACME HTTP-01 solver-poort 8089 toe in acme-http NetworkPolicy

cert-manager's HTTP-01 solver pod luistert op 8089 (de router stuurt de
challenge door naar de pod op 8089, niet 80). De gegenereerde
acme-http-<app>-network-policy selecteert alle pods (podSelector:{}) maar
stond alleen poort 80 toe, waardoor verkeer naar de solver op 8089 werd
gedropt: TCP connect lukt, HTTP-request hangt, self-check faalt met
'context deadline exceeded' en het certificaat wordt nooit Ready.

Eén van de drie generatie-locaties was al gecorrigeerd; de andere twee
(o.a. het pad dat rig-prd-ia-fky genereerde) stonden nog op [80]. Alle
drie nu op [80, 8089].

* fix(csrf): herstel CSRF-token op tune-knoppen (c-button quote-mangling)

De twee 'Tune resources' c-buttons gebruikten hx-headers met enkele
quotes rond JSON; de ROOS c-button re-serialiseert attributen met dubbele
quotes, waardoor de JSON-quotes botsten en de browser het attribuut in
ongeldige losse attributen opbrak. Daardoor kwam er geen geldig
X-CSRF-Token header mee en faalde de tune-POST.

Gebruik de &quot;-entityvorm die de c-button passthrough overleeft,
identiek aan de reeds werkende c-buttons (modal_wizard_review,
modals, wizard_review). Geverifieerd door het renderen via de echte
c-button: levert geldig {"X-CSRF-Token": "<token>"} op.

* feat(deployment): sta per-component runAsUser/runAsGroup/fsGroup override toe op sandbox

Custom images met een ingebakken non-root user op een andere UID dan 1001
(bijvoorbeeld openproject met UID 999) clasht met de hardcoded sandbox
pod-securityContext, waardoor er per image een chown-wrap nodig is.

Een verborgen `components[].security` blok in het project-YAML laat de
user nu run-as-user / run-as-group / fs-group per component overschrijven.
Alle drie de velden zijn optioneel; ontbrekende velden vallen terug op
1001 (behoud van bestaand gedrag). Op odcn-production wordt het blok
genegeerd — daar wijst OpenShift SCC zelf een UID per namespace toe.

Het veld is bewust niet in de wizard / detail-edit UI zichtbaar: users
bewerken de YAML rechtstreeks in Forgejo. Wel toegevoegd aan de Pydantic
ProjectFileModel en het JSON-schema zodat de validator het accepteert
zonder dat onbekende typo's (zoals camelCase) doorslippen.

* test(security): gebruik model_validate ipv kwargs voor Pyright-compat

* feat(deployment): sta optionele `command:` override toe per component / per deployment

Wie een upstream image als `openproject/openproject:17.4-slim` wil draaien
hoefde tot nu toe een eigen wrap-Dockerfile te bouwen om de ENTRYPOINT te
vervangen (bijv. een seeder voor de hoofd-proces). Kubernetes heeft daar
`containers[].command` voor — die werd alleen niet doorgegeven via ZAD.

Een nieuw, verborgen YAML-veld `command: list[str]` op `components[]` en
`deployments[].components[]` mapt 1-op-1 op het K8s-veld. Bij beide niveaus
gezet wint de deployment-override. Bij allebei leeg wordt geen `command:`
geemitteerd en blijft de ENTRYPOINT/CMD van de image leidend (bestaand
gedrag).

Bewust list[str] (geen shell-string) en `minItems: 1` zodat een lege lijst
de ENTRYPOINT niet stilletjes wist. Niet in de wizard / detail-edit UI —
users bewerken de YAML zelf in Forgejo, net als de security override.

* fix(delete): verwijder ook de project-ArgoCD-map uit de GitOps-repo bij projectverwijdering

Bij het verwijderen van een project bleven de AppProject-manifest, de
repository-secret en kustomization.yaml in {cluster}/{project}/ van de
argo-applications-repo staan. De root-app (selfHeal) bleef die resources
daardoor eindeloos terugzetten - de bestaande kubectl-cleanup van
AppProjects vocht tegen GitOps en verloor altijd.

Nieuwe stap 4.7 in delete_project verwijdert de map (zelfde patroon als
de -infrastructure/-mapdeletie), commit, en refresht user-applications
zodat AppProject en repo-secret direct gepruned worden.

Geconstateerd op productie: 11 weesmappen met levende AppProjects en
repo-credential-secrets van projecten verwijderd tussen jan en mei 2026.

* fix(aliases): route REDIS/NAMESPACE_REDIS aliases naar 'redis' category

* feat(env-vars): voeg PUBLIC_HOSTNAME toe naast PUBLIC_HOST voor publish-on-web

Apps zoals OpenProject hebben host-name configvelden die een volledige URL
weigeren. Geef daarom naast PUBLIC_HOST (met scheme) ook PUBLIC_HOSTNAME
(alleen de hostname) door, zodat gebruikers niet zelf de scheme uit
PUBLIC_HOST hoeven te strippen in user-env-vars.

* fix(tuning): OOM-floor klemt alleen nog de limit, scoped per deployment, met slim verval

Drie problemen in de OOM-floor die neerwaartse tuning structureel blokkeerden:

1. De floor klemde ook de request vast, terwijl OOM-bescherming alleen
   over de limit gaat. De request mag nu vrij zakken naar usage+buffer
   (genormeerd op request <= limit); de limit behoudt zijn burst-headroom.
   Facturatie loopt op requests - dit kostte clusterbreed ~5 GiB aan
   gereserveerd-maar-ongebruikt geheugen.

2. De component-definitie-tak van get_resource_history_floor filterde
   niet op deployment, waardoor een OOM in een PR-omgeving alle
   deployments (incl. productie) pinde. Def-level entries tellen nu
   alleen mee voor de deployment die ze heeft gezet (het deployment-veld
   wordt al sinds altijd meegeschreven; legacy entries zonder dat veld
   vervallen daarmee bewust).

3. De floor verliep nooit. Nieuw: een floor verloopt zodra de OOM-entry
   ouder is dan RESOURCE_TUNING_OOM_FLOOR_MIN_AGE_DAYS (10) en de
   waargenomen max-usage onder RESOURCE_TUNING_OOM_FLOOR_STABLE_PERCENT
   (50%) van de floor ligt - aantoonbaar stabiel, dus de floor is
   irrelevant geworden. Onparseerbare timestamps verlopen nooit (fail-safe).
   Productie-data: alle OOM-bursts lagen 43-73 dagen terug zonder herhaling.

Daarnaast onderdrukte de floor het memory-check-scherm volledig
(return None): gebruikers zagen niet dat er iets te besparen viel.
Floor-geblokkeerde componenten geven nu een eigen kaart met de
request-besparing en de floor-datum.

get_resource_history_floor geeft nu een ResourceFloor (waarde+timestamp)
terug i.p.v. een kale float.

* docs(features): VPA als tuning-recommender (updateMode Off) als toekomstidee

* feat(secrets): zet Replace=true sync-option zodat stale keys uit secrets drop'pen

* feat(keycloak): voeg OIDC_HOSTNAME variabele toe (hostname zonder scheme)

* fix(metrics-explorer): move JSON endpoint to /ui/ so it requires SSO

The handler had @requires_sso but lived under /api/, where the
authorization middleware passes through (API-key path). The page
route was already gated; this closes the matching JSON endpoint by
moving it under /ui/, where the standard session gate applies.
Adds a regression test.

* fix(encryption): stop niet-deterministische AGE-hercodering van projectbestand-blokken

Elke 'Process project' run hercodeerde het deployment 'configuration:'-blok
(AGE is niet-deterministisch: zelfde plaintext, compleet andere ciphertext).
Gevolg: ~52 regels pure ciphertext-churn per run, git-historie-bloat en
non-fast-forward pushconflicten tussen gelijktijdige runs (verloren user-edit
bij regel-k4c). Wizard-saves hercodeerden daarnaast elk user-env-vars blok,
ook zonder wijziging.

Het configuration-blok bleek een write-only kopie van alle credentials:
processing leest het nooit terug (creds komen uit live k8s-secrets), de enige
UI-decrypt voedde geen enkele template, geen API exposet het. Daarom:

- Drop het blok volledig: _save_encrypted_configs_to_project_file,
  _normalize_secret_keys, de _env_vars trackingmap en to_config_data
  verwijderd; dode decrypt in web/router.py weg; configuration-veld uit
  DeploymentModel/i18n/wizard-literalize.
- Strip bestaande 'configuration'/'decrypted_configuration'-blokken
  via de onvoorwaardelijke _fixup_v2_data (eenmalige cleanup per project).
- Wizard-saves: keep_existing_ciphertext_if_unchanged() in de editables-
  pipeline houdt opgeslagen ciphertext byte-identiek wanneer de plaintext
  niet wijzigde (key-rotatie decrypt-faalt en hercodeert dus gewoon).

Geverifieerd in sandbox: process schrijft geen configuration-blok meer,
fixup stripte bestaand blok, overige AGE-blokken byte-identiek na save.

NB: bevat ook de catalog-root fixup-hunks die in PR #120 zitten (zelfde
inhoud; merge-volgorde maakt niet uit).

* fix(rbac): herstel kubelet-scrape RBAC in sandbox cluster-role

nodes/nodes-proxy/nodes-metrics leesrechten waren per ongeluk verwijderd in
de RBAC-inscoping van #80, waardoor Prometheus kubelet/cAdvisor-scrapes 403
gaven (role:node service-discovery + resource-SAR op nodes/metrics;
nonResourceURLs dekt dat niet).

* docs: OpenProject-op-ZAD verkenning + future-features (TTL auto-suspend, Umami)

* fix(wizard): gewiste velden verdwijnen nu echt en samenvatting toont user-env-vars ontsleuteld

Twee bugs in de modal-wizard samenvatting:

1. Een leeggemaakt veld (bijv. aliases in Component bewerken) kwam terug
   met de oude waarde: de additieve merge in get_merged_data() kan een
   verwijderde sleutel niet uitdrukken. Gewiste velden krijgen nu een
   expliciete tombstone in het stap-fragment; get_merged_data() ruimt
   die op en verwijdert daarbij ook de oude waarde uit de snapshot.

2. user-env-vars verschenen als rauw AGE-blok: converter.view() werd
   aangeroepen met kwarg yaml_data= die geen enkele converter kent,
   waarna de except-TypeError fallback de context (en dus decryptie)
   liet vallen. Aanroep gebruikt nu context_data= en de fallback is weg.

* fix(keycloak): persisteer realm-admin credentials direct na aanmaak

Het gegenereerde admin-wachtwoord bestaat nergens anders dan in het
projectbestand. Het werd pas aan het einde van de taak gecommit; elke
fout daartussen (zoals een alias-validatiefout) liet de admin-user
achter in Keycloak met een onherstelbaar wachtwoord, waarna de
duplicate-admin guard elke herverwerking blokkeerde (napp-avm).

Het configuratieblok wordt nu direct na het aanmaken van de admin-user
gecommit en gepusht. De guard zelf blijft ongewijzigd weigeren (geen
automatische wachtwoordresets); alleen de fouttekst beschrijft nu de
echte oorzaak en de handmatige herstelprocedure.

* feat(parallel): wacht parallel op ArgoCD sync per deployment en verhoog taak-concurrency

Een project-refresh met N deployments wachtte serieel per applicatie
(tot 120s creatie + 300s sync elk); 20 deployments duurde daardoor
tot een uur. De creatie- en sync-waits (read-only polls) draaien nu
concurrent via asyncio.gather; de wandkloktijd is die van de traagste
deployment. Remediatie (OOM-tuning, image-pull uitschakelen) blijft
serieel omdat die het projectbestand in git muteert.

Bijvangst: component-failures van meerdere falende deployments worden
nu geaggregeerd in het taakresultaat (voorheen overschreef elke
falende app de vorige).

Capaciteit daarop afgestemd:
- TASK_WORKER_CONCURRENCY 4 -> 12 (backup-limiet blijft 2)
- DB-pool max_size 10 -> 20 (12 workers verhongerden op 10 connecties)
- git push rebase-retries 3 -> 5 (meer gelijktijdige pushers per repo)

* feat(argocd): verhoog controller-parallelisme en resources voor snellere syncs

Afgestemd op het parallelle deployment-wachten in de operations manager:
- controller.status.processors 5 -> 20, operation.processors 5 -> 10
- kubectl.parallelism.limit en reposerver.parallelism.limit 5 -> 10
- controller 2 CPU/4Gi -> 4 CPU/8Gi (OOMKilled vandaag op 4Gi door
  resync-churn; meer parallelisme vraagt sowieso meer geheugen)
- repo-server 1 CPU/1Gi -> 2 CPU/2Gi (rendert kustomize+SOPS voor 10
  parallelle syncs)

* fix(security): patch aiohttp, starlette, fastapi en pip kwetsbaarheden

pip-audit op main faalde op 5 bekende kwetsbaarheden in 3 packages:
- aiohttp 3.13.5 -> 3.14.1 (CVE-2026-47265 cross-origin redirect met
  per-request cookies, CVE-2026-34993 deserialisatie van untrusted data)
- starlette 0.50.0 -> 1.2.1 (PYSEC-2026-161: ontbrekende Host-header
  validatie vergiftigt request.url.path en omzeilt padgebaseerde
  security-checks; direct relevant voor onze authorization-middleware)
- fastapi 0.135.1 -> 0.136.3 (mee voor starlette 1.x compatibiliteit)
- pip 26.1.1 -> 26.1.2 (PYSEC-2026-196 entry-point pad-escape)

Volledige testsuite groen op de nieuwe stack (3737 tests; alleen
integratietests zonder cluster geven verwachte errors). pip-audit
lokaal: geen bevindingen meer.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants