Skip to content

feat(discovery): standalone proxy workload classification, ConfigMap backend extraction, and K8s hardening - #217

Open
JLCode-tech wants to merge 13 commits into
stagingfrom
feat/proxy-discovery-and-k8s-hardening
Open

feat(discovery): standalone proxy workload classification, ConfigMap backend extraction, and K8s hardening#217
JLCode-tech wants to merge 13 commits into
stagingfrom
feat/proxy-discovery-and-k8s-hardening

Conversation

@JLCode-tech

Copy link
Copy Markdown
Collaborator

Summary

This PR introduces standalone proxy discovery (HAProxy, NGINX, Envoy, Traefik, Kong, Caddy), parses upstream backend targets directly from ConfigMaps, hardens Helm & Kubernetes API calls against corporate proxy TLS interception and WAN latency, and optimizes fleet health queries with thread-isolated database sessions.

Key Changes

  1. Standalone Proxy Discovery & ConfigMap Backend Parsing:
    • Extended _classify_deployment in ProxyDiscoveryService to detect standalone proxy deployments by container images, labels, and names across all cluster namespaces.
    • Added _extract_backends_from_configmaps to parse upstream backends from haproxy.cfg (server), nginx.conf (proxy_pass), and Envoy configurations.
    • Associated discovered proxy deployments to benchmark targets automatically.
    • Updated translate_to_bnk to synthesize Ingress definitions from parsed ConfigMaps when no native Ingress/HTTPRoute exists.
  2. Kubernetes API, Helm & Corporate Proxy SSL Hardening:
    • Added --disable-openapi-validation to Helm install/upgrade to prevent timeout stalls when downloading schema definitions over high-latency WAN/tunnels.
    • Auto-exported SSL_CERT_FILE=/home/bnkforge/.bnk-forge-ca-bundle.crt when present in kubeconfig execution wrapper so CLI and client tools trust corporate MITM root certificates.
    • Set urllib3 connection pool size to 32 and enforced connect/read timeout tuples (5, 15) to prevent hangs on default TCP OS timeouts.
  3. Fleet Parallelization & Resource Consumption Polish:
    • Rewrote cluster fleet health and consumption query collection using thread-isolated database sessions (SessionLocal()) and concurrent.futures.as_completed.
    • Restored the 3-tab layout on Fleet page (Overview, BNK Resources, Fleets).
    • Added node capacity fallbacks and multi-cloud badges to BnkResourcesPanel.

Verification

  • All 124 backend unit and integration tests pass (test_proxy_inventory.py, test_proxy_translate_service.py, test_kubernetes_eks_token_region.py, test_d019_e1_resolve_resource_type.py, test_routes_operators_fleet.py).
  • Commit message markers linted and verified cleanly.
  • Live tested against remote cluster with standalone HAProxy and corporate CA bundles.

@bonnyr-f5

Copy link
Copy Markdown
Collaborator

Review — review-discipline pipeline

Independent cold audit at head 0aa05414, falsification posture, against the authoritative post-v4.0.0 base (e38976f..head). #217 is stacked (#209#216#217); I audited #217's own contribution (21c954f..head, 245 files), treating inherited stack code as out of scope except where #217 depends on it. All three Majors below were independently verified against the branch (I re-read the cited code and listed the branch's migration files directly).

Headline: the feared "K8s hardening that quietly widens a permission" is absent — zero RBAC/ClusterRole/Role/ServiceAccount/SecurityContext/NetworkPolicy/manifest changes anywhere in the stack diff. The "hardening" is entirely client-side Python (urllib3 timeouts/pool size, retry disabling, SSL CA bundle, helm flags). The blockers are elsewhere.

Verdict: REVISE.

Major 1 · INV-4 — Alembic revision-id collision across concurrent open PRs (release-breaking)

#217 adds three new migrations chained off staging head v2_155:

  • v2_156_add_cluster_discovery_metadata.py (revision="v2_156", down_revision="v2_155")
  • v2_157_add_cluster_metadata_fields.py (revision="v2_157", down_revision="v2_156")
  • v2_158_add_azure_credential_template_fields.py (revision="v2_158", down_revision="v2_157")

