SSH (Gitea) with Traefik V2

So for the last couple days I've been trying to get traefik and gitea to play nicely concerning the routing of gitea's ssh endpoint. It works if I don't route ssh via traefik at all, but as soon as I try to route it via traefik, I'll always get an error:

Permission denied (publickey).
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

Here are my configs:
Traefik docker-compose.yml

version: "3"

services:
  traefik:
    image: traefik:latest
    container_name: traefik
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - proxy
    ports:
      - 80:80
      - 443:443
      - 53:53/udp
      - 53:53/tcp
      - 8080:8080
      - 2222:2222/tcp
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik.yml:/traefik.yml:ro
      - ./acme.json:/acme.json
      - ./config:/configurations
      - traefik-logs:/var/log/traefik
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=${TZ}
      - CF_API_EMAIL=${CF_API_EMAIL}
      - CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN}
    labels:
      - traefik.enable=true
      - traefik.docker.network=proxy
      - traefik.http.routers.traefik-secure.entrypoints=websecure
      - traefik.http.routers.traefik-secure.rule=Host(`${FQDN}`)
      - traefik.http.routers.traefik-secure.service=api@internal
      - traefik.http.routers.traefik-secure.middlewares=user-auth@file,safe-ipwhitelist@file
      - traefik.http.routers.whoami-secure.service=noop@internal
      - traefik.http.routers.whoami-secure.tls.domains[0].main=${MAIN_DOMAIN}
      - traefik.http.routers.whoami-secure.tls.domains[0].sans=${SANS_DOMAIN}
      - traefik.http.middlewares.authelia.forwardauth.address=http://authelia:9091/api/verify?rd=https://${AUTHELIA_FQDN}/
      - traefik.http.middlewares.authelia.forwardauth.trustforwardheader=true
      - traefik.http.middlewares.authelia.forwardauth.authresponseheaders=Remote-User, Remote-Groups

networks:
  proxy:
    external: true
volumes:
  traefik-logs:

Traefik traefik.yml (excerpt)

entryPoints:
  web:
    address: :80
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https

  websecure:
    address: :443
    http:
      middlewares:
        - secureHeaders@file
        - crowdsec-bouncer@file
      tls:
        certResolver: cloudflare

  ssh:
    address: :2222

Gitea docker-compose.yml

version: "3"

services:
  gitea-db:
    image: postgres:alpine
    container_name: gitea-db
    networks:
      - gitea
    volumes:
      - ./data/postgres:/var/lib/postgresql/data
    restart: unless-stopped
    environment:
      - TZ=${TZ}
      - POSTGRES_USER=${DB_USER}
      - POSTGRES_PASSWORD=${DB_PW}
      - POSTGRES_DB=${DB_NAME}

  gitea:
    image: gitea/gitea:1.16.8
    container_name: gitea
    networks:
      - gitea
      - proxy
    environment:
      - TZ=${TZ}
      - DOMAIN=${FQDN}
      - ROOT_URL=https://${FQDN}
      - START_SSH_SERVER=true
      - SSH_DOMAIN=${FQDN}
      - SSH_PORT=22
      - SSH_LISTEN_PORT=2222
      - DB_TYPE=postgres
      - DB_HOST=${DB_HOST}
      - DB_NAME=${DB_NAME}
      - DB_USER=${DB_USER}
      - DB_PASSWD=${DB_PW}
    depends_on:
      - gitea-db
    volumes:
      - ./data/gitea:/data
    labels:
      - traefik.enable=true
      - traefik.docker.network=proxy
      - traefik.http.routers.gitea-secure.rule=Host(`${FQDN}`)
      - traefik.http.routers.gitea-secure.entrypoints=websecure
      - traefik.http.routers.gitea-secure.service=gitea
      - traefik.http.services.gitea.loadbalancer.server.port=3000
      - 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

networks:
  gitea:
  proxy:
    external: true

With this, I can access gitea via my browser and get clone links that look like this

git@gitea.mydomain.com:choque/Test.git

But when I try to clone it via

git clone git@gitea.mydomain.com:choque/Test.git

I get the error posted at the beginning. And yes, the ssh key is added to my profile and like I said, it works when I don't route gitea's ssh service through traefik.

Any help is appreciated!

Pretty sure this is gitea configuration related:

