Skip to content

fix: verwijder onbereikbare git-repo-aanmaakcode (latente RCE op git-host)#72

Merged
uittenbroekrobbert merged 3 commits into
mainfrom
fix/remove-dead-git-rce
May 27, 2026
Merged

fix: verwijder onbereikbare git-repo-aanmaakcode (latente RCE op git-host)#72
uittenbroekrobbert merged 3 commits into
mainfrom
fix/remove-dead-git-rce

Conversation

@anneschuth

Copy link
Copy Markdown
Member

Wat dit was

Drie functies vormden samen een ongebruikte keten om op afstand een bare git-repo aan te maken:

  • GitConnector.create_repository (statische methode in opi/connectors/git.py)
  • create_git_repository (module-helper in opi/connectors/git.py, riep de statische methode aan)
  • ProjectManager.create_project_repository (opi/manager/project_manager.py, riep de helper aan)

Ze bouwden de remote commando's als ongeescapete strings (mkdir -p /srv/git/{repo_name}.git, git init -b main --bare ...) en gaven die als één argument mee aan ssh host "<cmd>". De shell op de git-host herinterpreteert die string. repo_name kwam ongefilterd uit de repo-URL in het project-YAML via os.path.basename, zonder enige stripping van shell-metatekens. Een URL als .../$(...).git of ...;rm -rf ....git zou op de git-host als shell uitgevoerd worden: een latente kritieke remote command execution.

Waarom verwijderen het juiste is

create_project_repository had nergens in de hele boom een aanroep (geverifieerd met een volledige grep over opi/, tests/, web/, api/, scripts/, functional_tests/, geen dynamische dispatch). create_git_repository werd verder alleen gebruikt door functional_tests/setup_test_infrastructure.py, een los hulpscript met een __main__-entrypoint dat door geen enkele testrunner, Taskfile of CI wordt aangeroepen, alleen handmatig genoemd in een README. Geen bereikbare code dus.

Een patch met shell-escaping zou dode code repareren die niemand aanroept. De keten weghalen elimineert het latente risico volledig en verkleint het aanvalsoppervlak, zonder dat er gedrag verandert. Wordt later opnieuw remote-repo-creatie nodig, dan hoort dat met asyncio.create_subprocess_exec en argv-lijsten (zonder shell-herinterpretatie) opnieuw te worden opgebouwd.

Wat is verwijderd

  • De twee functies in opi/connectors/git.py
  • ProjectManager.create_project_repository plus de daardoor ongebruikt geworden import
  • De re-export uit opi/connectors/__init__.py
  • Het dode script functional_tests/setup_test_infrastructure.py en de bijbehorende README-sectie
  • Tests die uitsluitend deze verwijderde code testten: tests/test_git_connector.py (volledig), en in tests/test_git_ssh.py alleen test_git_connector_create_repository

Nog gebruikte code (o.a. create_or_update_file, de SSH-helpers, poll_for_changes, create_git_connector_from_repo_config) is niet aangeraakt.

Pre-existing en onafhankelijk

Deze wijziging staat los van de andere openstaande fix-PR's en raakt geen code die zij aanpassen. Het betreft dode code die al in de codebase stond.

