Repository navigation
feat(control-plane): issue a certificate for the gateway console Route #268
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -830,6 +830,35 @@ func reconcileConsoleExposure(ctx context.Context, dynamicClient dynamic.Interfa | |
| inactiveKind = "OpenShift Route" | ||
| case IngressModeRoute: | ||
| desired = buildConsoleOpenShiftRoute(namespace, host) | ||
| // Give the console Route its own publicly-trusted certificate when | ||
| // GATEWAY_ROUTE_TLS_ISSUER names a cert-manager ClusterIssuer. | ||
| // | ||
| // Without this the Route rides the router default certificate, which is | ||
| // only valid for the cluster ingress domain. Clusters whose gateway hosts | ||
| // live under a CUSTOM domain (e.g. *.gwlb.<instance>.<zone> on IBM ROKS) | ||
| // therefore serve a name-mismatched certificate and every browser refuses | ||
| // the console. Gateway-API clusters do not hit this because a wildcard | ||
| // certificate on the shared Gateway listener already covers the host, and | ||
| // HTTP-01 cannot issue wildcards -- so a per-host certificate is the only | ||
| // option where DNS-01 is unavailable. | ||
| if issuer := routeTLSIssuer(); issuer != "" { | ||
| annotateRouteForIssuer(desired, issuer) | ||
| // reconcileResource replaces the whole Route, and the spec built above | ||
| // intentionally omits certificate/key: openshift-routes co-owns this | ||
| // Route and injects them. Carry any injection forward, otherwise every | ||
| // reconcile strips the certificate and the console flaps. | ||
| if cert, key := readInjectedRouteCert(ctx, dynamicClient, namespace, consoleName); cert != "" { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice reuse of the gateway-Route carry-forward pattern. One caveat: |
||
| tls, _, _ := unstructured.NestedMap(desired.Object, "spec", "tls") | ||
| if tls == nil { | ||
| tls = map[string]interface{}{} | ||
| } | ||
| tls["certificate"] = cert | ||
| if key != "" { | ||
| tls["key"] = key | ||
| } | ||
| _ = unstructured.SetNestedMap(desired.Object, tls, "spec", "tls") | ||
| } | ||
| } | ||
| inactiveGVR = consoleHTTPRouteGVR | ||
| inactiveKind = "HTTPRoute" | ||
| default: | ||
|
|
@@ -1005,3 +1034,17 @@ func deleteConsole(ctx context.Context, dynamicClient dynamic.Interface, clients | |
|
|
||
| return errors.Join(errs...) | ||
| } | ||
|
|
||
| // annotateRouteForIssuer marks a Route for the cert-manager openshift-routes | ||
| // controller, which mints a certificate from the named ClusterIssuer and | ||
| // injects it into spec.tls.{certificate,key}. | ||
| func annotateRouteForIssuer(route *unstructured.Unstructured, issuer string) { | ||
| annotations, _, _ := unstructured.NestedStringMap(route.Object, "metadata", "annotations") | ||
| if annotations == nil { | ||
| annotations = map[string]string{} | ||
| } | ||
| annotations["cert-manager.io/issuer-name"] = issuer | ||
| annotations["cert-manager.io/issuer-kind"] = "ClusterIssuer" | ||
| annotations["cert-manager.io/issuer-group"] = "cert-manager.io" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor:
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| _ = unstructured.SetNestedStringMap(route.Object, annotations, "metadata", "annotations") | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -621,7 +621,7 @@ func reconcileRouteResources(ctx context.Context, dynamicClient dynamic.Interfac | |
| // co-owns this Route (we own termination + destinationCACertificate, it | ||
| // owns the edge cert), so carry forward any certificate/key it has already | ||
| // injected -- otherwise each reconcile strips the cert and flaps h2. | ||
| if cert, key := readInjectedRouteCert(ctx, dynamicClient, namespace); cert != "" { | ||
| if cert, key := readInjectedRouteCert(ctx, dynamicClient, namespace, gatewayRouteName); cert != "" { | ||
| tlsConfig["certificate"] = cert | ||
| if key != "" { | ||
| tlsConfig["key"] = key | ||
|
|
@@ -1663,6 +1663,11 @@ const ( | |
| const ( | ||
| RouteTerminationPassthrough = "passthrough" | ||
| RouteTerminationReencrypt = "reencrypt" | ||
|
|
||
| // gatewayRouteName is the OpenShift Route serving the gateway itself. | ||
| // openshift-routes injects the issued certificate into it, and each reconcile | ||
| // must carry that injection forward (see readInjectedRouteCert). | ||
| gatewayRouteName = "openshell-gateway" | ||
| ) | ||
|
|
||
| // routeTermination resolves the Route TLS termination for the "route" ingress | ||
|
|
@@ -1692,9 +1697,9 @@ func routeTLSIssuer() string { | |
| // The route reconcile does a full replace, so reconcileRouteResources carries | ||
| // these forward to avoid clobbering the injected edge certificate (which would | ||
| // flap ALPN h2, and hence gRPC, on every reconcile). | ||
| func readInjectedRouteCert(ctx context.Context, dynamicClient dynamic.Interface, namespace string) (string, string) { | ||
| func readInjectedRouteCert(ctx context.Context, dynamicClient dynamic.Interface, namespace, routeName string) (string, string) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now that |
||
| gvr := schema.GroupVersionResource{Group: "route.openshift.io", Version: "v1", Resource: "routes"} | ||
| existing, err := dynamicClient.Resource(gvr).Namespace(namespace).Get(ctx, "openshell-gateway", metav1.GetOptions{}) | ||
| existing, err := dynamicClient.Resource(gvr).Namespace(namespace).Get(ctx, routeName, metav1.GetOptions{}) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Minor] The |
||
| if err != nil { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor/robustness: this returns |
||
| return "", "" | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,15 @@ | |
| # Kind uses locally issued certificates. Other drivers may override this seam | ||
| # while reusing the production OIDC and role-assignment behavior below. | ||
|
|
||
| # Auto-detect container engine (docker or podman) if not explicitly set. | ||
| if [[ -z "${CONTAINER_ENGINE:-}" ]]; then | ||
| if command -v podman &>/dev/null; then | ||
| export CONTAINER_ENGINE=podman | ||
| elif command -v docker &>/dev/null; then | ||
| export CONTAINER_ENGINE=docker | ||
| fi | ||
| fi | ||
|
|
||
| # Kind's self-signed CA is not in the system trust store. Instruct the | ||
| # openshell CLI to skip TLS verification for gateway connections. curl already | ||
| # uses -sk (insecure) for all driver requests; this extends the same treatment | ||
|
|
@@ -76,18 +85,33 @@ _kind_start_gw_socat() { | |
| _KINDCCM_GW_PORT="${socat_port}" | ||
| } | ||
| _driver_curl() { | ||
| _kind_discover_port | ||
| local connect_args=() | ||
| if [[ -n "${_KINDCCM_PORT}" && "${_KINDCCM_PORT}" != "443" ]]; then | ||
| connect_args+=( | ||
| --connect-to "api.hypershell.localhost:443:127.0.0.1:${_KINDCCM_PORT}" | ||
| --connect-to "keycloak.hypershell.localhost:443:127.0.0.1:${_KINDCCM_PORT}" | ||
| --connect-to "console.hypershell.localhost:443:127.0.0.1:${_KINDCCM_PORT}" | ||
| --connect-to "health.hypershell.localhost:443:127.0.0.1:${_KINDCCM_PORT}" | ||
| --connect-to "observability.hypershell.localhost:443:127.0.0.1:${_KINDCCM_PORT}" | ||
| ) | ||
| # Try direct HTTPRoute access first. This works in most setups where the | ||
| # routes are directly accessible on port 443 (docker, podman, or iptables- | ||
| # forwarded 443). If direct access fails (connection refused/timeout), fall | ||
| # back to port-remapping via the ephemeral kindccm-gw port. | ||
| local output status | ||
| output=$(curl -sk --ipv4 "$@" 2>&1) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Capturing with |
||
| status=$? | ||
|
|
||
| # If direct access failed due to connection error, try port-remapping | ||
| if [[ $status -eq 7 ]]; then | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The port-remap fallback only triggers on curl exit code 7 (connection refused), but the comment says "connection refused/timeout." A real timeout is exit 28 and will not fall back. Either also handle 28 or drop "timeout" from the comment to avoid a false expectation. |
||
| _kind_discover_port | ||
| local connect_args=() | ||
| if [[ -n "${_KINDCCM_PORT}" && "${_KINDCCM_PORT}" != "443" ]]; then | ||
| connect_args+=( | ||
| --connect-to "api.hypershell.localhost:443:127.0.0.1:${_KINDCCM_PORT}" | ||
| --connect-to "keycloak.hypershell.localhost:443:127.0.0.1:${_KINDCCM_PORT}" | ||
| --connect-to "console.hypershell.localhost:443:127.0.0.1:${_KINDCCM_PORT}" | ||
| --connect-to "health.hypershell.localhost:443:127.0.0.1:${_KINDCCM_PORT}" | ||
| --connect-to "observability.hypershell.localhost:443:127.0.0.1:${_KINDCCM_PORT}" | ||
| ) | ||
| output=$(curl -sk --ipv4 "${connect_args[@]}" "$@" 2>&1) | ||
| status=$? | ||
| fi | ||
| fi | ||
| curl -sk --ipv4 "${connect_args[@]}" "$@" | ||
|
|
||
| echo "$output" | ||
| return $status | ||
| } | ||
|
|
||
| # discover_api_host - find the HyperShell API server base URL. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The console Route reuses
GATEWAY_ROUTE_TLS_ISSUER(viarouteTLSIssuer()) with edge termination, whereas that env var's doc comment states it is "only meaningful with reencrypt termination." The reuse is reasonable, but please update therouteTLSIssuerdoc so the next reader is not misled about its scope.