IPBan pour Windows Serveur.

Même s'il est préférable (et je vous le recommande) de ne pas exposer directement des serveurs sur internet, l'idéal est faire ça avec un reverse proxy, on n'a parfois pas le choix. Le confinement l'a démontré ou en urgence pas mal d'entreprises n'ont eu d'autres choix que d'exposer à la va-vite des serveurs RDP sur Internet à des fin de télétravail. La moindre des chose serait d'utiliser un VPN ou une passerelle RDP comme Guacamole, mais si ce n'est pas possible il faut sécuriser au maximum, RDP, pour ne citer que lui étant très vulnérable.

Je ne vais pas parler de ce qui est à faire sous Linux mais pour les serveurs Windows.

On a deux étapes :

  • Le firewall intégré
  • La protection des applications

Le firewall

Contrairement à une légende urbaine, le firewall intégré à Windows est solide, à condition toutefois de le configurer correctement et de n'exposer que ce qui est réellement utile.

Il y a 3 catégorie auxquelles on affecte les règles : Public, Private et Domain (équivalent Private si on est connecté à un domaine Active Directory).

Si on expose un Windows sur Internet la première chose à considérer est que l'interface exposée doit se trouver en mode Public :

PS C:\Users\Administrator> Get-NetConnectionProfile

Name             : Network 5
InterfaceAlias   : WAN
InterfaceIndex   : 3
NetworkCategory  : Private
IPv4Connectivity : Internet
IPv6Connectivity : Internet

Si ce n'est pas le cas, comme dans cet exemple on ajuste avec :

PS C:\> Set-NetConnectionProfile -InterfaceIndex 3 -NetworkCategory Public

Cette considération étant prise en compte, de base très peu de choses sont exposées en Public et on pourra ouvrir un port pour laisser passer un service. Et idéalement s'il s'agit de RDP on va restreindre cette exposition aux IP des clients... Idéalement, car dans la pratique peu de client disposent d'IP fixes et certains services doivent êtres accessibles de façon universelle.

L'interface du firewall de Windows datant du siècle dernier on pourra avantageusement se servir de eu se servir de WFC (récemment acquis par Malwarebytes) afin de le gérer plus facilement.

A ce stade on teste avec avec un scan externe afin de s'assurer que seul le ports utiles sont ouverts (Advanced port Scanner ou NMap par exemple)

Mails il va donc falloir renforcer cette sécurité par d'autres moyens !

Sous Windows il existe une seconde couche de défense moins connue : WFP (Windows Filtering Platform). Si WFP peut être très efficace, sa configuration est délicate et se passe en PowerShell, ce qui la rend peu accessible. Et c'est ici que va rentrer en action IPBan qui un peu à la manière de Fail2ban disponible sous Linux va nous permettre d'ajouter une ligne de défense supplémentaire.

IPBan

D'abord une petite précision, il existe deux versions d'IPBan :

  • Une version gratuite sans interface qui travaille uniquement avec un fichier de configuration XML qui donne mal au crane et surtout qui s'appuie uniquement sur le firewall de Windows.
  • Une version pro et payante qui dispose d'une interface et s'appuie sur WFP pour plus de réactivité. Cette interface peut être locale ou centralisée afin de gérer plusieurs serveurs.

C'est la version Pro à laquelle on va s'intéresser ici. Son cout annuel n'est pas prohibitif ($ 29.00 pour un serveur et $ 99.00 pour tous vos serveurs) si on le compare à d'autres produits qui en font moins pour bien plus cher. Cerise sur la gâteau, le support par mail ou sur Discord est vraiment très réactif !

La fonction de base d'IPBan est d'interdire l'accès à une IP après x tentatives infructueuses de connexion. 

De base il va surveiller les applications suivantes grâce aux Events de Windows, mais égaiement aux logs des applications :

  • OpenSSH
  • Microsoft Exchange
  • SmarterMail
  • MailEnable
  • Apache Tomcat
  • RDP
  • Microsoft SQL
  • MySQL
  • Postgre SQL
  • PhpMyAdmin
  • VNC
  • RRAS
  • SVN

Mais ce n'est pas limitatif et l'administrateur peut également ajouter ses propres surveillances en lui demandant d'aller scanner events et logs. Le GitHub de l'éditeur héberge d'ailleurs un certain nombre d'intégrations personnalisée qui vous faciliteront la tache, en voici un exemple pour surveiller le serveur FTP de Filezilla :

<LogFile>
	<Source>Filezilla</Source>
	<PathAndMask>C:\Program Files (x86)\FileZilla Server\Logs</PathAndMask>
	<FailedLoginRegex>
		<![CDATA[
			(?<timestamp>[^\s]+)\s.*\s\[FTP\sSession\s[0-9]+\s(?<ipaddress>[^\]]+)\]\sUSER\s(?<username>[^\n]+)\n.*\sspecify\sthe\spassword[^\n]*\n.*\sPASS\s[^\n]*\n.*\s(?<log>Login\s(?:or\spassword\s)?incorrect)[^\n]*
		]]>
	</FailedLoginRegex>
	<FailedLoginRegexTimestampFormat></FailedLoginRegexTimestampFormat>
	<SuccessfulLoginRegex>
		<![CDATA[
			(?<timestamp>[^\s]+)\s.*\s\[FTP\sSession\s[0-9]+\s(?<ipaddress>[^\]]+)\]\sPASS\s[^\n]*\n.*\[FTP\sSession\s[0-9]+\s[^\s]+\s(?<username>[^\]]+)\].*Login\ssuccessful[^\n]*
		]]>
	</SuccessfulLoginRegex>
	<SuccessfulLoginRegexTimestampFormat></SuccessfulLoginRegexTimestampFormat>
	<PlatformRegex>Windows</PlatformRegex>
	<PingInterval>10000</PingInterval>
	<MaxFileSize>0</MaxFileSize>
	<FailedLoginThreshold>0</FailedLoginThreshold>
</LogFile>

Au delà de ce blocage d'IP via WFP, il est également possible de :

  • Visualiser facilement les connections valides
  • Gérer des listes blanches et noires
  • Vous notifier
  • Bloquer ou autoriser les connections en provenance de certains pays (GeoIP)
  • Bloquer certains ASN
  • Bloquer des listes d'IP de réputation douteuse en s'abonant à des listes partagées.
  • Se synchroniser avec le reverse proxy de Cloudflare
  • ...

En conclusion c'est l'outil indispensable pour qui doit exposer un serveur directement sur Internet !

IPv6 @ Scaleway...

Depuis plus de 20 ans tout le monde parle d'IPv6, mais dans la pratique ce n'est pas encore l'euphorie au niveau de l'adoption. En France, une fois n'es pas coutume, on est un peu en avance, Free en tête qui a passé tout son réseau fibre en IPv6. En fait du 4rd pleinement documenté dans le RFC 7600. Dans la pratique c'est du tunneling (donc 2 clients qui discutent en IPv4 le font par un tunnel direct au dessus de leur IPv6, ca ne passe par une passerelle).  Par contre à ce que j'ai pu lire la sortie IPv4 vers le reste du monde ce fait via des 'Border Relay 4rd' ou du NAT64+. Et là si ces équipement ne sont pas suffisamment dimensionnés ça peut créer un goulot d'étranglement.

Il y a quelques temps je me suis aperçu que l'un de nos serveurs hébergé chez Scaleway présentait en soirée une importance latence, voire des lags pour les clients Free, ce qui est un comble car Free et Scaleway font partie de la même boutique !

Il semble donc qu'il y ait un entonnoir au niveau de la passerelle de sortie vers l'IPv6. Je ne connais pas les détails de l'infrastructure Free, mais je me suis dit qu'en rendant mon serveur accessible en IPv6 je pourrais contourner le problème, et que de toutes façons il était temps que je passe ma part d'IPv6 !

Sauf que ce n'est pas si simple, d'une par je n'ai jamais pratiqué, et d'autre par la documentation fournie par Scaleway est obsolète.

Chez Scaleway j'ai deux types d'approche, des serveurs loués (Dédibox) et une infra avec les propres serveurs installés dans une baie louée (Dédirack). Mon serveur étant une VM sous Windows 2019 sur un serveur Dédibox je me suis basé sur la documentation officielle qui fait référence à la fourniture d'un DUID (sur la console Online) après avoir demandé un bloc IPv6 et passé le serveur en SLAAC. Au vu des captures d'écrans cette doc doit dater de l'époque Windows NT (siècle dernier), j'espère qu'ils vont  la mettre à jour, aussi je mets les copies d'écran de la version actuelle ici : 1 | 2 .

Cette documentation mets en avant l'utilisation du DUID en modifiant une clé de registre de Windows. Hors changer le DUID dans l'implémentation Windows est devenu impossible comme l’expliquent ces deux articles parmi tant d’autres 1 | 2.

Le DUID change maintenant à chaque reboot en se basant sur l'adresse MAC de l’interface. C'est maintenant le comportement normal et attendu. Cette doc n'a donc plus lieu d'être dans sa version actuelle !

En fait pour obtenir une IPv6 sous Windows il n'y a rien de plus à faire que d'activer le stack IPv6 de Windows par défaut (client DHCP) (après avoir demandé un bloc IPv6 /48). Et c'est tout, l'obtention de l'IPv6 se fait via le serveur DHCPv6 de l'infrastructure Scaleway (il est également possible de renseigner une adresse prise dans le bloc).

Alors, pourquoi toute cette galère depuis des mois ?

Je me suis obstiné sur l'utilisation du DUID que la documentation mets en avant ! Normal j'ai appris à lire. Et d’autres sites y font référence. Ensuite il se trouve que cette machine avait un firewall plutôt blindé, et comme elle n'utilisait pas d'IPv6 auparavant tous les entrées Core Networking IPv6 étaient désactivées, donc les échanges avec le DHCPv6 et RA ne pouvaient pas se faire correctement. Je les ai réactivés dès lors que j’ai compris que la première option était obsolète.

A aucun moment le support Scaleway n'a su me dire qu'il ne fallait pas poursuivre dans la voie du DUID, ce qui aurait évité une perte de temps de toutes parts ! Le ticket de support n’est surement pas remonté auprès des bonnes personne et la culture Windows pas très présente, et meme si je les comprends il est impossible aujourd'hui d'ignorer le monde Microsoft !

Par contre de base Windows génère une IPv6 aléatoire, ce qui peut être utile sur un client mais gênante sur un serveur, on désactive en PS:

  Set-NetIPv6Protocol -RandomizeIdentifiers Disabled

Ou :

  netsh interface ipv6 set global randomizeidentifiers=disabled store=active
  netsh interface ipv6 set global randomizeidentifiers=disabled store=persistent
  netsh interface ipv6 set privacy state=disabled store=active
  netsh interface ipv6 set privacy state=disabled store=persistent

On peut également désactiver l'utilisation d'une IPv6 temporaire :

  Set-NetIPv6Protocol -UseTemporaryAddresses Disabled

Voilà, en espérant que Scaleway clarifie son article car si le passage de l'utilisation d'un DUID à du full DHCPv6 facilite largement la tâche, encore faut-il l'expliquer ! Je n'ai pas testé sous Linux, mais sous pfsense ou OPNsense la procédure est relativement identique.

Et dans un Dédirack ?

On aurait pu penser au vu de la console Online qui affiche sur une même pages les infos réseau de mon Dédirack (IPv4) et de mon bloc IPv6 /48 que ce bloc soit également utilisable dans la baie Dédirack. Mais non, après avoir lourdement insisté auprès du support j'ai enfin eu la réponse :

Pour faire de l'IPv6 dans un Dédirack (offre housing de Scaleway), il faut demander au support un bloc /48 et ensuite le gérer en statique à sa convenance. Ca ne s'invente pas.

Et ça fonctionne ? Et bien non, car il aura fallu plusieurs jours et plusieurs personnes pour que l'on s'aperçoive que quelqu'un ne sais pas faire de copié/collé chez eux (ou un farceur ?), et que dans le bloc /48 que l'on m'a fourni il y avait un C00D qu'il fallait lire C00C !

