diff --git a/deployment/chainloop/Chart.yaml b/deployment/chainloop/Chart.yaml index fd0081247..d468a5c11 100644 --- a/deployment/chainloop/Chart.yaml +++ b/deployment/chainloop/Chart.yaml @@ -7,7 +7,7 @@ description: Chainloop is an open source software supply chain control plane, a type: application # Bump the patch (not minor, not major) version on each change in the Chart Source code -version: 1.470.0 +version: 1.470.1 # Do not update appVersion, this is handled automatically by the release process appVersion: v1.127.0 diff --git a/deployment/chainloop/README.md b/deployment/chainloop/README.md index f1e2e03a2..16b341039 100644 --- a/deployment/chainloop/README.md +++ b/deployment/chainloop/README.md @@ -247,7 +247,7 @@ The existing gRPC ingress stays available. Existing clients can keep their curre To test the shared endpoint, point a gRPC client to the control plane HTTP hostname on port 443. The CAS shared endpoint mirrors the control plane (spec D-020): one standard HTTP listener dispatches each request, and native gRPC reaches the native transport through an in-process reverse proxy. `cas.ingress` is one catch-all route on the same hostname — one path `/` with an HTTP/2 (gRPC) upstream — so ByteStream uploads and downloads, browser downloads, and status probes share one Service port and one certificate, including mixed requests on one upstream connection. -Set gRPC transport annotations under `cas.ingress.annotations`, such as `backend-protocol: GRPCS` for TLS to the pod. The upload body-size annotations keep their existing defaults. +The chart selects gRPC transport annotations from the pod TLS configuration. You can override them under `cas.ingress.annotations`. The upload body-size annotations keep their existing defaults. Enable the shared endpoint with a `cas.ingress` block — for example: @@ -275,11 +275,25 @@ The chart configures HTTP/2 upstream traffic for these controllers: Native gRPC clients normally use TLS on the ingress. Plaintext client HTTP/2 needs controller configuration too. - Traefik: `controlplane.service.annotations` includes `traefik.ingress.kubernetes.io/service.serversscheme: h2c`. -These defaults apply to plaintext traffic between the ingress and the pod. Public ingress TLS still works as before. -If you enable TLS on the pod listener, configure the controller for encrypted HTTP/2 upstream traffic instead. -For ingress-nginx, use `GRPCS`. For F5 NGINX, also configure `nginx.org/ssl-services`. -For Traefik, use the `https` service scheme and configure certificate trust as required. -Other controllers need their own upstream protocol settings. +To enable pod TLS, provide each component with a Secret containing `tls.crt` and `tls.key`: + +```yaml +controlplane: + tls: + existingSecret: cp-tls +cas: + tls: + existingSecret: cas-tls +``` + +With pod TLS enabled, direct HTTP clients must use HTTPS and direct gRPC clients must use TLS, whether or not an Ingress is enabled. +The legacy `tlsConfig.secret.name` setting is also supported; `tls.existingSecret` takes precedence when both are set. + +The chart configures ingress-nginx, F5 NGINX, and Traefik to connect to the pods over TLS. Explicit annotation overrides take precedence. +Configure your ingress controller to trust the pod certificates. For other controllers or a Gateway, also configure TLS connections to the pods and certificate validation in your infrastructure. + +Public TLS termination remains controlled by `ingress.tls` and `ingressAPI.tls`, independently of pod TLS. +When upgrading a deployment that already sets a pod TLS Secret for gRPC, the shared HTTP listener also switches to TLS. Direct clients of that listener must use TLS; HTTP requests must use HTTPS. The NGINX gRPC module path supports the shared endpoint without path or content-type rules. The regression suite checks that path with NGINX Open Source. It does not certify every controller or NGINX Plus release. @@ -616,7 +630,7 @@ Once done, you can access with [two predefined users](https://github.com/chainlo | `controlplane.containerPorts.grpc` | controlplane gRPC container port | `9000` | | `controlplane.containerPorts.metrics` | controlplane prometheus metrics container port | `5000` | | `controlplane.enableProfiler` | Enable pprof profiling on port 6060 | `false` | -| `controlplane.tls.existingSecret` | Existing secret name containing TLS certificate to be used by the controlplane grpc server. NOTE: When it's set it will disable secret creation. The secret must contains 2 keys: tls.crt and tls.key respectively containing the certificate and private key. | `""` | +| `controlplane.tls.existingSecret` | Secret with tls.crt and tls.key for both listeners. Direct clients must use TLS. Public Ingress TLS termination remains separate. The legacy tlsConfig.secret.name value is also supported. | `""` | | `controlplane.pluginsDir` | Directory where to look for plugins | `/plugins` | | `controlplane.pluginsNetworkPolicy` | Outbound network policy for the plugins that accept an arbitrary target URL, currently the generic webhook and Dependency-Track plugins. Plugins whose destination is a known public service, such as the Slack and Discord webhooks, always refuse non-public destinations. | | | `controlplane.pluginsNetworkPolicy.blockPrivateTargets` | Refuse requests to destinations that are not publicly routable, such as loopback, private ranges and cloud metadata endpoints | `false` | @@ -679,7 +693,7 @@ Once done, you can access with [two predefined users](https://github.com/chainlo | `controlplane.service.loadBalancerIP` | controlplane service Load Balancer IP | `""` | | `controlplane.service.loadBalancerSourceRanges` | controlplane service Load Balancer sources | `[]` | | `controlplane.service.externalTrafficPolicy` | controlplane service external traffic policy | `Cluster` | -| `controlplane.service.annotations` | Annotations for the shared HTTP and gRPC service. Traefik uses h2c upstream. | See values.yaml | +| `controlplane.service.annotations` | Annotations for the shared HTTP and gRPC service. Traefik uses h2c or HTTPS to match shared listener TLS. | See values.yaml | | `controlplane.service.extraPorts` | Extra ports to expose in controlplane service (normally used with the `sidecars` value) | `[]` | | `controlplane.service.sessionAffinity` | Control where client requests go, to the same pod or round-robin | `None` | | `controlplane.service.sessionAffinityConfig` | Additional settings for the sessionAffinity | `{}` | @@ -846,7 +860,7 @@ Once done, you can access with [two predefined users](https://github.com/chainlo | `cas.containerPorts.http` | controlplane HTTP container port | `8000` | | `cas.containerPorts.grpc` | controlplane gRPC container port | `9000` | | `cas.containerPorts.metrics` | controlplane prometheus metrics container port | `5000` | -| `cas.tls.existingSecret` | Existing secret name containing TLS certificate to be used by the cas grpc server. NOTE: When it's set it will disable secret creation. The secret must contains 2 keys: tls.crt and tls.key respectively containing the certificate and private key. | `""` | +| `cas.tls.existingSecret` | Secret with tls.crt and tls.key for both listeners. Direct clients must use TLS. Public Ingress TLS termination remains separate. The legacy tlsConfig.secret.name value is also supported. | `""` | ### CAS Networking @@ -861,7 +875,7 @@ Once done, you can access with [two predefined users](https://github.com/chainlo | `cas.service.loadBalancerIP` | cas service Load Balancer IP | `""` | | `cas.service.loadBalancerSourceRanges` | cas service Load Balancer sources | `[]` | | `cas.service.externalTrafficPolicy` | cas service external traffic policy | `Cluster` | -| `cas.service.annotations` | Annotations for the shared HTTP and gRPC service. Traefik uses h2c for HTTP/2 upstream traffic. | See values.yaml | +| `cas.service.annotations` | Annotations for the shared HTTP and gRPC service. Traefik uses h2c or HTTPS to match shared listener TLS. | See values.yaml | | `cas.service.extraPorts` | Extra ports to expose in cas service (normally used with the `sidecars` value) | `[]` | | `cas.service.sessionAffinity` | Control where client requests go, to the same pod or round-robin | `None` | | `cas.service.sessionAffinityConfig` | Additional settings for the sessionAffinity | `{}` | diff --git a/deployment/chainloop/chart_test.go b/deployment/chainloop/chart_test.go index 50f0f45c5..6b1e35545 100644 --- a/deployment/chainloop/chart_test.go +++ b/deployment/chainloop/chart_test.go @@ -212,16 +212,21 @@ func testSharedCASIngress(t *testing.T) { } type chartResource struct { - Kind string `yaml:"kind"` + Kind string `yaml:"kind"` + Data map[string]string `yaml:"data"` Metadata struct { Name string `yaml:"name"` Annotations map[string]string `yaml:"annotations"` } `yaml:"metadata"` Spec struct { + Template struct { + Spec chartPodSpec `yaml:"spec"` + } `yaml:"template"` Ports []struct { - Name string `yaml:"name"` - Port int `yaml:"port"` - TargetPort string `yaml:"targetPort"` + Name string `yaml:"name"` + Port int `yaml:"port"` + TargetPort string `yaml:"targetPort"` + AppProtocol string `yaml:"appProtocol"` } `yaml:"ports"` TLS []struct { Hosts []string `yaml:"hosts"` @@ -246,6 +251,32 @@ type chartResource struct { } `yaml:"spec"` } +type chartPodSpec struct { + Containers []struct { + StartupProbe chartProbe `yaml:"startupProbe"` + LivenessProbe chartProbe `yaml:"livenessProbe"` + ReadinessProbe chartProbe `yaml:"readinessProbe"` + VolumeMounts []struct { + Name string `yaml:"name"` + MountPath string `yaml:"mountPath"` + } `yaml:"volumeMounts"` + } `yaml:"containers"` + Volumes []struct { + Name string `yaml:"name"` + Secret *struct { + SecretName string `yaml:"secretName"` + } `yaml:"secret"` + } `yaml:"volumes"` +} + +type chartProbe struct { + HTTPGet struct { + Path string `yaml:"path"` + Port string `yaml:"port"` + Scheme string `yaml:"scheme"` + } `yaml:"httpGet"` +} + func TestCASSharedIngressTLS(t *testing.T) { resources := renderChart(t, []string{helmSet, casIngressEnabled, helmSet, "cas.ingress.tls=true", helmSetString, `cas.ingress.annotations.cert-manager\.io/cluster-issuer=test-issuer`}) diff --git a/deployment/chainloop/templates/cas/configmap.yaml b/deployment/chainloop/templates/cas/configmap.yaml index fd0aa6e63..3fe4c86e8 100644 --- a/deployment/chainloop/templates/cas/configmap.yaml +++ b/deployment/chainloop/templates/cas/configmap.yaml @@ -19,6 +19,11 @@ data: staging_dir: {{ include "chainloop.cas.staging_dir" . | quote }} server: http: + {{- if include "cas.tls-secret-name" . }} + tls_config: + certificate: /data/server-certs/tls.crt + private_key: /data/server-certs/tls.key + {{- end }} addr: "0.0.0.0:{{ .Values.cas.containerPorts.http }}" # Timeouts for http downloads # grpc downloads/uploads don't require this because they don't have timeouts diff --git a/deployment/chainloop/templates/cas/deployment.yaml b/deployment/chainloop/templates/cas/deployment.yaml index b55a12e7d..5ee67ece9 100644 --- a/deployment/chainloop/templates/cas/deployment.yaml +++ b/deployment/chainloop/templates/cas/deployment.yaml @@ -89,15 +89,24 @@ spec: httpGet: path: /statusz port: http + {{- if include "cas.tls-secret-name" . }} + scheme: HTTPS + {{- end }} periodSeconds: 5 livenessProbe: httpGet: path: /statusz port: http + {{- if include "cas.tls-secret-name" . }} + scheme: HTTPS + {{- end }} readinessProbe: httpGet: path: /statusz?readiness=1 port: http + {{- if include "cas.tls-secret-name" . }} + scheme: HTTPS + {{- end }} {{- if .Values.cas.resources }} resources: {{- toYaml .Values.cas.resources | nindent 12 }} {{- else if ne .Values.cas.resourcesPreset "none" }} @@ -186,4 +195,4 @@ spec: {{- end }} {{- if .Values.cas.extraVolumes }} {{- include "common.tplvalues.render" (dict "value" .Values.cas.extraVolumes "context" $) | nindent 8 }} - {{- end }} \ No newline at end of file + {{- end }} diff --git a/deployment/chainloop/templates/cas/ingress.yaml b/deployment/chainloop/templates/cas/ingress.yaml index beb11b7e9..45821d920 100644 --- a/deployment/chainloop/templates/cas/ingress.yaml +++ b/deployment/chainloop/templates/cas/ingress.yaml @@ -11,8 +11,12 @@ metadata: name: {{ $fullName }} namespace: {{ include "common.names.namespace" . | quote }} labels: {{- include "chainloop.cas.labels" . | nindent 4 }} - {{- if or .Values.cas.ingress.annotations .Values.commonAnnotations }} - {{- $annotations := include "common.tplvalues.merge" (dict "values" (list .Values.cas.ingress.annotations .Values.commonAnnotations) "context" .) }} + {{- $tlsAnnotations := dict }} + {{- if include "cas.tls-secret-name" . }} + {{- $_ := set $tlsAnnotations "nginx.org/ssl-services" $fullName }} + {{- end }} + {{- if or .Values.cas.ingress.annotations .Values.commonAnnotations $tlsAnnotations }} + {{- $annotations := include "common.tplvalues.merge" (dict "values" (list .Values.cas.ingress.annotations .Values.commonAnnotations $tlsAnnotations) "context" .) }} annotations: {{- include "common.tplvalues.render" (dict "value" $annotations "context" $) | nindent 4 }} {{- end }} spec: @@ -57,4 +61,4 @@ spec: {{- include "common.tplvalues.render" (dict "value" .Values.cas.ingress.extraTls "context" $) | nindent 4 }} {{- end }} {{- end }} -{{- end }} \ No newline at end of file +{{- end }} diff --git a/deployment/chainloop/templates/cas/service-http.yaml b/deployment/chainloop/templates/cas/service-http.yaml index 8fad843f3..982c53677 100644 --- a/deployment/chainloop/templates/cas/service-http.yaml +++ b/deployment/chainloop/templates/cas/service-http.yaml @@ -39,6 +39,7 @@ spec: port: {{ $port }} targetPort: http protocol: TCP + appProtocol: {{ if include "cas.tls-secret-name" . }}kubernetes.io/h2{{ else }}kubernetes.io/h2c{{ end }} {{- if and (or (eq .Values.cas.service.type "NodePort") (eq .Values.cas.service.type "LoadBalancer")) (not (empty .Values.cas.service.nodePorts.http)) }} nodePort: {{ .Values.cas.service.nodePorts.http }} {{- else if eq .Values.cas.service.type "ClusterIP" }} diff --git a/deployment/chainloop/templates/controlplane/configmap.yaml b/deployment/chainloop/templates/controlplane/configmap.yaml index ea64c80e5..159d61cdb 100644 --- a/deployment/chainloop/templates/controlplane/configmap.yaml +++ b/deployment/chainloop/templates/controlplane/configmap.yaml @@ -28,6 +28,11 @@ data: enable_profiler: {{ .Values.controlplane.enableProfiler }} server: http: + {{- if include "controlplane.tls-secret-name" . }} + tls_config: + certificate: /data/server-certs/tls.crt + private_key: /data/server-certs/tls.key + {{- end }} addr: "0.0.0.0:{{ .Values.controlplane.containerPorts.http }}" timeout: 10s external_url: {{ include "chainloop.controlplane.external_url" . }} diff --git a/deployment/chainloop/templates/controlplane/deployment.yaml b/deployment/chainloop/templates/controlplane/deployment.yaml index 0a695a2c8..55c2b9798 100644 --- a/deployment/chainloop/templates/controlplane/deployment.yaml +++ b/deployment/chainloop/templates/controlplane/deployment.yaml @@ -106,6 +106,9 @@ spec: httpGet: path: /statusz port: http + {{- if include "controlplane.tls-secret-name" . }} + scheme: HTTPS + {{- end }} failureThreshold: 10 periodSeconds: 5 initialDelaySeconds: 10 @@ -113,6 +116,9 @@ spec: httpGet: path: /statusz port: http + {{- if include "controlplane.tls-secret-name" . }} + scheme: HTTPS + {{- end }} failureThreshold: 10 periodSeconds: 5 initialDelaySeconds: 10 @@ -120,6 +126,9 @@ spec: httpGet: path: /statusz?readiness=1 port: http + {{- if include "controlplane.tls-secret-name" . }} + scheme: HTTPS + {{- end }} periodSeconds: 5 {{- if .Values.controlplane.lifecycleHooks }} lifecycle: {{- include "common.tplvalues.render" (dict "value" .Values.controlplane.lifecycleHooks "context" $) | nindent 12 }} @@ -249,4 +258,4 @@ spec: - name: tsa-roots configMap: name: {{ include "chainloop.controlplane.fullname" $ }}-tsa - {{- end }} \ No newline at end of file + {{- end }} diff --git a/deployment/chainloop/templates/controlplane/ingress.yaml b/deployment/chainloop/templates/controlplane/ingress.yaml index e5725f5ac..468110927 100644 --- a/deployment/chainloop/templates/controlplane/ingress.yaml +++ b/deployment/chainloop/templates/controlplane/ingress.yaml @@ -11,8 +11,12 @@ metadata: name: {{ $fullName }} namespace: {{ include "common.names.namespace" . | quote }} labels: {{- include "chainloop.controlplane.labels" . | nindent 4 }} - {{- if or .Values.controlplane.ingress.annotations .Values.commonAnnotations }} - {{- $annotations := include "common.tplvalues.merge" (dict "values" (list .Values.controlplane.ingress.annotations .Values.commonAnnotations) "context" .) }} + {{- $tlsAnnotations := dict }} + {{- if include "controlplane.tls-secret-name" . }} + {{- $_ := set $tlsAnnotations "nginx.org/ssl-services" $fullName }} + {{- end }} + {{- if or .Values.controlplane.ingress.annotations .Values.commonAnnotations $tlsAnnotations }} + {{- $annotations := include "common.tplvalues.merge" (dict "values" (list .Values.controlplane.ingress.annotations .Values.commonAnnotations $tlsAnnotations) "context" .) }} annotations: {{- include "common.tplvalues.render" (dict "value" $annotations "context" $) | nindent 4 }} {{- end }} spec: diff --git a/deployment/chainloop/templates/controlplane/service-http.yaml b/deployment/chainloop/templates/controlplane/service-http.yaml index 5c6169901..044b8ab16 100644 --- a/deployment/chainloop/templates/controlplane/service-http.yaml +++ b/deployment/chainloop/templates/controlplane/service-http.yaml @@ -41,6 +41,7 @@ spec: targetPort: {{ .Values.controlplane.containerPorts.http }} {{- end }} protocol: TCP + appProtocol: {{ if include "controlplane.tls-secret-name" . }}kubernetes.io/h2{{ else }}kubernetes.io/h2c{{ end }} {{- if and (or (eq .Values.controlplane.service.type "NodePort") (eq .Values.controlplane.service.type "LoadBalancer")) (not (empty .Values.controlplane.service.nodePorts.http)) }} nodePort: {{ .Values.controlplane.service.nodePorts.http }} {{- else if eq .Values.controlplane.service.type "ClusterIP" }} diff --git a/deployment/chainloop/values.yaml b/deployment/chainloop/values.yaml index 6f8861577..983046a71 100644 --- a/deployment/chainloop/values.yaml +++ b/deployment/chainloop/values.yaml @@ -167,7 +167,7 @@ controlplane: ## TLS configuration ## tls: - ## @param controlplane.tls.existingSecret Existing secret name containing TLS certificate to be used by the controlplane grpc server. NOTE: When it's set it will disable secret creation. The secret must contains 2 keys: tls.crt and tls.key respectively containing the certificate and private key. + ## @param controlplane.tls.existingSecret Secret with tls.crt and tls.key for both listeners. Direct clients must use TLS. Public Ingress TLS termination remains separate. The legacy tlsConfig.secret.name value is also supported. existingSecret: "" ## @param controlplane.pluginsDir Directory where to look for plugins @@ -345,11 +345,11 @@ controlplane: ## ref http://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/#preserving-the-client-source-ip ## externalTrafficPolicy: Cluster - ## @extra controlplane.service.annotations Annotations for the shared HTTP and gRPC service. Traefik uses h2c for HTTP/2 upstream traffic. + ## @extra controlplane.service.annotations Annotations for the shared HTTP and gRPC service. Traefik uses h2c or HTTPS to match shared listener TLS. ## annotations: ## @skip controlplane.service.annotations.traefik.ingress.kubernetes.io/service.serversscheme - traefik.ingress.kubernetes.io/service.serversscheme: h2c + traefik.ingress.kubernetes.io/service.serversscheme: '{{ ternary "https" "h2c" (not (empty (include "controlplane.tls-secret-name" .))) }}' ## @param controlplane.service.extraPorts Extra ports to expose in controlplane service (normally used with the `sidecars` value) ## extraPorts: [] @@ -462,7 +462,7 @@ controlplane: ## annotations: ## @skip controlplane.ingress.annotations.nginx.ingress.kubernetes.io/backend-protocol - nginx.ingress.kubernetes.io/backend-protocol: GRPC + nginx.ingress.kubernetes.io/backend-protocol: '{{ ternary "GRPCS" "GRPC" (not (empty (include "controlplane.tls-secret-name" .))) }}' ## @skip controlplane.ingress.annotations.nginx.org/grpc-services nginx.org/grpc-services: '{{ include "chainloop.controlplane.fullname" . }}' ## @skip controlplane.ingress.annotations.nginx.org/http2 @@ -1077,7 +1077,7 @@ cas: ## TLS configuration ## tls: - ## @param cas.tls.existingSecret Existing secret name containing TLS certificate to be used by the cas grpc server. NOTE: When it's set it will disable secret creation. The secret must contains 2 keys: tls.crt and tls.key respectively containing the certificate and private key. + ## @param cas.tls.existingSecret Secret with tls.crt and tls.key for both listeners. Direct clients must use TLS. Public Ingress TLS termination remains separate. The legacy tlsConfig.secret.name value is also supported. existingSecret: "" ## @skip cas.serviceAccount @@ -1129,11 +1129,11 @@ cas: ## ref http://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/#preserving-the-client-source-ip ## externalTrafficPolicy: Cluster - ## @extra cas.service.annotations Annotations for the shared HTTP and gRPC service. Traefik uses h2c for HTTP/2 upstream traffic. + ## @extra cas.service.annotations Annotations for the shared HTTP and gRPC service. Traefik uses h2c or HTTPS to match shared listener TLS. ## annotations: ## @skip cas.service.annotations.traefik.ingress.kubernetes.io/service.serversscheme - traefik.ingress.kubernetes.io/service.serversscheme: h2c + traefik.ingress.kubernetes.io/service.serversscheme: '{{ ternary "https" "h2c" (not (empty (include "cas.tls-secret-name" .))) }}' ## @param cas.service.extraPorts Extra ports to expose in cas service (normally used with the `sidecars` value) ## extraPorts: [] @@ -1252,7 +1252,7 @@ cas: annotations: ## @skip cas.ingress.annotations.nginx.ingress.kubernetes.io/backend-protocol ## Tell nginx Ingress Controller to expect gRPC traffic - nginx.ingress.kubernetes.io/backend-protocol: "GRPC" + nginx.ingress.kubernetes.io/backend-protocol: '{{ ternary "GRPCS" "GRPC" (not (empty (include "cas.tls-secret-name" .))) }}' ## @skip cas.ingress.annotations.nginx.org/grpc-services nginx.org/grpc-services: '{{ include "chainloop.cas.fullname" . }}' ## @skip cas.ingress.annotations.nginx.org/http2