Validatie

  • uv run ruff check . --fix: alle checks geslaagd
  • uv run pyright op de gewijzigde bestanden: 0 errors, 0 warnings (bevestigt geen resterende verwijzingen)
  • Volledige tree-grep op create_project_repository, create_git_repository, .create_repository(, GitConnector.create_repository en setup_test_infrastructure: nul resultaten na de wijziging
  • AST-parse van alle gewijzigde Python-bestanden geslaagd
  • De volledige pytest-suite faalt al vóór deze wijziging bij collectie door een pre-existing omgevingsincompatibiliteit (Python 3.14.0b4 + gepinde pydantic 2.12.5 laat import fastapi falen); dat staat los van deze PR en is hier niet aangeraakt

@uittenbroekrobbert

Copy link
Copy Markdown
Contributor

LGTM — geverifieerd op de PR-branch.

Geverifieerd

  • Dead-code claim klopt: git grep op de PR-branch voor create_project_repository, create_git_repository, GitConnector.create_repository en .create_repository( levert nul hits op. Geen dynamische dispatch (getattr/hasattr/string-lookup) in opi/api/, opi/core/, opi/manager/, scripts/, of de Taskfile. Verwijderbaar.
  • RCE-vector echt maar onbereikbaar. De gecombineerde mkdir -p {repo_path}; git init -b main --bare {repo_path} via ssh host "<cmd>" met repo_name uit os.path.basename(repo_url) zonder sanitization is een echte shell-injection-keten — maar ProjectManager.create_project_repository had geen callers, dus niet reachable. Verwijderen ipv patchen is hier de juiste keuze (KISS); een patch op dode code zou een review-trap zijn voor de volgende ontwikkelaar.
  • Test-deletes proportioneel. tests/test_git_connector.py testte uitsluitend de verwijderde statische methode. In test_git_ssh.py is alleen test_git_connector_create_repository weggehaald; de andere twee tests raken niet-verwijderde code en blijven.

Eén kleine opvolg-observatie (geen blocker)

Na merge zijn GIT_SERVER_HOST, GIT_SERVER_PORT, GIT_SERVER_USER in opi/core/config.py:158-161 en de bijbehorende .env-entries orphaned — alleen GIT_SERVER_KEY_PATH blijft echt nodig (gelezen door opi/core/git_monitor.py:189-192 en argo_manager.py). Prima om in een aparte cleanup-PR mee te nemen.

Pre-existing buiten scope

tests/test_git_ssh.py:54 roept create_git_connector(...) aan zonder die import; functie bestaat ook op main niet. Test draait alleen niet vanwege slow+requires_infra markers — losse fix waardig maar niet van deze PR.

Bij eventueel toekomstig herintroduceren

Mocht remote-repo-aanmaken ooit opnieuw nodig zijn: asyncio.create_subprocess_exec met argv-lijsten (geen shell), of liever de Forgejo/Gitea REST-API ipv een SSH-pipe. De huidige aanpak (commando's via f-string in een ssh-string proppen) is intrinsiek niet veilig te maken.

@uittenbroekrobbert

Copy link
Copy Markdown
Contributor

Cleanup-commit toegevoegd (58758545): orphaned GIT_SERVER_HOST / GIT_SERVER_PORT / GIT_SERVER_USER config-keys verwijderd, omdat hun enige consumer de keten was die deze PR verwijderde.

Wat is bijgewerkt

operations-manager/python/opi/core/config.py:

  • GIT_SERVER_HOST, GIT_SERVER_PORT, GIT_SERVER_USER weg
  • GIT_SERVER_KEY_PATH blijft (nog gelezen door opi/core/git_monitor.py:190 voor ssh:///git://-URLs in de local docker-compose-flow)
  • Het stale "Legacy ... should be removed or fixed in the future"-blokcomment vervangen door een accurate beschrijving van de overgebleven setting

operations-manager/python/.env:

  • Dezelfde drie keys verwijderd, GIT_SERVER_KEY_PATH regel behouden

Verificatie

  • Grep over operations-manager/, bootstrap/, infrastructure/: nul resterende referenties naar de drie weggehaalde keys
  • Geen environment-overrides in productie- of sandbox-overlays
  • from opi.core.config import settings laadt nog steeds clean; hasattr(settings, 'GIT_SERVER_KEY_PATH') == True, hasattr(settings, 'GIT_SERVER_HOST') == False

Onafhankelijk audit van de originele PR-commit (5ffc43d)

Vóór deze cleanup heb ik de dead-code claim onafhankelijk nagelopen:

Symbool Callers op origin/main voor merge
create_project_repository 0 (alleen definitie op project_manager.py:1216)
create_git_repository Alleen het orphan functional_tests/setup_test_infrastructure.py script + de te-verwijderen test + __init__.py re-export
GitConnector.create_repository Alleen via create_git_repository-helper

Geen dynamic dispatch (getattr/string-lookup) richt zich op deze namen. De test_git_ssh.py import is netjes bijgewerkt (create_git_repository weg uit imports). De twee overgebleven tests (test_git_connector_ssh_command, test_create_git_connector_with_ssh) zijn slow + requires_infra markered en draaien niet in standaard CI.

Verder zijn er geen andere ssh-string-interpolation-patronen in de codebase — dit was de enige instantie van het anti-pattern (subprocess_exec → ssh user@host "string-met-tenant-input").

Architectonisch context

De keten lijkt een artefact van een vroeger ontwerp waarin OPI tenant-git-repos zelf aanmaakte via SSH naar een centraal git-server. Het huidige model gebruikt externe GitHub-repos voor tenant-app-code en in-cluster Forgejo (HTTP) voor sandboxed-local. Verwijderen is consistent met de actuele architectuur, niet alleen een security-fix.

Pre-existing observatie (geen actie op deze PR)

tests/test_git_ssh.py:54 roept create_git_connector(...) aan zonder import — die functie bestaat niet in git.py. Test draait alleen niet vanwege slow+requires_infra markers, valt buiten gate. Aparte fix-PR waardig, los van deze.


Audit: cleanup-commit direct op de PR-branch gepushed. Pre-push pytest-hook groen.

uittenbroekrobbert added a commit that referenced this pull request May 22, 2026
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.
uittenbroekrobbert pushed a commit that referenced this pull request May 22, 2026
fix(security): hygiene cleanup van development keys, dummy env en local cluster-role

De keys/git-server-key files in de repo waren ontwikkel-keys voor de oude
lokale Docker-git daemon (host.docker.internal:2222). Geen productie-risico —
productie GitOps gaat via HTTPS+PAT in een SOPS-Secret — maar wel overbodig
in de repo en image-lagen. Tegelijk de local/sandbox cluster-role getrimd
zodat de minimum-rechten netjes gedocumenteerd staan voor de
operations-manager ServiceAccount.

Wijzigingen:
- Dockerfile: COPY van keys/git-server-key, .env en .env.production
  verwijderd (waren dev-files, niet productie-secrets)
- keys/git-server-key, keys/git-server-key.pub uit repo verwijderd
- /keys/ toegevoegd aan .gitignore (forward-protection)
- base/deployment.yaml: ongebruikte git-server-key Secret-volume weg
- local + sandboxed-local ClusterRole namespace-manager: cluster-brede
  secrets list/update verwijderd; create/get/patch behouden voor de
  bootstrap-flow
- odcn-production overlay: _blueprint-cluster-role.yaml en
  _blueprint-cluster-binding.yaml als referentie van OPI's minimum-rechten
  (niet in kustomization; productie krijgt rechten via Capsule per-tenant
  RoleBindings naar de built-in admin ClusterRole)
- config.py: DEBUG default → False, zodat productie zonder expliciete env-set
  niet per ongeluk in debug-mode draait

Follow-ups op project ZAD: #90 (per-namespace Role+RoleBinding voor secrets),
#91 (git-server-key runtime-pad opruimen na #72), #92 (image digest-pin),
#93 (ODCN coördinatie cluster-role parity), #104 (KEYCLOAK_MASTER_OIDC naar
prod ConfigMap).
anneschuth and others added 3 commits May 27, 2026 11:19
…host)

GitConnector.create_repository, de module-helper create_git_repository
en ProjectManager.create_project_repository bouwden remote shell-commando's
als ongeescapete strings ("mkdir -p /srv/git/{repo_name}.git",
"git init -b main --bare ...") die als enkel argument aan
`ssh host "<cmd>"` werden meegegeven. De remote shell herinterpreteert
die string, en repo_name kwam ongefilterd uit de repo-URL in het
project-YAML (os.path.basename, geen stripping van shell-metatekens).
Dat is een latente kritieke remote command execution op de git-host.

De code was niet bereikbaar: create_project_repository had nergens
in de hele boom een aanroep, en create_git_repository werd verder
alleen gebruikt door het losse, niet door enige testrunner of Taskfile
aangeroepen hulpscript functional_tests/setup_test_infrastructure.py.
Dode code, maar wel een geladen wapen.

Verwijderd: de twee git.py-functies, de project_manager-methode plus
de nu ongebruikte import, de connectors-package re-export, het dode
setup-script en de bijbehorende README-sectie, en de tests die
uitsluitend deze verwijderde code testten (test_git_connector.py
volledig; in test_git_ssh.py alleen test_git_connector_create_repository).
Geen nog-gebruikte code aangeraakt. Gevalideerd met ruff en pyright;
geen resterende verwijzingen naar de verwijderde symbolen in de boom.
After the previous commit removed create_repository / create_git_repository
/ create_project_repository, these three settings have zero consumers in
Python code. Verified with grep over operations-manager/, bootstrap/ and
the .env files in all three overlays (odcn-production, sandboxed-local,
local) -- no GIT_SERVER_HOST / GIT_SERVER_PORT / GIT_SERVER_USER references
remain.

GIT_SERVER_KEY_PATH stays: it is still read by opi/core/git_monitor.py:190
when the monitored project URL starts with ssh:// or git:// (the local
cluster's docker-compose git-daemon flow). The block's stale 'Legacy /
should be removed or fixed in the future' comment is replaced with an
accurate description of what the remaining setting actually does.

.env in operations-manager/python is the only .env that named these keys;
removed those three lines too, kept GIT_SERVER_KEY_PATH.
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 uittenbroekrobbert force-pushed the fix/remove-dead-git-rce branch from 5875854 to 48a309a Compare May 27, 2026 09:43
@uittenbroekrobbert

Copy link
Copy Markdown
Contributor

Update — cleanup uitgebreid (commit 48a309a)

Na review en verificatie tegen devex-updated (de branch die Forgejo-bootstrap doet) bleek de oude docker-git SSH-keten breder dood dan alleen de RCE-vector. devex-updated gebruikt een eigen ForgejoConnector over HTTP REST API; raakt deze SSH-keten niet.

PR #80 had al de SSH-key files (keys/git-server-key + .pub) en de volume mount in deployment.yaml weggehaald. Daarmee was de SSH-key-injectie in OPI al functioneel kapot: een open("/app/keys/git-server-key") zou nu een RuntimeError gooien. Half-geamputeerde stub bewaren is verwarrender dan compleet opruimen — dat doen we nu.

Toegevoegd in de cleanup-commit

Verwijderd:

  • keys/authorized_keys — alleen gemount door git-test-server
  • git-test-server/ — docker-compose SSH git-daemon (localhost:2222) van de oude lokale setup
  • operations-manager/python/functional_tests/ — consumeerde alleen die docker-git server
  • GIT_SERVER_KEY_PATH + GIT_ARGO_APPLICATIONS_KEY uit config.py (beide hadden hardcoded Mac-path als default)
  • SSH-key file-read in argo_manager.py:215-222ssh_private_key = ""
  • SSH-key lookup in git_monitor.py:189-192ssh_key_path=None
  • GIT_ARGO_APPLICATIONS_KEY="" regels uit alle drie cluster overlays
  • GIT_*_KEY regels uit operations-manager/python/.env en .env.production
  • Docstring + CLAUDE.md referenties die nog naar de verwijderde symbolen verwezen

Bewust behouden:

  • argo-repository.yaml.jinja (niet-HTTPS template) + is_https template-switching in argo_manager — Argo's SSH/HTTPS/HTTP transport-support blijft renderbaar
  • GitConnector.ssh_key_path parameter en bijbehorende SSH-git-cmd plumbing in connectors/git.py — transport-generieke ondersteuning, geen binding aan de verwijderde globale settings
  • Per-project SSH-key flow (AGE-encrypted in project-file → Argo repo Secret) — apart code-pad, niet geraakt

Verificatie

  • test_argo_repository_no_hardcoded_key.py + test_git_ssh.py → 2 passed
  • rg "GIT_SERVER_KEY_PATH|GIT_ARGO_APPLICATIONS_KEY|git-test-server" → geen residue
  • Pydantic strict-mode (Settings) accepteert de geknipte .env files zonder extra_forbidden-fouten

@uittenbroekrobbert uittenbroekrobbert merged commit 13451b3 into main May 27, 2026
18 of 20 checks passed
@uittenbroekrobbert uittenbroekrobbert deleted the fix/remove-dead-git-rce branch May 27, 2026 10:02
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