Skip to content

feat(site-router): add routed site-to-site IPsec gateway app (Phase 1) - #3426

Draft
myasnikovdaniil wants to merge 8 commits into
mainfrom
feat/site-router
Draft

myasnikovdaniil wants to merge 8 commits into
mainfrom
feat/site-router

Conversation

@myasnikovdaniil

@myasnikovdaniil myasnikovdaniil commented Jul 22, 2026 •

Copy link
Copy Markdown
Contributor

What this PR does

This PR adds site-router, catalog app for routed site-to-site IPsec between tenant pods and remote network, remote source IP is preserved. Gateway runs as KubeVirt VM in tenant namespace. site-router-controller does what chart can't: deny-set validation of remoteCIDRs, kube-ovn return route, live config push over management API, source filter confirmation and route withdrawal on delete. Same deny-set check also runs at admission in Application REST handler.

PR is 5 commits, they can be reviewed one by one:

  1. deny-set admission
  2. gateway API client and config renderer
  3. site-router-controller
  4. app chart and platform registration (naas bundle, only with iaas enabled)
  5. two-VM e2e suite

Commits after them are review fixes: stale follow-ups in docs, amd64 node selector for gateway VM with multi-arch allowlist entry for its disk, stale comments in e2e.

Gateway boots KubeVirt disk of barerouter, router image built from VyOS rolling sources and published by Aenix with SBOM and sources manifest. PR pins v0.1.1 by digest and nothing in this repo builds or republishes image anymore, this is my answer to licensing review. I rewrote branch history, so earlier iterations including in-tree image build are gone.

Tested with Go unit tests, helm-unittest of every touched chart and bats under dash, e2e suite runs in CI on this push. Known gaps are listed in packages/apps/site-router/docs/followups.md.

Docs are in cozystack/website#728: reference page generated from this README and site-to-site IPsec guide, it should merge after this PR.

Screenshots

No UI code changes, app appears in catalog through its ApplicationDefinition.

Downstream repositories

Release note

feat(site-router): new site-router app for routed site-to-site IPsec connectivity from tenant namespaces, available in naas bundle when iaas is enabled.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@github-actions github-actions Bot added area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review kind/feature Categorizes issue or PR as related to a new feature size/XXL This PR changes 1000+ lines, ignoring generated files labels Jul 22, 2026
@dosubot dosubot Bot added the area/networking Issues or PRs related to networking (ingress, gateway, vpn, metallb, kube-ovn) label Jul 22, 2026
@coderabbitai

coderabbitai Bot commented Jul 22, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds the SiteRouter API, Helm charts, VyOS image pipeline and renderer, controller reconciliation with metrics and security mediation, deny-set admission validation, platform packaging, and a deferred live Chainsaw acceptance suite.

Changes

SiteRouter contracts and packaging

Layer / File(s) Summary
API and application packaging
api/apps/v1alpha1/siterouter/*, packages/apps/site-router/*, packages/system/site-router-rd/*
Defines SiteRouter configuration types, Helm values and templates, tenant resources, application metadata, and generated deepcopy methods.
Deny-set admission validation
internal/siterouter/denyset/*, pkg/registry/apps/application/*, pkg/apis/apps/validation/validation.go
Rejects malformed, unsupported, reserved, or cluster-overlapping remoteCIDRs during admission and reconciliation.
Platform package wiring
packages/core/platform/*, packages/system/cozystack-api/templates/rbac.yaml
Adds SiteRouter package sources and conditionally emits them with the IaaS-enabled NaaS bundle.
VyOS image pipeline
packages/system/vyos-router-image/*, packages/system/vm-default-images/*, hack/build-matrix*, Makefile
Builds a pinned VyOS qcow2/containerDisk image, publishes its digest tag, registers the golden image, and adds the controller image build.
CI and deferred E2E selection
.github/workflows/pull-requests.yaml, hack/select-e2e*
Adds VyOS-specific CI gating and excludes the deferred site-router suite from automatic E2E selection.

Controller and VyOS runtime

Layer / File(s) Summary
VyOS client and rendering
internal/vyos/*
Adds the VyOS HTTP client, observation parsers, deterministic configuration renderer, firewall rules, IPsec/BGP/static-route operations, and renderer tests.
Controller reconciliation and state
internal/controller/siterouter/*
Adds SiteRouter reconciliation, route annotation mediation, configuration hashing and confirmation, source-filter gating, metrics, status events, cleanup, and controller tests.
Controller deployment
cmd/site-router-controller/main.go, packages/system/site-router-controller/*
Wires controller-runtime setup, management CIDR validation, metrics and health endpoints, RBAC, deployment security settings, image packaging, and scraping.

Deferred acceptance coverage

Layer / File(s) Summary
Live SiteRouter Chainsaw suite
hack/e2e-chainsaw/site-router/*
Defines the two-site topology, route propagation checks, tunnel and MSS validation, negative-security attribution stubs, and teardown assertions.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested labels: kind/api-change, area/api, area/virtualization

Suggested reviewers: kvaps

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 64.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary change: adding the routed site-to-site IPsec gateway app for SiteRouter.
  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/site-router

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🧹 Nitpick comments (5)
internal/controller/siterouter/status.go (1)

65-68: 🚀 Performance & Scalability | 🔵 Trivial

Uncached full-namespace pod List on every 30s poll can pressure the apiserver at scale.

surfacePendingRoutePods runs on every reconcile (runtimePollInterval), and this List goes straight to the apiserver (uncached). In a busy tenant namespace with many pods and/or several SiteRouter instances, this is a recurring unindexed list against the API server. Consider gating it (e.g. only re-list when the programmed route set actually changed, or back off the surfacing cadence relative to the runtime poll) to bound the load.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/controller/siterouter/status.go` around lines 65 - 68, Reduce
repeated API-server load from the pod List in surfacePendingRoutePods by gating
or backing off the namespace relist instead of executing it on every
runtimePollInterval reconcile. Prefer relisting only when the programmed route
set changes, or otherwise enforce a slower bounded surfacing cadence, while
preserving pending-route detection and existing error handling.
packages/system/vyos-router-image/images/vyos-router-disk/Dockerfile (1)

10-11: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer COPY over ADD for local files.

ADD is only needed for its URL-fetch/archive-auto-extraction behavior; a local qcow2 copy doesn't use either. COPY is the idiomatic, unambiguous choice here.

🐛 Proposed fix
-ADD _out/assets/vyos-router-amd64.qcow2 /disk/vyos-router.qcow2
+COPY _out/assets/vyos-router-amd64.qcow2 /disk/vyos-router.qcow2
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/system/vyos-router-image/images/vyos-router-disk/Dockerfile` around
lines 10 - 11, Replace the ADD instruction in the Dockerfile with COPY for the
local vyos-router-amd64.qcow2 file, preserving the existing source and
destination paths.

Source: Linters/SAST tools

hack/e2e-chainsaw/site-router/chainsaw-test.yaml (1)

557-566: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Use bracket notation for the annotation key in this JSONPath filter. It’s the safer form for dotted annotation keys in kubectl JSONPath predicates, and this final check should avoid escaping quirks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hack/e2e-chainsaw/site-router/chainsaw-test.yaml` around lines 557 - 566,
Update the JSONPath predicate in the gateway pod query to access the
ovn.kubernetes.io/port_security annotation using bracket notation instead of
escaped dotted-key notation. Keep the existing filtering for annotations equal
to "false" and the surrounding validation unchanged.
internal/vyos/client.go (1)

97-113: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

WithInsecureSkipVerify clones the wrong base transport and skips MinVersion.

Two issues in this option:

  1. It clones http.DefaultTransport unconditionally instead of the client's current c.http.Transport. If a caller combines WithHTTPClient (supplying a custom *http.Client/Transport, e.g. for a proxy) with WithInsecureSkipVerify, this silently discards that custom transport's settings and mutates the caller-supplied http.Client object's Transport field in place — a surprising side effect for API consumers who don't expect option ordering to matter this much.
  2. Both tls.Config{InsecureSkipVerify: true} literals omit MinVersion. Even given the documented in-band-token rationale for skipping cert verification, pinning MinVersion: tls.VersionTLS12 (or higher) is still cheap defense-in-depth against protocol downgrade.
🔧 Suggested fix
 func WithInsecureSkipVerify() Option {
 	return func(c *Client) {
-		if base, ok := http.DefaultTransport.(*http.Transport); ok {
+		base, ok := c.http.Transport.(*http.Transport)
+		if !ok {
+			base, ok = http.DefaultTransport.(*http.Transport)
+		}
+		if ok {
 			clone := base.Clone()
-			clone.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} //nolint:gosec // VyOS self-signed cert; in-band token authenticates the channel
+			clone.TLSClientConfig = &tls.Config{InsecureSkipVerify: true, MinVersion: tls.VersionTLS12} //nolint:gosec // VyOS self-signed cert; in-band token authenticates the channel
 			c.http.Transport = clone

 			return
 		}

-		// Fallback when callers have replaced http.DefaultTransport with
-		// something that is not an *http.Transport.
 		c.http.Transport = &http.Transport{
-			TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec // VyOS self-signed cert; in-band token authenticates the channel
+			TLSClientConfig: &tls.Config{InsecureSkipVerify: true, MinVersion: tls.VersionTLS12}, //nolint:gosec // VyOS self-signed cert; in-band token authenticates the channel
 		}
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/vyos/client.go` around lines 97 - 113, Update WithInsecureSkipVerify
to derive the transport from the client’s current c.http.Transport, preserving
custom transport settings supplied through WithHTTPClient and avoiding mutation
of the caller’s client transport. Apply TLS configuration with
InsecureSkipVerify and MinVersion set to tls.VersionTLS12 in both the cloned and
fallback transport paths.

Source: Linters/SAST tools

packages/system/cozystack-api/templates/rbac.yaml (1)

9-17: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

ClusterRole grant is not namespace-scoped.

resourceNames: ["cozystack"] restricts by name but this is a ClusterRole, so the grant applies to any configmaps/cozystack in any namespace, not only cozy-system/cozystack (the only one rest_siterouter.go reads). Low risk given a colliding ConfigMap name is unlikely, but worth noting as a least-privilege gap.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/system/cozystack-api/templates/rbac.yaml` around lines 9 - 17,
Restrict the RBAC permission for the ConfigMap named cozystack to the
cozy-system namespace instead of granting it cluster-wide through the
ClusterRole. Update the surrounding rbac.yaml binding or role structure while
preserving the existing get-only, name-scoped access required by SiteRouter
admission.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/site-router-controller/main.go`:
- Around line 66-70: Update the shared ValidateManagementCIDR validator to
reject parsed networks whose IP does not have To4() != nil, while preserving
valid IPv4 CIDR handling and existing validation errors. Ensure the
managementCIDR flag path uses this validator so IPv6 values cannot start the
controller.

In `@hack/e2e-chainsaw/site-router/chainsaw-test.yaml`:
- Around line 425-494: Validate that GW_IP, BACKEND_IP, NODE_IP, API_CLUSTERIP,
and OTHER_POD_IP are all non-empty immediately after resolution and before any
assert_dropped or probe_from_b_stdout calls. Fail the negative-security step
with a clear error identifying every missing target, while preserving the
existing probe and attribution checks when all targets resolve.

In `@hack/select-e2e.sh`:
- Around line 37-40: Update the TODO comment in the site-router enablement
instructions to reference the active VyOS entry in
packages/system/vm-default-images/values.yaml and instruct CI to stamp
packages/system/vm-default-images/images/vyos-router-disk.tag, removing the
incorrect packages/system/vm-images and uncomment guidance.

In `@internal/vyos/render/render.go`:
- Around line 824-875: Update renderIPSec and the peer-name generation around
sanitisePeerName so names are tracked for the current tunnel batch and duplicate
sanitized descriptions receive a deterministic unique suffix, such as their
tunnel index. Use the disambiguated name consistently for both the site-to-site
peer and authentication PSK paths, while preserving existing names for
non-colliding descriptions and fallback behavior for empty descriptions.

In `@packages/apps/site-router/docs/PR-BODY.md`:
- Around line 45-47: Update the follow-up list in
packages/apps/site-router/docs/PR-BODY.md around lines 45-47 by removing the
landed reproducible VyOS build item and retaining only still-pending publishing,
digest stamping, or empirical validation work. Update
packages/apps/site-router/docs/image-lifecycle.md around line 3 to describe the
VyOS image entry as enabled and digest-pinned, replacing the outdated
“commented-out” wording.

In `@packages/apps/site-router/docs/security-model.md`:
- Around line 33-40: Update the Boundary A description to state that the
IPsec-decrypted local-input drop is emitted only when managementCIDR enforcement
is active, and explicitly qualify the open-management test path where the rule
is absent. Keep the existing explanation of tunnel coverage and the separate
Boundary B behavior unchanged.

In `@packages/apps/site-router/templates/_helpers.tpl`:
- Around line 109-120: Update the managementCIDR validation in
site-router.assertSafeVyOSInputs to perform semantic IPv4 CIDR and prefix-range
validation, not only regex shape checking. Reuse an available real IPv4-prefix
parser during template admission/rendering, and fail with the existing refusal
behavior when parsing fails before managementCIDR reaches the VyOS
configuration.

In `@packages/apps/site-router/tests/secret_cloudinit_test.yaml`:
- Around line 208-263: Update the Boundary A security-model documentation to
state that the guest guard seeds are applied only when managementCIDR
enforcement is enabled, not regardless of managementCIDR. Align the description
with the T08 tests and preserve the documented exception that
allowOpenManagement=true seeds no guest firewall guards.

In `@packages/apps/site-router/values.yaml`:
- Around line 98-100: Update the managementCIDR validation in values.yaml and
its admission/render validation to reject out-of-range IPv4 octets and prefixes
outside /0–/32, not merely unsafe characters. Preserve support for an empty
value when allowOpenManagement=true and the documented default CIDR, then add
regression cases covering malformed octets and prefix lengths.
- Around line 74-81: Enforce a minimum of one CPU core for resources.cpu in the
values schema/API definition, rejecting zero and negative values while retaining
whole-core validation. Regenerate the derived schema and API types, then extend
the validation tests to cover both zero and negative CPU values. Anchor the
changes to the resources.cpu definition and its existing validation tests.

In `@packages/system/site-router-controller/templates/rbac.yaml`:
- Around line 27-39: Restrict the RBAC permissions represented by the
ClusterRole rather than granting cluster-wide Secret and ConfigMap list/watch
access. Replace broad resources under the unnamed core API rules with
namespace-scoped bindings or narrowly targeted named-object access for the
specific per-instance Secrets and the cozy-system/cozystack ConfigMap required
by the controller, while preserving only the necessary read operations.
- Around line 16-24: Constrain the RBAC rules in rbac.yaml so the controller
does not receive cluster-wide patch access to arbitrary Pods or Namespaces.
Replace the broad patch permissions for the resources listed in the rules with
the narrowest supported authorization scope, and preserve only the read verbs
needed for discovery; if Kubernetes RBAC cannot express the required
single-resource or label restriction, remove patch from these cluster-wide rules
and handle the targeted updates through an appropriately scoped mechanism.

In `@packages/system/site-router-controller/values.yaml`:
- Around line 7-16: The values configuration currently hard-codes the kube-ovn
default in managementCidr. Update managementCidr to derive from the cluster’s
networking.podCIDR, or make it unset and require an explicit override for
non-default Pod CIDRs, while preserving the required match with the controller
--management-cidr and app chart managementCIDR values.

---

Nitpick comments:
In `@hack/e2e-chainsaw/site-router/chainsaw-test.yaml`:
- Around line 557-566: Update the JSONPath predicate in the gateway pod query to
access the ovn.kubernetes.io/port_security annotation using bracket notation
instead of escaped dotted-key notation. Keep the existing filtering for
annotations equal to "false" and the surrounding validation unchanged.

In `@internal/controller/siterouter/status.go`:
- Around line 65-68: Reduce repeated API-server load from the pod List in
surfacePendingRoutePods by gating or backing off the namespace relist instead of
executing it on every runtimePollInterval reconcile. Prefer relisting only when
the programmed route set changes, or otherwise enforce a slower bounded
surfacing cadence, while preserving pending-route detection and existing error
handling.

In `@internal/vyos/client.go`:
- Around line 97-113: Update WithInsecureSkipVerify to derive the transport from
the client’s current c.http.Transport, preserving custom transport settings
supplied through WithHTTPClient and avoiding mutation of the caller’s client
transport. Apply TLS configuration with InsecureSkipVerify and MinVersion set to
tls.VersionTLS12 in both the cloned and fallback transport paths.

In `@packages/system/cozystack-api/templates/rbac.yaml`:
- Around line 9-17: Restrict the RBAC permission for the ConfigMap named
cozystack to the cozy-system namespace instead of granting it cluster-wide
through the ClusterRole. Update the surrounding rbac.yaml binding or role
structure while preserving the existing get-only, name-scoped access required by
SiteRouter admission.

In `@packages/system/vyos-router-image/images/vyos-router-disk/Dockerfile`:
- Around line 10-11: Replace the ADD instruction in the Dockerfile with COPY for
the local vyos-router-amd64.qcow2 file, preserving the existing source and
destination paths.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8c8c7016-ab40-47eb-bbaa-21e2ba4539b7

📥 Commits

Reviewing files that changed from the base of the PR and between 706c3f0 and d5375fb.

⛔ Files ignored due to path filters (1)
  • packages/apps/site-router/logos/site-router.svg is excluded by !**/*.svg
