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