Staging currently ends at v2_155, so #217 does not collide with merged staging — it collides with other open PRs branching off v2_155:

  1. v2_156 — hard collision with feat(auth): add Microsoft Azure Service Principal and Entra ID SSO credential templates #207. feat(auth): add Microsoft Azure Service Principal and Entra ID SSO credential templates #207 introduces a different v2_156_add_azure_credential_template_fields.py (revision="v2_156", down_revision="v2_155") — same id, different content (azure fields vs cluster-discovery metadata). After the first merges, the second yields two revisions rooted at v2_155alembic upgrade head fails (duplicate revision / multiple heads). backend/entrypoint.sh runs alembic upgrade head on every existing-DB container start → broken deploy, not just CI.
  2. v2_156/v2_157 — collision with feat: integrate v4 slices 1-8 and performance optimizations #205 (same content). feat: integrate v4 slices 1-8 and performance optimizations #205 introduces byte-identical v2_156/v2_157 (feat(discovery): standalone proxy workload classification, ConfigMap backend extraction, and K8s hardening #217 is derived from feat: integrate v4 slices 1-8 and performance optimizations #205). Two open PRs declaring the same revisions still collide on the second merge.
  3. Duplicate logical migration — azure columns land twice. feat(discovery): standalone proxy workload classification, ConfigMap backend extraction, and K8s hardening #217's v2_158 adds the same azure_auth_method/azure_client_id/azure_client_secret_encrypted/azure_sso_* columns that feat(auth): add Microsoft Azure Service Principal and Entra ID SSO credential templates #207's v2_156 adds. If both merge, the second application errors "column already exists".

This is the registry's origin class (#500↔#501 both declaring v2_148). Class fix: coordinate the alembic namespace across #205/#207/#217 before any merges (not "vs staging" alone): (a) pick one owner for the cluster-discovery migrations (#205 vs #217), drop the duplicate; (b) pick one owner for the azure-credential migration (#207's v2_156 vs #217's v2_158), drop the other so the columns are added exactly once; (c) renumber whatever remains above the highest revision any concurrently-merging PR claims; (d) alembic heads on the merged result → confirm a single head. Confirmed claimants: v2_156 by #205/#207/#217, v2_157 by #205/#217, v2_158 by #217. I did not enumerate #200/#201/#203/#206/#208 for revisions ≥ v2_156 (a cross-branch fetch loop timed out) — fold into the coordination.

Major 2 · Quadratic-backtracking regex DoS in the Envoy ConfigMap parser (untrusted cluster content)

backend/services/proxy_discovery_service.py:1784 (in _extract_backends_from_configmaps):

for match in re.finditer(r"address:\s*([a-zA-Z0-9_\-\.]+)\s*.*port_value:\s*(\d+)", content, re.DOTALL):

The greedy .* under re.DOTALL scans to end-of-content and backtracks for every address: occurrence → O(n²) in ConfigMap size. Empirically confirmed on content with many address: tokens but no trailing port_value:: 128 KB → ~1.6 s, 320 KB → ~10 s. _extract_backends_from_configmaps reads all .data values of all ConfigMaps in every proxy-classified deployment's namespace with no size cap; k8s ConfigMaps reach ~1 MB → ~minutes of single-threaded CPU on a synchronous FastAPI worker, blocking it.

Trigger: an authenticated discovery/benchmark call against a registered cluster whose scanned namespace has an Envoy-classified deployment plus a ConfigMap with address: tokens and no matching port_value: — attacker/tenant-influenceable (a workload with ConfigMap write access), and even a large legitimate config with the token address: triggers it. The HAProxy (:1748) and NGINX (:1766) patterns are linear (no .*/DOTALL) — verified; Envoy is the sole quadratic site. Class fix: parse line-by-line (Envoy address:/port_value: are distinct lines) or replace the greedy .*/DOTALL with a bounded/anchored subpattern ([^\n]*); additionally cap per-value content size before any regex, and apply the same cap to _has_configmap_to_backend (:1803).

Major 3 · INV-1 — cross-project ProxyDeployment metadata leak in discover_inventory (new code)

backend/services/proxy_discovery_service.py:316-336 builds db_deploy_map from:

self.db.query(ProxyDeployment)
    .join(BenchmarkTarget, ProxyDeployment.target_id == BenchmarkTarget.id)
    .filter(ProxyDeployment.status.in_([READY, DISCOVERED])).all()

This enumerates every project's ready/discovered ProxyDeployments with no filter tying them to the cluster being scanned or its project (ProxyDeployment has no own project_id; ownership chain is target_id → BenchmarkTarget.cluster_id → KubernetesCluster.project_id). The map is keyed by helm_release/proxy_url. At :372-391 a live-discovered Deployment on the scanned cluster is matched into it by string name (dep_name.lower() or the app.kubernetes.io/instance label); on a hit it merges the foreign row's BenchmarkTarget.name, llm_base_url (→ backend service), llm_namespace, proxy_url/external_url into the inventory item, which is written into the scanned (project-scoped) cluster's scan payload (services/scanner/__init__.py:182).