📒 Files selected for processing (97)
  • .github/workflows/pull-requests.yaml
  • Makefile
  • api/apps/v1alpha1/siterouter/types.go
  • api/apps/v1alpha1/siterouter/zz_generated.deepcopy.go
  • cmd/site-router-controller/main.go
  • hack/build-matrix.sh
  • hack/build-matrix_test.bats
  • hack/e2e-chainsaw/site-router/_probe-lib.sh
  • hack/e2e-chainsaw/site-router/backend.yaml
  • hack/e2e-chainsaw/site-router/chainsaw-test.yaml
  • hack/e2e-chainsaw/site-router/fresh-route-pod.yaml
  • hack/e2e-chainsaw/site-router/pending-route-pod.yaml
  • hack/e2e-chainsaw/site-router/remote-site-b.yaml
  • hack/e2e-chainsaw/site-router/site-router-a.yaml
  • hack/select-e2e.sh
  • hack/select-e2e_test.bats
  • internal/controller/siterouter/cnimediation.go
  • internal/controller/siterouter/cnimediation_test.go
  • internal/controller/siterouter/metrics.go
  • internal/controller/siterouter/metrics_test.go
  • internal/controller/siterouter/reconciler.go
  • internal/controller/siterouter/reconciler_test.go
  • internal/controller/siterouter/status.go
  • internal/controller/siterouter/status_test.go
  • internal/controller/siterouter/vyospush.go
  • internal/controller/siterouter/vyospush_test.go
  • internal/siterouter/denyset/denyset.go
  • internal/siterouter/denyset/denyset_test.go
  • internal/vyos/client.go
  • internal/vyos/client_test.go
  • internal/vyos/observation.go
  • internal/vyos/parse.go
  • internal/vyos/parse_test.go
  • internal/vyos/render/render.go
  • internal/vyos/render/render_netnew_test.go
  • internal/vyos/render/render_security_test.go
  • internal/vyos/render/render_test.go
  • packages/apps/site-router/.helmignore
  • packages/apps/site-router/Chart.yaml
  • packages/apps/site-router/Makefile
  • packages/apps/site-router/README.md
  • packages/apps/site-router/charts/cozy-lib
  • packages/apps/site-router/docs/PR-BODY.md
  • packages/apps/site-router/docs/followups.md
  • packages/apps/site-router/docs/image-lifecycle.md
  • packages/apps/site-router/docs/security-model.md
  • packages/apps/site-router/templates/_helpers.tpl
  • packages/apps/site-router/templates/dashboard-resourcemap.yaml
  • packages/apps/site-router/templates/dv.yaml
  • packages/apps/site-router/templates/networkpolicy.yaml
  • packages/apps/site-router/templates/secret-cloudinit.yaml
  • packages/apps/site-router/templates/secret-psk.yaml
  • packages/apps/site-router/templates/service.yaml
  • packages/apps/site-router/templates/vm.yaml
  • packages/apps/site-router/templates/workloadmonitor.yaml
  • packages/apps/site-router/tests/dv_test.yaml
  • packages/apps/site-router/tests/networkpolicy_test.yaml
  • packages/apps/site-router/tests/policy_workloadmonitor_todo_test.yaml
  • packages/apps/site-router/tests/secret_cloudinit_test.yaml
  • packages/apps/site-router/tests/secret_psk_test.yaml
  • packages/apps/site-router/tests/service_test.yaml
  • packages/apps/site-router/tests/values_matrix_test.yaml
  • packages/apps/site-router/tests/vm_test.yaml
  • packages/apps/site-router/tests/workloadmonitor_test.yaml
  • packages/apps/site-router/values.schema.json
  • packages/apps/site-router/values.yaml
  • packages/core/platform/sources/site-router-application.yaml
  • packages/core/platform/sources/site-router-controller.yaml
  • packages/core/platform/templates/bundles/naas.yaml
  • packages/core/platform/tests/bundles_site_router_naas_test.yaml
  • packages/system/cozystack-api/templates/rbac.yaml
  • packages/system/site-router-controller/Chart.yaml
  • packages/system/site-router-controller/Makefile
  • packages/system/site-router-controller/images/site-router-controller/Dockerfile
  • packages/system/site-router-controller/templates/deployment.yaml
  • packages/system/site-router-controller/templates/podscrape.yaml
  • packages/system/site-router-controller/templates/rbac-bind.yaml
  • packages/system/site-router-controller/templates/rbac.yaml
  • packages/system/site-router-controller/templates/sa.yaml
  • packages/system/site-router-controller/values.yaml
  • packages/system/site-router-rd/Chart.yaml
  • packages/system/site-router-rd/Makefile
  • packages/system/site-router-rd/cozyrds/site-router.yaml
  • packages/system/site-router-rd/templates/cozyrd.yaml
  • packages/system/site-router-rd/values.yaml
  • packages/system/vm-default-images/images/vyos-router-disk.tag
  • packages/system/vm-default-images/templates/dv.yaml
  • packages/system/vm-default-images/values.yaml
  • packages/system/vyos-router-image/Chart.yaml
  • packages/system/vyos-router-image/Makefile
  • packages/system/vyos-router-image/flavors/vyos-router.toml
  • packages/system/vyos-router-image/hack/build-qcow2.sh
  • packages/system/vyos-router-image/images/vyos-router-disk/Dockerfile
  • pkg/apis/apps/validation/validation.go
  • pkg/registry/apps/application/rest.go
  • pkg/registry/apps/application/rest_siterouter.go
  • pkg/registry/apps/application/rest_siterouter_admission_test.go

Comment thread cmd/site-router-controller/main.go Outdated
Comment thread hack/e2e-chainsaw/site-router/chainsaw-test.yaml
Comment thread hack/select-e2e.sh Outdated
Comment thread internal/vyos/render/render.go
Comment thread packages/apps/site-router/docs/PR-BODY.md Outdated
Comment thread packages/apps/site-router/values.yaml
Comment thread packages/apps/site-router/values.yaml Outdated
Comment thread packages/system/site-router-controller/templates/rbac.yaml Outdated
Comment thread packages/system/site-router-controller/templates/rbac.yaml Outdated
Comment thread packages/system/site-router-controller/values.yaml Outdated

@IvanHunters IvanHunters left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: NOT LGTM

Large, well-executed Phase-1 feature, but it is enabled in the default presets (isp-full / isp-full-generic) and registered in the NaaS catalog, while its only supported load path cannot succeed on any cluster today (the golden VyOS image is not published and the digest is a zero placeholder). The failure surfaces as a silently stuck DataVolume with no legible status. The key negative-security acceptance suite that would prove the isolation model is deferred and never runs in CI.