Et sur l'offre Elastic Metal ?

Je n'ai pas encore testé, mais j'y ai installé un serveur ESXi et je crois savoir que la procédure est encore différente.

Sources et liens

 

Home Assistant & Versatile thermostat

J'ai souvent parlé ici de thermostats et de la planification de ceux ci. Si le modèle basic (climate:) a été amélioré au fil du temps, il manquait hélas toujours quelque chose, ce que le nouveau Versatile Thermostat vient combler. Je vais donc m'intéresser à celui ci afin de gérer mon climatiseur. Les intégrations de climatiseurs se calquent en général sur leur thermostat intégré, et de facto de la sonde qui y est intégré. Celle ci placée à l'intérieur de celui ci n'apporte pas la justesse d'une sonde externe qui elle sera placée à un emplacement plus idoine. On peut également imaginer une sonde virtuelle calculant la moyenne de plusieurs sondes physiques. C'est la principale utilité que je trouve à ce nouveau thermostat qui dans mon cas d'usage va commander le thermostat d'origine de mon climatiseur, mais VT peut également fonctionner de façon plus classique en commandant le switch ou le fil pilote d'un classique convecteur ou une vanne thermostatique. Et en plus c'est made in France !

Les plus du Versatile Thermostat :

  • La gestion d'une sonde externe (mais également de la température extérieure qui semble prise en compte dans son algorithme).
  • La gestion des ouvertures (encore que dans le cas d'un climatiseur je n'apprécie pas trop qu'il le passe à OFF plutôt qu'en ECO ou HG. Une évolution est en cours sur ce point).
  • La gestion de la présence et de l'occupation. Dans mon approche je gère déjà ça autrement avec proximity: et je ne vais pas m'en servir pour l'instant. Mais à terme certainement, ce qui me permettra de supprimer du code.
  • Le mode TPI (Time Proportional Interval). TPI est une fonction d'algorithme qui garantit que le dispositif s'allume le moins longtemps possible afin d'atteindre et de maintenir le bon au niveau de confort sélectionné en fonction de la sonde (très utile avec un climatiseur ou si on se contente de lui demander 21° il travaillera en fonction de sa sonde interne). Ce mode demande à faire évoluer les réglages après quelques semaines d'utilisation en fonction des résultats obtenus. De plus les climatiseurs se gèrent en général par paliers de 1°, avec VT on pourra utiliser des consignes intermédiaires.
  • La sécurisation, qui manque cruellement aux autres thermostats et peut conduire à des situations dangereuses et couteuses : si VT ne reçoit pas d'informations de la sonde dans un délais ajustable, il passe en mode sécurité et abaisse la température, voire coupe le convecteur.
  • La gestion des préréglages. Incomplet pour mon usage, mais cependant assez logique. 
  • Une carte Lovelace, qui est le fork de celle d'un autre thermostat. Louable et utile, mais j'aurais préféré que Jean-Marc fasse un fork de celle-ci qui semble laissée à l'abandon.
  • Le délestage, inutile dans mon cas, mais qui sera un gros plus quand on a que des convecteurs.

Les préréglages (preset)

Que ce soit manuellement ou avec une planification, il y a deux façons de gérer un appareil de chauffage, à la française ou on défini des modes (ECO/CONFORT/BOOST) et l'on s'en tient à ceux ci, soit on fait varier dynamiquement la température de consigne. Personnellement je fait varier la température de consigne avec la planification dont j'ai parlé plusieurs fois ici. C'est également ce qui est utilisé dans les pays nordiques ou les variation de température sont importantes.

L'objectif étant de mettre à disposition de l'utilisateur final une interface la plus claire possible, les préréglages disponibles dans VT peuvent m'être utiles. On peu par exemple imaginer l'utilisateur qui avoir besoin de booster le chauffage, il le fera alors avec le passage en mode BOOST depuis l'interface.

Se posent alors plusieurs questions :

  • Il faut désactiver la planification dynamique qui toutes les 5 minutes va réajuster le thermostat, et donc repasser le thermostat avec les paramètres planifiés.
  • Il faut également prévoir le mode de sortie du mode BOOST (que je considère comme une dérogation) :
    • Sortie manuelle immédiate : il faut un bouton
    • Sortie lors du prochain évènement (tranche horaire planifiée, couché, sortie, etc...) ou non : il faut pouvoir donner à l'utilisateur la possibilité de choisir : il faut un bouton !

Dans ma logique j'aimerais placer ces boutons dans la carte du thermostat. Mais on peut les placer ailleurs.

Sortie manuelle d'un preset

Dans mon cas je réactive simplement l'automation qui gère la planification.

L'idéal aurait été de disposer d'un preset AUTO dont je pourrais récupérer l'état et ainsi réactiver la planification. Il n'est pas possible d'utiliser le preset NONE/MANUAL car celui ci change d'état dès lors que l'on change la consigne.

Faute de disposer d'un preset AUTO (...), le seul preset que je peux détourner est donc celui du mode FROST (hors gel) dont je n'ai pas l'utilité. Je détecte l'action via l'état du VT et je réactive l'automation qui gère la planification (dans la même automation qui me permet de désactiver la planification lorsque je choisit ECO/COMFORT/BOOST), mais également les autres modes du climatiseur (FAN/DRY, etc.).

- id: 56dd275e-3f52-4d8gjk862-5eeft5708a82
  description: Comfort - AC - Mode Auto Versatile
  alias: "Comfort - AC - Mode Auto Versatile"
  mode: restart
  trigger:
    - platform: template
      value_template: "{{ state_attr('climate.ac_versatile', 'hvac_mode') == 'off' }}"
      id: "off"
    - platform: template
      value_template: "{{ state_attr('climate.ac_versatile', 'hvac_mode') == 'dry' }}"
      id: "off"
    - platform: template
      value_template: "{{ state_attr('climate.ac_versatile', 'hvac_mode') == 'fan_only' }}"
      id: "off"
    - platform: template
      value_template: "{{ state_attr('climate.ac_versatile', 'hvac_mode') == 'heat_cool' }}"
      id: "off"
    - platform: template
      value_template: "{{ state_attr('climate.ac_versatile', 'hvac_mode') == 'heat' and state_attr('climate.ac_versatile', 'preset_mode') in ['boost', 'comfort', 'eco'] }}"
      id: "heat_boost_comfort_eco"
    - platform: template
      value_template: "{{ state_attr('climate.ac_versatile', 'hvac_mode') == 'heat' and state_attr('climate.ac_versatile', 'preset_mode') == 'frost' }}"
      id: "heat_frost"
    - platform: template
      value_template: "{{ state_attr('climate.ac_versatile', 'hvac_mode') == 'heat' and state_attr('climate.ac_versatile', 'preset_mode') == 'none' }}"
      id: "heat_none"
    - platform: template
      value_template: "{{ state_attr('climate.ac_versatile', 'hvac_mode') == 'cool' and state_attr('climate.ac_versatile', 'preset_mode') in ['boost', 'comfort', 'eco'] }}"
      id: "cool_boost_comfort_eco"
    - platform: template
      value_template: "{{ state_attr('climate.ac_versatile', 'hvac_mode') == 'cool' and state_attr('climate.ac_versatile', 'preset_mode') == 'frost' }}"
      id: "cool_frost"
    - platform: template
      value_template: "{{ state_attr('climate.ac_versatile', 'hvac_mode') == 'cool' and state_attr('climate.ac_versatile', 'preset_mode') == 'none' }}"
      id: "cool_none"
  action:
    - choose:
        - conditions: "{{ trigger.id in ['off', 'heat_boost_comfort_eco', 'cool_boost_comfort_eco'] }}" # Dérogation, on coupe le schedulle
          sequence:
            - service: automation.turn_off
              target:
                entity_id: automation.comfort_ac_immediate
              data:
                stop_actions: true                
    - choose:
        - conditions: "{{ trigger.id in ['heat_frost', 'cool_frost'] }}"
          sequence:
            - service: automation.turn_on
              target:
                entity_id: automation.comfort_ac_immediate
            - service: automation.trigger
              target:
                entity_id: automation.comfort_ac_immediate
              data:
                skip_condition: true

Sortie automatique d'un preset

Il s'agit ici de savoir si le prochain évènement (plage horaire, lever/coucher, géoloc, etc...) va repasser le thermostat dans le mode planifié ou pas.

Là je vais utiliser une automation déclenchée par mes différents triggers, automation qui sera ON ou OFF selon la volonté de l'utilisateur.

- id: 56efdfd5e-3f52-4ddd-a862-5e21f5708a82
  description: Comfort - AC - Restart
  alias: "Comfort - AC - Restart"
  mode: restart
  trigger:
    - platform: state
      entity_id:
        - binary_sensor.heating_ac_1
        - binary_sensor.heating_ac_2
        - binary_sensor.heating_ac_3
        - binary_sensor.heating_ac_4
        - binary_sensor.heating_ac_1_d
        - binary_sensor.heating_ac_2_d
        - binary_sensor.heating_ac_3_d
        - binary_sensor.heating_ac_4_d
        - input_boolean.to_away
        - binary_sensor.life_windows_and_doors_delayed
        - input_boolean.presence_ac
        - input_boolean.to_sleep # eco heat off cool
        - input_boolean.thermostats_ac_on_off
        - binary_sensor.lionel_geo # eco heat off cool
        - input_boolean.thermostats_away
        - input_select.comfort_ac
  condition:
    - condition: state
      entity_id: automation.comfort_ac_immediate
      state: "off"
    - condition: state
      entity_id: input_boolean.thermostats_ac_on_off
      state: "on"
  action:
    - service: automation.turn_on
      target:
        entity_id: automation.comfort_ac_immediate
    - service: automation.trigger
      target:
        entity_id: automation.comfort_ac_immediate
      data:
        skip_condition: true

Surveillance

S'agissant d'un "sur thermostat" il est intéressant de savoir ce qui se passe et si possible visuellement. Je vais donc créer deux sensor: / template: qui vont me permettre de suivre et comparer l'état de ces deux thermostats.

sensor:
- platform: template
  sensors:
    temp_up_th_ac: # Pour History Graph
      friendly_name: "TH AC"
      value_template: > 
        {% if is_state('climate.daikin', 'heat') %}
          Heat {{state_attr ('climate.daikin', 'temperature')}}°
        {% elif is_state('climate.daikin', 'cool') %}
          Cool {{state_attr ('climate.daikin', 'temperature')}}°
        {% else %}
          Off
        {% endif %}
    temp_up_th_ac_vt: # Pour History Graph
      friendly_name: "TH AC Versatile"
      value_template: > 
        {% if is_state('climate.ac_versatile', 'heat') %}
          Heat {{state_attr ('climate.ac_versatile', 'temperature')}}°
        {% elif is_state('climate.ac_versatile', 'cool') %}
          Cool {{state_attr ('climate.ac_versatile', 'temperature')}}°
        {% else %}
          Off
        {% endif %}

Visuellement ça me donne deux lignes, la première représente le vrai thermostat du climatiseur tandis que la seconde celui du VT. Et l'on voit bien que pour une consigne fixée ici à 22° le VT pousse parfois le climatiseur à 23°.

Quand je l'utilisait en direct je lui demandais généralement 23° pour obtenir 22° à la sonde. C'est plus que les recommandations étatiques me direz vous, mais il y a une raison ! Avoir 22° dans le hall / couloir me permet d'avoir de 19° à 21° dans les pièces adjacentes, dans lesquelles les convecteurs ne se déclenchent que très rarement, au point que cet hiver je les ai laissés tous OFF. Le seul cas ou ils sont maintenant utiles étant lorsque mes enfants sont présents et ferment les portes de leur chambre (il est bien connu que les ados s'isolent... Et encore le, PC gamer de mon fils doit lui assurer une part de chauffage...).

Economies