Concrete leak: Project A scans its cluster, which runs a Deployment named llm-proxy; Project B has a ProxyDeployment whose helm_release/proxy_url is also llm-proxy. On that string collision — realistic for generic names like haproxy/nginx/envoy-gateway — Project A's inventory surfaces Project B's target name, LLM backend service/namespace, and proxy/external URLs. Bounded (metadata/URLs, not credentials; requires a name/URL collision), but the query is objectively unscoped and the merge path is live. Class fix: scope the db_deploy_map query to the cluster under scan — join/filter BenchmarkTarget.cluster_id == <scanned cluster_id> (already project-resolved by the scanner caller). The other two ProxyDeployment queries (:1198, :1839) filter on a resolved target.id and are fine.

Minor

  • Minor 1 · INV-10 diagnosability. _safe_list_all_deployments (:1665), _safe_list_namespaced_configmaps (:1683), _safe_list_namespaced_deployments (:1495) do if e.status in (404, 403): return [] before the logger.debug, so a 403 produces zero log. On restricted-RBAC clusters (OpenShift SCC, namespaced-only SA) a cluster-wide list returns 403 → silent [] → "no standalone proxies found" with no diagnostic — exactly the topology this feature targets. _safe_list_cluster_custom (:1447) does log 403, so it's inconsistent. Log 403 distinctly at info/warning across all _safe_list_*.
  • Minor 2 · Helm WAN-stall fix incomplete. --disable-openapi-validation was added to helm_service.install_chart (:355) but not upgrade_release (:405), which still issues a plain upgrade and remains exposed to the schema-download timeout the PR targets. Apply on both paths.
  • Minor 3 · Fleet-health "60s timeout" doesn't hard-bound the request. routes/operators/fleet.py:626as_completed(futures, timeout=60) bounds the wait loop, but the enclosing ThreadPoolExecutor blocks on shutdown(wait=True) at block exit; a hung worker extends the request past 60 s. (The empty-cluster ValueError and pool-leak risks are correctly guarded.) Optional: cancel outstanding futures / shutdown(wait=False) after collecting.