Serving on :222 working configuration

  gitea:
    image: gitea/gitea
    container_name: gitea
    deploy:
      replicas: 1
      labels:
        traefik.enable: "true"
        traefik.http.routers.gitea.rule: Host(`gitea.example.com`)
        traefik.http.services.gitea.loadBalancer.server.port: 3000
        traefik.tcp.routers.gitea-ssh.entrypoints: git
        traefik.tcp.routers.gitea-ssh.rule: HostSNI(`*`)
        traefik.tcp.services.gitea-ssh.loadbalancer.server.port: 22
    environment:
      - USER_UID=1000
      - USER_GID=1000
      - GITEA__repository__ENABLE_PUSH_CREATE_ORG=true
      - GITEA__server__OFFLINE_MODE=true
      - GITEA__server__ROOT_URL=https://gitea.example.com/
      - GITEA__server__SSH_PORT=222
      - GITEA__service__DISABLE_REGISTRATION=true
      - GITEA__service__REQUIRE_SIGNIN_VIEW=true
      - GITEA__service__DEFAULT_ALLOW_CREATE_ORGANIZATION=false
      - GITEA__service__DEFAULT_ENABLE_TIMETRACKING=false
      - GITEA__oauth2_client__ENABLE_AUTO_REGISTRATION=true
      - GITEA__oauth2_client__OPENID_CONNECT_SCOPES=email profile
      - GITEA__oauth2_client__USERNAME=email
      - GITEA__openid__ENABLE_OPENID_SIGNIN=false
2 Likes

Thanks for the reply @cakiwi . It works that way but as a result the git urls have ssh:// and the port added to them. So they look like this

ssh://git@gitea.mydomain.com:2222/choque/Test.git

When you set GITEA__server__SSH_PORT to 22 it results in much nicer URLs

git@gitea.mydomain.com:choque/Test.git

That's why I tried setting GITEA__server__SSH_PORT to 22 and GITEA__server__SSH_LISTEN_PORT to 2222. But I can't get it to work that way. I even tried using the built in ssh server via setting GITEA__server__START_SSH_SERVER to true

Anyone got an idea or a working configuration that does what I described?

Pushing for visibility.

Still looking for a working configuration

I'm sorry but why you want Træfik to handle the SSH connection for the container?

Either you use Træfik to listen on port 22 and you forward that to the container (and you don't have the port specified in the URLs and such) or you use another port and you need to have that specified.
Otherwise, if the host machine is Linux, use SSH redirection within the host configuration: Installation with Docker - Docs

1 Like

I solved it by using a macvlan/ipvlan network (with file provider):

/etc/traefik/dyn-config/gitea.yaml

tcp:
    routers:
        gitea-ssh-router:
            entryPoints:
                - ssh
            rule: HostSNI(`*`)
            service: gitea-ssh-service
    services:
        gitea-ssh-service:
            loadBalancer:
                servers:
                    - address: gitea:22

http:
    routers:
        router-gitea:
            rule: Host(`gitea.example.org`)
            service: service-gitea
            entryPoints:
                - web-secure

    services:
        service-gitea:
            loadBalancer:
                servers:
                    - url: http://gitea:3000

/etc/traefik/traefik.yaml

entryPoints:
    ssh:
        address: ":22"
    web:
        address: ":80"
        http:
            redirections:
                entryPoint:
                    to: web-secure
                    scheme: https
    web-secure:
        address: ":443"
        http:
            tls:
                certResolver: le

providers:
    file:
        directory: /etc/traefik/dyn-config
api:
    dashboard: true
global:
    sendAnonymousUsage: false
certificatesResolvers:
    le:
        acme:
            tlsChallenge: {}
            storage: /tls/acme.json

docker-compose.yaml for traefik:

services:
    traefik:
        image: traefik
        restart: always
        networks:
            traefik-net:
            macvlan50:
                ipv4_address: ${TRAEFIK_IP}
        expose:
            - "22"
            - "80"
            - "443"
        volumes:
            - ./dyn-config:/etc/traefik/dyn-config:ro
            - ./traefik.yaml:/etc/traefik/traefik.yaml:ro
            - ./tls:/tls

networks:
    traefik-net:
        name: traefik-net
    macvlan50:
        external: true

docker-compose.yaml for gitea

services:
    gitea:
        image: gitea/gitea
        environment:
            - USER_UID=1000
            - USER_GID=1000
            # server
            - GITEA__server__OFFLINE_MODE=true
            - GITEA__server__ROOT_URL=https://gitea.example.org/
            # database
            - GITEA__database__DB_TYPE=postgres
            - GITEA__database__HOST=db:5432
            - GITEA__database__NAME=gitea
            - GITEA__database__USER=gitea
            - GITEA__database__PASSWD=gitea
        restart: always
        networks:
            - gitea
            - traefik-net
        volumes:
            - ./gitea:/data
            - /etc/timezone:/etc/timezone:ro
            - /etc/localtime:/etc/localtime:ro
        expose:
            - "3000"
            - "22"
        depends_on:
            - db

    db:
        image: postgres
        restart: always
        environment:
            - POSTGRES_USER=gitea
            - POSTGRES_PASSWORD=gitea
            - POSTGRES_DB=gitea
        networks:
            - gitea
        volumes:
            - ./postgres:/var/lib/postgresql/data

networks:
    gitea:
    traefik-net:
        external: true

Optional (but fancy):

http:
    routers:
        dashboard:
            entryPoints:
                - web-secure
            rule: Host(`traefik.example.org`) # && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))
            service: dashboard@internal
        api:
            entryPoints:
                - web-secure
            rule: Host(`traefik.example.org`) && PathPrefix(`/api`)
            service: api@internal

Best regards