ssh

ssh

  • Configurando traefik com ssh

    Esse parece ser um tópico um tanto quanto... distópico? Talvez nem tanto. Mas todas as referências sobre o assunto na Internet estão incompletos e geralmente são pra versões mais antigas do traefik. Traefik é um roteador de conexões que funciona tanto como programa chamado por systemd (ou outro sistema de init se for um BSD), ou por container com algo como docker-compose, ou ainda diretamente em kubernetes. E não só funciona como proxy-reverse: pode também atuar como um servidor http pra suas conexões. E não somente tcp como udp. E escrito em Go!

    Eu precisei colocar um servidor ssh atrás de um traefik. E deu um certo trabalho. Pra testar e mostrar aqui também, usei docker-compose pro serviço. Isso facilitou a configuração. Pra backend com ssh, subi uma instância do gitea com o ssh na porta 2222. Usei também certificados letsencrypt nas portas https do serviço.

    Como um plus, ainda adicionei um allowlist pra acessar o serviço somente de certos IPs.

    Vamos pra configuração então. Primeiramente do traefik. Pra subir, eu configurei pra ouvir nas portas 80 (http), 443 (https), 8080 (traefik dashboard) e 2222 (ssh). O conteúdo de traefik/docker-compose.yml:

      
    services:
      traefik:
        image: "traefik:v3.3"
        container_name: "traefik"
        restart: unless-stopped
        environment:
          - TZ=Europe/Stockholm
        ports:
          - "80:80"
          - "443:443"
          - "8080:8080"
          - "2222:2222"
        volumes:
          - "./letsencrypt:/letsencrypt"
          - "/var/run/docker.sock:/var/run/docker.sock:ro"
          - "./traefik.yml:/traefik.yml:ro"
        networks:
          - traefik
    
    networks:
      traefik:
        name: traefik    
     
    

    Junto com essa configuração de container, ainda inclui a configuração do serviço em traefik/traefik.yml:

      
    api:
      dashboard: true
      insecure: true
      debug: true
    entryPoints:
      web:
        address: ":80"
        http:
          redirections:
            entryPoint:
              to: websecure
              scheme: https
      websecure:
        address: ":443"
      ssh:
        address: ":2222"
    serversTransport:
      insecureSkipVerify: true
    providers:
      docker:
        endpoint: "unix:///var/run/docker.sock"
        exposedByDefault: false
        network: traefik
    certificatesResolvers:
      letsencrypt:
        acme:
          email: This email address is being protected from spambots. You need JavaScript enabled to view it.
          storage: /letsencrypt/acme.json
          httpChallenge:
            # used during the challenge
            entryPoint: web
    
    log:
      level: WARN
     
    

    Aqui é possível ver que defino novamente os entrypoints pra 80, 443 e 2222. E no 80 (http) eu redireciono pro 443 (https). E certificado do letsencrypt.

    Tudo pronto no lado do traefik. Agora é o ponto de subir o serviço e preparar algumas coisas pra testar.

      
    # docker compose -p traefik -f traefik/docker-compose.yml up -d
    [+] Running 1/1
     ✔ Container traefik  Started                                0.4s
     
    

    Dentro do diretório traefik será criado um diretório letsencrypt e dentro dele terá o arquivo acme.json com seus certificados.

    Primeiramente vamos subir um container de teste pra ver se tudo funciona. O próprio projeto traefik fornece um container chamado whoami pra isso. O conteúdo de whois/docker-compose.yml:

      
    services:
      whoami:
        container_name: simple-service
        image: traefik/whoami
        labels:
            - "traefik.enable=true"
            - "traefik.http.routers.whoami.rule=Host(`whoami.tests.loureiro.eng.br`)"
            - "traefik.http.routers.whoami.entrypoints=websecure"
            - "traefik.http.routers.whoami.tls=true"
            - "traefik.http.routers.whoami.tls.certresolver=letsencrypt"
            - "traefik.http.services.whoami.loadbalancer.server.port=80"
        networks:
            - traefik
    networks:
      traefik:
        name: traefik    
     
    

    E habilitando o serviço com docker-compose:

      
    # docker compose -p whoami -f whoami/docker-compose.yml up -d
    WARN[0000] a network with name traefik exists but was not created for project "whoami".
    Set `external: true` to use an existing network 
    [+] Running 1/1
     ✔ Container simple-service  Started                                              0.3s 
     
    

    E um simples teste do serviço:

      
    ❯ curl -I http://whoami.tests.loureiro.eng.br
    HTTP/1.1 308 Permanent Redirect
    Location: https://whoami.tests.loureiro.eng.br/
    Date: Thu, 24 Apr 2025 12:43:55 GMT
    Content-Length: 18
    
    ❯ curl  https://whoami.tests.loureiro.eng.br
    Hostname: 0da540e1039c
    IP: 127.0.0.1
    IP: ::1
    IP: 172.18.0.3
    RemoteAddr: 172.18.0.2:52686
    GET / HTTP/1.1
    Host: whoami.tests.loureiro.eng.br
    User-Agent: curl/8.5.0
    Accept: */*
    Accept-Encoding: gzip
    X-Forwarded-For: 1.2.3.4
    X-Forwarded-Host: whoami.tests.loureiro.eng.br
    X-Forwarded-Port: 443
    X-Forwarded-Proto: https
    X-Forwarded-Server: 4cacedb0129a
    X-Real-Ip: 1.2.3.4
     
    

    O primeiro curl pro endpoint na porta 80 (http) recebe um redirect pra https. Perfeito! O segundo, os dados do container de forma transparente.

    O serviço funciona! Mas pra http e https. Falta ssh na porta 2222, que está nessa porta pra não atrapalhar o uso do ssh normal do sistema.

    Pra isso eu configurei um gitea, como descrevi no início do artigo. Seu docker-compose.yml é o seguinte:

      
        services:
      server:
        image: gitea/gitea:1.23-rootless #latest-rootless
        container_name: gitea
        environment:
         - GITEA__database__DB_TYPE=postgres
         - GITEA__database__HOST=db:5432
         - GITEA__database__NAME=gitea
         - GITEA__database__USER=gitea
         - GITEA__database__PASSWD=A4COU5a6JF5ZvWoSufi0L1aomSkzqww7s1wv039qy6o=
         - LOCAL_ROOT_URL=https://gitea.tests.loureiro.eng.br
         - GITEA__openid__ENABLE_OPENID_SIGNIN=false
         - GITEA__openid__ENABLE_OPENID_SIGNUP=false
         - GITEA__service__DISABLE_REGISTRATION=true
         - GITEA__service__SHOW_REGISTRATION_BUTTON=false
         - GITEA__server__SSH_DOMAIN=gitea.tests.loureiro.eng.br
         - GITEA__server__START_SSH_SERVER=true
         - GITEA__server__DISABLE_SSH=false    
        restart: always
        volumes:
          - gitea-data:/var/lib/gitea
          - gitea-config:/etc/gitea
          - /etc/timezone:/etc/timezone:ro
          - /etc/localtime:/etc/localtime:ro
        #ports:
        #  - "127.0.0.1:3000:3000"
        #  - "127.0.0.1:2222:2222"
        depends_on:
          - db
        networks:
          - traefik
        labels:
          - "traefik.enable=true"
          - "traefik.http.routers.giteaweb.rule=Host(`gitea.tests.loureiro.eng.br`)"
          - "traefik.http.routers.giteaweb.entrypoints=websecure"
          - "traefik.http.routers.giteaweb.tls=true"
          - "traefik.http.routers.giteaweb.tls.certresolver=letsencrypt"
          - "traefik.http.services.giteaweb.loadbalancer.server.port=3000"
          - "traefik.http.middlewares.giteaweb-ipwhitelist.ipallowlist.sourcerange=1.2.3.4, 4.5.6.7"
          - "traefik.http.routers.giteaweb.middlewares=giteaweb-ipwhitelist"
          - "traefik.tcp.routers.gitea-ssh.rule=HostSNI(`*`)"
          - "traefik.tcp.routers.gitea-ssh.entrypoints=ssh"
          - "traefik.tcp.routers.gitea-ssh.service=gitea-ssh-svc"
          - "traefik.tcp.services.gitea-ssh-svc.loadbalancer.server.port=2222"
          - "traefik.tcp.middlewares.giteassh-ipwhitelist.ipallowlist.sourcerange=1.2.3.4, 4.5.6.7"
          - "traefik.tcp.routers.gitea-ssh.middlewares=giteassh-ipwhitelist"
    
      db:
        image: postgres:17
        restart: always
        container_name: gitea_db
        environment:
          - POSTGRES_DB=gitea
          - POSTGRES_USER=gitea
          - POSTGRES_PASSWORD=A4COU5a6JF5ZvWoSufi0L1aomSkzqww7s1wv039qy6o=
    
        networks:
          - traefik
        volumes:
          - gitea-db:/var/lib/postgresql/data
    
    networks:
      traefik:
        name: traefik
    
    volumes:
      gitea-data:
      gitea-config:
      gitea-db:
     
    

    Pode ser visto que existe uma regra pra parte web, que roda na porta 3000 do container, e pra parte de ssh, que fica na porta 22222. Eu deixei comentado a parte que exporta as portas pra mostrar claramente que isso não é necessário. Aliás não funciona se especificar as portas.

    E finalmente rodando o serviço:

      
    # docker compose -p gitea -f gitea/docker-compose.yml up -d
    WARN[0000] a network with name traefik exists but was not created for project "gitea".
    Set `external: true` to use an existing network 
    [+] Running 2/2
     ✔ Container gitea_db  Started                   0.3s 
     ✔ Container gitea     Started                   1.0s     
     
    

    E vamos ao testes.

      
    ❯ curl -s  https://gitea.tests.loureiro.eng.br | head -n 10
    <!DOCTYPE html>
    <html lang="en-US" data-theme="gitea-auto">
    <head>
            <meta name="viewport" content="width=device-width, initial-scale=1">
            <title>Gitea: Git with a cup of tea</title>
            <link rel="manifest" href="data:application/json;base64,eyJuYW1lIjoiR2l0ZWE6IEdpdCB3aXRoIGEgY3VwIG9mIHRlYSIsInNob3J0X25hbWUiOiJHaXRlYTogR2l0IHdpdGggYSBjdXAgb2YgdGVhIiwic3RhcnRfdXJsIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwLyIsImljb25zIjpbeyJzcmMiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAvYXNzZXRzL2ltZy9sb2dvLnBuZyIsInR5cGUiOiJpbWFnZS9wbmciLCJzaXplcyI6IjUxMng1MTIifSx7InNyYyI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMC9hc3NldHMvaW1nL2xvZ28uc3ZnIiwidHlwZSI6ImltYWdlL3N2Zyt4bWwiLCJzaXplcyI6IjUxMng1MTIifV19">
            <meta name="author" content="Gitea - Git with a cup of tea">
            <meta name="description" content="Gitea (Git with a cup of tea) is a painless self-hosted Git service written in Go">
            <meta name="keywords" content="go,git,self-hosted,gitea">
            <meta name="referrer" content="no-referrer">
    ❯ telnet gitea.tests.loureiro.eng.br 2222
    Trying 1.2.3.4...
    Connected to gitea.tests.loureiro.eng.br.
    Escape character is '^]'.
    SSH-2.0-Go
    
    ^]
    telnet> q
    Connection closed.    
     
    

    E pronto! Temos o serviço funcionando e roteado pelo traefik. E podendo fazer ACL de ip do serviço sem precisar do firewall pra isso.

    Boa diversão!

    Nota: eu coloquei um básico da configuração do gitea pra exemplo. E não funciona se copiar e colar. Pra ter um serviço rodando, terá de adicionar mais algumas partes em environment.