Ici on entre dans un domaine difficilement mesurable. En effet la consommation d'un climatiseur est fonction d'une multitude de paramètres, les horaires, le temps qu'il fait, le nombre d'occupants, etc... Il n'en reste pas moins que si je compare la consommation de mon climatiseur avant et après la mise e place du VT j'ai l'impression d'avoir réduit la conso de ± 10%. C'est subjectif et ça demande à être affiné. Par contre ce qui est certain c'est que le fait de ne quasiment plus activer les convecteurs constitue un gain énorme. Et conjugué à une ITE réalisée en 2022 et l'automatisation des volets roulants en fonction de l'ensoleillement, j'ai grandement gagné en confort et en cout. 

L'interface

Il y a longtemps que j'utilise la carte Simple Thermostat qui est très malléable et permet l'affichage d'une multitude d'informations.

Hélas cette carte ne semble plus maintenue et je n'ai pas les compétences pour reprendre le développement, Jean-Marc si tu me lis...

Conclusion

VT est un vrai plus pour le confort ! Je n'ai pas abordé ici la partie refroidissement du climatiseur et je le ferait cet été quand els conditions seront réunies.

En plus c'est made in France et vous pouvez communiquer facilement avec Jean-Marc qui en est l'auteur et se montre très disponible sur le forum HACF ou sur GitHub. Merci à lui !

Chat's & Business

Petit à petit les messageries de type chat se sont imposées en entreprise, mais également entre les entreprises. Le but étant de garder le contact, de proposer / obtenir des réponses rapides et surtout de délester le mail souvent surchargé et parfois traité qu'une seule fois par jour, un peu comme le facteur qui jadis passait le matin... Au fil de ces dernières années certaines ont bien évoluées, tandis que d'autres s'imposaient par la force. Chacun ses préférences, mais je vais essayer de faire le tour de la question.

Attention, le seule option qui m'intéresse ici dans ces applications c'est le chat !

Je vais en retenir seulement quelques une, certaines sont à réserver à la sphère privée même si on les retrouve souvent en usage professionnel :

  • Messenger Facebook (Meta) : Usage privé, à proscrire en entre pro. Idem pour celle d'Instagram !
  • WhatsApp (Meta) : Usage privé. Je déteste quand un client me contacte par ce biais là. Je le ressent comme une intrusion. Il n'en reste pas moins que si l'on dispose que d'un seul numéro de mobile c'est inévitable. Il existe également une version business orientée B2C.
  • Google Chat : Le renouveau, et mon préféré !
  • Microsoft Teams : Imposé au forceps, lourd et confus.
  • Slack : Bien, mais couteux !
  • Telegram : Usage geek (et mafieux ?). L'entreprise n'est pas la cible. Par contre Telegram est beaucoup utilisé pour des groupes de discutions thématiques, usage auquel il est adapté et ou il se pose en concurrence avec les forums.
  • Signal : A réserver à un usage ou l'on recherche un maximum de confidentialité. Probablement le meilleur sur ce point. Pas d'intégration web.
  • Olvid : Il parait qu'il faut laisser cette application à nos gouvernants, la PM l'ayant imposé. Curieux pour un produit finalement pas si Français, avec un actionnariat luxembourgeois et un hébergement chez AWS...

A noter qu'avec le RCS les SMS peuvent s'apparenter à des messageries Chat. Tout comme l'a fait Apple avec iMessage, et ça sera encore plus le cas avec l'adoption par ce dernier du protocole RCS.

Je ne vais pas toutes les traiter car de mon point de vue certains n'ont pas leur place en entreprise ou en B2B. Soit parce que je les estime destiné à un usage privé, ce qui permet de poser des limites, soit parce que trop spécifiques. Les trois dont je vais parler disposent de clients mobile et desktop, mais également d'une version web qui permet de les intégrer à un agrégateur d'applications web comme WebCalatog (que je recommande), SideKick ou encore Ferdium (il en existe d'autres). Ces utilitaires permettent de rassemble en un seul endroit tous les chat's...

Microsoft Teams

Après avoir saboté MSN Messenger et cannibalisée Skype, Microsoft a fini par imposer Teams en profitant du confinement et surtout en l'incluant gratuitement dans toutes les offres 365. La méthode est plus que discutable et les différentes autorités de régulation se sont emparées du dossier suite à une plainte de Slack. Il n'en reste pas moins que Teams est devenu omniprésent et que le ver est dans la pomme. Même si beaucoup l'utilisent que pour du chat ou des réunions en visio, Teams va beaucoup plus loin en termes de fonctionnalités, certes utiles dans une multinationale, mais au point de rapidement perdre les utilisateurs. C'est mon cas et je n'utilise que le chat pour communiquer avec des contacts dont c'est le seul canal possible.

Teams a été conçu pour de très grandes entreprises et les échanges entre des utilisateurs de tenants différents sont limités et nécessitent une approbation. Essayez donc de faire glisser un document dans une conversation avec un contact externe. Impossible en a décidé Microsoft, pas plus que vous ne verrez les photos de profil de vos contacts externes. C'est comme ça, by design, simplement parce que ce n'est pas la cible. A destination des marchés SMB, Microsoft avait lancé il y a quelques années Kaizala, une sorte de clone de WhatsApp. Vous ne connaissez pas, normal, car rapidement retirée.

Bref, vous l'aurez compris je déteste Teams, mais je suis contraint de l'utiliser.

Slack

Comment justifier une licence de 6 à 12 € par mois et par utilisateur quand l'entreprise dispose déjà de Teams gratuitement ? Slack est un produit très intéressant et bien ficelé, jadis il était utilisable dans sa formule gratuite, hélas de plus en plus restrictive (3 mois d'historique seulement), ce qui me pousse à l'abandonner. Slack n'est toutefois pas exempt de défaut, et dans bien des facettes n'est pas vraiment adapté à l'utilisation facile inter entreprise. Slack c'est SalesForce, la cible principale n'est pas non plus les petites entreprises. Adieu Slack, je t'aimais bien, avant !

Google Chat

Et puis il y a le cas du (méchant) Google. Après des années d'errances (Hangouts) et de produits mal ficelés, je dois dire que je trouve l'actuelle version de Google Chat très séduisante. Elle n'impose pas de restrictions de communication entre les utilisateurs, qu'il s'agisse d'un simple compte Google personnel et gratuit ou d'un compte Google Workspace d'entreprise. L'interopérabilité est totale et l'interface claire et simple pour le chat, mais également des espaces de travail comme le propose Microsoft, mais de façon simple, intuitive et surtout légère. On peut dire qu'ils reviennent de loin, mais hélas Teams a entre temps cueilli le marché et j'y ai peu de contacts.

Alternatives ?

On ne vas pas ressortir ICQ et tout ce qui fonctionne sur la base IRC ou Jabber, voire Trillian. En intra entreprises certains pourront opter pour ce qui est proposé par Synology ou d'autres applications en auto hébergement, mais pas adapté au B2B. Il est également possible d'opter pour Discord, au départ dédié au gamers, certains l'utilisent en mode professionnel. Tout comme Zoom qui nous rappelle le confinement !

Les suisses proposent également Threema qui semble un beau produit destiné tant aux particuliers qu'au entreprise et qui préserve la confidentialité et la vie privé. Mais personne en connait en France !

Il n'en reste pas moins que le marché ne propose rien d'universel. La CE travaille à imposer une interopérabilité entre les messageries, à l'instar de ce qui existe pour le mail ou la téléphonie. Mais je doute fort de voir des résultats rapides et pour un bout de temps on sera encore contraints d'utiliser plusieurs clients.

 

AutoDiscover & AutoConfig IMAP/SMTP

