Skip to content

Stabiliteit, security en parallellisme: AGE-churn, Keycloak-credentials, wizard-fixes, parallelle deploys#124

Merged
uittenbroekrobbert merged 25 commits into
mainfrom
claude/sandbox-uid-override
Jun 10, 2026
Merged

Stabiliteit, security en parallellisme: AGE-churn, Keycloak-credentials, wizard-fixes, parallelle deploys#124
uittenbroekrobbert merged 25 commits into
mainfrom
claude/sandbox-uid-override

Conversation

@uittenbroekrobbert

Copy link
Copy Markdown
Contributor

Wat zit erin

Verzamelbranch met de fixes en features van de afgelopen periode, in vier clusters:

Stabiliteit / churn

  • AGE-hercodering gestopt (01712fee): deployment configuration:-blokken en user-env-vars worden niet meer hercodeerd als de inhoud ongewijzigd is. Dit stopt de permanente git-churn, de non-fast-forward pushconflicten en de ArgoCD resync-loop (OutOfSync↔Synced flapping) die vandaag de application-controller op 4Gi OOM joeg.
  • Keycloak realm-admin credentials direct gepersisteerd (faa2399d): het gegenereerde admin-wachtwoord wordt meteen na aanmaak gecommit/gepusht. Een latere taakfout kan geen onherstelbaar wachtwoord meer achterlaten (napp-avm-incident); de duplicate-admin guard blijft bewust weigeren, met een fouttekst die nu de echte oorzaak en remediatie beschrijft.

Parallellisme / snelheid

  • Parallel wachten op ArgoCD sync per deployment (d17c87ab): een refresh met N deployments wacht nu concurrent; wandkloktijd ≈ traagste deployment i.p.v. de som. Remediatie (OOM-tune, image-pull disable) blijft serieel. Taak-concurrency 4→12, DB-pool 10→20, push-retries 3→5.
  • ArgoCD controller opgeschaald (2f87ff09): processors 5→20/10, parallelism-limits →10, controller 4 CPU/8Gi, repo-server 2 CPU/2Gi.

Wizard / UI

  • Gewiste velden (bijv. aliases) komen niet meer terug in de samenvatting; user-env-vars tonen ontsleuteld i.p.v. als rauw AGE-blok (a94d33a2)
  • CSRF-herstel op tune-knoppen en htmx-forms, alias-parsing met '=' in waarden, quote-behoud in user-env-vars

Overig

  • Metrics-explorer JSON-endpoint achter SSO (/ui/)
  • Per-component command: en runAsUser/runAsGroup/fsGroup overrides (sandbox)
  • OOM-floor tuning scoped per deployment, PUBLIC_HOSTNAME env var, OIDC_HOSTNAME variabele, Replace=true sync-option voor secrets, ACME solver-poort in NetworkPolicy, kubelet-scrape RBAC sandbox, projectverwijdering ruimt ook ArgoCD-map op

Test

  • Alle geraakte testsuites lokaal groen (129 tests over wizard/keycloak/argo/task-gebied)
  • Kustomize build odcn-production overlay gevalideerd
  • Sandbox-verificatie van parallelle refresh staat nog open (aanbevolen vóór prod-rollout)

…aarden

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.
…oppen

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).
_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.
…orkPolicy

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].
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.
…ride 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.
… / 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.
…ij 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.
…sh-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.
…nt, 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.
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.
…ctbestand-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).
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).
…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.
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.
…og 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)
…ere 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)
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.
@uittenbroekrobbert uittenbroekrobbert merged commit c58ba4c into main Jun 10, 2026
20 checks passed
@uittenbroekrobbert uittenbroekrobbert deleted the claude/sandbox-uid-override branch June 10, 2026 21:20
uittenbroekrobbert added a commit that referenced this pull request Jun 10, 2026
…emplateResponse-signatuur

PR #124 hergenereerde uv.lock waardoor fastapi 0.136.3 → starlette 1.2.1
meekwam. Starlette 1.x verwijdert de deprecated TemplateResponse(name,
context)-signatuur; de context-dict wordt dan als templatenaam doorgegeven
en elke render faalt met 'cannot use tuple as a dict key (unhashable
type: dict)'. OPI heeft 57 call-sites in de oude stijl — productie gaf
500 op elke pagina.

Pin terug naar de laatst werkende versies. Echte fix volgt: alle
TemplateResponse-calls migreren naar request-first signatuur, daarna
unpinnen.
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.

1 participant