What the PR does

Adds a catalog app site-router: routed (L3) site-to-site connectivity between tenants over an IKEv2 IPsec tunnel terminated in a VyOS gateway (KubeVirt VM). The chart materializes the VM + boot disk + tunnel Service type: LoadBalancer + Secrets (PSK, api-key) + Cilium policies. A new site-router-controller does what the chart cannot express: deny-set validation of remote networks, kube-ovn return-route annotation, relaxation of port_security on the gateway port, live config push to VyOS over its management API, tunnel/BGP observability and status. Scope: +14239/-17, 100 files.

Blocking findings

[MAJOR] Feature is enabled by default without a working default load path
packages/core/platform/values-isp-full.yaml:11, values-isp-full-generic.yaml:11, templates/bundles/naas.yaml:17-20, packages/apps/site-router/templates/dv.yaml:21-23. On a standard install the AppDefinition is registered in the NaaS catalog and the controller starts, but the default instance (image.enabled=false) clones a non-existent PVC cozy-public/vm-default-images-vyos-router (the golden image is an unpublished follow-up). A catalog tenant gets a DataVolume stuck in Pending, a VM that never boots, and no Ready=False / Event. Suggested fix: do not include site-router in the presets and do not register it in the catalog until the golden image is published and vyos-router-disk.tag is stamped with a real digest; also emit a legible status on the missing-source path.

[MAJOR] Non-resolvable image reference (zero-placeholder digest)
packages/system/vm-default-images/images/vyos-router-disk.tag:1 = ...@sha256:0000...0000. dv.yaml is an unconditional {{- range .Values.images }}, so the added vyos-router entry renders a DataVolume importing docker://…@sha256:0000…, which never resolves. The package is opt-in, but on every cluster that enables it an upgrade adds a permanently failing CDI importer and a stuck 12Gi PVC. Suggested fix: skip entries whose digest is the placeholder until a real one is stamped.

Minor findings

[MINOR] internal/controller/siterouter/reconciler.go:398-421,662-669 — a deny-set failure on the reconcile path is invisible, and the docstring over-promises. validateRemoteCIDRs returns a hard error and classify does return ctrl.Result{}, err (669) before updateStatus (332): no Event, no condition, although the docstring (400-402) claims "status surfaces it machine-readably". On a CIDR reconfigure (or if admission is bypassed) the HR stays Ready=True while routes are silently not programmed. Minor because the primary tenant path is synchronous fail-closed admission. Fix: emit a Warning Event (as vyospush.go:329 does) and correct the docstring.

[MINOR] packages/apps/site-router/values.schema.json (bgp.required=["enabled"]) + internal/controller/siterouter/vyospush_test.go:786 — bgp.enabled=true without localASN is a silent no-op. The schema only requires enabled, though the description says localASN is required. The controller silently skips BGP with no Event/condition. Fix: make localASN required-when-enabled, or surface Ready=False / Event.

