├── example ├── sftpdir │ └── testfile.txt ├── docker-compose.proxy.yml ├── docker-compose.yml ├── docker-compose.all-in-one.yml └── docker-compose.proxy-separated.yml ├── dockergen ├── Dockerfile └── sshproxy.tmpl ├── bundled ├── Procfile ├── reload-on-change.sh ├── docker-entrypoint.sh ├── generateConfig.sh ├── Dockerfile └── sshproxy.tmpl ├── Dockerfile ├── scripts ├── generateConfig.sh └── docker-entrypoint.sh └── README.md /example/sftpdir/testfile.txt: -------------------------------------------------------------------------------- 1 | It works ! -------------------------------------------------------------------------------- /dockergen/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM jwilder/docker-gen 2 | 3 | COPY sshproxy.tmpl /etc/docker-gen/templates/sshproxy.tmpl -------------------------------------------------------------------------------- /bundled/Procfile: -------------------------------------------------------------------------------- 1 | dockergen: docker-gen -watch /etc/docker-gen/templates/sshproxy.tmpl /etc/sshpiper/docker.generated.conf 2 | watcher: /reload-on-change.sh 3 | sshpiperd: /go/bin/sshpiperd -------------------------------------------------------------------------------- /example/docker-compose.proxy.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | ssh-proxy: 5 | build: ../bundled 6 | ports: 7 | - 2222:2222 8 | networks: 9 | - sshproxy 10 | volumes: 11 | - /var/run/docker.sock:/tmp/docker.sock:ro 12 | restart: unless-stopped 13 | 14 | networks: 15 | sshproxy: 16 | 17 | 18 | -------------------------------------------------------------------------------- /example/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | sftp: 5 | command: foo:pass:1001 6 | environment: 7 | - SSH_PROXY_USER=proxy 8 | - SSH_REDIRECT_USER=foo 9 | image: atmoz/sftp 10 | networks: 11 | - default 12 | - sshproxy 13 | volumes: 14 | - ./sftpdir:/home/foo 15 | 16 | networks: 17 | sshproxy: 18 | external: 19 | name: example_sshproxy 20 | 21 | 22 | -------------------------------------------------------------------------------- /example/docker-compose.all-in-one.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | ssh-proxy: 5 | build: ../bundled 6 | ports: 7 | - 2222:2222 8 | networks: 9 | - sshproxy 10 | volumes: 11 | - /var/run/docker.sock:/tmp/docker.sock:ro 12 | restart: unless-stopped 13 | 14 | sftp: 15 | command: foo:pass:1001 16 | depends_on: 17 | - ssh-proxy 18 | environment: 19 | - SSH_PROXY_USER=proxy 20 | - SSH_REDIRECT_USER=foo 21 | image: atmoz/sftp 22 | networks: 23 | - sshproxy 24 | volumes: 25 | - ./sftpdir:/home/foo 26 | 27 | networks: 28 | sshproxy: 29 | 30 | 31 | -------------------------------------------------------------------------------- /example/docker-compose.proxy-separated.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | dockergen: 5 | command: -watch /etc/docker-gen/templates/sshproxy.tmpl /etc/sshpiper/docker.generated.conf 6 | build: ../dockergen 7 | networks: 8 | - sshproxy 9 | volumes: 10 | - /var/run/docker.sock:/tmp/docker.sock:ro 11 | - ../dockergen/sshproxy.tmpl:/etc/docker-gen/templates/sshproxy.tmpl 12 | volumes_from: 13 | - ssh-proxy 14 | restart: always 15 | ssh-proxy: 16 | build: ../ 17 | ports: 18 | - 2222:2222 19 | networks: 20 | - sshproxy 21 | restart: always 22 | 23 | networks: 24 | sshproxy: 25 | 26 | 27 | -------------------------------------------------------------------------------- /bundled/reload-on-change.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | PID= 5 | 6 | while [ -z "${PID}" ]; do 7 | PID=$(pgrep sshpiper) 8 | sleep 1 9 | done 10 | 11 | # Ensure that the configuration is properly generated 12 | # if file has been generated before the watcher 13 | /generateConfig.sh 14 | 15 | # Restart process if config file has changed 16 | # ------------------------------------------ 17 | while inotifywait -q -e create,delete,modify,attrib /etc/sshpiper/docker.generated.conf; do 18 | echo 19 | echo "-----------------------------------------" 20 | echo 21 | echo "Config has changed. Restarting service..." 22 | echo 23 | /generateConfig.sh 24 | kill ${PID} 25 | PID= 26 | while [ -z "${PID}" ]; do 27 | PID=$(pgrep sshpiper) 28 | sleep 1 29 | done 30 | done -------------------------------------------------------------------------------- /bundled/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | ## Ensure correct time-zone is set 5 | ## ------------------------------- 6 | if [ -z ${TZ} ]; then 7 | export TZ="Europe/Paris" 8 | fi 9 | 10 | if [ -f /usr/share/zoneinfo/${TZ} ]; then 11 | cp -f /usr/share/zoneinfo/${TZ} /etc/localtime 12 | echo "${TZ}" > /etc/timezone 13 | fi 14 | 15 | ## Run only in classic startup (not when entering the container) 16 | ## ------------------------------------------------------------- 17 | if [ "$1" = 'forego' ]; then 18 | 19 | ## Run additional startup scripts 20 | ## ------------------------------ 21 | for f in /docker-entrypoint.d/*; do 22 | case "$f" in 23 | *.sh) echo "$0: running $f"; . "$f" ;; 24 | *) echo "$0: ignoring $f" ;; 25 | esac 26 | done 27 | 28 | if [ ! -f /etc/ssh/ssh_host_rsa_key ]; then 29 | ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key -q -N "" 30 | fi 31 | fi 32 | 33 | exec "$@" 34 | 35 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:latest 2 | 3 | ENV VERSION=0.3.1 4 | ENV DOCKER_HOST unix:///tmp/docker.sock 5 | 6 | RUN apt-get update \ 7 | && apt-get install -y libpam0g-dev libpam-google-authenticator bash inotify-tools \ 8 | && ln -sf /usr/include/security/_pam_types.h /usr/include/security/pam_types.h \ 9 | && mkdir -p /etc/sshpiper \ 10 | && touch /etc/sshpiper/docker.generated.conf \ 11 | && mkdir -p /go/src/github.com/tg123/sshpiper \ 12 | && git clone --branch v${VERSION} https://github.com/tg123/sshpiper /go/src/github.com/tg123/sshpiper \ 13 | && go install -ldflags "$(/go/src/github.com/tg123/sshpiper/sshpiperd/ldflags.sh)" -tags pam github.com/tg123/sshpiper/sshpiperd \ 14 | && apt-get clean \ 15 | && rm -r /var/lib/apt/lists/* 16 | 17 | ## SCRIPTS 18 | ## ------- 19 | 20 | COPY ./scripts/docker-entrypoint.sh / 21 | COPY ./scripts/generateConfig.sh / 22 | 23 | RUN mkdir -p /docker-entrypoint.d \ 24 | && touch /etc/sshpiper/docker.generated.conf \ 25 | && chmod +x /docker-entrypoint.sh \ 26 | && chmod +x /generateConfig.sh 27 | 28 | EXPOSE 2222 29 | VOLUME ["/var/sshpiper", "/etc/sshpiper"] 30 | ENTRYPOINT ["/docker-entrypoint.sh"] 31 | CMD ["/go/bin/sshpiperd"] -------------------------------------------------------------------------------- /bundled/generateConfig.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | CONFIG_DIR=/var/sshpiper 5 | CONFIG_FILE_PATH=/etc/sshpiper/docker.generated.conf 6 | 7 | init() { 8 | # Remove existing configuration 9 | if [ -d ${CONFIG_DIR} ]; then 10 | rm -rf ${CONFIG_DIR}/* 11 | fi 12 | # Ensure configuration folder exists 13 | mkdir -p ${CONFIG_DIR} 14 | } 15 | 16 | createConfig() { 17 | local user=$1 18 | local redirect=$2 19 | 20 | mkdir -p ${CONFIG_DIR}/${user} 21 | cat > ${CONFIG_DIR}/${user}/sshpiper_upstream < ${CONFIG_DIR}/${user}/sshpiper_upstream < /etc/timezone 13 | fi 14 | 15 | ## Run only in classic startup (not when entering the container) 16 | ## ------------------------------------------------------------- 17 | if [ "$1" = '/go/bin/sshpiperd' ]; then 18 | 19 | ## Run additional startup scripts 20 | ## ------------------------------ 21 | for f in /docker-entrypoint.d/*; do 22 | case "$f" in 23 | *.sh) echo "$0: running $f"; . "$f" ;; 24 | *) echo "$0: ignoring $f" ;; 25 | esac 26 | done 27 | 28 | if [ ! -f /etc/ssh/ssh_host_rsa_key ]; then 29 | ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key -q -N "" 30 | fi 31 | 32 | # Restart process if config file has changed 33 | # ------------------------------------------ 34 | while inotifywait -q -e create,delete,modify,attrib /etc/sshpiper/docker.generated.conf; do 35 | echo 36 | echo "-----------------------------------------" 37 | echo 38 | echo "Config has changed. Restarting service..." 39 | echo 40 | /generateConfig.sh 41 | [ -z "${PID}" ] || kill ${PID} 42 | /go/bin/sshpiperd & 43 | PID=$! 44 | done 45 | fi 46 | 47 | exec "$@" 48 | 49 | -------------------------------------------------------------------------------- /bundled/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:latest 2 | 3 | # Install sshpiper 4 | ENV VERSION=0.3.1 5 | 6 | RUN apt-get update \ 7 | && apt-get install -y libpam0g-dev libpam-google-authenticator bash inotify-tools \ 8 | && ln -sf /usr/include/security/_pam_types.h /usr/include/security/pam_types.h \ 9 | && mkdir -p /etc/sshpiper \ 10 | && touch /etc/sshpiper/docker.generated.conf \ 11 | && mkdir -p /go/src/github.com/tg123/sshpiper \ 12 | && git clone --branch v${VERSION} https://github.com/tg123/sshpiper /go/src/github.com/tg123/sshpiper \ 13 | && go install -ldflags "$(/go/src/github.com/tg123/sshpiper/sshpiperd/ldflags.sh)" -tags pam github.com/tg123/sshpiper/sshpiperd \ 14 | && apt-get clean \ 15 | && rm -r /var/lib/apt/lists/* 16 | 17 | # Install Forego 18 | RUN go get -u github.com/ddollar/forego 19 | 20 | # Install docker-gen 21 | ENV DOCKER_GEN_VERSION 0.7.4 22 | 23 | RUN wget https://github.com/jwilder/docker-gen/releases/download/$DOCKER_GEN_VERSION/docker-gen-linux-amd64-$DOCKER_GEN_VERSION.tar.gz \ 24 | && tar -C /usr/local/bin -xvzf docker-gen-linux-amd64-$DOCKER_GEN_VERSION.tar.gz \ 25 | && rm docker-gen-linux-amd64-$DOCKER_GEN_VERSION.tar.gz 26 | 27 | ## SCRIPTS 28 | ## ------- 29 | 30 | COPY ./docker-entrypoint.sh / 31 | COPY ./Procfile / 32 | COPY ./reload-on-change.sh /reload-on-change.sh 33 | COPY ./sshproxy.tmpl /etc/docker-gen/templates/sshproxy.tmpl 34 | COPY ./generateConfig.sh / 35 | 36 | RUN mkdir -p /docker-entrypoint.d \ 37 | && touch /etc/sshpiper/docker.generated.conf \ 38 | && chmod +x /docker-entrypoint.sh \ 39 | && chmod +x /generateConfig.sh \ 40 | && chmod +x /reload-on-change.sh 41 | 42 | ENV DOCKER_HOST unix:///tmp/docker.sock 43 | 44 | WORKDIR / 45 | EXPOSE 2222 46 | VOLUME ["/var/sshpiper", "/etc/sshpiper"] 47 | ENTRYPOINT ["/docker-entrypoint.sh"] 48 | CMD ["forego", "start", "-r"] -------------------------------------------------------------------------------- /bundled/sshproxy.tmpl: -------------------------------------------------------------------------------- 1 | {{ $CurrentContainer := where $ "ID" .Docker.CurrentContainerID | first }} 2 | 3 | {{ define "config" }} 4 | {{ if .Address }} 5 | {{/* If we got the containers from swarm and this container's port is published to host, use host IP:PORT */}} 6 | {{ if and .Container.Node.ID .Address.HostPort }} 7 | {{ .Name }}|{{ if .Container.Env.SSH_REDIRECT_USER }}{{ .Container.Env.SSH_REDIRECT_USER }}@{{end}}{{ .Container.Node.Address.IP }}{{ if .Container.Env.SSH_REDIRECT_PORT }}:{{ .Container.Env.SSH_REDIRECT_PORT }}{{end}} 8 | {{/* If there is no swarm node or the port is not published on host, use container's IP:PORT */}} 9 | {{ else if .Network }} 10 | {{ .Name }}|{{ if .Container.Env.SSH_REDIRECT_USER }}{{ .Container.Env.SSH_REDIRECT_USER }}@{{end}}{{ .Network.IP }}{{ if .Container.Env.SSH_REDIRECT_PORT }}:{{ .Container.Env.SSH_REDIRECT_PORT }}{{end}} 11 | {{ end }} 12 | {{ else if .Network }} 13 | {{ if .Network.IP }} 14 | {{ .Name }}|{{ if .Container.Env.SSH_REDIRECT_USER }}{{ .Container.Env.SSH_REDIRECT_USER }}@{{end}}{{ .Network.IP }}{{ if .Container.Env.SSH_REDIRECT_PORT }}:{{ .Container.Env.SSH_REDIRECT_PORT }}{{end}} 15 | {{ end }} 16 | {{ end }} 17 | 18 | {{ end }} 19 | 20 | {{ range $host, $containers := groupByMulti $ "Env.SSH_PROXY_USER" "," }} 21 | 22 | {{ $host := trim $host }} 23 | {{ $is_regexp := hasPrefix "~" $host }} 24 | {{ $upstream_name := when $is_regexp (sha1 $host) $host }} 25 | 26 | {{ range $container := $containers }} 27 | {{ $addrLen := len $container.Addresses }} 28 | 29 | {{ range $knownNetwork := $CurrentContainer.Networks }} 30 | {{ range $containerNetwork := $container.Networks }} 31 | {{ if (and (ne $containerNetwork.Name "ingress") (or (eq $knownNetwork.Name $containerNetwork.Name) (eq $knownNetwork.Name "host"))) }} 32 | {{/* If only 1 port exposed, use that */}} 33 | {{ if eq $addrLen 1 }} 34 | {{ $address := index $container.Addresses 0 }} 35 | {{ template "config" (dict "Container" $container "Address" $address "Network" $containerNetwork "Name" $host) }} 36 | {{/* If more than one port exposed, use the one matching SSH_PORT env var, falling back to standard ssh port 22 */}} 37 | {{ else }} 38 | {{ $port := coalesce $container.Env.SSH_PORT "22" }} 39 | {{ $address := where $container.Addresses "Port" $port | first }} 40 | {{ template "config" (dict "Container" $container "Address" $address "Network" $containerNetwork "Name" $host) }} 41 | {{ end }} 42 | {{ end }} 43 | {{ end }} 44 | {{ end }} 45 | {{ end }} 46 | 47 | {{ end }} 48 | -------------------------------------------------------------------------------- /dockergen/sshproxy.tmpl: -------------------------------------------------------------------------------- 1 | {{ $CurrentContainer := where $ "ID" .Docker.CurrentContainerID | first }} 2 | 3 | {{ define "config" }} 4 | {{ if .Address }} 5 | {{/* If we got the containers from swarm and this container's port is published to host, use host IP:PORT */}} 6 | {{ if and .Container.Node.ID .Address.HostPort }} 7 | {{ .Name }}|{{ if .Container.Env.SSH_REDIRECT_USER }}{{ .Container.Env.SSH_REDIRECT_USER }}@{{end}}{{ .Container.Node.Address.IP }}{{ if .Container.Env.SSH_REDIRECT_PORT }}:{{ .Container.Env.SSH_REDIRECT_PORT }}{{end}} 8 | {{/* If there is no swarm node or the port is not published on host, use container's IP:PORT */}} 9 | {{ else if .Network }} 10 | {{ .Name }}|{{ if .Container.Env.SSH_REDIRECT_USER }}{{ .Container.Env.SSH_REDIRECT_USER }}@{{end}}{{ .Network.IP }}{{ if .Container.Env.SSH_REDIRECT_PORT }}:{{ .Container.Env.SSH_REDIRECT_PORT }}{{end}} 11 | {{ end }} 12 | {{ else if .Network }} 13 | {{ if .Network.IP }} 14 | {{ .Name }}|{{ if .Container.Env.SSH_REDIRECT_USER }}{{ .Container.Env.SSH_REDIRECT_USER }}@{{end}}{{ .Network.IP }}{{ if .Container.Env.SSH_REDIRECT_PORT }}:{{ .Container.Env.SSH_REDIRECT_PORT }}{{end}} 15 | {{ end }} 16 | {{ end }} 17 | 18 | {{ end }} 19 | 20 | {{ range $host, $containers := groupByMulti $ "Env.SSH_PROXY_USER" "," }} 21 | 22 | {{ $host := trim $host }} 23 | {{ $is_regexp := hasPrefix "~" $host }} 24 | {{ $upstream_name := when $is_regexp (sha1 $host) $host }} 25 | 26 | {{ range $container := $containers }} 27 | {{ $addrLen := len $container.Addresses }} 28 | 29 | {{ range $knownNetwork := $CurrentContainer.Networks }} 30 | {{ range $containerNetwork := $container.Networks }} 31 | {{ if (and (ne $containerNetwork.Name "ingress") (or (eq $knownNetwork.Name $containerNetwork.Name) (eq $knownNetwork.Name "host"))) }} 32 | {{/* If only 1 port exposed, use that */}} 33 | {{ if eq $addrLen 1 }} 34 | {{ $address := index $container.Addresses 0 }} 35 | {{ template "config" (dict "Container" $container "Address" $address "Network" $containerNetwork "Name" $host) }} 36 | {{/* If more than one port exposed, use the one matching SSH_PORT env var, falling back to standard ssh port 22 */}} 37 | {{ else }} 38 | {{ $port := coalesce $container.Env.SSH_PORT "22" }} 39 | {{ $address := where $container.Addresses "Port" $port | first }} 40 | {{ template "config" (dict "Container" $container "Address" $address "Network" $containerNetwork "Name" $host) }} 41 | {{ end }} 42 | {{ end }} 43 | {{ end }} 44 | {{ end }} 45 | {{ end }} 46 | 47 | {{ end }} 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ssh-proxy sets up a container running sshpiper and docker-gen generates reverse proxy configs for sshpiper and reloads sshpiper when containers are started and stopped. 2 | 3 | This largely inspired by nginx-proxy of jwilder. 4 | 5 | ### Usage 6 | 7 | To run it: 8 | 9 | $ docker run -d -p 2222:2222 -v /var/run/docker.sock:/tmp/docker.sock:ro mickaelperrin/ssh-proxy 10 | 11 | Then start any containers you want proxied with an env var `SSH_PROXY_USER=myproxyuser` and an optional env var `SSH_REDIRECT_USER=foo` 12 | 13 | The containers being proxied must [expose](https://docs.docker.com/engine/reference/run/#expose-incoming-ports) the port to be proxied, either by using the `EXPOSE` directive in their `Dockerfile` or by using the `--expose` flag to `docker run` or `docker create`. 14 | 15 | $ docker run -e SSH_PROXY_USER=myproxyuser -e SSH_REDIRECT_USER=foo --expose=22 ... 16 | 17 | ### Docker Compose 18 | 19 | ```yaml 20 | version: '2' 21 | 22 | services: 23 | ssh-proxy: 24 | build: ../bundled 25 | ports: 26 | - 2222:2222 27 | networks: 28 | - sshproxy 29 | volumes: 30 | - /var/run/docker.sock:/tmp/docker.sock:ro 31 | restart: unless-stopped 32 | 33 | sftp: 34 | command: foo:pass:1001 35 | depends_on: 36 | - ssh-proxy 37 | environment: 38 | - SSH_PROXY_USER=proxy 39 | - SSH_REDIRECT_USER=foo 40 | image: atmoz/sftp 41 | networks: 42 | - sshproxy 43 | volumes: 44 | - ./sftpdir:/home/foo 45 | 46 | networks: 47 | sshproxy: 48 | ``` 49 | 50 | ```shell 51 | $ docker-compose up 52 | $ sftp -P 2222 proxy@127.0.0.1:testfile.txt 53 | ``` 54 | 55 | ### Default User 56 | 57 | The ENV variable `SSH_REDIRECT_USER` can be omitted and will be defaulted to the value of `SSH_PROXY_USER` 58 | 59 | ### Separate Containers 60 | 61 | ssh-proxy can also be run as two separate containers using the [jwilder/docker-gen](https://index.docker.io/u/jwilder/docker-gen/) image and the separated image. 62 | 63 | You may want to do this to prevent having the docker socket bound to a publicly exposed container service. 64 | 65 | You can demo this pattern with docker-compose: 66 | 67 | ```console 68 | $ docker-compose --file example/docker-compose.proxy-separated.yml up 69 | $ docker-compose --file example/docker-compose.yml up 70 | $ sftp -P 2222 proxy@127.0.0.1:testfile.txt 71 | ``` 72 | 73 | To run ssh-proxy as a separate container you'll need to have sshproxy.tmpl file on your host system or use the dockergen version with in in dockergen folder. 74 | 75 | First create a network: 76 | 77 | $ docker create network ssh-proxy 78 | 79 | Then start sshpiper: 80 | 81 | $ docker run -d -p 2222:2222 --name ssh-proxy --net ssh-proxy -t mickaelperrin/ssh-proxy-alone 82 | 83 | Then start the docker-gen container with the template: 84 | 85 | ``` 86 | $ docker run --volumes-from ssh-proxy \ 87 | -v /var/run/docker.sock:/tmp/docker.sock:ro \ 88 | -v $(pwd)/dockergen/sshproxy.tmpl:/etc/docker-gen/templates/sshproxy.tmpl \ 89 | -t jwilder/docker-gen -watch /etc/docker-gen/templates/sshproxy.tmpl /etc/sshpiper/docker.generated.conf 90 | ``` 91 | 92 | Finally, start your containers with `SSH_PROXY_USER` environment variables. 93 | 94 | $ docker run -e SSH_PROXY_USER=proxy ... 95 | 96 | ### Contributing 97 | 98 | Before submitting pull requests or issues, please check github to make sure an existing issue or pull request is not already open. 99 | 100 | ### Acknowledgement 101 | 102 | Thanks a lot to : 103 | 104 | - jwilder for his awesome docker-gen and nginx-proxy tools 105 | - tg123 for his awesome ssh proxy tool 106 | 107 | --------------------------------------------------------------------------------