Par accès remote, on entend surtout accès externe, par opposition à l’accès intra-VM au MBean server. Cet accès est celui utilisé par des outils comme JConsole, VisualVM ou JDK Mission Control. Il existe deux cas d’accès externes : local ou distant.
Accès local
L’accès local se fait par l'Attach API. Cette technique ne nécessite aucune configuration pour être utilisée (depuis Java 6) : on lance une application java, puis on peut y connecter notre outil (jconsole par exemple). La connexion initiale se fait par un échange entre les process, puis une socket est ouverte sur un port aléatoire pour le reste de la communication. Le dialogue se fait donc par le réseau par l’interface locale.
Il est possible de désactiver l'Attach API, et donc l’accès local, avec le flag -XX:+DisableAttachMechanism.
Il semble qu’il soit impossible de désactiver l’accès local si on active l’accès distant.
L’accès local est aussi désactivé si on désactive les données de performances avec -XX:-UsePerfData et reste désactivé même si on active l’accès distant.
Accès distant
Pour activer l’accès distant, il faut ouvrir un port d’accès, avec le flag com.sun.management.jmxremote.port=<port>.
Une fois cet accès activé, se posent des questions concernant la sécurité (authentification et SSL) et la configuration réseau.
Authentification
La solution la plus simple est de désactiver l’authentification avec com.sun.management.jmxremote.authenticate=false.
Simple pour le développement, mais risqué en production.
Pour un accès sécurisé, on a deux fichiers properties : celui des mots de passe et celui des droits d’accès.
Par défaut ces fichiers sont dans <JDK_HOME>/conf/management, ce qui n’est pas très pratique.
On changera leur localisation avec com.sun.management.jmxremote.password.file=<jmxremote.password> et com.sun.management.jmxremote.access.file=<jmxremote.access>.
Le fichier password contient des couples username password :
alexis h@ssl3r
guest s3cr3t
Les permissions doivent être limitées à l’utilisateur qui lance l’application :
~$ chmod 600 jmxremote.password
Le fichier access contient des couples username permission :
alexis readwrite
guest readonly
Pour un utilisateur en readwrite, on peut affiner les droits, en ajouter les permissions de suppression (unregister) de MBeans, et de création (create) de MBeans, limités à certains packages ou certaines classes.
alexis readwrite unregister create fr.sewatech.management.*
TLS/SSL
Là aussi, la solution la plus simple est de désactiver le TLS avec com.sun.management.jmxremote.ssl=false.
Et là aussi, ça peut être risqué en production.
Pour sécuriser les communications, il faut donc générer une pair de clé dans un keystore.
~$ keytool -genkeypair -keystore conf/jmxremote.p12 -storepass javapwd \
-alias jmxremote -keypass javapwd -dname "cn=www.sewatech.fr"
Puis on utilise ce keystore au démarrage de l’application, avec les propriétés javax.net.ssl.keyStore et javax.net.ssl.keyStorePassword.
~$ java -Djavax.net.ssl.keyStore=conf/jmxremote.p12 -Djavax.net.ssl.keyStorePassword=javapwd ...
Du coté du client JMX, on exporte la clé publique, auto-signée, qu’on importe dans un truststore.
~$ keytool -certreq -keystore conf/jmxremote.p12 -storepass javapwd \
-alias jmxremote -keypass javapwd -file temp/jmxremote.cer
~$ keytool -gencert -keystore conf/jmxremote.p12 -storepass javapwd \
-alias jmxremote -keypass javapwd -infile temp/jmxremote.cer \
-outfile temp/jmxremote.cert
~$ keytool -importcert -keystore temp/jmxremote.p12 -storepass jmxkey \
-alias jconsole -keypass jmxkey -file temp/jmxremote.cert
Puis pour lancer le client JMX, il faut les propriétés javax.net.ssl.trustStore et javax.net.ssl.trustStorePassword :
~$ jconsole -J-Djavax.net.ssl.trustStore=temp/jmxremote.cert \
-J-Djavax.net.ssl.trustStorePassword=jmxkey
Ports
Lorsqu’on démarre une application avec un accès JMX distant, trois ports sont ouverts :
-
le port registry, qu’on a spécifié avec
-Dcom.sun.management.jmxremote.port, -
le port server, qui est choisi aléatoirement et qui sert à la communication en accès distant,
-
le port attach, qui sert à la communication en accès local.
Ce port server aléatoire peut poser des problèmes s’il faut passer par un firewall.
Pour contourner ce problème, il est possible de fixer la valeur de port avec com.sun.management.jmxremote.rmi.port=<port>.
On peut même utilser la même valeur que pour le port registry.
-Dcom.sun.management.jmxremote.port=9999
-Dcom.sun.management.jmxremote.rmi.port=9999
Le port attach est aussi aléatoire, mais ça ne pose pas de problème puisqu’il ne sert qu’en accès local.
Adresse IP
Par défaut, les trois ports sont en binding sur toutes les interfaces réseau.
~$ ss -pln | grep <pid>
tcp LISTEN 0 50 :9999 *: users:(("java",pid=131652,fd=13))
tcp LISTEN 0 50 :9998 *: users:(("java",pid=131652,fd=11))
tcp LISTEN 0 50 :38613 *: users:(("java",pid=131652,fd=15))
Avec -Dcom.sun.management.jmxremote.host=localhost, les ports registry et server passent en binding sur l’interface locale.
~$ ss -pln | grep <pid>
tcp LISTEN 0 50 [::ffff:127.0.0.1]:9999 : users:(("java",pid=131652,fd=13))
tcp LISTEN 0 50 [::ffff:127.0.0.1]:9998 : users:(("java",pid=131652,fd=11))
tcp LISTEN 0 50 :38613 *: users:(("java",pid=131652,fd=15))
Host name
C’est une bizarrerie du protocole, mais la connexion avec le port de registry et la communication via le port server ne se font pas forcément avec la même adresse IP et le même hostname.
La connexion se fait sur l’adresse demandée par le client puis le serveur envoie son hostname et son port server au client pour établir la connxion de communication.
Le hostname envoyé est le même que celui rendu par la commande hostname.
Ça peut poser problème losque cette commande renvoie un nom privé, connu uniquement de la machine elle-même, alors que les clients se connectent avec un hostname public.
Pour contourner ce problème, on peut forcer l’adresse qui sera utilisée pour la communication avec java.rmi.server.hostname=<public-hostname>.
WildFly
Concernant les vieux JBoss, le sujet est déjà traité dans la page dédiée au monitoring JMX dans JBoss 4 ou 5. A partir de JBoss AS 7, ou de JBoss EAP 6, le choses ont radicalement changé. Ces changements sont toujours valables dans WildFly.
Le changement est radical, puisqu’on n’utilise plus rien de l’accès JMX remote. A la place des ports cités ci-dessus, on réutilise le port d’administration (9990), avec un protocole maison. D’un point de vue réseau, ça simplifie les choses, mais ça impose des adaptations du coté du client JMX.
Tout d’abord, le client doit enrichir son classpath avec des fichiers jar supplémentaires.
Cette opération est faite par le script bin/jconsole.sh, qui peut être adapté pour un autre client.
~$ jconsole -J-Djava.class.path=$JAVA_HOME/lib/jconsole.jar:$JAVA_HOME/lib/tools.jar;$JBOSS_HOME/bin/client/jboss-cli-client.jar
ou
~$ visualvm -cp:a $JBOSS_HOME/bin/client/jboss-cli-client.jar
Ensuite, comme le protocole de communication n’est pas du RMI standard, l’adresse de connexion est plus compliquée :
-
service:jmx:remote+http://myserver:9990
L’authentification est la même que pour l’accès par jboss-cli ou par la console web.
Synthèse
Avec toutes les propriétés qu’on vient de voir, la ligne de commande pour démarrer une application peut être longue, avec beaucoup de -D. Par exemple, pour démarrer Tomcat avec JMX en mode développement, je mets ça dans mon fichier setenv.sh :
# Network
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.port=9999"
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.rmi.port=9999"
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.host=localhost"
# TLS
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.ssl=false"
# Authentication
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.authenticate=false"
Et pour démarrer Tomcat avec JMX en mode sécurisé, mon fichier bin/setenv.sh devient :
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.config.file=conf/jmxremote.properties"
# Cette propriété n'est pas dans la famille com.sun.management, elle ne va donc pas dans le fichier
JAVA_OPTS="$JAVA_OPTS -Djava.rmi.server.hostname=vaurion.jtips.info"
J’ai extrait la configuration spécifique à jmxremote dans un fichier jmxremote.properties.
# conf/jmxremote.properties
# Réseau
com.sun.management.jmxremote.port=9999
com.sun.management.jmxremote.rmi.port=9998
# TLS
com.sun.management.jmxremote.ssl.config.file=conf/jmxremote-tls.properties
# Authentication
com.sun.management.jmxremote.password.file=conf/jmxremote-pwd.properties
com.sun.management.jmxremote.access.file=conf/jmxremote-access.properties
# conf/jmxremote-tls.properties
javax.net.ssl.keyStore=conf/jmxremote.p12
javax.net.ssl.keyStorePassword=javapwd
