From 3c8af6dec5906a2ef802c027cc6abc23dd4bf951 Mon Sep 17 00:00:00 2001 From: AvivGuiser Date: Fri, 14 Nov 2025 17:41:39 +0200 Subject: [PATCH] set auto-mount service account token to false in keycloak pods (#40605) closes #38843 Signed-off-by: AvivGuiser Co-authored-by: Steven Hawkins --- .../operator/advanced-configuration.adoc | 2 + .../KeycloakDeploymentDependentResource.java | 17 ++- .../v2alpha1/deployment/KeycloakSpec.java | 11 ++ .../integration/KeycloakDeploymentTest.java | 11 +- .../testsuite/unit/CRSerializationTest.java | 8 +- ...lization-keycloak-cr-without-automount.yml | 143 ++++++++++++++++++ 6 files changed, 184 insertions(+), 8 deletions(-) create mode 100644 operator/src/test/resources/test-serialization-keycloak-cr-without-automount.yml diff --git a/docs/guides/operator/advanced-configuration.adoc b/docs/guides/operator/advanced-configuration.adoc index 5f228fc3b2d..f09ebf4294a 100644 --- a/docs/guides/operator/advanced-configuration.adoc +++ b/docs/guides/operator/advanced-configuration.adoc @@ -377,6 +377,8 @@ stringData: When running on a Kubernetes or OpenShift environment well-known locations of trusted certificates are included automatically. This includes `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt` and the `/var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt` when present. +In order to not include `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt` in Keycloak pods, set the `automountServiceAccountToken` field in the spec to `false` +This is useful if some security policies require that the service account token is not mounted in the pod, but it cannot be `false` if you plan to use an external infinispan cluster, or if plan to use the Kubernetes service accounts identity provider, or if you have some custom provider logic which expects to implicitly use the Kubernetes API. === Admin Bootstrapping diff --git a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeploymentDependentResource.java b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeploymentDependentResource.java index f11d9df6ffa..c873fd20c01 100644 --- a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeploymentDependentResource.java +++ b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeploymentDependentResource.java @@ -327,6 +327,8 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent if (!specBuilder.hasDnsPolicy()) { specBuilder.withDnsPolicy("ClusterFirst"); } + boolean automount = keycloakCR.getSpec().getAutomountServiceAccountToken(); + specBuilder.withAutomountServiceAccountToken(automount); handleScheduling(keycloakCR, schedulingLabels, specBuilder); // there isn't currently an editOrNewFirstContainer, so we need to do this manually @@ -463,15 +465,18 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent LinkedHashMap varMap = Stream.concat(Stream.concat(unsupportedEnv.stream(), firstClasssEnvVars.stream()), Stream.concat(additionalEnvVars.stream(), env)) .collect(Collectors.toMap(EnvVar::getName, Function.identity(), (e1, e2) -> e1, LinkedHashMap::new)); - String truststores = SERVICE_ACCOUNT_DIR + "ca.crt"; - if (useServiceCaCrt) { - truststores += "," + SERVICE_CA_CRT; + if (!Boolean.FALSE.equals(keycloakCR.getSpec().getAutomountServiceAccountToken())) { + String truststores = SERVICE_ACCOUNT_DIR + "ca.crt"; + + if (useServiceCaCrt) { + truststores += "," + SERVICE_CA_CRT; + } + + // include the kube CA if the user is not controlling KC_TRUSTSTORE_PATHS via the unsupported or the additional + varMap.putIfAbsent(KC_TRUSTSTORE_PATHS, new EnvVarBuilder().withName(KC_TRUSTSTORE_PATHS).withValue(truststores).build()); } - // include the kube CA if the user is not controlling KC_TRUSTSTORE_PATHS via the unsupported or the additional - varMap.putIfAbsent(KC_TRUSTSTORE_PATHS, new EnvVarBuilder().withName(KC_TRUSTSTORE_PATHS).withValue(truststores).build()); - setTracingEnvVars(keycloakCR, varMap); var envVars = new ArrayList<>(varMap.values()); diff --git a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakSpec.java b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakSpec.java index b0c03aed205..0fd352304cb 100644 --- a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakSpec.java +++ b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakSpec.java @@ -162,6 +162,10 @@ public class KeycloakSpec { @JsonPropertyDescription("Configuration related to the generated ServiceMonitor") private ServiceMonitorSpec serviceMonitorSpec; + @JsonProperty("automountServiceAccountToken") + @JsonPropertyDescription("Set this to to false to disable automounting the default ServiceAccount Token and Service CA. This is enabled by default.") + private boolean automountServiceAccountToken = true; + public HttpSpec getHttpSpec() { return httpSpec; } @@ -386,4 +390,11 @@ public class KeycloakSpec { public void setServiceMonitorSpec(ServiceMonitorSpec serviceMonitorSpec) { this.serviceMonitorSpec = serviceMonitorSpec; } + + public boolean getAutomountServiceAccountToken() { + return automountServiceAccountToken; + } + public void setAutomountServiceAccountToken(boolean automountServiceAccountToken) { + this.automountServiceAccountToken = automountServiceAccountToken; + } } diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakDeploymentTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakDeploymentTest.java index 02156dab865..1df79932764 100644 --- a/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakDeploymentTest.java +++ b/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakDeploymentTest.java @@ -747,7 +747,16 @@ public class KeycloakDeploymentTest extends BaseOperatorTest { assertThat(limits).isNotNull(); assertThat(limits.get("memory")).isEqualTo(config.keycloak().resources().limits().memory()); } - + @Test + public void testNoAutoMountServiceAccount() { + var kc = getTestKeycloakDeployment(true); + kc.getSpec().setAutomountServiceAccountToken(Boolean.FALSE); + deployKeycloak(k8sclient, kc, true); + var pods = k8sclient.pods().inNamespace(namespace).withLabels(Constants.DEFAULT_LABELS).list().getItems(); + assertThat(pods).isNotNull(); + assertThat(pods).isNotEmpty(); + assertThat(pods.get(0).getSpec().getAutomountServiceAccountToken()).isEqualTo(Boolean.FALSE); + } private void handleFakeImagePullSecretCreation(Keycloak keycloakCR, String secretDescriptorFilename) { diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/unit/CRSerializationTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/unit/CRSerializationTest.java index 5d6a03b1b81..fd401a0b2c2 100644 --- a/operator/src/test/java/org/keycloak/operator/testsuite/unit/CRSerializationTest.java +++ b/operator/src/test/java/org/keycloak/operator/testsuite/unit/CRSerializationTest.java @@ -336,5 +336,11 @@ public class CRSerializationTest { fail(); } } - + @Test + public void testNoAutoMountServiceAccountToken() { + var keycloak = Serialization.unmarshal(this.getClass().getResourceAsStream("/test-serialization-keycloak-cr-without-automount.yml"), Keycloak.class); + var keycloakSpec = keycloak.getSpec(); + assertNotNull(keycloakSpec); + assertFalse(keycloakSpec.getAutomountServiceAccountToken()); + } } diff --git a/operator/src/test/resources/test-serialization-keycloak-cr-without-automount.yml b/operator/src/test/resources/test-serialization-keycloak-cr-without-automount.yml new file mode 100644 index 00000000000..03cac06357d --- /dev/null +++ b/operator/src/test/resources/test-serialization-keycloak-cr-without-automount.yml @@ -0,0 +1,143 @@ +apiVersion: k8s.keycloak.org/v2alpha1 +kind: Keycloak +metadata: + name: test-serialization-kc +spec: + instances: 3 + image: my-image + automountServiceAccountToken: false + additionalOptions: + - name: key1 + value: value1 + - name: features + value: docker + db: + vendor: vendor + usernameSecret: + name: usernameSecret + key: usernameSecretKey + passwordSecret: + name: passwordSecret + key: passwordSecretKey + host: host + database: database + url: url + port: 123 + schema: schema + poolInitialSize: 1 + poolMinSize: 2 + poolMaxSize: 3 + ingress: + enabled: false + className: nginx + annotations: + myAnnotation: myValue + anotherAnnotation: anotherValue + readinessProbe: + periodSeconds: 50 + failureThreshold: 3 + livenessProbe: + periodSeconds: 60 + failureThreshold: 1 + startupProbe: + periodSeconds: 40 + failureThreshold: 2 + networkPolicy: + enabled: true + http: + - ipBlock: + cidr: 172.17.0.0/16 + except: + - 172.17.1.0/24 + - namespaceSelector: + matchLabels: + project: myproject + - podSelector: + matchLabels: + role: frontend + https: + - ipBlock: + cidr: 172.17.0.0/16 + except: + - 172.17.1.0/24 + - namespaceSelector: + matchLabels: + project: myproject + - podSelector: + matchLabels: + role: frontend + management: + - ipBlock: + cidr: 172.17.0.0/16 + except: + - 172.17.1.0/24 + - namespaceSelector: + matchLabels: + project: myproject + - podSelector: + matchLabels: + role: frontend + http: + httpEnabled: true + httpPort: 123 + httpsPort: 456 + tlsSecret: my-tls-secret + hostname: + hostname: my-hostname + admin: my-admin-hostname + adminUrl: https://www.my-admin-hostname.org:8448/something + strict: true + strictBackchannel: true + backchannelDynamic: true + cache: + configMapFile: + name: my-config-map + key: file.xml + features: + enabled: + - docker + - authorization + disabled: + - admin + - step-up-authentication + transaction: + xaEnabled: false + tracing: + enabled: true + endpoint: http://my-tracing:4317 + serviceName: my-best-keycloak + protocol: http/protobuf + samplerType: parentbased_traceidratio + samplerRatio: 0.01 + compression: gzip + resourceAttributes: + service.namespace: keycloak-namespace + service.name: custom-service-name + resources: + requests: + cpu: "500m" + memory: "500M" + limits: + cpu: "2" + memory: "1500M" + proxy: + headers: forwarded + truststores: + x: + secret: + name: my-secret + httpManagement: + port: 9003 + bootstrapAdmin: + user: + secret: something + service: + secret: else + update: + strategy: Auto + revision: 1 + unsupported: + podTemplate: + metadata: + labels: + my-label: "foo" \ No newline at end of file