diff --git a/charts/pulsar-resources-operator/README.md b/charts/pulsar-resources-operator/README.md index 2bfe1410..84a9e569 100644 --- a/charts/pulsar-resources-operator/README.md +++ b/charts/pulsar-resources-operator/README.md @@ -31,6 +31,84 @@ $ helm -n upgrade my-release streamnative/pulsar-resources-operator Uninstalling the release does not remove CRDs or existing custom resources. +## Hardened Configuration + +The chart leaves ServiceAccount token automount and security contexts unchanged by default. Set +`serviceAccount.automountServiceAccountToken` to `true` or `false` to configure the chart-created +ServiceAccount explicitly; omit it or use `null` to leave the field unset. When `serviceAccount.create=false`, +configure automount on the externally managed ServiceAccount instead. Changing only the ServiceAccount +setting does not trigger a Deployment rollout; recreate the operator Pods for the change to take effect. + +The operator needs Kubernetes API credentials for watches, status updates, and leader election. +Setting automount to `false` without supplying credentials breaks the standard in-cluster configuration. +To disable automatic mounting while retaining API access, save the following as `values-hardened.yaml`. +It explicitly projects a short-lived ServiceAccount token at client-go's default path and provides +writable temporary storage while keeping the container root filesystem read-only: + +```yaml +serviceAccount: + automountServiceAccountToken: false + +securityContext: + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + runAsNonRoot: true + capabilities: + drop: + - ALL + +extraVolumes: + - name: kube-api-access + projected: + sources: + - serviceAccountToken: + path: token + expirationSeconds: 3600 + - configMap: + name: kube-root-ca.crt + items: + - key: ca.crt + path: ca.crt + - downwardAPI: + items: + - path: namespace + fieldRef: + apiVersion: v1 + fieldPath: metadata.namespace + - name: tmp + emptyDir: {} + +extraVolumeMounts: + - name: kube-api-access + mountPath: /var/run/secrets/kubernetes.io/serviceaccount + readOnly: true + - name: tmp + mountPath: /tmp +``` + +Apply the override alongside your existing release values: + +```console +$ helm -n upgrade --install my-release streamnative/pulsar-resources-operator -f values.yaml -f values-hardened.yaml +``` + +- Merge any existing `extraVolumes` and `extraVolumeMounts` into the override before applying it: Helm + replaces lists rather than appending to them. Preserve existing authentication and cloud storage mounts. +- The cluster must support ServiceAccount token projection and provide the `kube-root-ca.crt` ConfigMap + in the release namespace. With no explicit audience, the token uses the API server's default audience. + The kubelet rotates the token, and client-go refreshes credentials from the mounted file. Do not use a + `subPath` mount for the token because it would prevent projected updates from reaching the container. +- Keep `/tmp` writable by the container's runtime UID: inline OAuth2 credentials and PulsarPackage + downloads create temporary files there. Verify these operations, not just operator startup, when + enabling a read-only root filesystem. Size temporary storage according to package sizes and concurrency. +- This example does not pin a UID or GID, so the image or cluster can supply the appropriate non-root + identity. Validate the actual image and any additional security requirements in your environment. +- Explicit token projection does not remove API credentials or reduce the ServiceAccount's RBAC + permissions. Check the rendered ServiceAccount and Deployment, the admitted Pod, and your policy reports. + A policy forbidding all API tokens still requires an approved exception or another authentication method. +- Before rollout, verify leader election, resource reconciliation, token rotation, and the OAuth2 and + package operations you use in a test environment. Rendering this example is not a runtime or policy check. + ## Values | Key | Type | Default | Description | @@ -89,6 +167,7 @@ Uninstalling the release does not remove CRDs or existing custom resources. | resources | object | `{}` | Add resource limits and requests | | securityContext | object | `{}` | Add security context for container | | serviceAccount.annotations | object | `{}` | Annotations to add to the service account | +| serviceAccount.automountServiceAccountToken | bool/null | `nil` | Optional token automount setting for the chart-created ServiceAccount. Null preserves Kubernetes' default behavior. When false, explicitly mount Kubernetes API credentials; see the hardened configuration example in the README. | | serviceAccount.create | bool | `true` | Specifies whether a service account should be created | | serviceAccount.name | string | `""` | Name of the service account to use. When empty and create=true, the chart generates a name. | | terminationGracePeriodSeconds | int | `10` | Graceful termination period in seconds. | diff --git a/charts/pulsar-resources-operator/README.md.gotmpl b/charts/pulsar-resources-operator/README.md.gotmpl index bf39b9de..0548bfed 100644 --- a/charts/pulsar-resources-operator/README.md.gotmpl +++ b/charts/pulsar-resources-operator/README.md.gotmpl @@ -29,4 +29,82 @@ $ helm -n upgrade my-release streamnative/pulsar-resources-operator Uninstalling the release does not remove CRDs or existing custom resources. +## Hardened Configuration + +The chart leaves ServiceAccount token automount and security contexts unchanged by default. Set +`serviceAccount.automountServiceAccountToken` to `true` or `false` to configure the chart-created +ServiceAccount explicitly; omit it or use `null` to leave the field unset. When `serviceAccount.create=false`, +configure automount on the externally managed ServiceAccount instead. Changing only the ServiceAccount +setting does not trigger a Deployment rollout; recreate the operator Pods for the change to take effect. + +The operator needs Kubernetes API credentials for watches, status updates, and leader election. +Setting automount to `false` without supplying credentials breaks the standard in-cluster configuration. +To disable automatic mounting while retaining API access, save the following as `values-hardened.yaml`. +It explicitly projects a short-lived ServiceAccount token at client-go's default path and provides +writable temporary storage while keeping the container root filesystem read-only: + +```yaml +serviceAccount: + automountServiceAccountToken: false + +securityContext: + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + runAsNonRoot: true + capabilities: + drop: + - ALL + +extraVolumes: + - name: kube-api-access + projected: + sources: + - serviceAccountToken: + path: token + expirationSeconds: 3600 + - configMap: + name: kube-root-ca.crt + items: + - key: ca.crt + path: ca.crt + - downwardAPI: + items: + - path: namespace + fieldRef: + apiVersion: v1 + fieldPath: metadata.namespace + - name: tmp + emptyDir: {} + +extraVolumeMounts: + - name: kube-api-access + mountPath: /var/run/secrets/kubernetes.io/serviceaccount + readOnly: true + - name: tmp + mountPath: /tmp +``` + +Apply the override alongside your existing release values: + +```console +$ helm -n upgrade --install my-release streamnative/pulsar-resources-operator -f values.yaml -f values-hardened.yaml +``` + +- Merge any existing `extraVolumes` and `extraVolumeMounts` into the override before applying it: Helm + replaces lists rather than appending to them. Preserve existing authentication and cloud storage mounts. +- The cluster must support ServiceAccount token projection and provide the `kube-root-ca.crt` ConfigMap + in the release namespace. With no explicit audience, the token uses the API server's default audience. + The kubelet rotates the token, and client-go refreshes credentials from the mounted file. Do not use a + `subPath` mount for the token because it would prevent projected updates from reaching the container. +- Keep `/tmp` writable by the container's runtime UID: inline OAuth2 credentials and PulsarPackage + downloads create temporary files there. Verify these operations, not just operator startup, when + enabling a read-only root filesystem. Size temporary storage according to package sizes and concurrency. +- This example does not pin a UID or GID, so the image or cluster can supply the appropriate non-root + identity. Validate the actual image and any additional security requirements in your environment. +- Explicit token projection does not remove API credentials or reduce the ServiceAccount's RBAC + permissions. Check the rendered ServiceAccount and Deployment, the admitted Pod, and your policy reports. + A policy forbidding all API tokens still requires an approved exception or another authentication method. +- Before rollout, verify leader election, resource reconciliation, token rotation, and the OAuth2 and + package operations you use in a test environment. Rendering this example is not a runtime or policy check. + {{ template "chart.valuesSection" . }} diff --git a/charts/pulsar-resources-operator/templates/serviceaccount.yaml b/charts/pulsar-resources-operator/templates/serviceaccount.yaml index 2d6cb732..ac3ca8c4 100644 --- a/charts/pulsar-resources-operator/templates/serviceaccount.yaml +++ b/charts/pulsar-resources-operator/templates/serviceaccount.yaml @@ -28,4 +28,7 @@ metadata: {{- with .Values.serviceAccount.annotations }} {{- toYaml . | nindent 4 }} {{- end }} +{{- if kindIs "bool" .Values.serviceAccount.automountServiceAccountToken }} +automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }} +{{- end }} {{- end }} diff --git a/charts/pulsar-resources-operator/tests/deployment_test.yaml b/charts/pulsar-resources-operator/tests/deployment_test.yaml index e2100845..bb0ff4c8 100644 --- a/charts/pulsar-resources-operator/tests/deployment_test.yaml +++ b/charts/pulsar-resources-operator/tests/deployment_test.yaml @@ -40,9 +40,17 @@ tests: - isNull: path: spec.template.spec.containers[0].resources.limits - isNull: - path: spec.template.spec.containers[1].volumeMounts + path: spec.template.spec.containers[0].volumeMounts - isNull: path: spec.template.spec.volumes + - notExists: + path: spec.template.spec.automountServiceAccountToken + - equal: + path: spec.template.spec.securityContext + value: {} + - equal: + path: spec.template.spec.containers[0].securityContext + value: {} - it: should update the image version successfully set: @@ -119,3 +127,86 @@ tests: - name: connection-credentials mountPath: /var/secrets/connection readOnly: true + + - it: should support explicit API credentials and writable tmp with a read-only root filesystem + set: + serviceAccount.automountServiceAccountToken: false + serviceAccount.name: operator + securityContext: + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + runAsNonRoot: true + capabilities: + drop: + - ALL + extraVolumes: + - name: kube-api-access + projected: + sources: + - serviceAccountToken: + path: token + expirationSeconds: 3600 + - configMap: + name: kube-root-ca.crt + items: + - key: ca.crt + path: ca.crt + - downwardAPI: + items: + - path: namespace + fieldRef: + apiVersion: v1 + fieldPath: metadata.namespace + - name: tmp + emptyDir: {} + extraVolumeMounts: + - name: kube-api-access + mountPath: /var/run/secrets/kubernetes.io/serviceaccount + readOnly: true + - name: tmp + mountPath: /tmp + asserts: + - equal: + path: spec.template.spec.serviceAccountName + value: operator + - notExists: + path: spec.template.spec.automountServiceAccountToken + - equal: + path: spec.template.spec.containers[0].securityContext + value: + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + runAsNonRoot: true + capabilities: + drop: + - ALL + - equal: + path: spec.template.spec.volumes + value: + - name: kube-api-access + projected: + sources: + - serviceAccountToken: + path: token + expirationSeconds: 3600 + - configMap: + name: kube-root-ca.crt + items: + - key: ca.crt + path: ca.crt + - downwardAPI: + items: + - path: namespace + fieldRef: + apiVersion: v1 + fieldPath: metadata.namespace + - name: tmp + emptyDir: {} + - equal: + path: spec.template.spec.containers[0].volumeMounts + value: + - name: kube-api-access + mountPath: /var/run/secrets/kubernetes.io/serviceaccount + readOnly: true + - name: tmp + mountPath: /tmp diff --git a/charts/pulsar-resources-operator/tests/serviceaccount_test.yaml b/charts/pulsar-resources-operator/tests/serviceaccount_test.yaml index faf3ae76..82b01307 100644 --- a/charts/pulsar-resources-operator/tests/serviceaccount_test.yaml +++ b/charts/pulsar-resources-operator/tests/serviceaccount_test.yaml @@ -26,6 +26,50 @@ tests: - matchRegex: path: metadata.name pattern: -pulsar-resources-operator$ + - notExists: + path: automountServiceAccountToken + + - it: should omit token automount when explicitly null + set: + serviceAccount.automountServiceAccountToken: null + asserts: + - notExists: + path: automountServiceAccountToken + + - it: should enable token automount when explicitly true + set: + serviceAccount.automountServiceAccountToken: true + asserts: + - equal: + path: automountServiceAccountToken + value: true + + - it: should disable token automount without changing service account metadata + set: + serviceAccount.automountServiceAccountToken: false + serviceAccount.name: operator + serviceAccount.annotations: + example.com/identity: operator + asserts: + - equal: + path: automountServiceAccountToken + value: false + - equal: + path: metadata.name + value: operator + - equal: + path: metadata.annotations + value: + example.com/identity: operator + + - it: should not create an externally managed service account + set: + serviceAccount.create: false + serviceAccount.name: external-operator + serviceAccount.automountServiceAccountToken: false + asserts: + - hasDocuments: + count: 0 - it: should be the specified name set: diff --git a/charts/pulsar-resources-operator/values.schema.json b/charts/pulsar-resources-operator/values.schema.json index c623e94b..1eb7b6e3 100644 --- a/charts/pulsar-resources-operator/values.schema.json +++ b/charts/pulsar-resources-operator/values.schema.json @@ -199,6 +199,9 @@ "annotations": { "type": "object" }, + "automountServiceAccountToken": { + "type": ["boolean", "null"] + }, "create": { "type": "boolean" }, diff --git a/charts/pulsar-resources-operator/values.yaml b/charts/pulsar-resources-operator/values.yaml index 6b465b87..6a2ea85c 100644 --- a/charts/pulsar-resources-operator/values.yaml +++ b/charts/pulsar-resources-operator/values.yaml @@ -60,6 +60,8 @@ nameOverride: "" fullnameOverride: "" serviceAccount: + # -- (bool/null) Optional token automount setting for the chart-created ServiceAccount. Null preserves Kubernetes' default behavior. When false, explicitly mount Kubernetes API credentials; see the hardened configuration example in the README. + automountServiceAccountToken: null # -- Specifies whether a service account should be created create: true # -- Annotations to add to the service account