You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The exposure half of #2811 is passthrough by design, and passthrough is the right answer for a data protocol and the wrong answer for a browser. Several managed apps ship both: an admin UI over HTTP and a client protocol over TLS. Nothing in the epic currently owns the UI leg, and because #2815 reads as "external exposure, solved", the gap is easy to mistake for covered. This issue names the two legs, records why they cannot share one mechanism, and collects the decisions the UI leg needs.
The two legs
For an app like rabbitmq or opensearch the same release has to be reachable two ways, and the two ways want opposite treatment:
data leg (e.g. AMQPS 5671, OpenSearch HTTP API 9200)
UI leg (e.g. management 15672/15671, Dashboards 5601)
The decisive asymmetry is what the consumer can be asked to do. Handing an AMQP client a ca.crt and a --cafile flag is a documented, one-time configuration step, which is exactly what makes passthrough acceptable there: nothing terminates, so the certificate the client validates is byte-identical to the interior one, and no private key sits at the edge. A browser cannot be configured that way. Route a management UI through a passthrough listener and every visitor gets an interstitial warning, per release, forever, because the interior certificate is a per-release self-signed CA. Telling tenants to import a CA into their browser trust store is not a supported answer, and telling them to click through is worse than not publishing the UI at all.
So the UI leg has to terminate at the edge, on the wildcard or ACME certificate the edge workstream already delivers (#2812, #2400, #2820). That is not a weakening of the end-to-end story recorded in #2816 — it is a different consumer with a different threat model, and it should be stated as a deliberate split rather than discovered later as an inconsistency.
How it works today
Four incompatible shapes, verified against main at 2823b7680. Only three app charts in packages/apps render any HTTP routing object at all.
Edge-terminated, both data planes — harbor.packages/apps/harbor/templates/ingress.yaml and httproute.yaml, branching on the injected _namespace.gateway to render either an Ingress with ingressClassName or an HTTPRoute with parentRefs at the cozystack Gateway. Host is .Values.host | default "<release>.<_namespace.host>". Three certificate modes are branched in-template: an operator wildcard (skip the per-host secret, let nginx serve its default SSL cert), http01 (cert-manager.io/cluster-issuer plus acme.cert-manager.io/http01-ingress-ingressclassname), and dns01 (issuer annotation only). Backend is backend-protocol: "HTTP". This is the only shape a UI leg should want, and it exists exactly once.
nginx-level passthrough for a nested cluster — kubernetes.packages/apps/kubernetes/templates/ingress.yaml sets ssl-passthrough: "true", backend-protocol: AUTO_HTTP and a configuration-snippet rewriting $proxy_upstream_name, republishing the tenant cluster's own ingress-nginx. Correct for what it does and not reusable as a UI-leg pattern.
Published by the tenant module, not the app — bucket.packages/apps/bucket/templates renders no routing object; the S3 endpoint is published by packages/extra/seaweedfs, and the app chart retains only hooks/cleanup-acme.yaml to delete the legacy per-bucket Ingress and Certificate it used to render.
A UI with no publish path at all.opensearch ships Dashboards behind dashboards.enabled with a Service on 5601 and no route of any kind, so it is reachable only by port-forward. rabbitmq is the same: the chart pins the -management image, so the plugin is enabled and the operator's client Service carries the management port, and there is no route to it. The one way to reach either from outside today is external: true, which turns the app's Service into a LoadBalancer and publishes the admin UI on a public IP in plaintext, next to the plaintext data port.
Point 4 is the actual state of the art for every data app with a UI, and it is the thing this issue exists to fix. Point 1 is the shape to factor out, not to copy a fourth time.
Why this needs a decision before the per-app TLS PRs land
#2683 (rabbitmq) makes tls.disableNonTLSListeners reachable while adding no route, and #2682 does the equivalent for OpenSearch Dashboards. That is not a defect in either PR — routing was never in their scope — but it means the interaction below arrives with them and is caught by nothing:
The UI leg's backend protocol depends on a TLS value the app owns. With disableNonTLSListeners off, the edge speaks plaintext to the management port, exactly like harbor. With it on, that port is gone and the edge must speak HTTPS to the TLS management port against a per-release private CA, which means backend-protocol: "HTTPS" plus either proxy-ssl-secret pointed at the trust anchor or proxy-ssl-verify: "off" on the nginx path, and a BackendTLSPolicy on the Gateway path. A UI route that hardcodes HTTP, as the only existing example does, breaks silently the moment a tenant sets the flag. Whatever renders the UI leg has to derive the backend protocol from the app's resolved TLS state rather than assume it.
Hostname scheme.Unified TLS certificate and external-exposure model #2811 settles on per-engine subdomains (*.<engine>.<apex>) for the data leg in v1 on Cilium-isolation grounds. Does the UI leg follow that, or take the flat <release>.<apex> form the existing edge-terminated shape already uses? Two schemes for one release is a documentation cost worth paying only if something forces it.
Authentication. A RabbitMQ management UI authenticates against the broker's own user database, which the chart's users map populates; OpenSearch Dashboards authenticates against the security plugin's. Publishing either puts a login form backed by tenant-chosen passwords on the public internet, and neither is covered by the platform SSO path the dashboard and Grafana use. Is app-native auth sufficient for a published UI leg, or must the UI leg sit behind platform authentication? This is the one question here that is a policy call rather than a mechanism choice, and it should not be settled implicitly by whoever writes the first route.
Whether the plaintext LoadBalancer path stays reachable.Unified TLS certificate and external-exposure model #2811 records that plaintext external endpoints are rejected at admission by a VAP on the typed apps.cozystack.io kinds. If a published UI leg exists, external: true publishing an admin UI in plaintext on a LoadBalancer IP should presumably be rejected by the same policy rather than remaining the only working route.
Publishing a UI leg for apps that have no UI, or for Kamaji-backed kubernetes, whose PKI is Kamaji-owned and out of the model.
Part of #2811. Related: #2814 (trust-anchor convergence, which the data leg depends on and the UI leg does not), #2815 (data leg), #2683 and #2682 (the two per-app PRs that make the backend-protocol interaction reachable), #3164 and PR #3218 (the exposure API this may or may not belong to).
Summary
The exposure half of #2811 is passthrough by design, and passthrough is the right answer for a data protocol and the wrong answer for a browser. Several managed apps ship both: an admin UI over HTTP and a client protocol over TLS. Nothing in the epic currently owns the UI leg, and because #2815 reads as "external exposure, solved", the gap is easy to mistake for covered. This issue names the two legs, records why they cannot share one mechanism, and collects the decisions the UI leg needs.
The two legs
For an app like rabbitmq or opensearch the same release has to be reachable two ways, and the two ways want opposite treatment:
ca.crt, per the #2814 contractThe decisive asymmetry is what the consumer can be asked to do. Handing an AMQP client a
ca.crtand a--cafileflag is a documented, one-time configuration step, which is exactly what makes passthrough acceptable there: nothing terminates, so the certificate the client validates is byte-identical to the interior one, and no private key sits at the edge. A browser cannot be configured that way. Route a management UI through a passthrough listener and every visitor gets an interstitial warning, per release, forever, because the interior certificate is a per-release self-signed CA. Telling tenants to import a CA into their browser trust store is not a supported answer, and telling them to click through is worse than not publishing the UI at all.So the UI leg has to terminate at the edge, on the wildcard or ACME certificate the edge workstream already delivers (#2812, #2400, #2820). That is not a weakening of the end-to-end story recorded in #2816 — it is a different consumer with a different threat model, and it should be stated as a deliberate split rather than discovered later as an inconsistency.
How it works today
Four incompatible shapes, verified against
mainat2823b7680. Only three app charts inpackages/appsrender any HTTP routing object at all.packages/apps/harbor/templates/ingress.yamlandhttproute.yaml, branching on the injected_namespace.gatewayto render either anIngresswithingressClassNameor anHTTPRoutewithparentRefsat thecozystackGateway. Host is.Values.host | default "<release>.<_namespace.host>". Three certificate modes are branched in-template: an operator wildcard (skip the per-host secret, let nginx serve its default SSL cert), http01 (cert-manager.io/cluster-issuerplusacme.cert-manager.io/http01-ingress-ingressclassname), and dns01 (issuer annotation only). Backend isbackend-protocol: "HTTP". This is the only shape a UI leg should want, and it exists exactly once.packages/apps/kubernetes/templates/ingress.yamlsetsssl-passthrough: "true",backend-protocol: AUTO_HTTPand aconfiguration-snippetrewriting$proxy_upstream_name, republishing the tenant cluster's own ingress-nginx. Correct for what it does and not reusable as a UI-leg pattern.packages/apps/bucket/templatesrenders no routing object; the S3 endpoint is published bypackages/extra/seaweedfs, and the app chart retains onlyhooks/cleanup-acme.yamlto delete the legacy per-bucketIngressandCertificateit used to render.opensearchships Dashboards behinddashboards.enabledwith a Service on 5601 and no route of any kind, so it is reachable only by port-forward. rabbitmq is the same: the chart pins the-managementimage, so the plugin is enabled and the operator's client Service carries the management port, and there is no route to it. The one way to reach either from outside today isexternal: true, which turns the app's Service into a LoadBalancer and publishes the admin UI on a public IP in plaintext, next to the plaintext data port.Point 4 is the actual state of the art for every data app with a UI, and it is the thing this issue exists to fix. Point 1 is the shape to factor out, not to copy a fourth time.
Why this needs a decision before the per-app TLS PRs land
#2683 (rabbitmq) makes
tls.disableNonTLSListenersreachable while adding no route, and #2682 does the equivalent for OpenSearch Dashboards. That is not a defect in either PR — routing was never in their scope — but it means the interaction below arrives with them and is caught by nothing:The UI leg's backend protocol depends on a TLS value the app owns. With
disableNonTLSListenersoff, the edge speaks plaintext to the management port, exactly like harbor. With it on, that port is gone and the edge must speak HTTPS to the TLS management port against a per-release private CA, which meansbackend-protocol: "HTTPS"plus eitherproxy-ssl-secretpointed at the trust anchor orproxy-ssl-verify: "off"on the nginx path, and aBackendTLSPolicyon the Gateway path. A UI route that hardcodesHTTP, as the only existing example does, breaks silently the moment a tenant sets the flag. Whatever renders the UI leg has to derive the backend protocol from the app's resolved TLS state rather than assume it.Decisions this issue is asking for
TLSRouterendering" question, whoseExposureClass/ServiceExposureAPI is itself under reconsideration in Reconsider network.cozystack.io (ExposureClass / ServiceExposure): necessity vs native loadBalancerClass, and coordination with the tenant-networking surface #3164 and PR refactor(network): drop ExposureClass/ServiceExposure for native loadBalancerClass #3218, or does it get a narrower home so it is not blocked behind that API? These are the same rendering concern seen from two directions, and the answer decides whether the UI leg ships this cycle or waits.*.<engine>.<apex>) for the data leg in v1 on Cilium-isolation grounds. Does the UI leg follow that, or take the flat<release>.<apex>form the existing edge-terminated shape already uses? Two schemes for one release is a documentation cost worth paying only if something forces it.usersmap populates; OpenSearch Dashboards authenticates against the security plugin's. Publishing either puts a login form backed by tenant-chosen passwords on the public internet, and neither is covered by the platform SSO path the dashboard and Grafana use. Is app-native auth sufficient for a published UI leg, or must the UI leg sit behind platform authentication? This is the one question here that is a policy call rather than a mechanism choice, and it should not be settled implicitly by whoever writes the first route.external, for the same class of reason feat(rabbitmq): add TLS support for AMQPS and management HTTPS #2683 declines to infer TLS fromexternaland bug(apps/nats): enabling TLS removes plaintext access, and it is enabled implicitly from external #3348 raises for nats: a value whose meaning silently widens on upgrade is the failure mode this series keeps rediscovering.apps.cozystack.iokinds. If a published UI leg exists,external: truepublishing an admin UI in plaintext on a LoadBalancer IP should presumably be rejected by the same policy rather than remaining the only working route.Non-goals
external-database-exposureproposal stand; this issue is additive to them.kubernetes, whose PKI is Kamaji-owned and out of the model.Part of #2811. Related: #2814 (trust-anchor convergence, which the data leg depends on and the UI leg does not), #2815 (data leg), #2683 and #2682 (the two per-app PRs that make the backend-protocol interaction reachable), #3164 and PR #3218 (the exposure API this may or may not belong to).