From 7817e020f9564a1b19760e643760fdb6ed85692f Mon Sep 17 00:00:00 2001 From: Georgiy Sitnikov Date: Thu, 12 Feb 2026 09:59:49 +0100 Subject: [PATCH 01/19] Release automate --- .github/workflows/docker-release.yml | 56 ++++++++++++++++++++++++++++ Dockerfile | 8 +++- 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/docker-release.yml diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml new file mode 100644 index 0000000..cb032ff --- /dev/null +++ b/.github/workflows/docker-release.yml @@ -0,0 +1,56 @@ +name: Build and Push Docker Image on Tag + +on: + push: + tags: + - "v*.*.*" # triggers on tags like v1.0.0 + branches: ["master"] + +jobs: + build-and-push: + runs-on: ubuntu-latest + # runs-on: docker:cli + + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Extract tag name + id: tag + run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + + - name: Set metadata + id: meta + run: | + echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + echo "VCS_REF=${GITHUB_SHA}" >> $GITHUB_OUTPUT + echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64 + build-args: | + VERSION=${{ steps.tag.outputs.TAG }} + VCS_REF=${{ steps.meta.outputs.SHA }} + BUILD_DATE=${{ steps.meta.outputs.DATE }} + tags: | +# justinazoff/ssh-auth-logger:${{ steps.tag.outputs.TAG }} +# justinazoff/ssh-auth-logger:latest + gas85/ssh-auth-logger:${{ steps.tag.outputs.TAG }} + gas85/ssh-auth-logger:latest diff --git a/Dockerfile b/Dockerfile index ed3ef64..fbbd485 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,18 @@ FROM golang:latest +ARG VERSION=dev +ARG VCS_REF=dev +ARG BUILD_DATE=unknown + LABEL maintainer="Justin Azoff " \ org.opencontainers.image.title="ssh-auth-logger" \ org.opencontainers.image.description="A low/zero interaction ssh authentication logging honeypot" \ org.opencontainers.image.source="https://github.com/JustinAzoff/ssh-auth-logger" \ org.opencontainers.image.url="https://hub.docker.com/r/justinazoff/ssh-auth-logger" \ org.opencontainers.image.documentation="https://github.com/JustinAzoff/ssh-auth-logger#" \ - org.opencontainers.image.version="0.1.0" + org.opencontainers.image.version=$VERSION \ + org.opencontainers.image.revision=$VCS_REF \ + org.opencontainers.image.version=$VERSION ENV USER=nobody ENV SSHD_BIND=:2222 From 2dc4f2a1993bdff6b698d7ea015a79bc06294f98 Mon Sep 17 00:00:00 2001 From: Georgiy Sitnikov Date: Thu, 12 Feb 2026 10:01:31 +0100 Subject: [PATCH 02/19] Update build arguments in docker-release workflow --- .github/workflows/docker-release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index cb032ff..a42cde3 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -46,9 +46,9 @@ jobs: push: true platforms: linux/amd64,linux/arm64 build-args: | - VERSION=${{ steps.tag.outputs.TAG }} - VCS_REF=${{ steps.meta.outputs.SHA }} - BUILD_DATE=${{ steps.meta.outputs.DATE }} + VERSION=${{ steps.meta.outputs.VERSION }} + VCS_REF=${{ steps.meta.outputs.VCS_REF }} + BUILD_DATE=${{ steps.meta.outputs.BUILD_DATE }} tags: | # justinazoff/ssh-auth-logger:${{ steps.tag.outputs.TAG }} # justinazoff/ssh-auth-logger:latest From d1c8e368af0771092d58c9f22870faebb0159cd0 Mon Sep 17 00:00:00 2001 From: Georgiy Sitnikov Date: Thu, 12 Feb 2026 10:03:25 +0100 Subject: [PATCH 03/19] Minor update --- .github/workflows/docker-release.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index a42cde3..7cc888d 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -19,10 +19,6 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Extract tag name - id: tag - run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT - - name: Set metadata id: meta run: | @@ -52,5 +48,5 @@ jobs: tags: | # justinazoff/ssh-auth-logger:${{ steps.tag.outputs.TAG }} # justinazoff/ssh-auth-logger:latest - gas85/ssh-auth-logger:${{ steps.tag.outputs.TAG }} + gas85/ssh-auth-logger:${{ steps.meta.outputs.VERSION }} gas85/ssh-auth-logger:latest From 43b2c3390cc92d6144f915a62dfb4e4b71f2d612 Mon Sep 17 00:00:00 2001 From: Georgiy Sitnikov Date: Thu, 12 Feb 2026 10:04:12 +0100 Subject: [PATCH 04/19] Clean up Docker tags in docker-release.yml Removed commented-out Docker tags for justinazoff/ssh-auth-logger. --- .github/workflows/docker-release.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index 7cc888d..cc53fb4 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -46,7 +46,5 @@ jobs: VCS_REF=${{ steps.meta.outputs.VCS_REF }} BUILD_DATE=${{ steps.meta.outputs.BUILD_DATE }} tags: | -# justinazoff/ssh-auth-logger:${{ steps.tag.outputs.TAG }} -# justinazoff/ssh-auth-logger:latest gas85/ssh-auth-logger:${{ steps.meta.outputs.VERSION }} gas85/ssh-auth-logger:latest From 9130aa506f185b3d1679c080aebd42db1d65b7af Mon Sep 17 00:00:00 2001 From: Georgiy Sitnikov Date: Thu, 12 Feb 2026 10:06:01 +0100 Subject: [PATCH 05/19] Temporary disable master --- .github/workflows/docker-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index cc53fb4..9015c01 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -4,7 +4,7 @@ on: push: tags: - "v*.*.*" # triggers on tags like v1.0.0 - branches: ["master"] +# branches: ["master"] jobs: build-and-push: From ff58be827a3f5e88f0c60e4679279df02bdf38df Mon Sep 17 00:00:00 2001 From: Georgiy Sitnikov Date: Thu, 12 Feb 2026 11:08:50 +0100 Subject: [PATCH 06/19] add original repository --- .github/workflows/docker-release.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index 9015c01..89a81c0 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -4,7 +4,6 @@ on: push: tags: - "v*.*.*" # triggers on tags like v1.0.0 -# branches: ["master"] jobs: build-and-push: @@ -46,5 +45,5 @@ jobs: VCS_REF=${{ steps.meta.outputs.VCS_REF }} BUILD_DATE=${{ steps.meta.outputs.BUILD_DATE }} tags: | - gas85/ssh-auth-logger:${{ steps.meta.outputs.VERSION }} - gas85/ssh-auth-logger:latest + justinazoff/ssh-auth-logger:${{ steps.meta.outputs.VERSION }} + justinazoff/ssh-auth-logger:latest From 1fb61cb355b43e8763b155c64985995a5b75a0d2 Mon Sep 17 00:00:00 2001 From: Georgiy Sitnikov Date: Mon, 2 Mar 2026 14:33:02 +0100 Subject: [PATCH 07/19] - Add `.dockerignore` to reduce image size - Update Dockerfile with Telnet ports - Add Basic Telnet logger - Documentation update with default ports that were different as configure - Increase default rate as it was too low so many bots disconnects --- .dockerignore | 4 ++ Dockerfile | 3 +- README.md | 9 ++-- main.go | 147 ++++++++++++++++++++++++++++++++++++++++++-------- 4 files changed, 136 insertions(+), 27 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6d81417 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +.git +README.md +Dockerfile +Makefile diff --git a/Dockerfile b/Dockerfile index ed3ef64..68e754c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,6 +10,7 @@ LABEL maintainer="Justin Azoff " \ ENV USER=nobody ENV SSHD_BIND=:2222 +ENV TELNET_BIND=:2323 WORKDIR /app @@ -22,6 +23,6 @@ RUN go install . && \ USER $USER -EXPOSE 2222 +EXPOSE 2222 2323 CMD test -f /var/log/ssh-auth-logger.log || { echo 'Creating log file...' && touch /var/log/ssh-auth-logger.log ; }; /go/bin/ssh-auth-logger 2>&1 | tee -a /var/log/ssh-auth-logger.log \ No newline at end of file diff --git a/README.md b/README.md index 7b4c765..c0f51f3 100644 --- a/README.md +++ b/README.md @@ -73,8 +73,8 @@ services: container_name: ssh-auth-logger environment: # Following are default values -# - SSHD_RATE=120 # bits per second, emulate very slow connection -# - SSHD_BIND=:22 # Port and interface to listen +# - SSHD_RATE=320 # bits per second, emulate very slow connection +# - SSHD_BIND=:2222 # Port and interface sshd to listen # - SSHD_KEY_KEY="Take me to your leader" # It's a secret key that is used to generate a deterministic hash value for a given host IP address # - SSHD_MAX_AUTH_TRIES=6 # The minimum number of authentication attempts allowed # - SSHD_RSA_BITS=3072 # If you use 'rsa' you can also set RSA key size, 2048, 3072, 4096 (very rare) @@ -82,12 +82,15 @@ services: # - SSHD_SEND_BANNER=false # Send SSH Login Banner before Password prompt # - SSHD_LOG_CLEAR_PASSWORD=true # Log Passwords as clear text or Base64 coded # - SSHD_LOGS_FILTER="" # Comma-separated list of allowed fields. 'msg', 'level' and 'time' can't be removed. Following combinations are possible: "duser,src,spt,dst,dpt,client_version,server_version,password,keytype,fingerprint,server_key_type,destinationServicename,product" +# - TELNET_BIND=:2323 # Port and interface telnetd to listen +# - TELNET_LOG_CLEAR_PASSWORD=true # Log Passwords as clear text or Base64 coded - TZ=Europe/Berlin # You can set Time Zone to see logs with your local time volumes: # Mount log file if needed - /var/docker/ssh-auth-logger/log:/var/log ports: - - 2222:22 # SSH Auth Logger + - 2222:2222 # SSH Auth Logger + - 2323:2323 # SSH Auth Logger Telnet networks: # Use isolated docker network, so that other containers will be not reachable from it - isolated_net diff --git a/main.go b/main.go index 025248d..75faeb2 100644 --- a/main.go +++ b/main.go @@ -7,13 +7,13 @@ import ( "crypto/sha256" "encoding/binary" "errors" - "log" "math/rand" "net" "os" "strconv" "time" "strings" + "encoding/base64" "github.com/sirupsen/logrus" "golang.org/x/crypto/ssh" @@ -21,6 +21,10 @@ import ( const appName = "ssh-auth-logger" +var telnetBind string + +var telnetLogClearPassword bool + var errAuthenticationFailed = errors.New(":)") var commonFields = logrus.Fields{ @@ -63,6 +67,73 @@ type serverProfile struct { HostKeyType string // "rsa" or "ed25519" } +// Telnet handler +func handleTelnetConnection(conn net.Conn) { + defer conn.Close() + + logger.WithFields(connLogParameters(conn)). + WithField("destinationServicename", "telnetd"). + Info("Connection") + + limitedConn := newRateLimitedConn(conn, rate) + + // Fake banner + limitedConn.Write([]byte("Ubuntu 24.04 LTS\r\n")) + limitedConn.Write([]byte("login: ")) + + username, _ := readLine(limitedConn) + + limitedConn.Write([]byte("Password: ")) + password, _ := readLine(limitedConn) + + var loggedPassword any = password + // This will show the password in cleartext if telnetLogClearPassword is true, otherwise it will log the base64 encoded if telnetLogClearPassword is false + if telnetLogClearPassword { + loggedPassword = string(password) + } else { + loggedPassword = base64.StdEncoding.EncodeToString([]byte(password)) + } + + fields := connLogParameters(conn) + fields["duser"] = username + fields["password"] = loggedPassword + fields["protocol"] = "telnet" + + logger.WithFields(fields). + WithField("destinationServicename", "telnetd"). + Info("Telnet login attempt") + + time.Sleep(2 * time.Second) + limitedConn.Write([]byte("\r\nLogin incorrect\r\n")) +} + +// Simple Telnet Parser +func readLine(conn net.Conn) (string, error) { + buf := make([]byte, 1) + var result []byte + + for { + n, err := conn.Read(buf) + if err != nil || n == 0 { + return "", err + } + + // Ignore CR + if buf[0] == '\r' { + continue + } + + // End on LF + if buf[0] == '\n' { + break + } + + result = append(result, buf[0]) + } + + return strings.TrimSpace(string(result)), nil +} + // newRateLimitedConn returns a new rateLimitedConn. func newRateLimitedConn(conn net.Conn, rate int) *rateLimitedConn { return &rateLimitedConn{ @@ -277,6 +348,7 @@ func makeSSHConfig(conn net.Conn) ssh.ServerConfig { time.Sleep(base + jitter) var loggedPassword any = password + // This will convert bytes to string if logClearPassword is true, otherwise it will log the byte slice (which will be base64 encoded if LogClearPassword is false) if logClearPassword { loggedPassword = string(password) } @@ -397,9 +469,11 @@ func (f *FilteredJSONFormatter) Format(entry *logrus.Entry) ([]byte, error) { func init() { logrus.SetFormatter(&logrus.JSONFormatter{}) + telnetBind = getEnvWithDefault("TELNET_BIND", ":23") + sshd_bind = getEnvWithDefault("SSHD_BIND", ":22") sshd_key_key = getEnvWithDefault("SSHD_KEY_KEY", "Take me to your leader") - rateStr := getEnvWithDefault("SSHD_RATE", "120") // default rate is 120 bytes per second very slow... + rateStr := getEnvWithDefault("SSHD_RATE", "320") // default rate is 320 bytes per second very slow... var err error rate, err = strconv.Atoi(rateStr) if err != nil { @@ -424,20 +498,24 @@ func init() { sendBanner = sendBannerStr == "1" || sendBannerStr == "true" || sendBannerStr == "yes" logClearPasswordStr := getEnvWithDefault("SSHD_LOG_CLEAR_PASSWORD", "true") logClearPassword = logClearPasswordStr == "1" || logClearPasswordStr == "true" || logClearPasswordStr == "yes" + telnetLogClearPasswordStr := getEnvWithDefault("TELNET_LOG_CLEAR_PASSWORD", "true") + telnetLogClearPassword = telnetLogClearPasswordStr == "1" || telnetLogClearPasswordStr == "true" || telnetLogClearPasswordStr == "yes" // Comma-separated list of allowed fields, "" means all, " " means none logsEnv := getEnvWithDefault("SSHD_LOGS_FILTER", "") // Show Configuration on Startup logrus.WithFields(logrus.Fields{ - "SSHD_BIND": sshd_bind, - "SSHD_KEY_KEY": sshd_key_key, - "SSHD_RATE": rate, - "SSHD_MAX_AUTH_TRIES": maxAuthTries, - "SSHD_RSA_BITS": rsaBitsStr, - "SSHD_PROFILE_SCOPE": profileScope, - "SSHD_SEND_BANNER": sendBanner, - "SSHD_LOG_CLEAR_PASSWORD": logClearPassword, - "SSHD_LOGS_FILTER": logsEnv, + "SSHD_BIND": sshd_bind, + "SSHD_KEY_KEY": sshd_key_key, + "SSHD_RATE": rate, + "SSHD_MAX_AUTH_TRIES": maxAuthTries, + "SSHD_RSA_BITS": rsaBitsStr, + "SSHD_PROFILE_SCOPE": profileScope, + "SSHD_SEND_BANNER": sendBanner, + "SSHD_LOG_CLEAR_PASSWORD": logClearPassword, + "SSHD_LOGS_FILTER": logsEnv, + "TELNET_BIND": telnetBind, + "TELNET_LOG_CLEAR_PASSWORD": telnetLogClearPassword, }).Info("Starting SSH Auth Logger") // Configure allowed log fields from environment variable @@ -461,22 +539,45 @@ func init() { } func main() { - socket, err := net.Listen("tcp", sshd_bind) - if err != nil { - panic(err) - } - for { - conn, err := socket.Accept() + // SSH listener + go func() { + socket, err := net.Listen("tcp", sshd_bind) if err != nil { - log.Panic(err) + panic(err) } + // logrus.Infof("SSH listening on %s", sshd_bind) + for { + conn, err := socket.Accept() + if err != nil { + logrus.WithError(err).Warn("SSH listener accept failed") + continue + } - logger.WithFields(connLogParameters(conn)).Info("Connection") + logger.WithFields(connLogParameters(conn)).Info("SSH connection") - limitedConn := newRateLimitedConn(conn, rate) + limitedConn := newRateLimitedConn(conn, rate) + config := makeSSHConfig(conn) + go handleConnection(limitedConn, &config) + } + }() - config := makeSSHConfig(conn) // NEW CONFIG PER CONNECTION - go handleConnection(limitedConn, &config) - } + // Telnet listener + go func() { + telnetSocket, err := net.Listen("tcp", telnetBind) + if err != nil { + panic(err) + } + // logrus.Infof("Telnet listening on %s", telnetBind) + for { + conn, err := telnetSocket.Accept() + if err != nil { + logrus.WithError(err).Warn("Telnet listener accept failed") + continue + } + go handleTelnetConnection(conn) + } + }() + // Block forever + select {} } \ No newline at end of file From a2d869fd25c52c4827f3bd3e86410c6c4d88ce34 Mon Sep 17 00:00:00 2001 From: Georgiy Sitnikov Date: Mon, 2 Mar 2026 14:42:39 +0100 Subject: [PATCH 08/19] Add `TELNET_RATE` --- README.md | 5 ++++- main.go | 10 +++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c0f51f3..7c6fe16 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,9 @@ services: image: justinazoff/ssh-auth-logger:latest container_name: ssh-auth-logger environment: + - TZ=Europe/Berlin # You can set Time Zone to see logs with your local time # Following are default values + # SSHD Part # - SSHD_RATE=320 # bits per second, emulate very slow connection # - SSHD_BIND=:2222 # Port and interface sshd to listen # - SSHD_KEY_KEY="Take me to your leader" # It's a secret key that is used to generate a deterministic hash value for a given host IP address @@ -82,9 +84,10 @@ services: # - SSHD_SEND_BANNER=false # Send SSH Login Banner before Password prompt # - SSHD_LOG_CLEAR_PASSWORD=true # Log Passwords as clear text or Base64 coded # - SSHD_LOGS_FILTER="" # Comma-separated list of allowed fields. 'msg', 'level' and 'time' can't be removed. Following combinations are possible: "duser,src,spt,dst,dpt,client_version,server_version,password,keytype,fingerprint,server_key_type,destinationServicename,product" + # Telnet Part # - TELNET_BIND=:2323 # Port and interface telnetd to listen # - TELNET_LOG_CLEAR_PASSWORD=true # Log Passwords as clear text or Base64 coded - - TZ=Europe/Berlin # You can set Time Zone to see logs with your local time +# - TELNET_RATE=20 # bits per second, emulate very slow connection volumes: # Mount log file if needed - /var/docker/ssh-auth-logger/log:/var/log diff --git a/main.go b/main.go index 75faeb2..afc7be5 100644 --- a/main.go +++ b/main.go @@ -25,6 +25,8 @@ var telnetBind string var telnetLogClearPassword bool +var telnetRate int + var errAuthenticationFailed = errors.New(":)") var commonFields = logrus.Fields{ @@ -75,7 +77,7 @@ func handleTelnetConnection(conn net.Conn) { WithField("destinationServicename", "telnetd"). Info("Connection") - limitedConn := newRateLimitedConn(conn, rate) + limitedConn := newRateLimitedConn(conn, telnetRate) // Fake banner limitedConn.Write([]byte("Ubuntu 24.04 LTS\r\n")) @@ -479,6 +481,11 @@ func init() { if err != nil { logrus.Fatal("Invalid SSHD_RATE environment variable") } + telnetRateStr := getEnvWithDefault("TELNET_RATE", "20") // Could be slower than SSH + telnetRate, err = strconv.Atoi(telnetRateStr) + if err != nil || telnetRate <= 0 { + logrus.Fatal("Invalid TELNET_RATE environment variable") + } maxAuthTriesStr := getEnvWithDefault("SSHD_MAX_AUTH_TRIES", "6") // default amount of tries is 6-10. maxAuthTries, err = strconv.Atoi(maxAuthTriesStr) if err != nil { @@ -516,6 +523,7 @@ func init() { "SSHD_LOGS_FILTER": logsEnv, "TELNET_BIND": telnetBind, "TELNET_LOG_CLEAR_PASSWORD": telnetLogClearPassword, + "TELNET_RATE": telnetRate, }).Info("Starting SSH Auth Logger") // Configure allowed log fields from environment variable From 39e2859430d6d0ad7fb40a837e5aad6db30e8ebd Mon Sep 17 00:00:00 2001 From: Georgiy Sitnikov Date: Mon, 2 Mar 2026 15:44:32 +0100 Subject: [PATCH 09/19] Support existing banners for SSH in Telnet. Add Ubuntu 24.04 --- main.go | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index afc7be5..163a233 100644 --- a/main.go +++ b/main.go @@ -79,8 +79,35 @@ func handleTelnetConnection(conn net.Conn) { limitedConn := newRateLimitedConn(conn, telnetRate) - // Fake banner - limitedConn.Write([]byte("Ubuntu 24.04 LTS\r\n")) + + // Determine profile key (same logic as SSH) + var profileKey string + if profileScope == "remote_ip" { + host, _, err := net.SplitHostPort(conn.RemoteAddr().String()) + if err != nil { + host = conn.RemoteAddr().String() + } + profileKey = host + } else { + profileKey = getHost(conn.LocalAddr().String()) + } + + profile := getServerProfile(profileKey) + + // Start from SSH login banner + banner := profile.LoginBanner + + // Replace protocol-specific words for Telnet realism + banner = strings.ReplaceAll(banner, "SSH", "Telnet") + banner = strings.ReplaceAll(banner, "ssh", "telnet") + + // Convert LF to CRLF for telnet + banner = strings.ReplaceAll(banner, "\n", "\r\n") + + if banner != "" { + limitedConn.Write([]byte(banner)) + } + limitedConn.Write([]byte("login: ")) username, _ := readLine(limitedConn) @@ -293,6 +320,11 @@ var serverProfiles = []serverProfile{ LoginBanner: "Ubuntu 20.04.6 LTS\n\nUnauthorized access prohibited.\n", HostKeyType: "ed25519", }, + { + ServerVersion: "SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.14", + LoginBanner: "Ubuntu 24.04.6 LTS\n\nUnauthorized access prohibited.\n", + HostKeyType: "ed25519", + }, { ServerVersion: "SSH-2.0-OpenSSH_8.4", LoginBanner: "Debian GNU/Linux 11\n\nAuthorized users only.\n", From d0808c308b873388f97bbd7cb5e6a6abad44c6cc Mon Sep 17 00:00:00 2001 From: Georgiy Sitnikov Date: Thu, 5 Mar 2026 10:24:02 +0100 Subject: [PATCH 10/19] Add Dynamic Repo to the tags --- .github/workflows/docker-release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index 89a81c0..95defe4 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -45,5 +45,5 @@ jobs: VCS_REF=${{ steps.meta.outputs.VCS_REF }} BUILD_DATE=${{ steps.meta.outputs.BUILD_DATE }} tags: | - justinazoff/ssh-auth-logger:${{ steps.meta.outputs.VERSION }} - justinazoff/ssh-auth-logger:latest + ${{ github.repository }}:${{ steps.meta.outputs.VERSION }} + ${{ github.repository }}:latest From 87639bbab428f5f635f2b28a9b3932a4b03411c5 Mon Sep 17 00:00:00 2001 From: Georgiy Sitnikov Date: Thu, 5 Mar 2026 11:07:06 +0100 Subject: [PATCH 11/19] Repo Name lower Case correction --- .github/workflows/docker-release.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index 95defe4..e2356f4 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -34,6 +34,10 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Prepare image name + id: repo + run: echo "REPO=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT + - name: Build and push Docker image uses: docker/build-push-action@v5 with: @@ -45,5 +49,5 @@ jobs: VCS_REF=${{ steps.meta.outputs.VCS_REF }} BUILD_DATE=${{ steps.meta.outputs.BUILD_DATE }} tags: | - ${{ github.repository }}:${{ steps.meta.outputs.VERSION }} - ${{ github.repository }}:latest + docker.io/${{ steps.repo.outputs.REPO }}:${{ steps.meta.outputs.VERSION }} + docker.io/${{ steps.repo.outputs.REPO }}:latest From 769b443f6c5775d06b0a9f1c72d6d3778e91b52e Mon Sep 17 00:00:00 2001 From: Georgiy Sitnikov Date: Thu, 5 Mar 2026 11:54:21 +0100 Subject: [PATCH 12/19] Reduce Image size from 300+ MB to 10 MB --- .dockerignore | 6 ++++++ Dockerfile | 17 +++++++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..df41766 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +.git +README.md +Dockerfile +Makefile +.github +.gitignore \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index fbbd485..1963fd3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,12 @@ -FROM golang:latest +FROM golang:alpine AS builder + +WORKDIR /app + +COPY . . + +RUN go install . + +FROM alpine:latest ARG VERSION=dev ARG VCS_REF=dev @@ -17,12 +25,9 @@ LABEL maintainer="Justin Azoff " \ ENV USER=nobody ENV SSHD_BIND=:2222 -WORKDIR /app - -COPY . . +COPY --from=builder /go/bin/ssh-auth-logger /go/bin/ssh-auth-logger -RUN go install . && \ - touch /var/log/ssh-auth-logger.log && \ +RUN touch /var/log/ssh-auth-logger.log && \ chown $USER /var/log/ssh-auth-logger.log && \ chmod 644 /var/log/ssh-auth-logger.log From e872c7acf95bd68ef6e24ef74b60cd5458cb0980 Mon Sep 17 00:00:00 2001 From: Georgiy Sitnikov Date: Thu, 5 Mar 2026 12:13:43 +0100 Subject: [PATCH 13/19] strips basic `IAC ` sequences to make logging cleaner --- main.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/main.go b/main.go index 163a233..bd7152b 100644 --- a/main.go +++ b/main.go @@ -147,6 +147,16 @@ func readLine(conn net.Conn) (string, error) { return "", err } + b := buf[0] + + // TELNET IAC handling (skip command sequences) + if b == 255 { // IAC + // read next two bytes (command + option) + conn.Read(buf) + conn.Read(buf) + continue + } + // Ignore CR if buf[0] == '\r' { continue From d8ead9c5e22915aac7fdd5afe01e8df27b4c38e5 Mon Sep 17 00:00:00 2001 From: Georgiy Sitnikov Date: Thu, 5 Mar 2026 12:13:43 +0100 Subject: [PATCH 14/19] strips basic `IAC ` sequences to make logging cleaner --- main.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/main.go b/main.go index 163a233..bd7152b 100644 --- a/main.go +++ b/main.go @@ -147,6 +147,16 @@ func readLine(conn net.Conn) (string, error) { return "", err } + b := buf[0] + + // TELNET IAC handling (skip command sequences) + if b == 255 { // IAC + // read next two bytes (command + option) + conn.Read(buf) + conn.Read(buf) + continue + } + // Ignore CR if buf[0] == '\r' { continue From 1651df7627caa64b635051ca84f51b933ffb7f01 Mon Sep 17 00:00:00 2001 From: Georgiy Sitnikov Date: Thu, 5 Mar 2026 13:22:27 +0100 Subject: [PATCH 15/19] As we moved to Alpine, wget will not work as before --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7b4c765..790a7f9 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ services: memory: 100M healthcheck: # Will test if port is still open AND log file was not vanished by host machine log rotate - test: wget -v localhost$$SSHD_BIND --no-verbose --tries=1 --spider && test -s /var/log/ssh-auth-logger.log || exit 1 + test: pgrep ssh-auth-logger && test -s /var/log/ssh-auth-logger.log || exit 1 interval: 5m00s timeout: 5s retries: 2 From 38185973633adaa08c70eab7204d88d5d85515f8 Mon Sep 17 00:00:00 2001 From: Georgiy Sitnikov Date: Thu, 5 Mar 2026 13:33:37 +0100 Subject: [PATCH 16/19] Harmonize logging output --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index bd7152b..52d9f39 100644 --- a/main.go +++ b/main.go @@ -75,7 +75,7 @@ func handleTelnetConnection(conn net.Conn) { logger.WithFields(connLogParameters(conn)). WithField("destinationServicename", "telnetd"). - Info("Connection") + Info("Telnet connection") limitedConn := newRateLimitedConn(conn, telnetRate) From 6d16d818390aa6b3d20da86cbb0233df69e1e7ae Mon Sep 17 00:00:00 2001 From: Georgiy Sitnikov Date: Sat, 7 Mar 2026 13:21:07 +0100 Subject: [PATCH 17/19] Revise README with Docker and fail2ban details Updated example log entry and corrected Docker port mapping. Added fail2ban configuration instructions. --- README.md | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ef0d52e..921e542 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ ssh-auth-logger logs all authentication attempts as json making it easy to consu ssh-auth-logger uses HMAC to hash the destination IP address and a key in order to generate a consistently "random" key for every responding IP address. This means you can run ssh-auth-logger on a /16 and every ip address will appear with a different host key. Random sshd version reporting as well. -## Example log entry +### Example log entry This is normally logged on one line @@ -56,7 +56,7 @@ sudo setcap cap_net_bind_service=+ep ~/go/bin/ssh-auth-logger Bind to port 2222 in a host machine ```shell -docker run -t -i --rm -p 2222:22 justinazoff/ssh-auth-logger +docker run -t -i --rm -p 2222:2222 justinazoff/ssh-auth-logger ``` Docker compose example: @@ -115,3 +115,57 @@ services: options: max-size: 10m ``` + +## fail2ban configuration + +To configure [fail2ban](https://github.com/fail2ban/fail2ban) you have to create a filter: + +```shell +sudo nano /etc/fail2ban/filter.d/ssh-auth-logger.local +``` + +with following content: + +```shell +[Definition] +_daemon = ssh-auth-logger + +# Match JSON log line with a "time", "msg" fields and a "src" IP +failregex = ^.*"msg":"Request with (password|key)".*"src":"".*$ + ^.*"msg":"Telnet login attempt".*"src":"".*$ + +datepattern = %%Y-%%m-%%dT%%H:%%M:%%S(?:Z|%%z) + +ignoreregex = +``` + +The you have to update your `jail.local`: + +```shell +sudo nano /etc/fail2ban/jail.local +``` + +with following config: + +```shell +[ssh-auth-honeypot] +enabled = true +filter = ssh-auth-logger +action = iptables-allports + # Additonally you can setup abuseipdb reporting as per https://github.com/fail2ban/fail2ban/blob/master/config/action.d/abuseipdb.conf + #abuseipdb[custom_message="SSH Honeypot attack.\n", abuseipdb_category="18,22"] +# Docker mount log to the localsystem +logpath = /var/docker/ssh-auth-logger/log/ssh-auth-logger.log +# maxretry should be equal or more than in a SSHD_MAX_AUTH_TRIES +maxretry = 6 +# For very slow attacker that tries 3 passwords per day +findtime = 1d +# Ban time can be anything you like +bantime = 1d +``` + +After that you have to reload your fail2ban with command: + +```shell +sudo fail2ban-client reload +``` From daa62802824866af246434afaa132b1bd55f0cf0 Mon Sep 17 00:00:00 2001 From: GAS85 Date: Sun, 8 Mar 2026 13:49:01 +0100 Subject: [PATCH 18/19] Remove unsupported part of fail2ban --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 921e542..6a9a3db 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,7 @@ enabled = true filter = ssh-auth-logger action = iptables-allports # Additonally you can setup abuseipdb reporting as per https://github.com/fail2ban/fail2ban/blob/master/config/action.d/abuseipdb.conf - #abuseipdb[custom_message="SSH Honeypot attack.\n", abuseipdb_category="18,22"] + #abuseipdb[abuseipdb_category="18,22"] # Docker mount log to the localsystem logpath = /var/docker/ssh-auth-logger/log/ssh-auth-logger.log # maxretry should be equal or more than in a SSHD_MAX_AUTH_TRIES From c441176874fc30ec77ed57e486d2cc367b596d4b Mon Sep 17 00:00:00 2001 From: Georgiy Sitnikov Date: Thu, 21 May 2026 15:33:33 +0200 Subject: [PATCH 19/19] Rollback all changes to be synced with upstream master --- .dockerignore | 6 ---- .github/workflows/docker-release.yml | 53 ---------------------------- Dockerfile | 25 ++++--------- 3 files changed, 7 insertions(+), 77 deletions(-) delete mode 100644 .dockerignore delete mode 100644 .github/workflows/docker-release.yml diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index e6dad68..0000000 --- a/.dockerignore +++ /dev/null @@ -1,6 +0,0 @@ -.git -README.md -Dockerfile -Makefile -.github -.gitignore diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml deleted file mode 100644 index e2356f4..0000000 --- a/.github/workflows/docker-release.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: Build and Push Docker Image on Tag - -on: - push: - tags: - - "v*.*.*" # triggers on tags like v1.0.0 - -jobs: - build-and-push: - runs-on: ubuntu-latest - # runs-on: docker:cli - - permissions: - contents: read - packages: write - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set metadata - id: meta - run: | - echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT - echo "VCS_REF=${GITHUB_SHA}" >> $GITHUB_OUTPUT - echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Prepare image name - id: repo - run: echo "REPO=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT - - - name: Build and push Docker image - uses: docker/build-push-action@v5 - with: - context: . - push: true - platforms: linux/amd64,linux/arm64 - build-args: | - VERSION=${{ steps.meta.outputs.VERSION }} - VCS_REF=${{ steps.meta.outputs.VCS_REF }} - BUILD_DATE=${{ steps.meta.outputs.BUILD_DATE }} - tags: | - docker.io/${{ steps.repo.outputs.REPO }}:${{ steps.meta.outputs.VERSION }} - docker.io/${{ steps.repo.outputs.REPO }}:latest diff --git a/Dockerfile b/Dockerfile index de2f722..68e754c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,4 @@ -FROM golang:alpine AS builder - -WORKDIR /app - -COPY . . - -RUN go install . - -FROM alpine:latest - -ARG VERSION=dev -ARG VCS_REF=dev -ARG BUILD_DATE=unknown +FROM golang:latest LABEL maintainer="Justin Azoff " \ org.opencontainers.image.title="ssh-auth-logger" \ @@ -18,17 +6,18 @@ LABEL maintainer="Justin Azoff " \ org.opencontainers.image.source="https://github.com/JustinAzoff/ssh-auth-logger" \ org.opencontainers.image.url="https://hub.docker.com/r/justinazoff/ssh-auth-logger" \ org.opencontainers.image.documentation="https://github.com/JustinAzoff/ssh-auth-logger#" \ - org.opencontainers.image.version=$VERSION \ - org.opencontainers.image.revision=$VCS_REF \ - org.opencontainers.image.version=$VERSION + org.opencontainers.image.version="0.1.0" ENV USER=nobody ENV SSHD_BIND=:2222 ENV TELNET_BIND=:2323 -COPY --from=builder /go/bin/ssh-auth-logger /go/bin/ssh-auth-logger +WORKDIR /app + +COPY . . -RUN touch /var/log/ssh-auth-logger.log && \ +RUN go install . && \ + touch /var/log/ssh-auth-logger.log && \ chown $USER /var/log/ssh-auth-logger.log && \ chmod 644 /var/log/ssh-auth-logger.log