Moniteur de tunnels SSH Tunnelmon en version 1.1

Posté par  (site web personnel, Mastodon) . Édité par Benoît Sibaud et Florent Zara. Modéré par Nils Ratusznik. Licence CC By‑SA.
25
19
août
2022
Administration système

Tunnelmon, un moniteur de tunnels sécurisés SSH, sort en version 1.1. Il est publié sous GPLv3. Le code est en Python.

  • Il propose une interface de supervision et de gestion des tunnels SSH s'exécutant sur un système.
  • Il peut afficher soit une liste des tunnels sur la sortie standard, soit une interface interactive en mode texte dans le terminal.
  • Il peut également afficher la liste des connexions réseaux issues de chaque tunnel et leur état.

Capture d'écran

Tunnelmon gère aussi les tunnels mis en place avec autossh, une application qui peut redémarrer automatiquement vos tunnels en déroute.
Avec cette version 1.1, il gère maintenant les trois méthodes de redirection de ports proposées par SSH.

Aller plus loin

  • # utilisation ?

    Posté par  (site web personnel) . Évalué à 5.

    Il y a un moyen de rendre ses tunnels "industriel". En général, la DSI n'est pas joie que l'on fasse des troues partout dans ses réseaux. J'avoue que laisser passer un flux est cool, mais n'importe quoi peut passer à travers un tunnel SSH.

    Est-ce qu'il existe un moyen de n’autoriser qu'une application ? Je m'explique : le mot de passe est utile pour mettre en place le tunnel mais pas pour l'emprunter.

    En tout cas, avoir un outil qui remet en place les tunnels est utile.

    "La première sécurité est la liberté"

    • [^] # Re: utilisation ?

      Posté par  (site web personnel, Mastodon) . Évalué à 4. Dernière modification le 19 août 2022 à 09:24.

      Je ne suis pas sûr de comprendre ce que veux dire « le mot de passe est utile pour mettre en place le tunnel mais pas pour l'emprunter », mais il n'y a probablement pas de moyen de rendre un tunnel compatible avec les directives d'une DSI standard.

      • [^] # Re: utilisation ?

        Posté par  . Évalué à 2.

        « le mot de passe est utile pour mettre en place le tunnel mais pas pour l'emprunter »

        Cela signifie que, s'il faut un mot de passe¹ pour créer un tunnel avec SSH, il n'est pas nécessaire d'en avoir un pour utiliser le tunnel ainsi créé, puisqu'il correspond à un point d'entrée réseau, tout comme une interface réseau classique.


        ¹ … ou une phrase de passe, vu que c'est pour la phase d'authentification.

    • [^] # Re: utilisation ?

      Posté par  . Évalué à 10.

      Hello

      Dans ton authorizedkey tu peux limiter ce qu'il est possible de faire sur la machine de destination.

      command="/bin/myscript.sh",no-port- forwarding,no-X11-forwarding,no-agent-forwarding,no-pty
      ssh-dss AAAAB3....o9M9qz4xqGCqGXoJw= user@host
      

      Très utile pour les scripts de backup.

    • [^] # Re: utilisation ?

      Posté par  (site web personnel) . Évalué à 8.

      On peut limiter les tunnels ssh à un seul port ("port forwarding"), et même limiter l'hôte cible.
      Pour les tunnels complets, comme il s'agit d'interfaces réseau (virtuelles), on doit pouvoir utiliser iptable/netfilter ou autres firewalls.

  • # autossh

    Posté par  (site web personnel) . Évalué à 3.

    Franchement, je l'ai remplacé par un service systemd, j'ai pas vu la différence.

    [Unit]
    Description = Proxy Socks et tunnels
    After = network-online.target
    Conflicts = shutdown.target
    
    [Service]
    ExecStart = bash -c 'ssh -N proxy'
    Restart = always
    RestartSec = 1
    
    [Install]
    WantedBy = default.target
    
    • [^] # Re: autossh

      Posté par  . Évalué à 8.

      Pourquoi un bash -c ? Tu peux directement lancer /bin/ssh. Par contre, ce genre de service est assez dangereux (lancer en restart automatique un ssh en root, c'est s'exposer à des failles type TOCTOU vu le nombre de fichier parcourus par ssh pour démarrer). Je pense que stunnel est dans ce cas bien plus adapté (ou mieux, Wireguard)

      • [^] # Re: autossh

        Posté par  . Évalué à 4. Dernière modification le 21 août 2022 à 11:17.

        Wireguard, c'est bien. Quand UDP est autorisé. Ce n'est pas le cas dans mon entreprise, donc ça reste ssh sur le port 443 + sslh en face.

        Pour le service, il suffit de le mettre dans le répertoire utilisateur, et de le lancer avec systemd --user start tunnel.service

        • [^] # Re: autossh

          Posté par  . Évalué à 2.

          Si cela suffit, c'est que personne n'a encore pensé à descendre tout ce qui démarre en exposant la bannière, qui passe en clair:

          ~$ telnet localhost 22
          Trying ::1…
          Connected to localhost.
          Escape character is ']'.
          SSH-2.0-OpenSSH_8.4p1 Debian-5+deb11u1

          Oups! Même sur un 443 au lieu du 22, chez moi ils sont hélas plus malins.

  • # stunnel

    Posté par  . Évalué à 5.

    Juste pour info, pour créer des connexions distantes et chiffrées entre machine, il y a aussi stunnel.org qui est pas mal (et dans l'esprit de tunnels ssh).

    On peut le configurer avec des clés ssh, mais également avec un simple token partagé (psk). Voir ici: https://www.stunnel.org/auth.html

  • # Port unique, interface tun et sens

    Posté par  (site web personnel) . Évalué à 2.

    A noter qu'il est possible d'ouvrir un tunnel pour un port unique (au hasard, 443 pour un flux https) ou pour l'ensemble des flux avec une interface réseau de type tun de chaque côté.
    Dans chaque cas il est possible d'ouvrir dans le sens client (ssh) vers serveur (sshd) ou l'inverse.

  • # Petite revue de code

    Posté par  (site web personnel) . Évalué à 10.

    Bonjour,

    je ne vais pas faire une PR GitHub (je ne saurai pas tester que je n'ai rien cassé). Mais comme j'aime bien relire du Python, je me permets quelques petites remarques, parce que le code est plutôt de bonne facture.

    • À beaucoup d'endroits (pas partout) on trouve des logging.debug("Log in %s" % logfile) ; il est préférable d'écrire logging.debug("Log in %s", logfile) car l'interpolation de chaîne ne sera faite que si le message est affiché (le niveau est DEBUG ou moins dans cet exemple).
    • logging.debug("ici: %i %s", i, cmd[i]) j'imagine que le "ici" est un reliquat.
    • if type(tunnel) == AutoTunnel: je pense qu'un if isinstance(tunnel, AutoTunnel): est plus propre (même si au final tester les types c'est pas très élégant, et genre une méthode/propriété "is_auto", par exemple, serait plus appropriée je trouve).
    • if t.status != 'ESTABLISHED' and t.status != 'LISTEN': => if t.status in ('ESTABLISHED', 'LISTEN'):
    • Il y a beaucoup d'utilisation de eval() qui gagneraient à être remplacées par des getattr() ; outre le caractère sécurité, on évite aussi de reparser du code à chaque fois (eval() est vraiment à éviter ; je doute m'en être servi en presque 20 ans de Python).
    • Dans la mesure où Python 3.8 est requis, tu peux utiliser à moult endroits les f-strings qui sont vraiment super sympathiques.

    Il y a beaucoup de double-recherches dans les dictionnaires, quand une recherche simple sera plus efficace et souvent plus compacte.

    if forward in self.forwards:
        self.forward = self.forwards[forward]
    else:
        self.forward = "unknown"

    peut s'écrire simplement self.forward = self.forwards.get(forward, "unknown").

    De la même manière le bout de code suivant est intéressant (car plusieurs améliorations sont possibles)

        def __repr__(self):
            reps = [self.header]
            for t in self.tunnels:
                reps.append(str(self.tunnels[t]))
            return "\n".join(reps)

    On utilise .values() vu qu'on n'a pas besoin de la clé:

        def __repr__(self):
            reps = [self.header]
            for tunnel in self.tunnels.values():
                reps.append(str(tunnel))
            return "\n".join(reps)

    On utilise .extend() et une generator expression pour éviter d'avoir N appels à append() tout en étant plus court:

        def __repr__(self):
            reps = [self.header]
            # Remarque 1: pas besoin de mettre une autre paire de parenthèses car le generator est le seul paramètre
            reps.extend(str(tunnel) for tunnel in self.tunnels.values())
            # Remarque 2: version alternative avec map()
            # reps.extend(map(str, self.tunnels.values()))
    
            return "\n".join(reps)

    Avec les nouvelles fonctionnalités d'unpacking de Python 3, on arrive à ce résultat qui a un petit goût de programmation fonctionnelle plutôt cool:

        def __repr__(self):
            return "\n".join([self.header, *map(str, self.tunnels.values())])

    if c.raddr:
        raddr, rport = c.raddr
    else:
        raddr, rport = (None, None)

    ==> raddr, rport = c.raddr or (None, None)


    En espérant avoir été utile.
    Bon week-end !

    • [^] # Re: Petite revue de code

      Posté par  (site web personnel) . Évalué à 7.

      if t.status != 'ESTABLISHED' and t.status != 'LISTEN': => if t.status in ('ESTABLISHED', 'LISTEN'):

      Ça ne serait pas plutôt : if t.status not in ('ESTABLISHED', 'LISTEN'): (ajout de not) ?

      Pour nous émanciper des géants du numérique : Zelbinium !

      • [^] # Re: Petite revue de code

        Posté par  (site web personnel) . Évalué à 4.

        Oui tout à fait, erreur d’inattention toussa.

        • [^] # Re: Petite revue de code

          Posté par  (site web personnel, Mastodon) . Évalué à 7.

          Il ne faut vraiment pas hésiter à faire une pull request, même bancale. Ou au moins une issue. J'aurais oublié comment retrouver cet article quand je retrouverai l'énergie de bosser sur Tunnelmon (la base du code a genre 15 ans et je n'ai jamais fait de refactoring pour suivre les améliorations de Python) ; alors qu'un post sur Github sera toujours à portée de main. Et avec une PR, vous êtes crédités directement.

    • [^] # Re: Petite revue de code

      Posté par  . Évalué à 1.

      Merci pour cette intervention.
      Je débute/moyenne en Python, c'est très instructif de voir votre factorisation à coup de *map.

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.