Discrepancies with the PR claims

  • [PARTIAL] PR body item 8 "Negative-security acceptance suite passes": the suite is committed, but hack/select-e2e.sh:42 sets DEFERRED_SUITES="site-router" and strip_deferred removes it from all CI paths, so it never runs. The security guards (Cilium egressDeny, forward default-deny, source filter, two-boundary API isolation) have zero executed evidence of a packet drop, only render-shape unit tests.
  • [OK, verified] "api-key not tenant-readable": confirmed. The api-key Secret is absent from both the AppDefinition secrets.include and the dashboard-resourcemap.yaml Role; only the PSK (tenant's own key) and the tunnel Service are exposed to use.

Main operational risk

The isolation security model is not proven on the data plane. templates/vm.yaml:28 permanently bakes ovn.kubernetes.io/port_security: "false" onto the gateway pod (the controller only verifies, it does not re-enforce: reconciler.go:502), with an acknowledged boot window where the port is relaxed before the VyOS firewall is stamped. The only compensating control is the in-guest VyOS firewall, whose drop behavior is exactly what the deferred negative-security e2e would prove, and that suite does not run in CI. Cross-tenant reach is bounded by Cilium identity (hence not Critical), but the headline isolation claim rests on render-shape unit tests rather than executed evidence.

Verified, non-blocking notes

  • charts-direct-edit on packages/apps/site-router/charts/cozy-lib is a false positive: it is a symlink (git mode 120000), same as apps/vpn / apps/kafka. Not a vendored edit.
  • The 3 chart_lint.render_errors are harness artifacts (site-router renders cleanly under tenant-*; platform OCIRepository is a lookup dependency; the cozystack-api _cluster nil is a runtime cozystack-values injection).
  • The deny-set (denyset.go:84-89) intentionally leaves NodeCIDRs / LBPools empty, so overlap with the node subnet / LB pool is not rejected, but the effect is self-inflicted (tenant's own routing), not cross-tenant.
  • VyOS management uses InsecureSkipVerify (internal/vyos/client.go:97), bounded by --management-cidr + a per-instance token.
  • The upgrade adds a cluster-wide read-only grant (secrets / configmaps / services) to the controller SA, judged acceptable least-privilege (read-only, no exec / secret writes).
  • Watch / pod-discovery wiring is correct (the lineage webhook stamps apps.cozystack.io/application.* on the virt-launcher pod), admission is fail-closed on Create+Update, leader election is on (--leader-elect, replicas:1), securityContext is hardened, cozyrds resourceNames match the Role, no reconcile amplifier, go build/vet/test green, deny-set / admission tests are non-vacuous (checked by mutation).

Recommended follow-up

Run an end-to-end test on a disposable dev cluster: (1) whether the zero-digest DataVolume wedges the vm-default-images HelmRelease on an opt-in upgrade or just retries in the background; (2) whether the default site-router instance without the golden image gives any legible status; (3) wire up and run the deferred negative-security Chainsaw suite to get real evidence of a packet drop.

@myasnikovdaniil

Copy link
Copy Markdown
Contributor Author

IvanHunters Both MAJORs were real and are fixed, though the first one's root cause turned out to be deeper than the gating change you suggested. Range f0619b4a3..10cb5f302.

The default load path could not work whether or not the image was published. vm-default-images is registered through cozystack.platform.package.optional.default, so the golden PVC was never guaranteed on any install, and f0619b4a3 had already dropped the dependsOn to escape an install-time deadlock — leaving nothing guaranteeing the clone source. Not registering the app until publication would have hidden that rather than fixed it.

The boot disk now imports the digest-pinned appliance containerDisk straight from the registry, per instance, with the reference living in the app chart it belongs to. Worth being straight about the path there, since the history shows it: I first moved the golden into a dedicated package shipped with the app, and reviewing that found three defects in it. CDI populates a DataVolume only at creation, so a shared golden could never be advanced — a new digest would either fail the upgrade on an immutable field or be silently ignored, leaving later gateways booting the previous appliance while its cloud-init contract had moved on. Its hard dependsOn reintroduced exactly the deadlock class f0619b4a3 escaped, because bundles.disabledPackages is per-name and an operator could strand the app by disabling only the golden. And the namespaced Role it needed in cozy-public had no ordering guarantee, since site-router-controller's dependency closure never reaches cozystack-basics. Importing per instance removes all three: nothing shared to go stale, no package to disable, no cross-chart name contract. The cost is one 12Gi import per gateway instead of a shared clone, which is the right trade for an appliance a tenant runs one or two of.

On the placeholder digest — a real one is a merge prerequisite, and that is now mechanical rather than remembered: hack/image-refs-no-placeholder.bats fails the build on any all-zero digest anywhere in packages/, and is red right now on purpose. Worth recording why the shape is dangerous: it passes required and every schema check, so the failure moves from render time to a stuck import at runtime, and nightly-mirror.sh skips it silently because its ownership filter matches the build-registry host while a not-yet-published ref still carries the public one.

Both MINORs are fixed. The deny-set rejection records a Warning Event and the docstring no longer claims status surfaces it, since it returns a hard error and classify stops the pipeline before updateStatus runs. bgp.enabled without localASN records a Warning too — localASN is optional in the schema, so that document is valid and silently rendered no BGP at all.

Your "legible status on the missing-source path" is addressed, but not the way I first tried. I added a controller-side BootImageUnavailable Event and then found it unreachable in the only scenario it existed for: no image means no gateway pod, so pushVyOSConfig returns GatewayPending and classify requeues before updateStatus — which is what called it. It also never set Ready=False as I had written, since this package makes no condition write at all (D9 keeps status off the app CR). Rather than ship dead code I removed it: with a per-instance import there is no missing-source state, and an unresolvable digest now surfaces as ImportFailed on the tenant's own boot DataVolume, in their namespace, where they can actually see it.

On item 8 — the status column did read deferred-to-empirical and item 9 disclosed the CI exclusion, but the item title said "suite passes", which is what invited the reading. It now says authored-not-executed and states the zero executed drop evidence explicitly.

On the port_security operational risk: that trade-off was raised and accepted in the design proposal, cozystack/community#30. §Design records that kube-ovn v1.15.10 cannot express a CIDR in its allowed-address-pair path, so Phase 1 ships the gateway port fully relaxed with scoped port security as follow-up hardening; §Security states the guest source allow-list is therefore mandatory and platform-owned rather than defence in depth, and that Cilium bounds destinations rather than claimed source identity. Worth re-reading there — if that conclusion should change, the proposal is the place to change it.

Also narrowed the RBAC you judged acceptable, since two verbs were dead: list/watch are gone from Secrets, Services, ConfigMaps and Namespaces.

Your recommended dev-cluster run is still outstanding: build-vyos has not produced a digest yet. Its first run failed on an unrelated stale-branch test failure — this branch is 113 commits behind main, and main's naas.yaml gained a bundles.system.enabled guard that the suite's set: blocks did not satisfy, which is fixed in 10cb5f302.

@IvanHunters IvanHunters left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict

NOT LGTM. The engineering quality is high and most Phase-1 residuals are documented honestly, but on several tenant-reachable paths the security model's stated isolation properties do not hold as written, and the guards' runtime efficacy is deferred from CI. Reviewed at c2abcf19 against merge-base 2f8c953c.

Findings

[MAJOR] internal/siterouter/denyset/denyset.go:90-106, internal/controller/siterouter/reconciler.go:407-431, internal/vyos/render/render.go:649-709 — node/host network is not in the deny-set → node source-impersonation into tenant pods.

ClusterNetworksFromConfigMap only ever populates pod/service/join (plus the always-reserved link-local/loopback/default-route); NodeCIDRs/LBPools stay empty. A tenant may therefore legally declare the node subnet as a remoteCIDR — admission and reconcile both pass — and the controller then emits a TUNNEL-INGRESS accept source ∈ <node-subnet> AND dest ∈ pod/svc. The authenticated tunnel peer (the tenant's remote site) can then send decrypted packets sourced as a cluster node IP to that tenant's workloads, defeating any node-IP-based trust those workloads apply. Separately this is a doc/comment contradiction: reconciler.go:407-408 and the security-model "Deny-set validation" section state node/LB-pool are rejected, while residual #6 admits they are not. Fix: reject any remoteCIDR overlapping the node/host network, and correct the overclaiming comment + doc.

[MAJOR] internal/controller/siterouter/vyospush.go:648-661 → internal/vyos/render/render.go:686-695 — the guest source filter is not a tenant-isolation control, contrary to the doc.

tenantNetworkCIDRs returns the whole cluster pod+service CIDR, so each TUNNEL-INGRESS accept is destination-constrained to all tenants, not this instance's networks. A decrypted packet with a valid remote source and a destination that is another tenant's pod IP is accepted and forwarded by the gateway. docs/security-model.md presents this filter as enforcing "must never let tunnel traffic reach ... unrelated tenants" — it does not; cross-tenant containment rests entirely on Cilium identity at the destination pod (which a static review cannot verify). Either stop claiming the guest filter provides tenant isolation, or scope the destination set per-tenant.

[MAJOR] values.schema.json (image), templates/dv.yaml:35-37, templates/vm.yaml:28, docs/security-model.md — tenant-settable boot image nullifies every guest-side guard while port_security is off from boot.

image.enabled/url is exposed in the cozyrd openAPISchema to tenant use (dashboard-resourcemap binds use) with no operator gating, letting a tenant boot an arbitrary image as the gateway VM, which carries ovn.kubernetes.io/port_security: "false" from pod creation. Every guest-side guard (source filter, forward default-deny, Boundary-A management drop, management firewall) lives inside the image the tenant just replaced. The threat model assumes the shipped VyOS guest enforces them and does not cover the custom-image case. The remaining controls are the two namespaced Cilium policies (egressDeny covers only 169.254.0.0/16 by default; gateway-ingress is additively re-opened per the documented Boundary-B residual) plus the unverified baseline Cilium identity. Net: a tenant obtains an anti-spoof-disabled pod running arbitrary code on the tenant OVN subnet, egress-denied only to link-local. Fix: gate image.* behind an operator/admin tier (not tenant use), or document the trust boundary and default security.egressDenyCIDRs to include node/management ranges.

[MINOR] internal/controller/siterouter/cnimediation.go:143-160 — removeRoutes deletes a co-tenant's live route on delete. When the deleting instance's gateway pod is already gone (gatewayIP==""), ownership falls back to dst-matching and drops every entry whose dst ∈ this instance's remoteCIDRs, including a co-tenant site-router's entry for the same dst (gw = the co-tenant's IP). Transient blackhole of the co-tenant's return path until its next 30s reconcile — the exact co-tenant clobber the gateway-IP-known branch was written to avoid.

[MINOR] internal/controller/siterouter/cnimediation.go:85-118 — route flap when two same-namespace SiteRouters declare the same remoteCIDR. Each reconcile upserts the shared dst's gw to its own gateway IP, so the two instances flip the route back and forth every 30s, churning the namespace annotation and disrupting that dst's return path. No conflict detection.

[MINOR] internal/controller/siterouter/reconciler.go:312-317 — deny-set hard error short-circuits before route withdrawal. A remoteCIDR valid when routes were programmed but later overlapping a cluster network (admin re-scopes pod/svc CIDR) makes validateRemoteCIDRs return before programNamespaceRoutes/removeNamespaceRoutes, so stale kube-ovn return routes persist and blackhole the now-cluster-owned range. Silent (Event only; HR stays Ready).

[MINOR] mediation health has no durable status signal. The SiteRouter app-instance has no status subresource (D9); every mediation failure (InvalidRemoteCIDR, ConfigureFailed, SourceFilterPending, PortSecurityRelaxationPending, PSK/APIKey/TunnelAddress pending) surfaces only as an ephemeral Warning Event on the HelmRelease, which stays Ready=True (chart applied) while the tunnel is down. Events age out (~1h); reconciler.go:420 acknowledges this. Recommend a durable condition or a documented "check Events + WorkloadMonitor" runbook.

[MINOR] values.schema.json — input-validation asymmetry. peer.address, staticRoutes.destination/nextHop, bgp.neighbors.address are unconstrained free strings and are not deny-set-checked, while managementCIDR has a strict IPv4 pattern and remoteCIDRs is deny-set-validated. Not an injection hole (the API push uses structured JSON Operation values; the cloud-init path is guarded by assertSafeVyOSInputs), but a hostile value fails late as a ConfigureFailed→Degraded requeue loop rather than a clean admission rejection, and a staticRoute toward a cluster CIDR is pushed to the gateway. Add CIDR/IP/hostname patterns for admission-time rejection + defense-in-depth.

[MINOR] cmd/site-router-controller/main.go:67 — --management-cidr help says it governs "HTTPS 443 and SSH (22)", but the render (render.go:517-521) and cloud-init never open 22 (the appliance has no SSH). Remove the SSH mention so operators don't assume 22 is reachable/controlled by this flag.

Claim mismatches

  • [PARTIAL] security-model.md "Deny-set validation ... rejects ... node / LB-pool networks" — node/LB-pool are not enforced (Finding 1).
  • [PARTIAL] security-model.md threat model "must never let tunnel traffic reach ... unrelated tenants" as a property of the guest source filter — the destination set is cluster-wide pod+svc; cross-tenant containment is Cilium-identity-only (Finding 2).
  • [PARTIAL] security-model.md guest-guard guarantees are silently void when image.enabled=true (Finding 3).

Caveats

  • Upgrade path: N/A — all-new packages (apps/site-router, system/site-router-controller, system/site-router-rd, system/vyos-router-image), new SiteRouter kind, new cozy-site-router namespace. No pre-existing customer objects; no upgrade-regression surface (verified: no removed/renamed in-tree identifiers).
  • Fresh-install: sound — PackageSource dependsOn (networking/kubevirt/cdi/engine/controller), naas gated on bundles.iaas.enabled (avoids the isp-hosted #3376 deadlock), controller dependsOn victoria-metrics-operator for its unconditional VMPodScrape, cozystack-api ConfigMap grant name-scoped. go build + all Go unit tests + all 63 helm-unittests green; make generate not stale.
  • Render-blindness / live-only claims: the kube-ovn v1.15.10 "port_security reconciled only at pod create" behavior and the VyOS 1.5 firewall leaf syntax are author-claimed "validated live" — not executed by this static review (needs a live cluster / booted gateway).
  • Cross-tenant/node containment (Findings 1-3) cannot be verified statically (live Cilium/kube-ovn required).
  • Negative-security e2e (the Phase-1 acceptance gate proving the guards actually drop spoofed / world-destined packets) is committed but deferred from CI (hack/select-e2e.sh DEFERRED_SUITES="site-router"), so the guards' runtime efficacy is unproven in CI.
  • Management-API isolation: managementCIDR defaults to the whole pod CIDR (every pod), and Boundary B is additively re-opened, so the api-key Secret's namespace RBAC isolation is effectively the sole control on the API. That isolation holds cross-tenant (api-key excluded from the dashboard Role + AppDef secrets.include — verified), but a tenant admin can read its own gateway's token and reconfigure the router (within the tenant's own blast radius).

Recommended follow-ups

  • cozystack-pr-test negative-isolation run: (a) a spoofing custom-image gateway, (b) a tunnel-peer packet sourced as a node IP and destined to another tenant's pod IP — verify no cross-tenant/node reach; plus run the deferred negative-security chainsaw suite once the golden image ships.

Checked and sound (recorded so they need not be re-litigated): deny-set admission wired into both Create and Update; controller RBAC is strictly least-privilege (get-only on secrets/services/configmaps); secrets are redacted before truncation (correct order — a secret straddling the 256-byte cut cannot leak); deny-set masks host bits, rejects /0 and non-IPv4 (incl. IPv4-mapped-IPv6), fails closed on a ConfigMap read error, and matches admission↔reconcile via the shared helper; the cozyrd hides the api-key and exposes PSK + tunnel Service with the correct release.prefix; push failure is fail-closed (hash not recorded, Degraded + requeue, no more-open end state); reconcile is single-writer under leader election; the cloud-init config.boot path is guarded by assertSafeVyOSInputs.

@myasnikovdaniil

Copy link
Copy Markdown
Contributor Author

IvanHunters Addressed the actionable code and documentation gaps in f1ded230b and 869bafbea.

  • Node and LoadBalancer protection: admission and reconciliation now share live discovery of Node Internal/External IPs and allocated Service external/LoadBalancer IPs, represented as /32s. A topology change that makes an existing remote CIDR unsafe withdraws its programmed route.
  • Tenant isolation: the guest filter now admits the pod CIDR plus same-namespace Service ClusterIPs as /32s, not the entire service CIDR. The documentation explicitly assigns cross-tenant pod isolation to Cilium identity policy; the guest filter blocks world/non-cluster forwarding but does not claim to distinguish tenant pod identities.
  • Boot-image trust: the tenant-settable image override is removed. Gateways always use the platform’s digest-pinned appliance because the relaxed OVN port relies on guest-enforced controls.
  • Route cleanup: route ownership is persisted on the HelmRelease. Cleanup uses persisted/current gateway IPs and never falls back to destination matching, so it cannot remove a sibling’s same-destination route after the pod disappears.
  • Route conflicts: sibling instances declaring the same destination now produce RouteConflict, preserve the first route, and stop before guest configuration instead of flapping the namespace annotation.
  • Deny-set failures: a newly unsafe CIDR withdraws existing owned routes before returning the hard error, and mediation failures remove stale tunnel/BGP gauges.
  • Observability: the controller still has no durable per-instance condition, and the documentation no longer claims one. Typed pending states now emit Warning Events, while a durable status design remains an explicit follow-up.
  • Input validation: peer, static-route, and BGP-neighbor fields now receive admission-time hostname/IP/CIDR validation. The deny-set remains specific to remoteCIDRs, which are the values that program namespace return routes.
  • Flag documentation: the incorrect SSH references were removed from --management-cidr help and warnings.

The tenant-admin RBAC omission for siterouters was also corrected, and the controller/API RBAC now covers the live Node and Service reads used by deny-set discovery.

Validation passed with the full Go suite, the standalone application API module, all affected Helm unit suites, package generation, and make manifests. The runtime negative-security e2e and durable controller-owned status remain documented follow-ups rather than being claimed as completed here.

@IvanHunters IvanHunters left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: request changes

One hard merge blocker (the PR's own new CI gate fails on the committed tree), and one cross-tenant reachability question on the tunnel-ingress filter that the stated Cilium mitigation does not actually close. The security spine (deny-set, admission fail-closed, api-key isolation, RBAC) is otherwise solid and was verified against the code.

Blockers

1. Placeholder image digest breaks this PR's own CI gate and makes every gateway non-bootable

packages/apps/site-router/images/vyos-router-disk.tag carries an all-zeros digest (...@sha256:0000…0000).

  • This PR adds hack/image-refs-no-placeholder.bats, which make unit-tests runs in CI. Reproduced: grep -rIlE '@sha256:0{64}' packages/ --exclude-dir=charts returns exactly this file, so the gate exit 1s. The PR ships a guard its own tree violates — the unit-tests job is red and it cannot merge as-is.
  • Functionally: the VyOS golden image is not published, so a tenant's SiteRouter boot DataVolume resolves nowhere → boot PVC stays Pending forever and the VM never boots. dv.yaml's required guard only rejects an empty .tag; a well-formed placeholder digest passes render. Since the app is wired into the naas bundle, an install/upgrade exposes the SiteRouter kind in the tenant catalog while no gateway can boot.

Fix: publish the cozystack-owned VyOS image and stamp the real digest before merge; do not activate the catalog entry until the image exists.

Security (needs resolution before merge)

2. Decrypted tunnel ingress can reach any tenant's pods, and the documented Cilium backstop does not enforce cross-tenant isolation for it

  • internal/controller/siterouter/vyospush.go:665 (tenantNetworkCIDRs) builds the decrypted-traffic destination allow-list as PodCIDR (the whole cluster pod CIDR, e.g. 10.244.0.0/16) plus this-namespace Service ClusterIPs. Services are tenant-scoped; pods are not.
  • internal/vyos/render/render.go:686 (renderTunnelIngressFilter) turns that into accepts of source ∈ remoteCIDR AND destination ∈ podCIDR. The IPsec local selector is 0.0.0.0/0 and no NAT is rendered, so the decrypted packet keeps the remote peer's real source IP.
  • docs/security-model.md:27 states this pod-CIDR breadth is safe because "a destination inside the cluster pod CIDR proceeds to Cilium, whose identity policy enforces cross-tenant isolation." The tenant baseline allow-external-communication (packages/apps/tenant/templates/networkpolicy.yaml) unconditionally admits fromEntities: [world, cluster]. A decrypted packet with an external source IP is classified as identity world, so a victim pod in another tenant governed by that baseline will accept it. The stated mitigation therefore does not hold for this traffic.

Failure scenario: tenant A sets remoteCIDRs=[203.0.113.0/24]; a host on that subnet sends a packet to tenant B's pod IP; it is decrypted, matches source∈203.0.113.0/24 AND dest∈podCIDR, is forwarded, and tenant B's pod accepts it as world ingress.

What is proven from code: the filter's destination breadth (whole pod CIDR), source-IP preservation, and that the baseline admits world. What is not empirically confirmed (needs a live cluster, blocked on the golden image): the actual cross-tenant delivery through kube-ovn. Please either scope the destination to the owning tenant's own pods, or empirically prove the delivery is denied and correct the security-model wording.

Non-blocking findings

3. BGP is a half-wired feature

The public schema api/apps/v1alpha1/siterouter/types.go (BGP) exposes only enabled / localASN / neighbors. resolveInputs (vyospush.go:591) constructs render.BGPPeer{PeerAddress, PeerAsn} only. The render fields AdvertisedNetworks, RouterID, Timers, and BGPPasswords (render.go:140) are unreachable from the controller — dead code. A tenant setting bgp.enabled=true gets a session that originates no routes; the instance stays Ready with no warning event (unlike the missing-ASN path, which does warn). Either document this as receive-only for Phase 1, or wire advertisedNetworks into the schema.

4. No runtime detection of a managementCIDR lockout

The chart and controller defaults are both the whole pod CIDR 10.244.0.0/16. ValidateManagementCIDR only checks the flag is a valid CIDR, not that it covers the controller's source. On a cluster with a non-default pod CIDR, or if the chart managementCIDR and the controller --management-cidr drift, the controller silently firewalls itself out of every gateway, surfacing only as an opaque ConfigureFailed. Consider deriving the CIDR from the same ConfigMap the deny-set already reads, or emitting a distinct diagnostic when pushes time out.

5. Checklist item 9 ("unit green in CI") is partial

helm-unittest (60 tests) and the Go unit tests pass locally, but make unit-tests (the CI job) also runs the new placeholder-digest bats gate, which is red on the committed tag (see Blocker 1).

6. Stale scaffolding comments

internal/controller/siterouter/reconciler.go:15,258,414 describe fully-implemented methods (validateRemoteCIDRs, programNamespaceRoutes, verifyGatewayPortSecurityRelaxed, removeNamespaceRoutes, …) as "ordered stubs" / "no-op stub" / "placeholder returning nil". Update the comments so they don't misrepresent what is implemented.

7. Repo hygiene

No ```release-note ``` fenced block in the PR body (CONTRIBUTING requires one). Add'site-router': 'area/networking'to.github/workflows/pr-labeler.yaml` (already noted in the description).

Verified sound

  • Deny-set validator (denyset.Validate): masking, IPv4-only rejection (incl. 4-in-6), /0 rejection, unconditional link-local/loopback, and bidirectional Overlaps are all correct; admission and controller share the same helper, so they cannot diverge.
  • Admission fails closed and is wired on both Create and Update (pkg/registry/apps/application/rest.go:208,561); deny-set discovery errors (ConfigMap non-NotFound / Node / Service list) propagate.
  • The api-key Secret is genuinely not tenant-readable: not in the cozyrd secrets.include, not in dashboard-resourcemap.yaml, and the tenant ClusterRoles grant no blanket get secrets; the guest login is locked (encrypted-password "*", no service ssh).
  • Controller RBAC is least-privilege (secrets: get only, no cluster-wide list); secret redaction happens before message truncation; route merge/withdraw keying does not hijack a co-tenant's same-destination route; --leader-elect is hardcoded, so the in-memory hash cache is safe.

@myasnikovdaniil

Copy link
Copy Markdown
Contributor Author

IvanHunters rebased this onto current main (branch was 276 commits behind) and fixed what CI actually fails on now, old logs were useless. Your two blockers are untouched, details at the end.

Result is one squashed commit 8fbe38938 on 09dc7f47f. Only two textual conflicts, both in hack/select-e2e.sh, and resolving them turned up something worse than the deferred-suite gap you flagged.

DEFERRED_SUITES="site-router" stripped the name from selector's final output. Main has since grown per-path coverage escalation (#3330). Together they give empty selection, not reduced one: site-router-application depends on kubevirt-cdi, so reverse walk gave kubevirt-cdi and cozystack-basics exactly one suite, and stripping turned that one into zero. Both e2e lanes read empty as "skip chainsaw" and post E2E Tests green.

packages/system/cozystack-basics        21 suites -> EMPTY
packages/system/kubevirt-cdi            21 suites -> EMPTY
packages/system/site-router-controller  21 suites -> EMPTY

Fixed by parking the suite as chainsaw-test.yaml.disabled, which is main's own convention already (hack/e2e-chainsaw/backup/). That drops it out of selector universe and out of select-install.sh round-trip at once, so hack/select-e2e.sh is byte-identical to main now and un-parking is just renaming the file back. One deliberate behaviour change, a packages/apps/site-router change escalates to full suite instead of selecting nothing, which is what #3330 prescribes for a package with no runnable suite.

Three real unit test failures behind your item 5:

  1. cozystack-api rbac_test pinned rule count at 16, branch adds name-scoped cozystack ConfigMap read for deny-set. Bumped to 17 and added coverage pinning resourceNames and verbs, so a widening cannot pass under it.
  2. Main now errors when a package declares test: and runs zero suites, and site-router-controller chart shipped none. Added rbac and deployment tests, 18 of them, pinning what the RBAC comments claim: Secret get only, Namespace get and patch only, HelmRelease never update or delete, and fail-closed --allow-open-management.
  3. EXIT traps in the 4 new bats files. Per docs/agents/e2e-testing.md that count is a ratchet review keeps from growing, so i removed the traps instead of raising it.

Security surface is byte-identical to pre-rebase, checked file by file: denyset.go, discovery.go, rest_siterouter.go, render.go, cnimediation.go and the whole chart including both Cilium policies. rest.go was auto-merged so i verified admission separately rather than trusting the merge, still invoked fail-closed on create and update.

Both your blockers stay open. Placeholder digest is deliberate, hack/image-refs-no-placeholder.bats is red until the appliance image is published and that is what its commit message says it is for, so make unit-tests cannot go green before that ships. Tunnel-ingress destination breadth needs the scoping call, i did not touch it.

Please take another look at the delta.

@myasnikovdaniil
myasnikovdaniil force-pushed the feat/site-router branch 2 times, most recently from b17e76d to ce01379 Compare August 15, 2026 07:21
myasnikovdaniil added a commit that referenced this pull request Aug 16, 2026
Both are cases of the walk reaching the wrong set of suites for a
package, and both were found by enabling the site-router suite on #3426,
which is the first suite in the tree to land downstream of kubevirt-cdi.

`vm-disk-application` now maps to the `vminstance` suite. It mapped to a
`vm-disk` suite that does not exist, which intersect_suites() drops, so
the source reached nothing runnable and neither did kubevirt-cdi above
it: every CDI change ran all 21 suites. The vminstance suite creates a
VMDisk and asserts the DataVolume behind it
(hack/e2e-chainsaw/vminstance/vmdisk.yaml, vmdisk-vmi.yaml), so it is the
suite that covers both. A CDI change now selects `vminstance` alone.

`cozystack.cozystack-basics` joins `cozystack.cozystack-engine` as a
propagation hub excluded from the reverse-dependency walk. Every edge
into it exists so a namespace or a platform-wide policy is in place
before the dependent installs -- kubevirt-cdi says exactly that in its
own source -- which is install ordering, not behaviour. The damage runs
opposite to the engine's: the engine fans one change out to every app,
while basics narrows down instead. It reaches no suite today so it
escalates, but it sits upstream of kubevirt-cdi, so the first suite to
land under CDI silently converts the platform's namespace-and-policy
package from the full run to that one suite. On #3426 that is 22 suites
down to `site-router` alone, with nothing in the output to say coverage
was lost. Dropping the reverse edges keeps the package reachable, stops
it propagating, and leaves a change to it running everything through the
per-path escalation.

Both rules have a test, each verified to redden with its rule removed:
the mapping test falls back to the full-suite escalation, and the hub
test -- which seeds the hazard by giving redis-application an edge onto
basics, since no suite sits downstream of it in this tree yet -- selects
`redis vminstance` instead of everything.

Assisted-By: Claude <[email protected]>
Signed-off-by: Myasnikov Daniil <[email protected]>
@IvanHunters

Copy link
Copy Markdown
Collaborator

Correction to my last review: I over-graded it

My previous review filed 11 MAJOR findings. That grading was wrong, and the effect on this PR was to block it on a list far longer than the evidence supports. Sorry for the churn that caused.

I checked my own calibration across 358 reviews rather than guess. The median review I write has 1 MAJOR; 164 have none, and only 14 of 358 have five or more. The round I posted here had 11, the highest I have ever produced. On this PR the trend is worse than the number: across five rounds my MAJOR count went 3, 0, 3, 5, 11, while you closed 31 of the 41 findings carried forward with zero regressions. The code got better and my verdict got harder. That is my failure, not yours.

What actually blocks, in my judgement, is two things:

  • internal/controller/siterouter/reconciler.go:1015 — a failed ConfigMap read re-stamps the management ACL to the hard-coded default and locks the controller out of the gateway until the VM is recreated.
  • packages/apps/site-router/templates/service.yaml:20 — no loadBalancerClass immutability guard, where extra/ingress implements one for the same platform value.

Everything else in that review is non-blocking. The rule-count bound is a strong should-fix. The rest (the set -e diagnostics in negative-security, the missing test on the dashboard Secret grant, the 30s convergence window, the config.boot skew I could not verify, the management-firewall scope I said was not exploitable, the _ values) are MINOR or notes. Take or leave them.

I also withdraw three findings outright: the uncached LISTs, the unbounded rule product as a re-file, and the install-wait annotations. You had moved those to docs/followups.md as accepted residuals and I re-filed them as MAJOR anyway. That was me re-opening your decision, which is not review. I will not re-file an accepted residual again.

Separately: E2E (in-tree) is green on dcb65895, and the negative-security gate passed with the allowed flow preserved. That closes the acceptance item I had been carrying as unverifiable.

myasnikovdaniil added a commit that referenced this pull request Sep 24, 2026
…#4411)

## What this PR does

This PR makes `REST.Update` keep live `spec.suspend` of the HelmRelease,
on first write and on conflict retry. Update rebuilds HelmRelease from
Application and writes it back whole, Application has no suspension
field, so every edit through aggregated API resumed a release that
operator or controller had suspended.

Caller that depends on it is cnpg restore driver. It suspends target
release, patches Postgres app through this API, purges Cluster and
resumes only after purge is done, so next chart render lands
`bootstrap.recovery` on empty namespace. When the patch resumes release,
helm-controller upgrades right away, chart can render recovery Cluster
while purge is still running, then driver deletes it as the Cluster
being replaced and nothing renders it again, because values do not
change anymore. RestoreJob stays `Running` until its deadline.

On main it works only because of timing. Same write also drops
`finalizers.fluxcd.io`, and helm-controller v1.5.0 spends its first pass
after that on putting finalizer back and requeue (750ms minimum retry
delay) before it upgrades, this delay usually lets purge finish first.
#3426 keeps controller finalizers across this write, so delay is gone,
and `postgres-2-backup-roundtrip` failed on both its runs there while it
passes on other PRs. In
[35714873620](https://github.com/cozystack/cozystack/actions/runs/35714873620)
stuck restore is `pg-src-to-pg-target-pitr`: `startedAt` 11:40:37, helm
upgrade v3 at 11:40:37, new Cluster created at 11:40:38 and gone by
11:40:47, no render after that.
[35652855170](https://github.com/cozystack/cozystack/actions/runs/35652855170)
has same picture on first restore, all inside 22:10:51.

Tests are in `rest_suspend_test.go`: edit leaves `spec.suspend` as it
was (both values) with values still taken from Application, and suspend
that lands between read and write survives conflict retry. Removing
either line fails its test.

### Downstream repositories

Change is in how aggregated API writes HelmRelease back, no downstream
repo restates that. Rollback guide on website (`guides/concepts.md`)
tells operators to suspend HelmRelease while they work, with this fix
that suspension holds across app edit, so docs need no change.

- [x] No downstream repository is affected by this change
- [ ] [cozystack/website](https://github.com/cozystack/website) -
follow-up:
- [ ]
[cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack)
- follow-up:
- [ ]
[cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack)
- follow-up:
- [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up:
- [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up:
- [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) -
follow-up:
- [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) -
follow-up:
- [ ]
[cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server)
- follow-up:
- [ ]
[cozystack/external-apps-example](https://github.com/cozystack/external-apps-example)
- follow-up:
- [ ] [cozystack/examples](https://github.com/cozystack/examples) -
follow-up:
- [ ] [cozystack/community](https://github.com/cozystack/community) -
follow-up:

### Release note

```release-note
fix(api): app edit through cozystack API no longer resumes a suspended HelmRelease, so postgres restore waits for its purge before cluster is rendered again.
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Application updates now preserve a Helm release’s suspended or active
state instead of unintentionally changing it.
* The release state is also preserved when an update encounters a
conflict and retries, preventing a concurrent suspension from being
lost.
* Application updates continue to use the application’s configured
values while retaining the release’s current suspension state.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
@myasnikovdaniil

Copy link
Copy Markdown
Contributor Author

IvanHunters thanks for recalibrating, both blockers are fixed: ConfigMap read error in e7c1664 and loadBalancerClass guard in 5deb9cf.

Step 12 comment says now it is the 30s poll, you were right there (ae26708).

Main is merged back with #4411 in it, rule-count bound stays in followups.md.

Andrei Kvapil (@kvaps) Timofei Larkin (@lllamnyp) API Review Gate needs one of you.

IvanHunters
IvanHunters previously approved these changes Sep 25, 2026

@IvanHunters IvanHunters left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

Both blockers from my last review are closed, each checked by mutation rather than by reading:

  • e7c1664c9 — resolveManagementCIDR now returns an error and resolveInputs propagates it, so a failed ConfigMap read skips the push and the guest keeps the ACL it has. Restoring the default substitution reddens TestResolveManagementCIDR/a_ConfigMap_that_cannot_be_read_fails_instead_of_falling_back.
  • 5deb9cfe8 — the loadBalancerClass guard is ported from extra/ingress, with an error naming the exact recovery command and warning that the external address is reallocated. Removing the guard reddens three tests covering set, change and clear.

The rebase also brought 41ef4a643, which retires the placeholder-appliance finding entirely, and ae2670869 corrects the cause recorded for the probe wait.

Scope of this approval, stated plainly: it clears the findings I raised. It is not a certification of all 21k lines, and the non-blocking notes from my previous review stay non-blocking. The Require API owner review gate is separate and still needs an API owner.

@lllamnyp Timofei Larkin (lllamnyp) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking this pending a more detailed review. VyOS' license is not compatible with Cozystack.

@lllamnyp

Copy link
Copy Markdown
Member

I'm requesting changes on licensing grounds. The problem is with the appliance image this PR builds and publishes, and it blocks the PR regardless of the technical review.

packages/system/vyos-router-image downloads the official VyOS Stream 2026.03 ISO, converts it with build-vyos-image --reuse-iso, adds an overlay, and publishes the result as a containerDisk. make build pushes it to ghcr.io/cozystack/cozystack/vyos-router-disk, and every installation that enables site-router imports it from there. The ISO carries its own EULA at /usr/share/vyos/EULA, and the build leaves the branding unchanged (/etc/os-release still reads NAME="VyOS"). My reading of that EULA, which is not legal advice:

  • §2.2: the licensee "shall not unbundle or repackage the Software for distribution". Converting the ISO to a qcow2 and then to a containerDisk is repackaging it for distribution.
  • §3.1(g) prohibits "duplicate the Software or publish the Software for others to copy". The only exception §3.1 allows is "a separate custom agreement". Publishing the image to a public registry is exactly this.
  • §4: the VyOS trademarks and logo artwork, boot splash included, are all rights reserved, and the EULA "does not permit you to distribute the Software using VyOS trademarks, regardless of whether the Software has been modified".
  • §3.1(a) and (c) prohibit "using the Software on behalf of third parties" and "providing use of the Software in a service bureau arrangement". Running this image as a router for tenants is the app's whole use case. So even if the project published nothing, the app's default path would leave every operator who adopts it in breach of the EULA of the image it boots.

The same EULA says the open-source components remain under their own licences (§2, §12), and §4 allows redistribution once every VyOS mark and logo is removed and replaced. That leaves an open-source project three ways forward:

  1. Operator-supplied image. Cozystack ships no VyOS binary. The app takes the appliance image as a platform-level value, each operator obtains VyOS under terms that cover their own use, and the controller and chart remain Apache-2.0 code that manages a VyOS-API-compatible appliance.

  2. A debranded build from source.

    • Every VyOS mark and logo is replaced, boot splash included.
    • The EULA file and every reference to it are left out.
    • Sources are published as the GPL requires.

    This is closer to what community#30 planned. It also brings back the dependency on the public rolling repository, which the Makefile records dropping the pinned kernel package on 2026-09-08, so someone has to own that build.

  3. A non-VyOS appliance (for example FRR, strongSwan and nftables on a plain distribution). It needs no licence carve-outs, but internal/vyos and internal/vyos/render would have to be rewritten against a different target.

Whichever option is chosen, vyos-router-image needs to come out of make build and out of this PR. The build-vyos job also needs to stop pushing the image, which it currently does to the CI registry on every run.

The image-lifecycle section of community#30 should also be amended to record the choice. Its premise, that the platform builds and publishes the image, doesn't hold for the Stream binary, and it doesn't hold reliably for the public rolling repository either.

@myasnikovdaniil

Copy link
Copy Markdown
Contributor Author

Timofei Larkin (@lllamnyp) agree on licensing, going with option 2, debranded build from source.

Operator-supplied image doesn't fit here: gateway VM runs with relaxed port security, so image it boots has to be one the platform controls.

Build moves out of this PR to its own repo, cozystack/barerouter. It builds from vyos-build rolling at pinned commit, replaces name and logo artwork on every user-visible surface and leaves EULA file out, and a check fails the build if name or artwork is still in the finished ISO. Each release publishes ISO, SBOM, list of installed packages and manifest of where source of each package is (Debian packages via snapshot.debian.org, VyOS packages via their GitHub repos and vyos-build package-build scripts).

In this PR vyos-router-image stops building from Stream ISO, it takes barerouter release ISO pinned by URL and SHA256 and only adds appliance layer on top. Nothing Stream-based is pushed anymore. Artifact names drop vyos, and default NTP servers move off time*.vyos.net.

Image lifecycle section of cozystack/community#30 will be amended in separate PR.

@mattia-eleuteri

Copy link
Copy Markdown
Collaborator

myasnikovdaniil the answer rules out option 1 but says nothing about option 3. Why not just use FreeBSD for the gateway?

It looks like it would remove most of what makes VyOS painful here:

  • Licensing. FreeBSD is BSD-licensed. No EULA to strip and no marks to replace, so there's nothing to debrand and no need for a barerouter fork to own.
  • Build ownership. There's no dependency on vyos-build rolling at a pinned commit, whose package repo already dropped the kernel package this Makefile had pinned (2026-09-08). A FreeBSD release plus pinned pkg versions of strongSwan and FRR is a stable base that someone else maintains, and FreeBSD publishes cloud-init VM images we could layer the appliance on.
  • Syntax churn. The firewall is pf, whose syntax is stable across releases. The PR currently has to keep the VyOS 1.5 firewall ipv4 ... family behind single-point helpers in case it changes. MSS clamping is scrub max-mss.
  • Footprint. The image should be much smaller than a 12Gi import per gateway.

The cost is the one Timofei Larkin (@lllamnyp) named for option 3: internal/vyos and internal/vyos/render get rewritten against a different target. The controller also loses the VyOS HTTP API and would have to push config some other way, such as a small agent in the guest or rendered files plus a reload. That's real work, but it happens once, while the debranded fork has to be maintained on every VyOS release.

Is there a reason to stay on VyOS that I'm missing, like a feature we can't easily get from strongSwan + FRR + pf? If there isn't, I'd rather weigh that rewrite against owning barerouter before the new repo gets created.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

myasnikovdaniil NOT LGTM. The branch merges cleanly, but the merged tree no longer builds the controller image, and the history and shipped docs carry planning material that has no place in main.

This pass does not repeat what IvanHunters already cleared at this head, and it does not touch the VyOS license question Timofei Larkin (@lllamnyp) raised. That block stands on its own. I read the controller, the admission path, the chart and its registration closely. I only skimmed the appliance build scripts, the chainsaw suite and the e2e harness changes, so silence there is not approval.

  1. The merged tree cannot build site-router-controller. The branch base is from 2026-09-24, and two things landed on main after the last push. go.mod now says go 1.27.1 (ec73871), while this Dockerfile builds on golang:1.26-alpine. The official golang image sets ENV GOTOOLCHAIN=local, so go mod download refuses the module instead of fetching a newer toolchain. Separately, hack/image-builders-cross-compile.bats from d9cbbec fails on the merge result (git merge-tree of origin/main and this head), and this Dockerfile is the only one it names: "not on BUILDPLATFORM". The green Pull Request run from 2026-09-24T11:21Z (attempt 1) predates both, so CI here cannot show it. Fix: rebase and match the sibling controllers, e.g. securitygroup-controller: FROM --platform=$BUILDPLATFORM golang:1.27-alpine@<main's digest>.

  2. Four commit bodies narrate the debugging history instead of the change. The repo merges with merge commits, so these land in main as written, and contributing.md lists review-iteration vocabulary as a blocker:

    • 867f5ba: "This is the round that makes the next one able to name it."
    • 06b444e: "Three rounds of this were spent reading the wrong source ... the last three commit messages described code the guest does not run."
    • 75c8eb5: "Had it been there, this round would have named the offending line instead of narrowing to it."
    • ba74344: "Round 1 made this a tenant-facing chart value ... Round 2's collection is untouched."

    Rewording them during the rebase is enough. Trailers are fine: all 52 commits pass main's hack/check-commit-trailers.sh.

  3. Shipped code and docs point at planning documents that are not in the repository. DECISIONS.md and PLAN.md are referenced from Go, chart and doc files, and neither exists in the tree. Comments also cite decision and task IDs like D7, D8, D9, D10, T06, T07 and T13 (for example reconciler.go:716, "validated empirically in T13"). There are dozens of these outside tests. A reader with only the repo cannot resolve any of them. Where the reason matters, write it inline; otherwise drop the ID.

  4. packages/apps/site-router/docs/PR-BODY.md is a draft PR body, and it ships inside the chart (.helmignore excludes only logos and Makefile). It is also stale: it still marks the boot and the tunnel as "deferred-to-empirical (live boot needs the published image)". docs/followups.md has the same problem. Line 8 still calls publishing the image a merge prerequisite for a "deferred" e2e, and line 12 says the last suite run "failed several steps earlier". The PR body says the suite ran green on a later run. Please delete PR-BODY.md and update those two items.

What I checked and found fine. Go tests for every touched package that has them, and helm unittest for site-router, site-router-controller, platform and cozystack-api all pass on the merged tree. Dropping bgp.neighbors[].address from the admission inputs turns TestSiteRouterAdmission_RejectsStaticRouteAndBGPNeighbor red, so that guard is pinned. The tenant cannot set the boot image or any _ key. The controller runs non-root with a read-only root filesystem, and it reads secrets only by name, in the instance namespace. The website and terraform follow-up issues are open.

@@ -0,0 +1,24 @@
FROM golang:1.26-alpine@sha256:0178a641fbb4858c5f1b48e34bdaabe0350a330a1b1149aabd498d0699ff5fb2 AS builder

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After a rebase onto main this line breaks the build twice. main's go.mod requires go 1.27.1, and the golang image pins GOTOOLCHAIN=local, so Go 1.26 refuses the module. hack/image-builders-cross-compile.bats also fails on the merged tree for this file only, because the builder stage is not on $BUILDPLATFORM. FROM --platform=$BUILDPLATFORM golang:1.27-alpine@<digest from main> fixes both, same as securitygroup-controller.

@@ -0,0 +1,59 @@
<!--

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a draft PR body, and it ships in the chart package. Its statuses are stale too ("live boot needs the published image"). Please delete the file.


## Security hardening

- **Negative-security e2e runtime proof.** The guard structure (management firewall, tunnel-ingress source filter, forward default-deny, IPsec-match jump, Boundary-A management drop, MSS clamp, forced ESP-in-UDP encapsulation) is implemented and unit-tested, and its VyOS leaf syntax is validated live against the shipped image — the `firewall ipv4 {input,forward} filter` / `firewall ipv4 name <NAME>` family and the `ipsec match-ipsec-in` / `ipsec match-none-in` matchers (a full render-equivalent config commits cleanly), kept behind single-point helpers so a future image whose syntax differs is a one-place change. What remains is the runtime proof on a booted two-VM topology that an undeclared-source packet, and a valid remote-source packet with a world / non-tenant destination, are both actually dropped. That is the `negative-security` step of `hack/e2e-chainsaw/site-router`; it is written and it runs in CI, so what it is waiting on is not the appliance image but a run that gets that far — the last one failed several steps earlier.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"the last one failed several steps earlier" contradicts the PR body, which says the suite, negative-security step included, passed on a later run. Line 8 has the same problem, and line 3 points at PLAN.md / DECISIONS.md, which are not in the repo.

// reconcile pipeline (D8 Ready-gate: the controller does not treat the relaxation
// as effective until the guard is confirmed up).
//
// port_security timing (D8, validated empirically in T13): kube-ovn v1.15.10 only

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

T13 and D8 point at planning documents that are not in the repository. Nobody reading the tree can resolve them. The kube-ovn behaviour itself is worth keeping, just without the IDs. Same pattern across the controller, render, admission and chart.

A site-router tunnel routes its remote networks into the tenant
namespace, so a remote CIDR overlapping the pod, service or join
network, a node address or an allocated LoadBalancer address would
capture cluster traffic. The deny-set validator in
internal/siterouter/denyset checks a declaration against the networks
in the cluster-wide cozystack ConfigMap and the live node and Service
addresses, and the Application REST handler runs it on Create and
Update of SiteRouter instances, so the overlap is refused at apply time
with the offending CIDR named. cozystack-api gets read access to that
one ConfigMap.

The REST handler already rejects top-level values keys that start with
an underscore. site-router keeps its platform-only switches under such
keys, so that rejection gets tests of its own.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <[email protected]>
The controller configures the gateway over its HTTPS management API
rather than through the image, so a day-2 change applies as one
transaction without restarting the VM. internal/vyos is the client for
that API: configure, show and retrieve, with the CA pinned per gateway
because the token goes to a pod IP whose anti-spoofing is off, plus
parsers for the tunnel, BGP and interface state read back from the
guest. internal/vyos/render turns a SiteRouter's inputs into the
operation set: interfaces, the management firewall, IPsec with forced
UDP encapsulation, static routes, BGP, the MSS clamp, and the guest
guards that hold decrypted traffic to the declared remote sources and
keep it off the management API.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <[email protected]>
What the chart cannot express needs a controller watching SiteRouter
HelmReleases and their gateway pods. It validates the remote networks
with the deny-set, programs the kube-ovn return route on the tenant
namespace, verifies the gateway port carries the relaxed port security
the chart bakes into the pod template, pushes the rendered
configuration and keeps it pushed, confirms the guest's source filter
before it treats the instance as ready, reports tunnel and BGP state as
metrics and tenant pods still missing the route as an Event, and
withdraws its routes on delete. The package ships the deployment, its
RBAC with a check that the chart role matches the kubebuilder markers,
and the image build.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <[email protected]>
@myasnikovdaniil

Copy link
Copy Markdown
Contributor Author

Timofei Larkin (@lllamnyp) small correction to my previous comment: image repo is aenix-io/barerouter, not cozystack/barerouter. v0.1.1 is released and this PR now pins its KubeVirt disk by digest, nothing here builds or republishes image anymore. Also I rewrote history into 5 commits.

Tenants need routed connectivity between their pods and a remote site,
with the remote source address preserved so policy on either side
still sees who is talking. The site-router chart materializes the
gateway in the tenant namespace: a KubeVirt VM booting the KubeVirt
disk of barerouter, a router image built from the VyOS rolling sources
and published by Aenix, pinned by digest; its boot DataVolume; a
LoadBalancer Service for IKE and NAT-T; the PSK and management API key
Secrets; the first-boot configuration; and Cilium policies around the
gateway. The platform emits it in the naas bundle only when IaaS is
enabled, since the gateway needs KubeVirt and CDI, and tenant roles
gain the siterouters resource.

The gateway port has port security relaxed, so the guest's source
filter is the only thing between a decrypted packet and the tenant
network. The management API therefore accepts only the controller's
source range, the appliance has no SSH and a locked login, and its
bootloader is locked so the console cannot reach a root shell.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <[email protected]>
Most of what site-router promises only shows on a booted gateway: the
tunnel coming up through the LoadBalancer, the return route reaching
new pods, the MSS clamp, and the negative cases, a source outside the
declared networks and the management API over the tunnel, being
dropped. The suite boots the app next to a plain VM that stands in for
the remote site and drives both through a probe pod. On failure it
collects the dataplane and the gateways' serial consoles, and e2e
selection runs it when a change touches site-router.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <[email protected]>
Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <[email protected]>
@myasnikovdaniil

Copy link
Copy Markdown
Contributor Author

Aleksei Sviridkin (@lexfrei)

  1. Rebased on main, controller Dockerfile is FROM --platform=$BUILDPLATFORM golang:1.27-alpine with main's digest, same as other Go controllers. Full CI with e2e passed on 0e138c4 (https://github.com/cozystack/cozystack/actions/runs/36970845851), last commit 231fd99 only drops stale docs.
  2. History rewritten into 5 commits (deny-set admission, gateway API client and renderer, controller, app, e2e), old commit bodies are gone.
  3. DECISIONS.md / PLAN.md references and decision/task ids removed everywhere, where reason mattered it is written inline now.
  4. PR-BODY.md deleted. In followups.md both stale items removed, image is published and pinned by digest and negative-security step passes in e2e.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

myasnikovdaniil NOT LGTM, for one new reason: the router disk this app pins is built for one platform only, and the release workflow refuses to publish a tree that pins such an image.

My three earlier blockers are closed. This time I also read the chainsaw suite, the harness changes and the guest diagnostics script, which I only skimmed last time.

The new blocker. images/barerouter-disk.tag pins ghcr.io/aenix-io/barerouter/kubevirt-disk:0.1.1@sha256:9c88d087…, and that is a plain manifest.v2, not an index. hack/lib/image-refs.sh enumerates *.tag files, so hack/verify-multiarch.sh packages checks this ref. The gate is already in this branch's base (144d080), and tags.yaml runs it in "Stitch multi-arch indexes and republish". On a tree holding only this tag file it prints single-platform manifest and exits 1. So the first release tag cut after this merges fails at that step.

There are two ways out. One is to publish the disk as an amd64+arm64 index. The other is an entry in hack/multiarch-allowlist, which needs a # reason, and the allowlist is meant for images that cannot exist for arm64, not ones nobody has built yet. If you take the allowlist route, I think the VM also needs a kubernetes.io/arch: amd64 node selector. vm.yaml has no arch constraint now, so on a mixed-arch cluster the VM can land on an arm64 node that cannot boot this disk.

Status of the earlier points:

  1. Controller build is fixed. The Dockerfile now matches securitygroup-controller (--platform=$BUILDPLATFORM, golang 1.27). On a trial merge with main (a7ac37b) hack/image-builders-cross-compile.bats passes, and so do go build, go vet and go test for every touched Go package. The controller build job is green on this head.
  2. Commit bodies are fine. The six rebased commits describe the change and nothing else, DCO is green, and each has one Assisted-by: LLM trailer.
  3. The planning references are gone. DECISIONS.md, PLAN.md, PR-BODY.md and the D/T IDs appear nowhere in the diff, and followups.md now matches the suite.
  4. License: the code side looks resolved to me. Nothing here builds or republishes a VyOS image any more. The chart pulls a debranded disk by digest, and the VyOS EULA allows redistribution once the trademarks and logos are removed. The logo and catalog description in this tree are your own. Whether a catalog app may depend on a GPL appliance image is Timofei Larkin (@lllamnyp)'s call, not mine.

One minor thing. In hack/e2e-chainsaw/site-router/chainsaw-test.yaml, lines 428-431 and 523-526 say site_router_tunnel_up under-reports because the parser only knows the swanctl format. internal/vyos/parse.go handles the 1.5 table now, and parse_test.go pins it against output captured from the shipped image. So these comments are stale, and the suite could assert the metric instead of polling show vpn ipsec sa. Also, the PR body says five commits, but there are six.

What else I checked, all on the trial merge. Helm unittest passes for site-router, site-router-controller, platform, cozystack-api and cozystack-basics. make generate for site-router leaves no drift. The site-router bats pass, except one guest-diag test that reads /proc/uptime and cannot run on macOS.

Four mutations, each caught by a test. Dropping the join CIDR from the deny set fails TestValidate. Narrowing the tunnel management drop to port 22 fails TestRenderManagementAPIDrop_OnIPsecDecryptedInput. Opening 443 to world in the gateway policy fails networkpolicy_test.yaml. Disabling per-field truncation in guest-diag.sh fails "no single field can bury the rest of the sample".

The suite has no test-level traps and no retry around a chainsaw step. Its polling loops wait for the guests to boot, each with a deadline, and print the last value seen on timeout.

The tenant gets the PSK and nothing more. The api-key Secret is in neither the AppDef secrets.include nor the dashboard Role, and _ keys are rejected in the REST handler. Remote CIDRs are checked against the cluster networks and the node and LoadBalancer addresses. The controller writes the return route only on the instance's own namespace, so a declaration affects only that tenant's traffic.

CI on 231fd99: pre-commit, codegen drift, commit trailers and all builds are green. Unit & controller tests and E2E Tests were still running when I wrote this. The only red check is the API-owner gate, which is governance, not code.

The barerouter disk is an amd64 guest, and the release refuses a pinned
image that is not an amd64 and arm64 index. The disk is never run as a
container: CDI's importer fetches it as data, so a single-platform
manifest imports on a node of either architecture, and only the VM is
bound to amd64. The VM now says so with a node selector, which also
keeps it off an arm64 node it could not boot on in a mixed cluster.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <[email protected]>
The parser behind site_router_tunnel_up reads the table that
`show vpn ipsec sa` returns on the shipped image, so the comments that
justified polling the SA state over the gauge described a parser that
is no longer there.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <[email protected]>
@myasnikovdaniil

Copy link
Copy Markdown
Contributor Author

Aleksei Sviridkin (@lexfrei) I took allowlist route, arm64 build of barerouter is not planned now. Disk is never run as container: dv.yaml sets no pullMethod, so CDI importer fetches it as data and single-platform manifest imports on node of any arch, only the VM is bound to amd64. VM now has kubernetes.io/arch: amd64 node selector with helm test, and hack/verify-multiarch.sh passes on the pin with the allowlist entry.

Stale tunnel gauge comments in chainsaw are fixed, polling stays as is. PR body now says what the commits after the first five are.

@lllamnyp

Copy link
Copy Markdown
Member

Thanks for moving the appliance to a debranded image. Pinning a barerouter release by digest, with the SBOM, per-package source manifest and written offer published alongside it, resolves the licensing concern I raised earlier. This is the technical review. I'm still requesting changes, but mostly about where this code lives and what it depends on, not about how it's written. Several premises of community#30 have changed since it was accepted, so I think the proposal needs an amendment before this lands anywhere. Proposed points for that amendment are at the end.

1. This belongs next to barerouter, as an external package

Most of the Go code in this PR drives a single appliance. Excluding tests, generated types and comments, there are about 2,900 lines of code. Of those, internal/vyos (API client and parsers of the router's show output), internal/vyos/render, and the bulk of vyospush.go are a configuration-management client for barerouter. The part specific to Kubernetes and Cozystack (namespace route mediation, deny-set, gateway pod discovery and the port-security check) is roughly 400–500 lines. The code imports nothing from the rest of the repository except its own new packages.

The image and the code that drives it also form one contract, and the PR's own documentation says so:

  • docs/image-lifecycle.md requires the image and the configuration contract to "advance atomically".
  • Moving to a new barerouter release means re-capturing the config-version footer in _helpers.tpl, in the e2e remote-site fixture, and in a chart unit test.
  • The renderer's syntax is "validated live against the shipped image", and the parsers scrape command output whose format a rolling rebuild can change.

Today every barerouter release therefore needs a matching PR in this repository. If the client, renderer and controller lived in the barerouter repository, release N would ship client N, tested against a booted image of N.

I'd suggest moving the controller, the client and renderer, the chart, the ApplicationDefinition, the tenant roles and the e2e suite next to barerouter, and shipping them as an opt-in external package: a PackageSource that operators apply, which serves the charts and images from that repository. Concretely:

  • Tenant RBAC can ship as ClusterRoles carrying the rbac.cozystack.io/aggregate-to-tenant* labels, instead of editing cozystack-basics.
  • cozy-lib: the chart uses a single helper, which can be vendored.
  • The generated api/apps/v1alpha1/siterouter types aren't needed for the app to work.
  • What this repository has to provide is generic. Every PackageSource on main currently points at cozystack-packages, so serving a catalog app (an ApplicationDefinition plus a chart with libraries) from an external source needs checking and probably some small platform work. That work is worth doing in its own right, and site-router would be its first user.

This also takes care of naming. Cozystack never talks to VyOS here; it talks to barerouter. Code identifiers referring to the software you interoperate with are ordinary referential use, but in this repository, outside the barerouter project, there's no reason for internal/vyos or VyOSClient to exist at all.

2. Site-router-specific admission in the application API server

This is the one part that can't be moved out, which is a good sign it's the wrong shape. rest_siterouter.go, the SiteRouterKind constant, the calls in rest.go, and the cozystack-api ConfigMap grant add a branch to the aggregated API server that runs only for one application kind. community#30 puts this check in the controller, with the rejection reported as a status condition (Reason: InvalidRemoteCIDR). The aggregated API should stay application-agnostic.

followups.md explains why a synchronous 403 was added: controller failures don't reach the Application's readiness, so the tenant never sees the rejection. The fix for that is the generic path the follow-up describes, from controller-owned conditions into Application status, which every app with a controller would benefit from. Please drop the admission code and its RBAC, and let the controller's condition carry the rejection.

3. The CNI coupling

community#30 accepted a dependency on kube-ovn for routed mode. In practice the PR also depends on Cilium, and both matter now that a networking variant without either is in progress (#3149, the cozyplane eBPF CNI). Under that variant:

  • The return path disappears. The ovn.kubernetes.io/routes namespace annotation is only acted on by kube-ovn's webhook. Without it, tenant pods send replies for remote sources out their default route.
  • Forwarding breaks. The ovn.kubernetes.io/port_security: "false" pod-template annotation has no effect, and the datapath's anti-spoofing drops every packet the gateway forwards from a remote source.
  • The deny-set and the security model assume this datapath. The deny-set reads kube-ovn's join network, and the security analysis relies on Cilium's destination containment.
  • The bundle gate only implies kube-ovn. It checks bundles.iaas.enabled, which today implies an isp-full* install and therefore kube-ovn. Nothing ties it to the networking variant.

Instances created on a kube-ovn cluster would stop working when that cluster changes networking variant, and there would be no migration path.

I'd suggest putting the CNI-specific part behind a small platform contract that each networking variant implements. Something like "this workload is an authorized forwarder for CIDRs X in this namespace, and traffic to X is routed to it":

  • kube-ovn would implement it with the routes annotation and the port-security relaxation, as this PR does now.
  • cozyplane would implement it with a scoped forwarder grant and a VPC route that resolves to the workload's identity, not its IP, which is the design sketched in lllamnyp/cozyplane#6. That design also replaces "port security off" with a narrow grant for the declared CIDRs, which is the hardening followups.md already wants.

The site-router controller would consume the contract and stop knowing which CNI it runs on, and its gateway policies would go through whatever policy layer the variant provides. As an external, opt-in package, Phase 1 can still ship as kube-ovn-only before that contract exists, because only operators who choose it are exposed.

4. Before it ships, wherever it lives

  • The multi-arch release gate. images/barerouter-disk.tag matches the */*/images/*.tag paths that hack/lib/image-refs.sh enumerates, and hack/verify-multiarch.sh runs in the release and nightly arm64 workflows. The barerouter containerDisk is a single-arch manifest and isn't in hack/multiarch-allowlist. That allowlist is meant for images that can't exist for arm64, so the right fix is an arm64 barerouter build. In an external package the gate doesn't apply, but the arm64 question remains for arm64 clusters.
  • Negative-security e2e. followups.md and security-model.md both say the negative-security step has never been reached in a run. community#30 makes those tests the acceptance bar for Phase 1, so one green run that reaches and passes that step is needed.
  • followups.md should become issues. It's a good list, and items such as the tenant super-admin being able to rewrite the gateway VM, and the tenant baseline network policy not excluding the gateway, deserve tracking on their own. A file inside the chart isn't where they'll be found.
  • Comment volume. Comments are still roughly 45% of reconciler.go, vyospush.go and render.go, and 158 of the 194 lines in values.yaml. Much of it is history of earlier revisions, which belongs in commit messages or the proposal.

Proposed amendments to community#30

  1. Image. Replace "built in the monorepo, next to Talos" with a reference to an external, debranded appliance image pinned by digest, and record why: the EULA on VyOS's published images, and the rolling repository dropping pinned packages.
  2. Packaging. Site-router, and later site-gateway, ship as external opt-in packages next to the appliance and its client. The core repository only provides generic extension points: catalog apps from external package sources, and controller conditions in Application status.
  3. Validation. Restate that validation stays in the values schema and in the controller, with no per-application logic in the API server.
  4. CNI. Replace the kube-ovn dependency with a forwarder or route contract that networking variants implement, and note which variants support routed mode.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

myasnikovdaniil the code is fine now. The allowlist entry and the amd64 nodeSelector hold up: hack/verify-multiarch.sh passes on the pin with the entry and fails with an empty allowlist, and the site-router chart tests are green.

One thing is left in the PR body, and the body becomes the merge commit. "Commits after them are review fixes" describes how the PR was reviewed, not what lands in main. Please either fold those three commits into the five they belong to, or list them by what they change. After that I'll approve.

@myasnikovdaniil
myasnikovdaniil marked this pull request as draft October 2, 2026 13:41
@myasnikovdaniil

Copy link
Copy Markdown
Contributor Author

Timofei Larkin (@lllamnyp) thanks for the review, I will rebuild this instead of patching the PR. Below is architecture I plan to build, I want to agree on it before writing code.

What is built

Standalone operator with own namespaced SiteRouter CRD. One SiteRouter brings up whole gateway (kubevirt vm with barerouter disk pinned by digest, seed and credentials secrets, LoadBalancer service for the tunnel, optional cilium policies) and does everything controller does today: validation against cluster networks, config push, source filter confirmation, return routes. Nothing in it is tied to cozystack, vyos or barerouter, or to any particular CNI. Cozystack side is app wrapper that writes the CR, it will be separate package in some repo with external apps (which one is not decided yet), tenants never create CR themselves.

apiVersion: siterouter.cozystack.io/v1alpha1
kind: SiteRouter
metadata:
  name: office
  namespace: tenant-acme
spec:
  peer:
    address: 203.0.113.10
    auth:
      secretRef: {name: office-psk}
  remoteCIDRs: [192.0.2.0/24]
  resources: {cpu: 2, memory: 2Gi}
status:
  tunnelAddress: 198.51.100.7
  conditions:
    - {type: Validated, status: "True"}
    - {type: Configured, status: "True"}
    - {type: RoutesProgrammed, status: "True"}
    - {type: TunnelEstablished, status: "False"}

For this CR operator creates vm site-router-office, DataVolume site-router-office-boot, secrets site-router-office-cloud-init and site-router-office-api-key, service site-router-office-tunnel and a Forwarder, all owned by SiteRouter with owner references.

Forwarder is how operator talks to the network, it says "this workload is authorized forwarder for these CIDRs, route them to it":

apiVersion: siterouter.cozystack.io/v1alpha1
kind: Forwarder
metadata:
  name: site-router-office
  namespace: tenant-acme
spec:
  workloadSelector:
    matchLabels: {vm.kubevirt.io/name: site-router-office}
  cidrs: [192.0.2.0/24]
status:
  forwarder:
    addresses: [10.244.1.9]
  pendingWorkloads: []

Operator fills cidrs only after engine confirms source filter. Connector (we write them, one per network, outside operator) watches Forwarder, finds gateway pods by workloadSelector, allows them to forward traffic for these CIDRs and routes the CIDRs to them. Then it writes back to status gateway address in tenant network (next hop for operator) and pendingWorkloads, pods created before the route (some networks apply routes only at pod creation). Operator shows pending workloads in RoutesProgrammed condition and never restarts anything. On delete operator removes Forwarder and connector withdraws routes and grant by finalizer. Operator does not know which network is under it, so another network is just new connector.

Where things live

  • operator is own repo, which org is first question below
  • appliance engine (client, renderer, seed template, image pin) is go module in barerouter repo, so release N of barerouter ships the image and the code that drives it together
  • network connectors live outside operator, kube-ovn first and cozyplane later
  • nothing from this PR goes to cozystack/cozystack, admission in aggregated api, rbac changes, bundle gate and PackageSources are dropped

What is better than in this PR

  • resources have clear owner. Now chart creates vm, disk, service and secrets and controller finds them by naming and lineage labels, there are no owner references and no status. New operator creates what it manages, owns it with owner references and reports health in status.conditions of the CR. Platform specific settings become typed operator config instead of _ values
  • flow is easy to follow: validate, connect, apply, confirm, grant and route, observe. Routes are programmed only after engine confirms source filter, this PR does it in opposite order
  • validation does not touch api server, static checks are in schema and CEL, cluster network check is in controller and rejection goes to status condition (Validated=False, InvalidRemoteCIDR), as you suggested
  • two seams instead of kube-ovn and vyos wired into reconciler. Network seam is Forwarder above. Engine seam is small go interface (render, push, confirm, observe, bootstrap artifacts), so replacing appliance later doesn't touch reconciler or CRD

I checked the network seam on kube-ovn 1.15.10. Forwarding grant (ovn.kubernetes.io/port_security) is applied to running pod immediately in both directions, docs in this PR say opposite. Routes still apply only at pod creation, so connector reports workloads that predate the route.

Also e2e suite already has negative-security step as acceptance gate and it passed in this run on current head, so I port the suite as is.

I have detailed local design and work plan and can share it, but don't want to paste all of it here. Now I need to agree on direction and implementation, then start building.

Questions

  1. Where do you see the operator, cozystack org, aenix-io or somewhere else? I'm not attached to answer, can discuss or argue other view. What do you want to see for it to be in cozystack org?
  2. Any objections to this shape (operator, Forwarder with connectors, engine seam) before I start?
  3. I think architecture and decisions should go to existing design proposal as amendment (plus decision records when a question is closed), and work plan to an epic with checklist of phases in operator repo. Does it work for you?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/networking Issues or PRs related to networking (ingress, gateway, vpn, metallb, kube-ovn) area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review kind/feature Categorizes issue or PR as related to a new feature size/XXL This PR changes 1000+ lines, ignoring generated files

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

docs: add site-router to the reference and component lists

6 participants