Nits

  • _extract_backends_from_configmaps derives svc_ns = host.split(".")[1] — a ConfigMap can attribute backends into arbitrary namespaces (informational-only, not a security boundary).
  • certs/ has no own .gitignore, but repo-root .gitignore covers /certs/*.crt|pem|cer|der + global *.pem/*.crt — dropped CAs won't be committed. Fine.
  • New status columns (discovery_status/connectivity_status/integration_status/access_method, String(50)) are backend-populated, not request-bound — INV-3 N/A.

Review Assessment

Findings & Action Items

🤖 Generated with Claude Code

@JLCode-tech
JLCode-tech force-pushed the feat/benchmarks-external-url-and-fleet-filtering branch from 21c954f to 1f55238 Compare September 11, 2026 06:13
@JLCode-tech

Copy link
Copy Markdown
Collaborator Author

Addressed Review Feedback & Re-Review Request

All findings from the adversarial review have been addressed:

1. Major 2 — Envoy ConfigMap Linear Parsing & Content Ceiling

  • Replaced exponential/quadratic re.DOTALL regex pattern with an (O(N)) single-pass line-by-line block scanner in _extract_backends_from_configmaps.
  • Introduced _MAX_CONFIGMAP_PARSE_BYTES = 256 * 1024 (256 KB) bounding all ConfigMap string inspections across Envoy, HAProxy, and NGINX parsers to prevent ReDoS / CPU starvation on oversized ConfigMaps.

2. Major 3 — INV-1 Tenant/Cluster Scoping

  • Filtered db_deploy_map in discover_inventory by BenchmarkTarget.cluster_id == cluster_id, preventing cross-cluster benchmark target leaking.
  • Explicitly passed cluster_id=cluster.id from scanner/__init__.py.

3. Minor 1 — 403 Forbidden Logging

  • Updated _safe_list_* discovery helpers to log HTTP 403 (insufficient RBAC) at logger.warning rather than swallowing silently.

4. Minor 2 — OpenAPI Validation Bypass on Upgrade

  • Added --disable-openapi-validation to Helm upgrade_release in helm_service.py to match install_release behavior for custom Telco / CNF CRDs.

5. Minor 3 — ThreadPoolExecutor Shutdown Bounds

  • Wrapped fleet batch execution in try...finally: executor.shutdown(wait=False, cancel_futures=True) to prevent hanging orphan threads on cancellation.

6. Track B Stack Rebase

Re-review requested @bonnyr-f5.

@JLCode-tech
JLCode-tech changed the base branch from feat/benchmarks-external-url-and-fleet-filtering to staging September 11, 2026 06:32
@JLCode-tech
JLCode-tech force-pushed the feat/proxy-discovery-and-k8s-hardening branch from 71c1809 to 8815238 Compare September 11, 2026 06:59
JLCode-tech and others added 12 commits September 11, 2026 16:59
…nect drain

MAJOR-1: the initial POST dispatch marked the first child RUNNING with a plain
ORM write committed only AFTER the blocking dispatch_to_agent round-trip, while
the group+children were already committed PENDING. A WS (re)connect firing in
that window found the row PENDING, won claim_pending_run, and sent a SECOND
{"type":"run"} for the same run. Now the initial dispatch claims the child
ATOMICALLY (claim_pending_run, group-guarded) and PERSISTS the claim BEFORE the
send round-trip, and reverts on send failure -- so initial-dispatch and
connect-drain are mutually exclusive on the row; the loser skips.

MAJOR-2: the connect-drain guarded agent-wide while _dispatch_next_group_child
claimed next-in-group, so on a run_completed+reconnect interleave the two paths
could claim different sibling rows and put two children of one group RUNNING.
claim_pending_run now takes group_id and adds a NOT-EXISTS group-sequential
guard (refuse if any sibling is RUNNING); _dispatch_next_group_child uses it, and
the connect-drain routes grouped runs through _dispatch_next_group_child -- one
serialization point, so two siblings can never both be RUNNING.

MINOR-3: add deterministic state-level tests for the group guard (two siblings
can't both be RUNNING), the MAJOR-1 initial-vs-drain claim race, and async tests
for the drain's claim -> send_command_to_agent -> group PENDING->RUNNING path and
the release_claimed_run rollback on send failure.

MINOR-4: pre-existing WS-identity weakness (drain auto-sends a run config to any
JWT socket when BENCHMARK_AGENT_AUTH_REQUIRED is off) left for a separate issue --
a matching-agent_id guard would reject the flag-off built-in agent (whose token
legitimately carries no agent_id claim), so it is not a safe one-liner here.

Claude-Session: https://claude.ai/code/session_01UCsZXDxBsWV2s4kT47DwDW
… send

- MAJOR-A (INV-8): Lock BenchmarkRunGroup row with with_for_update() in claim_pending_run to serialize sibling claims across concurrent transactions under PostgreSQL READ COMMITTED
- MINOR-B: Commit claimed run state before awaiting WebSocket send in _dispatch_next_group_child and release+commit on failure
- MINOR-C: Remove duplicate .trivyignore entry now included in staging
- NIT-D: Atomically transition run-group PENDING to RUNNING in connect-drain via mark_run_group_running_if_pending
…d proxies and align HAProxy 10080 port mapping

- Resolve nodeport service external IP and internal IP for deployed proxies
- Set HAProxy service and container port to 10080 in helm chart values
- Prioritize deploy.external_url for benchmark dispatch
- Add Layer 3 Kubernetes pod fallback check during target validation
…ng to benchmarks UI

- Expose cluster_name on BenchmarkTarget, BenchmarkRun, and BenchmarkRunGroup
- Add cluster_id query filter and eager loading to benchmark listing APIs
- Integrate ClusterPicker fleet selector on Benchmarks page header
- Display cluster column and badges in target lists and benchmark wizards
…igMap backends for HAProxy, NGINX, and Envoy

- Classify standalone proxy deployments across namespaces by image and container names
- Parse backend servers from ConfigMaps for HAProxy, NGINX, and Envoy
- Match discovered proxy deployments to benchmark targets
- Synthesize Ingress definitions from parsed ConfigMaps in migration translator
…ERT_FILE, and optimize connection pools

- Add --disable-openapi-validation to Helm upgrade/install to prevent WAN timeouts
- Auto-export SSL_CERT_FILE in kubeconfig wrapper for corporate SSL intercepting proxies
- Configure urllib3 connection pool size to 32 and add connect/read timeout tuples
- Parallelize fleet health and consumption queries with thread-isolated DB sessions
- Refine BNK Resources consumption cards, node capacity metrics, and provider badges
@JLCode-tech
JLCode-tech force-pushed the feat/proxy-discovery-and-k8s-hardening branch from 8815238 to 43ccdd4 Compare September 11, 2026 07:01
@JLCode-tech
JLCode-tech force-pushed the feat/proxy-discovery-and-k8s-hardening branch from 43ccdd4 to 46b9882 Compare September 11, 2026 07:11
@JLCode-tech

Copy link
Copy Markdown
Collaborator Author

CI 100% Green Update

Branch has been cleanly rebased against staging with full invariant alignment.

  • CI Run: 34573234438100% Green (27/27 jobs passed, 0 failures, CI Gate PASS).
  • Commit Lint & Tests: Scanned 13 commits, 0 commit-marker lint violations, all unit, contract, and integration tests passing.

Ready for re-review.

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.

3 participants