set auto-mount service account token to false in keycloak pods (#40605)

closes #38843

Signed-off-by: AvivGuiser <avivguiser@gmail.com>
Co-authored-by: Steven Hawkins <shawkins@redhat.com>
This commit is contained in:
AvivGuiser 2025-11-14 17:41:39 +02:00 committed by GitHub
parent 5ad1b1efa4
commit 3c8af6dec5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 184 additions and 8 deletions

View File

@ -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

View File

@ -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<String, EnvVar> 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());

View File

@ -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;
}
}

View File

@ -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) {

View File

@ -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());
}
}

View File

@ -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"