En 2020, pour m'occuper pendant le confinement, j'avais écrit un article sur la configuration automatique des clients POP/IMAP. Ca fonctionnait sur Thunderbird, mais pas sur les clients Microsoft... Bon, les clients Microsoft il y en a de plusieurs sorte (Courrier, Outlook nouveau, ancien, 365, jadis Express et j'en passe) et la chose n'est (volontairement ?) pas simple. On va oublier les versions 365 qui même si elles supportent IMAP le font mal et ne sont pas vraiment destinés à cet usage mais plus à du MAPI.

Pour cet article j'ai monté un serveur Cloudron. Ce projet gère très bien le mail dans sa version de base gratuite. Hélas il ne gère pas la configuration des clients, ce qui est regrettable car il pourrait très facilement intégrer ce qui suit afin d'être totalement dans la philosophie du projet intégré !

Il existe plusieurs façons de faire de l'AutoDiscovery ou de l'AutoConfig

La méthode RFC 6186

Elle se base sur des enregistrements DNS :

_imap._tcp 300 IN SRV 0 0 0 .
_imaps._tcp 300 IN SRV 0 1 993 mail.domain.tld.
_pop3._tcp 300 IN SRV 0 0 0 .
_pop3s._tcp 300 IN SRV 10 1 995 mail.domain.tld.
_submission._tcp 300 IN SRV 0 0 0 .
_submissions._tcp 300 IN SRV 0 1 587 mail.domain.tld.

Normalisée mais visiblement trop simple est abandonnée. Encore que certaines versions d'Outlook utiliseraient ça. Etrange.

La méthode Thunderbird 

Thunderbird va chercher les paramètres dans un fichier XML sur l'url :

http://autoconfig.domain.tld/mail/config-v1.1.xml

La méthode Outlook

Outlook cherche l’enregistrement DNS de type SRV :

_autodiscover._tcp.domain.tld 3600 IN SRV 10 10 443 autodiscover.domain.tld.

qui va lui donner l'adresse ou interroger le fichier XML, donc un CNAME qui pointe sur le serveur web qui héberger le fichier XML, ce qui au final donnera une requête sur

https://autodiscover.domain.tld/AutoDiscover/AutoDiscover.xml

Simple ! Attention à bien respecter la casse du mot AutoDiscover... Et ne pas oublier que dans le cas Microsoft on travaille en HTTPS. Dans certains cas on pourrait faire pointer ça sur n'importe quelle url, mais il semblerait que certaines versions d'Outlook (et il y en a !) cherchent une url qui commence par autodiscover...

Un site pour les rassembler

Dans la méthode qui va suivre on va utiliser un seul site capable de répondre aux url autoconfig et autodiscover, on renseignera donc ces enregistrement dans le dns :

_autodiscover._tcp.domain.tld 1800 IN SRV 10 10 443 autodiscover.domain.tld.
autoconfig 1800 IN CNAME web_server.domain.tld.
autodiscover 1080 IN CNAME web_server.domain.tld.

Ensuite on crée un site web capable de répondre à ces deux url pour l'ensemble des domaines de messagerie pour lesquels on souhaite proposer ce service. Le site doit répondre en HTTP et HTTPS, mais surtout sans redirection automatique HTTP vers HTTPS. Pour cet exemple j'ai utilisé un serveur web NGINX avec du PHP.

On va utiliser ce code que j'ai trouvé ici, je l'ai utilisé tel que en adaptant les enregistrements DNS et les redirections pour NGINX que l'on verra après le code. On copie donc ce code PHP dans autoconfig-mail.php :

<?php
 
/*
By David Mercereau
Licence Beerware
*/
 
function extract_domain($domain) {
    if(preg_match("/(?P<domain>[a-z0-9][a-z0-9\-]{1,63}\.[a-z\.]{2,6})$/i", $domain, $matches)) {
        return $matches['domain'];
    } else {
        return $domain;
    }
}
 
$domain = extract_domain($_SERVER['SERVER_NAME']);
$mailServeur='mail.'.$domain;
 
if (preg_match('/^\/mail\/config-v1\.1\.xml/', $_SERVER['REQUEST_URI'])) {
    header('Content-Type: text/xml');
    header('Content-Type: application/xml');
    ?>
<clientConfig version="1.1">
    <emailProvider id="<?= $domain ?>">
      <domain><?= $domain ?></domain>
      <displayName><?= $domain ?></displayName>
      <displayShortName><?= $domain ?></displayShortName>
      <incomingServer type="imap">
         <hostname><?= $mailServeur ?></hostname>
         <port>143</port>
         <socketType>STARTTLS</socketType>
         <username>%EMAILADDRESS%</username>
         <authentication>password-cleartext</authentication>
      </incomingServer>
      <outgoingServer type="smtp">
         <hostname><?= $mailServeur ?></hostname>
         <port>587</port>
         <socketType>STARTTLS</socketType>
         <username>%EMAILADDRESS%</username>
         <authentication>password-cleartext</authentication>
      </outgoingServer>
      <documentation url="https://webmail.<?= $domain ?>">
          <descr lang="fr">Connexion Webmail</descr>
          <descr lang="en">Webmail connexion</descr>
      </documentation>
      <documentation url="http://projet.retzo.net/projects/hebergement/wiki">
        <descr lang="fr">Documentation</descr>
        <descr lang="en">Generic settings page</descr>
      </documentation>
    </emailProvider>
</clientConfig>
    <?php
} else {
    // Outlook
    //get raw POST data so we can extract the email address
    $data = file_get_contents("php://input");
    preg_match("/\<EMailAddress\>(.*?)\<\/EMailAddress\>/", $data, $matches);
 
    //set Content-Type
    header('Content-Type: text/xml');
    header('Content-Type: application/xml');
    echo '<?xml version="1.0" encoding="utf-8" ?>'; 
    ?>
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006">
   <Response xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a">
       <Account>
           <AccountType>email</AccountType>
           <Action>settings</Action>
           <Protocol>
               <Type>IMAP</Type>
               <Server><?= $mailServeur ?></Server>
               <Port>993</Port>
               <DomainRequired>off</DomainRequired>
               <LoginName><?php echo $matches[1]; ?></LoginName>
               <SPA>off</SPA>
               <SSL>on</SSL>
               <AuthRequired>on</AuthRequired>
           </Protocol>
           <Protocol>
               <Type>POP3</Type>
               <Server><?= $mailServeur ?></Server>
               <Port>995</Port>
               <DomainRequired>off</DomainRequired>
               <LoginName><?php echo $matches[1]; ?></LoginName>
               <SPA>off</SPA>
               <SSL>on</SSL>
               <AuthRequired>on</AuthRequired>
           </Protocol>
           <Protocol>
               <Type>SMTP</Type>
               <Server><?= $mailServeur ?></Server>
               <Port>587</Port>
               <DomainRequired>off</DomainRequired>
               <LoginName><?php echo $matches[1]; ?></LoginName>
               <SPA>off</SPA>
               <Encryption>TLS</Encryption>
               <AuthRequired>on</AuthRequired>
               <UsePOPAuth>off</UsePOPAuth>
               <SMTPLast>off</SMTPLast>
           </Protocol>
       </Account>
   </Response>
</Autodiscover>
    <?php
}
 
?>

Ensuite dans configuration NGINX on ajoute les redirections suivantes qui va permettre de servir la bonne version XML selon que la requête utilise la méthode Outlook ou Thunderbird (attention à la casse...) :

location /AutoDiscover {
  rewrite ^/AutoDiscover/AutoDiscover.xml /autoconfig-mail.php;
}
location /mail {
  rewrite ^/mail/config-v1.1.xml /autoconfig-mail.php;
}

Sources


https://github.com/gronke/email-autodiscover
https://github.com/SpicyWeb-de/isp-mailConfig

Home Assistant, Zigbee & Legrand

On m'a demandé récemment d'intégrer des appareils Legrand dans Home Assistant, je l'avais déjà fait pour certains et j'en avais parlé dans cet article. Aujourd'hui il s'agit d'intégrer des commandes sans fil vue come des télécommandes dans HA. Vous allez me demander quel est l'intérêt quand un simple bouton Ikea (E1743) à moins de 10 fait le job pour mes volets roulants. C'est purement esthétique, quand on a un logement équipé en Legrand Céliane ou Mosaic, on veut parfois que les commandes des volets soient dans la même collection.

Soit, Legrand propose des commandes en Zigbee, il n'y a plus qu'à !

Dans la pratique on va voir que ce n'est pas si simple, en tous cas bien moins simple qu'avec mon bouton Ikea à 10 € ! Je précise le prix car les commandes Legrand sont plutôt à 80 €. Mais quand on aime on ne compte pas.

Il existe plusieurs type d'équipements Zigbee chez Legrand

  • Ceux qui sont filaires, j'en ai parlé ici (micro modules, contacteur DIN, prise connectée, etc...)
  • La gamme des commandes sans fil à pile
  • La gamme des commandes sans fil sans piles (gamme Self-e). J'y reviendrait certainement, mais d'après mes infos cette gamme supporte tous les canaux et serait mieux intégrée ZHA/Z2M.

Je me suis donc penché sur la gamme avec pile.

Première observation, ces devices sont généralement livrés avec un firmware de niveau 42 qui ne permet que l'utilisation du canal Zigbee 11. Don impossible à faire fonctionner sur mon ZHA qui est en 15. Je l'ai donc appairé sur mon Z2M en 11 et ça fonctionne. Et là je me suis dit que j'allais pouvoir mettre à jour ce firmware en OTA. Mais non, ça ne fonctionne pas et on se retrouve dans la problématique des équipements franco Français qui n'intéressent pas grand monde au niveau international (on pense par exemple à tout ce qui est lié à l'intégration Overkiz...).

Après moultes lectures des forums de diverses plateformes domotique, la conclusion est que la mise à jour du firmware ne peut se faire qu'au travers d'une passerelle officielle Legrand ! J’ai donc acheté le kit qui comprend :

  • La prise Control qui sert de passerelle
  • Un inter sans fil
  • Une ampoule

Il faut bien sur installer l’appli Legrand et ensuite tenter d’associer la prise au WIFI. Facile ? Non, ça m’a pris de plombes car vu que leur process est trop long, le mobile qui se connecte à la prise pour la configurer repasse sur le WIFI normal ou il retrouve Internet, et perd la config en cours. Solution faire ça avec un vieux mobile… Bref, un premier amateurisme car on fait facilement ce genre d'association avec la majorité des objets mobile en WIFI !

Bon, la logique de l’appli est orientée électricien très grand public, avec une logique d'électricien qui n’est pas nécessairement la notre. C'est un choix, il faut s'y faire, mais n'oublions pas que nous sommes ici uniquement pour faire un mise à jour...

Ensuite il faut ajouter les inter. Sauf que la il faut comprendre qu’on ne peut pas ajouter un inter seul. Dans la logique Legrand si tu ajoute un inter, en fin de dialogue ça te demande ce que tu veux commander, sans quoi ça bloque et il n’y a plus qu’à forcer le redémarrage de l’appli et recommencer... Je n’avais pas de prise Legrand sous la main, mais au bout d’un moment j’ai fini par penser à appairer l’ampoule. Et la je vois enfin l’inter livré et mon inter de volet roulant qui ne peut rien commander, c’est le même mais avec un firmware différent.

Mais souvenons nous que nous somme là à jouer avec ce bazar dans le seul but de mettre à jour le firmware de ces putains d’inter qui au delà de couter un rein ne fonctionnent (mal) que sur le canal 11. Hélas il n’y arien pour faire cette mise à jour, mon inter de VR apparait en 42 dans l’appli et en 002a dans z2m, donc identique l’un étant en hexa. D’après ce que j’ai pu lire, la mise à jour se fera, un jour, mais on ne peut pas la forcer. Il faut juste laisser branché, en espérant que mon inter qui n’est pas connecté à un appareil se mette à jour tout seul…

Donc je laisse branché, la suite pour bientôt…

EDIT un peu plus tard :

  • L’inter (0 677 73N) est passé de 50 à 70
  • L’inter VR (0 777 48LA) n’a pas bougé…
  • Je testerais si on peut l’appairer hors du canal 11 mais je commence à douter…

EDIT le lendemain :

  • L’inter s’appaire bien sur un autre canal après sa mise à jour. Testé en ZHA sur 15, mais ZHA ne le supporte pas et rien ne bouge, aucun event.
  • L’inter VR ne s’est toujours pas mis à jour. Un peu comme s’il lui fallait une charge...

EDIT le lendemain soir :

  • L’inter VR est bien passé de 42 à 70, mon ZHA qui est sur le canal 15 le voit mais n’en fait rien. Quand à Z2M il n’en veut plus ! Pire ça me fait planter Z2M…

EDIT le sur lendemain midi :

  • L’inter VR qui ne veut toujours pas de ZHA ou Z2M s’est appairé correctement sur deconz en canal 15, il retourne dans les event les valeurs suivantes :

Montée : 1002 / Montée puis relâché : 1002/3002
Descente : 2002 / Descente puis relâché : 2002/3003
Stop (Les deux en même temps) : 3003

Ce comportement est identique à ce qu’il était sous Z2M et contrairement à d'autres télécommandes (le on/off Ikea (E1743 par exemple) il manque le fait qu’un second appui bref provoque un stop. Il faut donc bien appuyer au milieu pour faire un « stop » et ce n’est pas toujours pris en compte (problème physique).

Qu’il ne soit pas supporté sur ZHA est un fait, il faudrait développer un quirk et ça me dépasse. Je ne sais pas pourquoi il ne veut plus s’appairer sur Z2M, mais c’est peut être lié à mon installation, ma clé, que sais-je...

Il y a des chances que l’aventure Legrand va prendre fin et je que je retourne fissa ce Kit à Amazon !

Homekit

A signaler toutefois que cette passerelle est Homekit. Ce qui veut dire que ses équipements remontent dans Home Assistant. Mais ne rêvez pas trop, si l’ampoule remonte bien, et certainement tous les actionneurs connectés (prises, modules DIN), pas les inter En fait si, il faut juste attendre un peu. Néanmoins tout ne semble pas remonter, uniquement une action par bouton ! Mais j’avais déjà remarqué ça avec les télécommandes Tuya qui ne remontent pas via une passerelle Tuya Homekit. Il y a une forme de logique, Homekit sert à commander un équipement, comme une télécommande, hors on ne commande pas une télécommande…

Par contre ça veut dire que les équipements reconnus par Legrand vont remonter dans Homekit, ce qui peut être une solution de contrôle facile pour des produit pas reconnus par HA (Profalux, Bubendorf, Aldes, etc…).

La suite

Je vous propose de poursuivre ici et que chacun y apporte ses retours.

Sources

  • https://developer.legrand.com/production-firmware-download/

Home Assistant & Mi Boxer

J'avais acheté cette télécommande Mi Boxer il y a quelques mois afin de gérer les éclairages de mon séjour, en me disant que les curseurs seraient plus pratiques que mon Opple avec ses 6 boutons actuelle. Hélas elle n'était reconnue nulle part et avait terminé sa course dans le tiroir aux oubliettes du Zigbee...

C'était sans compter sur la ténacité de quelques amateurs de reverse engineering sur lesquels je suis tombé il y a quelques semaines et qui on fait un travail formidable qui a aboutit à une extension pour Zigbee2MQTT, ce qui rend cette télécommande enfin utilisable, tout au moins partiellement pour l'instant. Mais l'essentiel est là !

Ce qui fonctionne :

  • 7 boutons avec ON et OFF (deux actions et non un toggle). Celui du bas à droite n'est pas actif et je vous déconseille de l'utiliser...
  • 1 bouton avec ON et OFF (en haut)
  • 1 bouton W (si on veut bricoler...)
  • La barre de réglage de la luminosité
  • La remontée de l'état de la batterie

Ce qui ne fonctionne pas (pour l'instant) :

  • La barre de réglage des couleurs
  • La barre de réglage de la température du blanc
  • Les touches RGB
  • Les touches de temporisation (en bas à droite)

Une fois la télécommande reconnue sous Z2M on peut commencer à créer des actions. On remarque tout de suite que l'affaire va manquer de sensor: et que certains ne fonctionnent pas. Pas de panique, il y a une solution comme je vais vous le démonter avec l'automation: qui suit. 

J'ai fait un mélange de choose: / conditions: / trigger.id . C'est un peu long, mais je la colle en entier ce qui vous évitera une fastidieuse saisie. Il faudra tout de même y coller vous id: et entity: ! Ah j'allais oublier, il faut aussi ajouter un petit input_select: ... Voir plus bas EDIT du 11/01/2024 !

J'ai tenté de faire ça avec ControllerX que j'adore et qui me sert pour toutes mes télécommandes, mais pour l'instant c'est un échec !

alias: GUI - Mi Boxer
description: ""
trigger:
  - platform: state
    entity_id:
      - sensor.mi_boxeur_brightness
    id: bright
  - platform: device
    domain: mqtt
    device_id: 86c1403c24491ce021ac3ee081a86308
    type: button_short_press
    subtype: button_group_1_on
    discovery_id: 0x003c84fffec29c71_zone_1_button_on
    id: 1on
  - platform: device
    domain: mqtt
    device_id: 86c1403c24491ce021ac3ee081a86308
    type: button_short_press
    subtype: button_group_1_off
    discovery_id: 0x003c84fffec29c71_zone_1_button_off
    id: 1off
  - platform: device
    domain: mqtt
    device_id: 86c1403c24491ce021ac3ee081a86308
    type: button_short_press
    subtype: button_group_2_on
    discovery_id: 0x003c84fffec29c71_zone_2_button_on
    id: 2on
  - platform: device
    domain: mqtt
    device_id: 86c1403c24491ce021ac3ee081a86308
    type: button_short_press
    subtype: button_group_2_off
    discovery_id: 0x003c84fffec29c71_zone_2_button_off
    id: 2off
  - platform: device
    domain: mqtt
    device_id: 86c1403c24491ce021ac3ee081a86308
    type: button_short_press
    subtype: button_group_3_on
    discovery_id: 0x003c84fffec29c71_zone_3_button_on
    id: 3on
  - platform: device
    domain: mqtt
    device_id: 86c1403c24491ce021ac3ee081a86308
    type: button_short_press
    subtype: button_group_3_off
    discovery_id: 0x003c84fffec29c71_zone_3_button_off
    id: 3off
  - platform: device
    domain: mqtt
    device_id: 86c1403c24491ce021ac3ee081a86308
    type: button_short_press
    subtype: button_group_4_on
    discovery_id: 0x003c84fffec29c71_zone_4_button_on
    id: 4on
  - platform: device
    domain: mqtt
    device_id: 86c1403c24491ce021ac3ee081a86308
    type: button_short_press
    subtype: button_group_4_off
    discovery_id: 0x003c84fffec29c71_zone_4_button_off
    id: 4off
  - platform: device
    domain: mqtt
    device_id: 86c1403c24491ce021ac3ee081a86308
    type: button_short_press
    subtype: button_group_5_on
    discovery_id: 0x003c84fffec29c71_zone_5_button_on
    id: 5on
  - platform: device
    domain: mqtt
    device_id: 86c1403c24491ce021ac3ee081a86308
    type: button_short_press
    subtype: button_group_5_off
    discovery_id: 0x003c84fffec29c71_zone_5_button_off
    id: 5off
  - platform: device
    domain: mqtt
    device_id: 86c1403c24491ce021ac3ee081a86308
    type: button_short_press
    subtype: button_group_6_on
    discovery_id: 0x003c84fffec29c71_zone_6_button_on
    id: 6on
  - platform: device
    domain: mqtt
    device_id: 86c1403c24491ce021ac3ee081a86308
    type: button_short_press
    subtype: button_group_6_off
    discovery_id: 0x003c84fffec29c71_zone_6_button_off
    id: 6off
  - platform: device
    domain: mqtt
    device_id: 86c1403c24491ce021ac3ee081a86308
    type: button_short_press
    subtype: button_group_7_on
    discovery_id: 0x003c84fffec29c71_zone_7_button_on
    id: 7on
  - platform: device
    domain: mqtt
    device_id: 86c1403c24491ce021ac3ee081a86308
    type: button_short_press
    subtype: button_group_7_off
    discovery_id: 0x003c84fffec29c71_zone_7_button_off
    id: 7off
  - platform: device
    domain: mqtt
    device_id: 86c1403c24491ce021ac3ee081a86308
    type: button_short_press
    subtype: button_group_8_on
    discovery_id: 0x003c84fffec29c71_zone_8_button_on
    id: 8on
  - platform: device
    domain: mqtt
    device_id: 86c1403c24491ce021ac3ee081a86308
    type: button_short_press
    subtype: button_group_8_off
    discovery_id: 0x003c84fffec29c71_zone_8_button_off
    id: 8off
    
condition: []
action:
  - choose:
      - conditions: "{{ trigger.id in ['1on'] }}"
        sequence:
          - service: light.turn_on
            data: {}
            target:
              entity_id: light.shellydimmer_f3d426
          - service: input_select.select_option
            data:
              option: "1"
            target:
              entity_id: input_select.mi_boxer_select
      - conditions: "{{ trigger.id in ['1off'] }}"
        sequence:
          - service: light.turn_off
            data: {}
            target:
              entity_id: light.shellydimmer_f3d426

      - conditions: "{{ trigger.id in ['2on'] }}"
        sequence:
          - service: light.turn_on
            data: {}
            target:
              entity_id: light.shellydimmer_d3e57c
          - service: input_select.select_option
            data:
              option: "2"
            target:
              entity_id: input_select.mi_boxer_select
      - conditions: "{{ trigger.id in ['2off'] }}"
        sequence:
          - service: light.turn_off
            data: {}
            target:
              entity_id: light.shellydimmer_d3e57c

      - conditions: "{{ trigger.id in ['3on'] }}"
        sequence:
          - service: light.turn_on
            data: {}
            target:
              entity_id: light.ikea_e27_tv
          - service: input_select.select_option
            data:
              option: "3"
            target:
              entity_id: input_select.mi_boxer_select
      - conditions: "{{ trigger.id in ['3off'] }}"
        sequence:
          - service: light.turn_off
            data: {}
            target:
              entity_id: light.ikea_e27_tv

      - conditions: "{{ trigger.id in ['4on'] }}"
        sequence:
          - service: light.turn_on
            data: {}
            target:
              entity_id: light.lidl_led_stand
          - service: input_select.select_option
            data:
              option: "4"
            target:
              entity_id: input_select.mi_boxer_select
      - conditions: "{{ trigger.id in ['4off'] }}"
        sequence:
          - service: light.turn_off
            data: {}
            target:
              entity_id: light.lidl_led_stand

      - conditions: "{{ trigger.id in ['5on'] }}"
        sequence:
          - service: light.turn_on
            data: {}
            target:
              entity_id: light.dimmable_sm309
          - service: input_select.select_option
            data:
              option: "5"
            target:
              entity_id: input_select.mi_boxer_select
      - conditions: "{{ trigger.id in ['5off'] }}"
        sequence:
          - service: light.turn_off
            data: {}
            target:
              entity_id: light.dimmable_sm309

      - conditions: "{{ trigger.id in ['6on'] }}"
        sequence:
          - service: light.turn_on
            data: {}
            target:
              entity_id: light.shelly_bulb_1
          - service: input_select.select_option
            data:
              option: "6"
            target:
              entity_id: input_select.mi_boxer_select
      - conditions: "{{ trigger.id in ['6off'] }}"
        sequence:
          - service: light.turn_off
            data: {}
            target:
              entity_id: light.shelly_bulb_1

      - conditions: "{{ trigger.id in ['7on'] }}"
        sequence:
          - service: light.turn_on
            data: {}
            target:
              entity_id: light.led_strip_1
          - service: input_select.select_option
            data:
              option: "7"
            target:
              entity_id: input_select.mi_boxer_select
      - conditions: "{{ trigger.id in ['7off'] }}"
        sequence:
          - service: light.turn_off
            data: {}
            target:
              entity_id: light.led_strip_1

      - conditions: "{{ trigger.id in ['8on'] }}"
        sequence:
          - service: light.turn_on
            data: {}
            target:
              entity_id: 
                - light.shellydimmer_f3d426
                - light.shellydimmer_d3e57c
                - light.ikea_e27_tv
                - light.dimmable_sm309
                - light.shelly_bulb_1
                - light.lidl_led_stand
                - light.plug_tz_10_switch
                - light.led_strip_1
          - service: input_select.select_option
            data:
              option: "8"
            target:
              entity_id: input_select.mi_boxer_select
      - conditions: "{{ trigger.id in ['8off'] }}"
        sequence:
          - service: light.turn_off
            data: {}
            target:
              entity_id: 
                - light.shellydimmer_f3d426
                - light.shellydimmer_d3e57c
                - light.ikea_e27_tv
                - light.dimmable_sm309
                - light.shelly_bulb_1
                - light.lidl_led_stand
                - light.plug_tz_10_switch
                - light.led_strip_1
              
      - conditions: >-
          {{ is_state('input_select.mi_boxer_select', '1') and trigger.id in ['bright'] }}
        sequence:
          - service: light.turn_on
            data:
              brightness_pct: "{{ trigger.to_state.state }}"
              transition: 0.2
            target:
              entity_id: light.shellydimmer_f3d426
            enabled: true

      - conditions: >-
          {{ is_state('input_select.mi_boxer_select', '2') and trigger.id in ['bright'] }}
        sequence:
          - service: light.turn_on
            data:
              brightness_pct: "{{ trigger.to_state.state }}"
              transition: 0.2
            target:
              device_id: 1ff4112785e14b8b8cba18d45fec3b11
            enabled: true

      - conditions: >-
          {{ is_state('input_select.mi_boxer_select', '3') and trigger.id in ['bright'] }}
        sequence:
          - service: light.turn_on
            data:
              brightness_pct: "{{ trigger.to_state.state }}"
              transition: 0.2
            target:
              entity_id: light.ikea_e27_tv
            enabled: true

      - conditions: >-
          {{ is_state('input_select.mi_boxer_select', '4') and trigger.id in ['bright'] }}
        sequence:
          - service: light.turn_on
            data:
              brightness_pct: "{{ trigger.to_state.state }}"
              transition: 0.2
            target:
              entity_id: light.lidl_led_stand
            enabled: true

      - conditions: >-
          {{ is_state('input_select.mi_boxer_select', '5') and trigger.id in ['bright'] }}
        sequence:
          - service: light.turn_on
            data:
              brightness_pct: "{{ trigger.to_state.state }}"
              transition: 0.2
            target:
              entity_id: light.dimmable_sm309
            enabled: true

      - conditions: >-
          {{ is_state('input_select.mi_boxer_select', '6') and trigger.id in ['bright'] }}
        sequence:
          - service: light.turn_on
            data:
              brightness_pct: "{{ trigger.to_state.state }}"
              transition: 0.2
            target:
              entity_id: light.shelly_bulb_1
            enabled: true

      - conditions: >-
          {{ is_state('input_select.mi_boxer_select', '7') and trigger.id in ['bright'] }}
        sequence:
          - service: light.turn_on
            data:
              brightness_pct: "{{ trigger.to_state.state }}"
              transition: 0.2
            target:
              entity_id: light.led_strip_1
            enabled: true
mode: restart

EDIT 11/01/2024

Avec la dernière mise à jour Zigbee2MQTT (1.35.1-1) il n’y a plus besoin de l’extension. Par contre ce que j’avais fait sur la base de ce qui est proposé ici ne fonctionne plus (bien que toujours dans la doc).

J’ai donc biaisé en faisant du mqtt direct :

Pour la détection (trigger:) des boutons (à dupliquer) :

- platform: mqtt
    topic: zigbee2mqtt/Mi Boxer
    payload: ('on', 101)
    value_template: "{{ value_json.action , value_json.action_group }}"
    id: 1on
  - platform: mqtt
    topic: zigbee2mqtt/Mi Boxer
    payload: ('off', 101)
    value_template: "{{ value_json.action , value_json.action_group }}"
    id: 1off

Et pour la luminosité (il me reste à trouver comment ne prendre en compte que la charge action_level) :

- platform: mqtt
    topic: zigbee2mqtt/Mi Boxer
    id: bright

Ensuite on modifie dans le chose: coté action (à dupliquer bien entendu :

      - conditions:
          - condition: template
            value_template: >-
              {{ is_state('input_select.mi_boxer_select', '2') and trigger.id in ['bright'] }}
        sequence:
          - service: light.turn_on
            data_template:
              entity_id: light.shellydimmer_d3e57c
              brightness_pct: "{{ trigger.payload_json.action_level }}"

EDIT 11/01/2024

Le problème avec la solution de mon EDIT précédent est que le trigger sur la charge MQTT globale génère trop de bruit. On va donc faire deux sensor: basés sur MQTT afin d'isoler la valeur de la luminosité (merci Mathieu) ainsi que le groupe (bouton) correspondant à la light: active, ce qui va nous permettre d'éliminer l'input_select: dans le filtrage à venir :

mqtt:
  sensor:
    - name: "Mi Boxer Bright"
      unique_id: "mi_boxer_bright"
      state_topic: "zigbee2mqtt/Mi Boxer"
      value_template: "{{ value_json.action_level |int }}"

    - name: "Mi Boxxer Group"
      unique_id: "mi_boxer_group"
      state_topic: "zigbee2mqtt/Mi Boxer"
      value_template: "{{ value_json.action_group |int }}"

En trigger: on utilisera l'action : action_brightness_move_to_level qui remonte dans HA :

trigger:
  - platform: device
    domain: mqtt
    device_id: 86c1403c24491ce021ac3ee081a86308
    type: action
    subtype: brightness_move_to_level
    discovery_id: 0x003c84fffec29c71 action_brightness_move_to_level
    id: bright

Et on complète notre action déclenchée par l'id: bright et filtrée sur le sensor: correspondant au bon bouton, et j'ai ajouté le light: actif afin de ne pas risquer une fausse manœuvre :

      - conditions:
          - condition: template
            value_template: >-
              {{ is_state('sensor.mi_boxer_group', '101') and trigger.id in ['bright'] and is_state('light.shellydimmer_f3d426', 'on') }}
        sequence:
          - service: light.turn_on
            data_template:
              entity_id: light.shellydimmer_f3d426
              brightness_pct: "{{ states('sensor.mi_boxer_bright') | int}}"

Il ne reste plus qu'à attendre que Z2M remonte les informations liées à la température du blanc et la roue chromatique et on pourra les traiter avec la même méthode.

 
 

 

 

RustDesk

Au fil des ans TeamViewer s’est imposé dans l'IT. Au départ pseudo gratuit ce produit offre maintenant de plus en plus de fonctionnalités (souvent inutiles) et son cout augmente d’années en années. On pourrait imaginer le remplacer par AnyDesk, mais hélas leur politique est quasiment identique et ce serait reculer pour mieux sauter.

Pendant le confinement certains en ont eu assez de se faire “raquetter”. Si le mot vous parait fort, constatez simplement les augmentations successives dont la dernière de 20% que rien ne justifie à mes yeux, l'éditeur se justifiant quant à lui par l'ajout de fonctionnalités qui me sont inutiles. Entendons nous bien, tout a un prix et tout travail ou service mérite une rémunération, mais tout doit rester dans la nuance, même quand on se retrouve dans une position de domination, ce qui est d'ailleurs le cas pour d'autres, comme Microsoft et ses dernières augmentations au niveau 365 ou Azure...

Bref, c'est donc ainsi qu'une petite équipe de développeurs a créé RustDesk. RustDesk offre les mêmes fonctionnalités de base dans le cadre de l’open source. Je teste Rustdesk depuis près d’un an et j’estime que l’heure est venue de franchir le pas.

RustDesk est disponible sous différentes distributions Linux, MacOS, Windows, Android et IOS.

Il existe plusieurs façons d’utiliser RustDesk :

Version isolée sans serveur

La plus simple et totalement gratuite, mais qui présente quelques inconvénients :

  • Pas de chiffrement, cela veut dire que toutes les infos circulent en clair (pas sécure pour les clients, mauvaise pratique).
  • Lenteurs possibles car ça utilise les serveurs publics, ce qui reste limité.
  • Pas de carnet d’adresse synchronisé entre les devices clients. Et là ça bloque car comme beaucoup de monde j'utilise un PC fixe au bureau et un laptop quand je me déplace, sur lequel je souhaite retrouver mon environnement de travail. Ils auraient pu envisager une synchronisation de type "drive", mais cela aurait posé des questions de sécurité car en plus de synchroniser les identifiants il faut stocker des mots de passe.

Version gratuite avec serveur

Toujours gratuit (en dehors du cout d'un serveur VPS (qui peut être mutualisé)), et on résous la question du chiffrement :

  • Le serveur sert de relais (amélioration de la vitesse) et de serveur de chiffrement afin de sécuriser les liens.

Version payante avec serveur privé

La totale qui va couter quelques euros (10 € / mois pour deux utilisateurs, ou 20 € /mois pour 20 utilisateurs + le serveur), mais à mon sens indispensable dans un cadre professionnel

  • Le serveur offre une console qui permet de lister les clients installés et éventuellement les partager
    • Seul l’admin peut activer les partages à ce stade. Partager fait remonter les ID pour un autre utilisateur, mais le mot de passe associé au device reste nécessaire.
  • Avoir des logs, qui se connecte à quoi (dispo pour chaque utilisateur), transferts de fichiers.
  • Changer l’ID d’un client
  • Utiliser un carnet d’adresse qui sera synchronisé entre les clients de chaque utilisateur, mais non accessible à l’admin.
  • Appliquer des stratégies
  • Et d'autres fonctionnalités à venir car le projet est en constante évolution....

Il n'existe pour l'instant pas de version Cloud et je n'ai pas le sentiment que ce soit dans l'ordre des priorités de l'équipe.

Installation

Je me place ici dans le cadre de la version serveur après avoir acquis une licence.

L'installation du serveur se fait sous Linux ou dans un container Docker. La procédure est décrite ici et je ne vais pas la reprendre. J'ai choisit Docker pour la facilité d'un Compose...

Coté client, contrairement à TeamViewer qui fonctionne uniquement avec une infrastructure cloud, RustDesk fonctionne avec une infrastructure privée si l’on veut disposer de toutes ses fonctionnalités. Quand on installe le client il va donc falloir configurer les informations liées au serveur. Ce device sera alors uniquement accessible par des clients également configurés pour ce serveur (tout au moins pour l’instant car il y a des évolutions en cours).

Pour configurer le client afin qu’il soit reconnu par le serveur on a plusieurs solutions :

  • Soit on installe l’exe normalement et on copie la config dans ID / Relay server
Qfi0TWlxGWrUdfqdsfVENxxxxxxxxxxxxxxxxxxxxxxxqfsqdfN3boJye

  • Soit on renomme l’exe d’installation afin d’y intégrer la clé, ce qui va donner quelque chose du genre:
rustdesk-licensed-Qfi0TWlxGWrUkR5Vxxxxxxxdfsdfsdfsdfye.exe
  • Soit on crée un script de déploiement et ainsi appliquer la configuration après l’installation dans le cadre d’un script PowerShell, on dispose alors de ces trois commandes, et on verra plus bas ce à quoi peut ressembler un script de déploiement  :

Se connecter

En général quand on utilise ce genre de produit, souvent pour de l'assistance client ou de la gestion de serveurs, il y a deux types d'installations :

  • Le device client, le mien, sur lequel après avoir configuré le serveur (chapitre précédent), je vais me connecter avec l'identifiant fournit par l'administrateur du serveur : 
  • Le device distant auquel on va affecter (ou pas) un utilisateur

Affecter un device à un utilisateur

Si on affecte un device à un utilisateur, cet utilisateur (et uniquement lui) verra ce device distant monter dans son logiciel client. Il pourra également l’ajouter à son carnet d’adresse personnel. Si on ne l'affecte pas, tous les utilisateurs le verront, pourront l'ajouter à leur carnet d'adresse et s'y connecter (sous réserve de disposer du mot de passe préalablement défini).

Des fonctionnalités de groupe et de partage de groupe sont également prévues. Il est ainsi possible de partager les devises affectés à un groupe à un autre utilisateur. A noter que pour l'instant groupe = utilisateur et que l'on ne peut pas directement affecter des devices à un groupe isolé. La solution consiste donc à créer un utilisateur virtuel qui partagera ses devices avec d'autres utilisateurs. Par exemple j'ai créé un utilisateur virtuel Société A qui partage ses devices avec les deux personnes chargés de la maintenance de Société A. Ca consomme une licence utilisateur mais je n'ai pas trouvé mieux pour l'instant.

Il existe plusieurs façons pour y parvenir :

  • En se connectant avec son user/password (mais c’est une très mauvaise idée que de faire ça sur le PC d’un client)
  • En l’associant dans l’interface d’administrateur (mais seul l’admin peut le faire)
  • En utilisant l’API sur le client dans le cadre d’un déploiement vie un script

Pour ça il faut un token. Les tokens qui permettent l’accès à l’API sont délivrés par l’admin.

PS C:\Program Files\RustDesk>.\RustDesk.exe  –assign –token "rapi_xxxxxxxxdyw==" –user_name “admin” | more

La console

Chaque utilisateur peut voir la liste de ses devices, l’état de la connexion ainsi que diverses infos (par exemple :

ip: 82.65.xxx.xxx;
version: 1.2.3;
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz, 2.53GHz, 4/4 cores; mem: 8GB

Un log est également disponible tant pour les connections que pour les transferts de fichiers. Cette console est destinée à évoluer pour plus de fonctionnalités.

L'administrateur peut quant à lui, outre gérer utilisateurs et devices, affecter des stratégies aux utilisateurs et devices :

Script PowerShell d’installation

On ne dispose pour l'instant pas de .msi et ce script est un peu simpliste (il en existe pour d'autres O/S ici). Pour faire simple je conseille d’ouvrir une session PS en mode admin. Mais il est également possible de faire une élévation dans le script (voir à la fin). Pour l’instant attention à indiquer la version de l'exe sur la bonne ligne…

Version client isolé

$rustdesk_pw="mot_de_passe_permanent"
$ErrorActionPreference= 'silentlycontinue'

################################### Please Do Not Edit Below This Line #########################################

If (!(Test-Path C:\Temp))
{
    New-Item -ItemType Directory -Force -Path C:\Temp > null
}

[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

cd C:\Temp
Invoke-WebRequest "https://github.com/rustdesk/rustdesk/releases/download/1.2.3/rustdesk-1.2.3-x86_64.exe" -Outfile "rustdesk.exe"
Start-Process .\rustdesk.exe --silent-install # -wait
$ServiceName = 'Rustdesk'
$arrService = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
if ($arrService -eq $null)
{
    Start-Sleep -seconds 20
}
cd $env:ProgramFiles\RustDesk\
$rustdesk_id = (.\RustDesk.exe --get-id | out-host)

.\RustDesk.exe --password $rustdesk_pw | out-host

Version "mes machines"

################################# VARIABLES #######################################
$rustdesk_pw="PasswordPermanent"  # Celui du device distant
$rustdesk_token="MonTocken"
$rustdesk_cfg="Qfi0TWlxxxxxxxxxxxxxxxxxxxxxxxxxxN3boJye" # Clé de configuration
$ErrorActionPreference= 'silentlycontinue'
################################### SCRIPT ########################################

If (!(Test-Path C:\Temp))
{
    New-Item -ItemType Directory -Force -Path C:\Temp > null
}
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
cd C:\Temp
Invoke-WebRequest "https://github.com/rustdesk/rustdesk/releases/download/1.2.3/rustdesk-1.2.3-x86_64.exe" -Outfile "rustdesk.exe" # Changer la version le cas échéant

Start-Process .\rustdesk.exe --silent-install # -wait
$ServiceName = 'Rustdesk'
$arrService = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue

if ($arrService -eq $null)
{
    Start-Sleep -seconds 20
}
cd $env:ProgramFiles\RustDesk\
$rustdesk_id = (.\RustDesk.exe --get-id | out-host)

.\RustDesk.exe --config $rustdesk_cfg | out-host
.\RustDesk.exe --password $rustdesk_pw | out-host
Start-Sleep -Seconds 5
.\RustDesk.exe --assign --token $rustdesk_token --user_name manu | out-host

Elévation (le cas échéant à ajouter au début du script)

if (-Not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
    if ([int](Get-CimInstance -Class Win32_OperatingSystem | Select-Object -ExpandProperty BuildNumber) -ge 6000) {
        Start-Process PowerShell -Verb RunAs -ArgumentList "-NoProfile -ExecutionPolicy Bypass -Command `"cd '$pwd'; & '$PSCommandPath';`"";
        Exit;
    }
}

Le cas des serveurs Windows et du RDP

Si on installe RustDesk sur un serveur Windows va se retrouver face à deux possibilités, activer ou pas le partage de session RDP (une problématique identique existe avec TeamViewer qui a choisit de fournir deux ID, cela concerne principalement les administrateurs de serveurs, qu’ils soient premise ou cloud) :

  • Non activé : RustDesk va proposer une ouverture de session, il est possible d’enregistrer le mot de passe dans le client, et cette ouverture de session déconnectera l’éventuel utilisateur connecté en RDP.
  • Activé : Dans ce cas RustDesk partage la connexion RDP active et si on déconnecte la session RDP ça déconnecte la session RustDesk (qui peut toutefois se reconnecter s’il dispose des identifiants Windows.
    • Il existe toutefois un problème, dans ce mode rien ne permet actuellement de choisir la session sur laquelle va s’opérer le partage, il y a donc un risque de se concerter sur la session d’un utilisateur actif dans le cas d’un serveur de bureaux virtuels. (ticket ouvert en cours).

Conclusion

RustDesk est constante évolution et j'y reviendrait certainement ici. En attendant j'ai déployé une trentaine de devices sans observer de problèmes particulier si je compare à mon classique TeamViewer.

EDIT 10/12/2023

J'avais demandé une option afin de simplifier la mise en œuvre et ne pas associer des installations distantes et non contrôlées de façon permanentent à mon serveur. Sur le client on installe simplement Rustdesk de façon interactive, ou on l'installer avec le script simplifié qui permet seulement de définir un mot de passe permanent.

Il est maintenait possible, dans une infrastructure avec un serveur Pro de se connecter à un client isolé tout en bénéficiant du chiffrage du serveur en saisissant son ID@public :

Ainsi la machine sera contrôlée et chiffrée de façon temporaire par le serveur, sans pour autant l'enregistrer de faon permanente dans celui ci. Il sera toutefois possible de l'ajouter à son carnet d'adresse et de lui affecter une étiquette, mais pas de la partager avec d'autres utilisateurs du serveur. Autre avantage, s'autres intervenant pourront se connecter à cette machine, même s'ils ne fort pas partie du périmètre du serveur.

 

 

RDP & Guacamole

Et non, je ne suis hélas pas parti en vacances au Mexique déguster du guacamole arrosé de téquila ! Mais j'ai trouvé la solution que je cherchait depuis longtemps pour sécuriser les accès RDP. Comme chacun le sait il n'est vraiment pas conseillé de laisser ouvert sur internet le port RDP qui est plutôt vulnérable. Hélas pensant le confinement beaucoup de clients ont du mettre en place des solutions dans l'urgence et dans bien des cas il était impossible de verrouiller au minimum ce port sur une IP fixe....

Bien sur Microsoft a une solution (RDP Gateway), couteuse et complexe, donc pas adaptée à de petites entreprises dont les couts IT explosent déjà. Je restait donc en éveil à la recherche d'une solution quand je suis tombé sur le projet open-source Guacamole en HTML5. Et surprise, celui ci fonctionne vraiment bien ! Le principe est simple et reprends celui des autres projets, une machine va servir de proxy afin d'exposer le RDP (mais aussi SSH, Telnet et VNC).

Installation

S'il est possible d'installer Guacamole en natif comme c'est très bien expliqué ici, après avoir testé différentes options, par facilité je vais faire ça sous Docker en utilisant ce projet.

Je vais donc monter une VM Linux Ubuntu et y configurer Docker et Docker Compose. Pour cette partie je vous laisse chercher, d'autres expliquent mieux que moi.

Pour déployer Guacamole on a besoin de 3 services :

  • GUACD : Le cœur de Guacamole qui va servir à connecter les différents services (RDP, SSH, etc...)
  • PostgreSQL : Une base de donnée (qui peut être également MySQL ou encore l'utilisation d'une base externe)
  • Guacamole : Le composant FrontEnd qui va gérer les connections, les services et les utilisateurs.

Docker Compose

L'auteur de l'intégration a eu la bonne idée de nous fournir un script d'installation. On va donc l'utiliser et on se servira de Docker Compose plus tard afin d'ajuster la configuration.

sudo git clone "https://github.com/boschkundendienst/guacamole-docker-compose.git"
cd guacamole-docker-compose
./prepare.sh
sudo docker-compose up -d

Si tout se passe bien on peut se connecter sur https://ip_serveur:8443/guacamole. Le nom d'utilisateur par défaut est guacadmin avec mot de passe guacadmin. La première chose à faire est bien sur de le changer.

On pourra facilement configurer un serveur RDP et s'y connecter pour avoir les premières impressions. Je trouve que le rendu RDP en HTML5 est fluide, même en regardant des vidéos sur YouTube à une bonne résolution. Attention à ne pas perdre de vue que c'est le serveur Guacamole qui doit encaisser le trafic entrant et sortant. Selon l'usage et le nombre de clients il faudra donc le dimensionner correctement.

Je ne vais pas m'étendre sur les différentes options, l'interface et claire, la documentation également et d'autres en parlent très bien.

Sécurisation

Il n'est bien sur pas question d'exposer ce serveur directement sur internet et je vais tester deux solutions de reverse proxy, je commence par éditer le fichier docker-compose.yml afin de supprimer le proxy Nginx préinstallé histoire de ne pas empiler les proxys. J'ajuste également en 8080:8080 pour une utilisation en direct et REMOTE_IP_VALVE_ENABLED: 'true'  pour activer le proxy externe et WEBAPP_CONTEXT: 'ROOT' afin que Guacamole soit accessible en racine :

version: '2.0'
networks:
  guacnetwork_compose:
    driver: bridge

services:
  # guacd
  guacd:
    container_name: guacd_compose
    image: guacamole/guacd
    networks:
      guacnetwork_compose:
    restart: always
    volumes:
    - ./drive:/drive:rw
    - ./record:/record:rw
  # postgres
  postgres:
    container_name: postgres_guacamole_compose
    environment:
      PGDATA: /var/lib/postgresql/data/guacamole
      POSTGRES_DB: guacamole_db
      POSTGRES_PASSWORD: 'ChooseYourOwnPasswordHere1234'
      POSTGRES_USER: guacamole_user
    image: postgres:15.2-alpine
    networks:
      guacnetwork_compose:
    restart: always
    volumes:
    - ./init:/docker-entrypoint-initdb.d:z
    - ./data:/var/lib/postgresql/data:Z

  guacamole:
    container_name: guacamole_compose
    depends_on:
    - guacd
    - postgres
    environment:
      REMOTE_IP_VALVE_ENABLED: 'true'   # On active ici l'utilisation via un proxy externe
      WEBAPP_CONTEXT: 'ROOT'
      GUACD_HOSTNAME: guacd
      POSTGRES_DATABASE: guacamole_db
      POSTGRES_HOSTNAME: postgres
      POSTGRES_PASSWORD: 'ChooseYourOwnPasswordHere1234'
      POSTGRES_USER: guacamole_user
    image: guacamole/guacamole
    volumes:
    - ./custom/server.xml:/home/administrator/guacamole-docker-compose/server.xml
    links:
    - guacd
    networks:
      guacnetwork_compose:
    ports:
    - 8080:8080
    restart: always

Je relance docker :

administrator@guacamole:~/guacamole-docker-compose$ sudo docker-compose up -d

En local je me connecte maintenant en http://ip_serveur:8080 sans SSL car le SSL sera géré par le proxy.

Option 1 : pfsense

J'ai un pfsense installé avec HAProxy et Acme pour gérer les certificats Lets'Encrypt, je vais donc me servir de ca pour publier le service. Sur HAProxy on configure le BackEnd et le FronteEnd qui lui utilisera le certificat préalablement créé avec Acme. Dans ma configuration je partage l'IP avec plusieurs sites et ce qui est important c'est d'activer l'option forwardfor qui permettra de transférer les adresses sources à Guacamole.

Je mets ici le code de configuration qui sera utile à ceux qui n'utilisent pas HAProxy sous pfsense qui lui dispose d'une interface graphique. Comme on peut le constater le HTTP to HTTPS se fait au niveau du HAProxy et c'est lui également qui redirigera les requetés HTTTP vers HTTPS et mon serveur sera accessible sur https://guacamole.mondomaine.tld :

global
  maxconn     10000
  stats socket /tmp/haproxy.socket level admin  expose-fd listeners
  uid     80
  gid     80
  nbproc      1
  nbthread      1
  hard-stop-after   15m
  chroot        /tmp/haproxy_chroot
  daemon
  tune.ssl.default-dh-param 1024
  server-state-file /tmp/haproxy_server_state

listen HAProxyLocalStats
  bind 127.0.0.1:2200 name localstats
  mode http
  stats enable
  stats admin if TRUE
  stats show-legends
  stats uri /haproxy/haproxy_stats.php?haproxystats=1
  timeout client 5000
  timeout connect 5000
  timeout server 5000

frontend Shared_WAN-merged
  bind      x.x.x.x:443 (IP WAN) name x.x.x.x:443 (IP WAN)   ssl crt-list /var/etc/haproxy/Shared_WAN.crt_list  
  mode      http
  log       global
  option    http-keep-alive
  option    forwardfor
  acl https ssl_fc
  http-request set-header   X-Forwarded-Proto http if !https
  http-request set-header   X-Forwarded-Proto https if https
  timeout client    30000
  acl     Admin var(txn.txnhost)      -m str -i admin.domain.tld
  acl     guacamole var(txn.txnhost)  -m str -i guacamole.domain.tld
  http-request set-var(txn.txnhost) hdr(host)
  use_backend Admin_ipvANY  if  Admin 
  use_backend Guacamole_8443_ipvANY  if  guacamole 

frontend http-to-https
  bind            x.x.x.x:80 (IP WAN) name x.x.x.x:80 (IP WAN)
  mode            http
  log             global
  option          http-keep-alive
  timeout client  30000
  http-request redirect code 301 location https://%[hdr(host)]%[path]  

backend Admin_ipvANY
  mode     http
  id       100
  log      global
  timeout  connect   30000
  timeout  server    30000
  retries  3
  server   admin 192.168.55.44:80 id 101  

backend Guacamole_8443_ipvANY
  mode     http
  id       102
  log      global
  timeout  connect   30000
  timeout  server    30000
  retries  3
  server   guacamole 192.168.66.55:8080 id 101

Option 2 : CloudFlared

Cette option est encore plus simple pour ceux qui disposent d'un domaine chez Cloudflare. On va utiliser les possibilités offertes gratuitement par Cloudflared et ajouter une couche de sécurité supplémentaire.

On commence par créer un tunnel CloudFlared avec une instance Docker supplémentaire (le code est fournit par le site de configuration Zero Trust de Cloudflare)

docker run cloudflare/cloudflared:latest tunnel --no-autoupdate run --token eyJhIjoiZjk0YjdkZTBiMzFmYWNkNjZlshhsgfhsfghsgfhgfhssgfhsfgzRiMC00MmNlLWJjMghsfghsghsgfhsgfhsgfhsgmpaR00tyyretu(-yenebybvewWXpGaUxUazVNell0TnpRek56WXhNMlkxWXpFeiJ9

Ensuite une fois que le tunnel est monté

On va publier et pointant sur l'ip privée et le port de notre serveur, cela va automatiquement créer l'entrée DNS dans CloudFlare et gérer la problématique du certificat. Ll sera accessible en SSL. :

Ensuite on va ajouter une couche de sécurité supplémentaire en créant une application de type SelfHosted à laquelle on affecte une policie qui impose un code supplémentaire lors d'une connexion. Ce code sera envoyé sur le mail de l'utilisateur sur une adresse individuelle ou un domaine spécifique. Ce n'est pas tout à fait du MFA, mais on considère que si l'utilisateur reçoit bien le code sur son mail professionnel, il s'agit bien de lui et on peut lui permettre de saisir son login / password pour accéder au service :

C'est la méthode la plus simple et de nombreuses option sont exploitables, de même qu'il est possible d'utiliser de nombreux providers externes d'authentification :

Lorsqu'il souhaite se connecter, l'utilisateur tombe sur un portail que l'on peu personnaliser aux couleurs de l'entreprise.

Voilà, et surtout maintenant on retrouve bien les IP publiques dans le log de Guacamole :

Il n'y a plus qu'a ajuster les différentes options de Guacamole et pour ça la documentation est très bien faite. Et bien sur si on se sert de Guacamole pour exposer des serveurs RDP accessibles sur l'internet on prendra soin de restreindre leur usage à l'IP publique du serveur Guacamole, ou mieux de faire transiter ce flux par un tunnel (Wireguard, Tailscale ou Zerotier par exemple que l'on peu facilement installer sur les deux serveurs).

Home Assistant & Arrosage

Je n'ai pas vraiment la main verte et et mon jardin ressemble souvent à un no man's land, mais au printemps dernier une amie m'a convaincu de créer un carré d'herbes aromatiques, l'idée à fait son chemin et au centre on a même planté des plants de tomates qui produisent bien cet été ! Bon, vous imaginez bien que je ne vais pas ici vous conter ma vie privée mais plutôt la part domotique de cette réalisation !

En Provence, si on veut un peu de verdure et espérer manger ses propres tomates, il n'y a pas de secret, il faut arroser ! J'ai donc commencé par disposer un tuyau poreux sur mon carré. Ensuite j'ai acheté un robinet Zigbee et préparé un petit scheduler qui me permet facilement d'activer ou pas la plage journalière d'arrosage. J'ai fait quelque chose de simple en m'inspirant de ce que j'avais fait pour mon chauffe eau. Je publie ici le code suite à quelques demandes, même si ça n'a pas une grande valeur ajoutée.

Pour l'instant ça ne prends pas en compte les valeurs remontés sur les capteurs de plantes car je n'en suis pas satisfait.

L'offre en matière de capteurs de plantes n'est pas énorme :

  • Le capteur Xiaomi (ou ses copies) qui fonctionnent en Bluetooth et que l'on trouve sur Amazon ou Ali Express : C'est ce qui fonctionne le "mieux", mais cela nécessite un proxy BLE à l'extérieur que l'on peut facilement se bricoler avec un ESP sous ESPHome.
  • Le capteur Rehent en Zigbee que j'ai acheté chez Domadoo : Et là c'est la déconvenue. Il est un peu encombrant mais ne pose pas de soucis particulier pour l'appairer en ZHA ou Z2M (ou sur une passerelle Tuya), sauf que si ce capteur remonte parfaitement la température du sol, l'humidité du sol reste invariablement entre 75% et 85% (voir les sources au bas de cet article). Et bien sur chez Domadoo on est pas chez Amazon, le client a tort et l'éventuel retour est à votre charge. Bref capteur et fournisseur à éviter ! Il semblerait qu'autres séries de ce capteur que l'on trouve chez AliExpress fonctionnent, mais là il faudra aussi oublier le retour...

J'utilise une vanne Zigbee Woox qui alimente le tuyaux poreux (que je devrais remplacer par un un goute à goute). Pour avoir un réseau Zigbee fiable à l'extérieur, j'ai installé sous la toiture de la terrasse des prises Zigbee dont le relais est HS mais qui continuent à remplir parfaitement leur rôle de routeur Zigbee.

Tout cela reste très expérimental... Coté intégration j'utilise Home Assistant Plant et la carte qui va avec.

Automation

Une automation de schedule simple et visuelle :

input_datetime:
  watering_start:
    has_date: false
    has_time: true
  watering_stop:
    has_date: false
    has_time: true

input_boolean:
  watering_day_monday:
    name: "WATERING : Lundi"
    icon: mdi:toggle-switch
  watering_day_tuesday:
    name: "WATERING : Mardi"
    icon: mdi:toggle-switch
  watering_day_wednesday:
    name: "WATERING : Mercredi"
    icon: mdi:toggle-switch
  watering_day_thursday:
    name: "WATERING : Jeudi"
    icon: mdi:toggle-switch
  watering_day_friday:
    name: "WATERING : Vendredi"
    icon: mdi:toggle-switch
  watering_day_saturday:
    name: "WATERING : Samedi"
    icon: mdi:toggle-switch
  watering_day_sunday:
    name: "WATERING : Dimanche"
    icon: mdi:toggle-switch

automation:

- id: 'xx8d0e1-fcb6-4412-abvxx-99c4d37be5xx'
  alias: 'WATERING ON'
  trigger:
  - platform: template
    value_template: '{{ states.sensor.time.state == states.input_datetime.watering_start.state[0:5] }}'
  condition:
    condition: or
    conditions:
      - '{{ (now().strftime("%a") == "Mon") and is_state("input_boolean.watering_day_monday", "on") }}'
      - '{{ (now().strftime("%a") == "Tue") and is_state("input_boolean.watering_day_tuesday", "on") }}'
      - '{{ (now().strftime("%a") == "Wed") and is_state("input_boolean.watering_day_wednesday", "on") }}'
      - '{{ (now().strftime("%a") == "Thu") and is_state("input_boolean.watering_day_thursday", "on") }}'
      - '{{ (now().strftime("%a") == "Fri") and is_state("input_boolean.watering_day_friday", "on") }}'
      - '{{ (now().strftime("%a") == "Sat") and is_state("input_boolean.watering_day_saturday", "on") }}'
      - '{{ (now().strftime("%a") == "Sun") and is_state("input_boolean.watering_day_sunday", "on")}}'
  action:
  - service: switch.turn_on
    entity_id: switch.vanne_woox_switch
  - service: notify.slack_hass_canaletto
    data:
      message: "{{now().strftime('%d/%m/%Y, %H:%M')}} > WATERING | START | Soil : {{ states.sensor.soil_01_soil_moisture.state }}%" 


- id: 'zz9csdfsef-76dd-4fdd-9dzz-40bfsdq158zz'
  alias: 'WATERING OFF'
  trigger:
  - platform: template
    value_template: '{{ states.sensor.time.state == states.input_datetime.watering_stop.state[0:5] }}'
  action:
  - service: switch.turn_off
    entity_id: switch.vanne_woox_switch
  - service: notify.slack_hass_canaletto
    data:
      message: "{{now().strftime('%d/%m/%Y, %H:%M')}} > WATERING | STOP | Soil : {{ states.sensor.soil_01_soil_moisture.state }}%"   

La carte Lovelace :

type: vertical-stack
cards:
  - type: entities
    entities:
      - entities:
          - entity: automation.watering_on
            name: false
          - entity: sensor.energy_total_yearly_1pm_watering
            name: false
            unit: kWh
            format: precision2
          - entity: sensor.soil_01_soil_moisture
            name: false
        entity: switch.vanne_woox_switch
        name: Arrosage
        icon: mdi:watering-can-outline
        show_state: false
        state_color: true
        type: custom:multiple-entity-row
  - type: horizontal-stack
    cards:
      - type: custom:button-card
        color_type: card
        entity: input_boolean.watering_day_monday
        name: Lundi
        show_last_changed: false
        show_state: false
        tap_action:
          action: toggle
        state:
          - value: 'on'
            color: green
            icon: mdi:water-boiler
          - value: 'off'
            color: grey
            icon: mdi:water-boiler-off
        styles:
          card:
            - height: 60px
            - border-radius: 5px
            - font-size: 12px
      - type: custom:button-card
        color_type: card
        entity: input_boolean.watering_day_tuesday
        name: Mardi
        show_last_changed: false
        show_state: false
        tap_action:
          action: toggle
        state:
          - value: 'on'
            color: green
            icon: mdi:water-boiler
          - value: 'off'
            color: grey
            icon: mdi:water-boiler-off
        styles:
          card:
            - height: 60px
            - border-radius: 5px
            - font-size: 12px
      - type: custom:button-card
        color_type: card
        entity: input_boolean.watering_day_wednesday
        name: Mercredi
        show_last_changed: false
        show_state: false
        tap_action:
          action: toggle
        state:
          - value: 'on'
            color: green
            icon: mdi:water-boiler
          - value: 'off'
            color: grey
            icon: mdi:water-boiler-off
        styles:
          card:
            - height: 60px
            - border-radius: 5px
            - font-size: 12px
      - type: custom:button-card
        color_type: card
        entity: input_boolean.watering_day_thursday
        name: Jeudi
        show_last_changed: false
        show_state: false
        tap_action:
          action: toggle
        state:
          - value: 'on'
            color: green
            icon: mdi:water-boiler
          - value: 'off'
            color: grey
            icon: mdi:water-boiler-off
        styles:
          card:
            - height: 60px
            - border-radius: 5px
            - font-size: 12px
      - type: custom:button-card
        color_type: card
        entity: input_boolean.watering_day_friday
        name: Vendredi
        show_last_changed: false
        show_state: false
        tap_action:
          action: toggle
        state:
          - value: 'on'
            color: green
            icon: mdi:water-boiler
          - value: 'off'
            color: grey
            icon: mdi:water-boiler-off
        styles:
          card:
            - height: 60px
            - border-radius: 5px
            - font-size: 12px
      - type: custom:button-card
        color_type: card
        entity: input_boolean.watering_day_saturday
        name: Samedi
        show_last_changed: false
        show_state: false
        tap_action:
          action: toggle
        state:
          - value: 'on'
            color: green
            icon: mdi:water-boiler
          - value: 'off'
            color: grey
            icon: mdi:water-boiler-off
        styles:
          card:
            - height: 60px
            - border-radius: 5px
            - font-size: 12px
      - type: custom:button-card
        color_type: card
        entity: input_boolean.watering_day_sunday
        name: Dimanche
        show_last_changed: false
        show_state: false
        tap_action:
          action: toggle
        state:
          - value: 'on'
            color: green
            icon: mdi:water-boiler
          - value: 'off'
            color: grey
            icon: mdi:water-boiler-off
        styles:
          card:
            - height: 60px
            - border-radius: 5px
            - font-size: 12px
  - type: conditional
    conditions:
      - entity: automation.watering_on
        state: 'on'
    card:
      type: custom:vertical-stack-in-card
      cards:
        - type: horizontal-stack
          cards:
            - type: markdown
              content: '#### <center> Heure de début'
            - type: markdown
              content: '#### <center> Arrosage'
            - type: markdown
              content: '#### <center> Heure de Fin'
        - type: horizontal-stack
          cards:
            - entity: input_datetime.watering_start
              type: custom:time-picker-card
              name: Début
              layout:
                align_controls: center
                embedded: true
              hide:
                name: true
                icon: true
            - type: glance
              show_state: true
              show_name: false
              entities:
                - switch.vanne_woox_switch
            - entity: input_datetime.watering_stop
              type: custom:time-picker-card
              layout:
                align_controls: center
                embedded: true
              hide:
                name: true
                icon: true
  - color_thresholds:
      - color: '#039BE5'
        value: 0
      - color: '#0da035'
        value: 19
      - color: '#e0b400'
        value: 25
      - color: '#e45e65'
        value: 2400
    color_thresholds_transition: hard
    entities:
      - entity: sensor.plant_01_moisture
        name: Humidité du sol
      - entity: sensor.plant_01_temperature
        name: Températire du sol
      - color: rgba(0,0,255,1)
        entity: binary_sensor.night_reworked
        name: Nuit
        show_line: false
        y_axis: secondary
    group: false
    hour24: true
    hours_to_show: 24
    line_width: 2
    name: Humidité et température du sol
    points_per_hour: 4
    show:
      extrema: true
      fill: fade
      icon: true
      labels: false
      name: true
      state: true
    state_map:
      - label: Day
        value: 'off'
      - label: Night
        value: 'on'
    type: custom:mini-graph-card
  - type: custom:flower-card
    entity: plant.jardin
    show_bars:
      - illuminance
      - humidity
      - moisture
      - conductivity
      - temperature
      - dli
    battery_sensor: sensor.demo_battery

Souces

Capteur Rehent