design-proposal: structured, additive external exposure for managed applications#29
design-proposal: structured, additive external exposure for managed applications#29Andrei Kvapil (kvaps) wants to merge 1 commit into
Conversation
…pplications Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces a design proposal for structured, additive external exposure for managed applications in Cozystack, replacing the binary external boolean with a flexible expose list of {target, class} entries. The feedback highlights several important areas for improvement: expanding the target list for MariaDB, MongoDB, and Redis to support read-replicas; including the actual class name in the endpoint status for better traceability; and addressing a critical TLS verification issue where dynamically allocated external IPs will not match the certificate SANs when TLS is terminated directly at the database pod.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| | clickhouse | `http`, `tcp` | `http` | | ||
| | rabbitmq | `amqp`, `management` | `amqp` | | ||
| | kafka | `bootstrap` | `bootstrap` | | ||
| | mariadb / mongodb / redis | `primary` | `primary` | |
There was a problem hiding this comment.
For databases like MariaDB, MongoDB, and Redis, it is highly common to route read-only traffic to replicas or secondaries to scale read performance. Restricting these engines to only a primary target prevents users from exposing read-replicas externally.
Consider expanding the target list to support replicas, for example:
mariadb:primary,replicamongodb:primary,secondaryredis:master,replica
|
|
||
| ### Status | ||
|
|
||
| The application reports every way to reach it as `status.endpoints[]`, each entry `{target, scope, host, port, tls}` where `scope` is one of `in-cluster`, `public`, or `private`. The in-cluster entry is always present; public/private hosts are read back from the corresponding `ServiceExposure.status.assignedIPs`, never fabricated client-side. For the two-entry Postgres example above: |
There was a problem hiding this comment.
Since ExposureClass names are arbitrary and defined by the cluster administrator (e.g., dmz, office-vpn, public-direct), relying solely on a fixed scope enum (in-cluster, public, private) in status.endpoints[] makes it difficult for consumers to map an endpoint back to its specific ExposureClass. This is especially problematic if there are multiple classes with the same scope or if the scope mapping is not obvious.
Including the actual class (or exposureClass) name in the endpoint status would provide clear traceability.
For example, the endpoint entry could be:
- target: rw
scope: private
class: private-vpn
host: 10.0.0.5
port: 5432
tls: true|
|
||
| ## Security | ||
|
|
||
| **TLS.** External exposure implies TLS — there is no plaintext-external combination. TLS is controlled per engine by a tri-state `tls.enabled`: unset defaults to "on when externally exposed", an explicit value always wins, and termination happens at the database pod itself (passthrough-friendly, never at an edge or gateway). External clients validate against the merged `ca.crt`-only trust anchor, the `<release>-ca-cert` Secret (#2989), surfaced through `core.cozystack.io/tenantsecrets`. TLS is already merged for Postgres, Kafka, NATS, and Qdrant; it is still converging for Redis, MongoDB, RabbitMQ, OpenSearch, and MariaDB (#2729 / #2692 / #2683 / #2682 / #2680). Until those land, the "external implies TLS" guarantee is engine-by-engine, and this convergence is in progress. |
There was a problem hiding this comment.
Terminating TLS directly at the database pod introduces a significant challenge regarding certificate SANs (Subject Alternative Names).
Since the external IP addresses (e.g., 10.0.0.5, 203.0.113.7) are allocated dynamically by the serviceexposure-controller after the database pods are already running, the certificates initially minted for the pods will not contain these external IPs in their SANs. If an external client attempts to connect to these IPs, TLS hostname/IP verification will fail unless the certificates are dynamically re-issued and reloaded by the database engine.
Not all database engines support hot-reloading TLS certificates without a restart, and frequent restarts upon IP allocation/re-allocation are undesirable.
It would be highly beneficial to address how SANs for dynamically allocated IPs/hosts will be handled, or if a DNS-based approach (where the certificate uses a wildcard or stable DNS SAN, and external clients connect via DNS rather than raw IPs) is expected to mitigate this.
What
Adds a design proposal under
design-proposals/structured-external-exposure/that replaces the chart-level booleanexternal: true|falseon managed applications with a structured, additiveexposefield — a list of{target, class}entries.Why
The boolean is a dead end: it publishes exactly one endpoint, offers no way to choose the address source, cannot select which named listener of an engine is published, and reports its result by mutating the in-cluster connection details in place. Each chart also implements it slightly differently, so there is no shared model or status contract.
The model
expose: []— a list of{target, class}entries. Absent or empty means in-cluster only. The in-clusterClusterIPService is always present and never touched; every entry is additive.target— a per-engine closed enum derived from the engine's existing named listeners (Postgresrw/ro, ClickHousehttp/tcp, and so on); omitted selects the engine's primary listener.class— a reference to an admin-definedExposureClass(StorageClass-like, #3081); omitted selects the cluster default. Each entry renders an additiveClusterIPService plus aServiceExposure, and the controller allocates the external address and reports it.status.endpoints[]—{target, scope, host, port, tls}, resolved fromServiceExposure.status.assignedIPs, never fabricated client-side.ca.crt-only trust anchor (#2989).SecurityGroup(#2922), with the deny/allow posture owned by the consuming orchestrator, not Cozystack core.external: truestays accepted as a deprecated alias for a single primary entry.Scope
Cozystack side only — the chart API, the rendered objects, and the reported status. It builds entirely on already-merged primitives (#3081, #2989, #2922, #2470) and stays forward-compatible with future Gateway/SNI endpoint consolidation (community #20) without implementing it.
Status
Draft — opening for early design feedback before the API shape is locked.