Attackers constantly probe infrastructure you never intended to expose — admin panels, .env files, forgotten login pages. A honeypot logger captures those attempts silently and fires an alert the moment something interesting hits. This article walks through building a minimal but production-useful honeypot in Go that sends Telegram notifications in real time, giving you immediate visibility into who is scanning your network.
What We're Building
The setup is intentionally simple:
- A Go HTTP server listening on a configurable port (80, 8080, or whatever you expose)
- Every incoming request is logged with: IP, timestamp, method, path, user-agent, and optional body
- High-value hits (e.g.,
/.env,/admin,/wp-login.php) immediately trigger a Telegram message - All logs are written as newline-delimited JSON for easy parsing
This is not a replacement for a full deception stack like Canarytokens. It is a lightweight trip wire you can deploy in under 10 minutes on any VPS.
Setting Up the Telegram Bot
Before writing any Go, you need a bot token and a chat ID:
- Message
@BotFatheron Telegram and send/newbot - Copy the token (format:
123456789:AAF...) - Start a conversation with your bot, then call
https://api.telegram.org/bot<TOKEN>/getUpdatesin a browser - Find your
chat_idin the JSON response
Store both values as environment variables. Never hardcode them.
Building the Honeypot Server
Initialize the module and create main.go:
mkdir honeypot && cd honeypot
go mod init honeypot
Here is the complete server code:
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"time"
)
var sensitivePaths = []string{
"/.env", "/admin", "/wp-login", "/wp-admin",
"/config", "/.git", "/phpmyadmin", "/xmlrpc",
"/login", "/actuator", "/.aws", "/setup",
}
type HitLog struct {
Timestamp string `json:"ts"`
RemoteAddr string `json:"ip"`
Method string `json:"method"`
Path string `json:"path"`
UserAgent string `json:"ua"`
Headers map[string]string `json:"headers"`
Body string `json:"body,omitempty"`
Alert bool `json:"alert"`
}
var logFile *os.File
func isSensitive(path string) bool {
lower := strings.ToLower(path)
for _, prefix := range sensitivePaths {
if strings.HasPrefix(lower, prefix) {
return true
}
}
return false
}
func honeypotHandler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(io.LimitReader(r.Body, 4096))
defer r.Body.Close()
headers := make(map[string]string)
for k, v := range r.Header {
headers[k] = strings.Join(v, ", ")
}
hit := HitLog{
Timestamp: time.Now().UTC().Format(time.RFC3339),
RemoteAddr: r.RemoteAddr,
Method: r.Method,
Path: r.URL.RequestURI(),
UserAgent: r.UserAgent(),
Headers: headers,
Body: string(body),
Alert: isSensitive(r.URL.Path),
}
line, _ := json.Marshal(hit)
fmt.Fprintln(logFile, string(line))
if hit.Alert {
msg := fmt.Sprintf(
"Honeypot hit\nIP: %s\nPath: %s\nMethod: %s\nUA: %s\nTime: %s",
hit.RemoteAddr, hit.Path, hit.Method, hit.UserAgent, hit.Timestamp,
)
go sendTelegramAlert(msg)
}
// Return 200 to avoid triggering retry logic on the scanner side
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "OK")
}
func main() {
var err error
logFile, err = os.OpenFile("honeypot.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
if err != nil {
log.Fatal(err)
}
defer logFile.Close()
http.HandleFunc("/", honeypotHandler)
addr := ":8080"
log.Printf("Honeypot listening on %s", addr)
log.Fatal(http.ListenAndServe(addr, nil))
}
A few deliberate choices here:
- Return 200 to everything. This keeps automated scanners from marking the host as down and stopping their scan. You want them to keep probing.
- 4096-byte body limit. Enough to capture credential dumps and config file probes, not enough to crash under a payload flood.
- Goroutine for Telegram. The alert call must not block the HTTP response. If the Telegram API is slow, the request still completes and the log entry is written.
Sending Telegram Alerts
Create telegram.go alongside main.go:
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
func sendTelegramAlert(message string) {
token := os.Getenv("TELEGRAM_TOKEN")
chatID := os.Getenv("TELEGRAM_CHAT_ID")
if token == "" || chatID == "" {
return
}
payload, _ := json.Marshal(map[string]string{
"chat_id": chatID,
"text": message,
})
url := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", token)
resp, err := http.Post(url, "application/json", bytes.NewReader(payload))
if err != nil || resp.StatusCode != 200 {
// Silent fail — Telegram outages must not affect the honeypot
return
}
defer resp.Body.Close()
}
Run the whole thing with:
TELEGRAM_TOKEN=123456789:AAF... TELEGRAM_CHAT_ID=987654321 go run .
Deploying to a VPS
Build a static binary and deploy it with a minimal systemd unit:
[Unit]
Description=Go Honeypot Logger
[Service]
ExecStart=/usr/local/bin/honeypot
EnvironmentFile=/etc/honeypot/env
WorkingDirectory=/var/log/honeypot
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
Store TELEGRAM_TOKEN and TELEGRAM_CHAT_ID in /etc/honeypot/env with permissions 0600. To bind port 80 without running as root:
sudo setcap cap_net_bind_service=+ep /usr/local/bin/honeypot
For log rotation, logrotate handles the newline-delimited JSON cleanly. You can also ingest logs into SQLite with a small Python script that parses fields into proper columns — this makes querying by IP or path significantly faster than grepping raw JSON.
Adding Rate Limiting on Alerts
In production, a single scanner can generate hundreds of hits per minute across different paths. A simple in-memory rate limiter prevents Telegram spam:
import "sync"
var (
alertCounts = make(map[string]int)
alertMu sync.Mutex
)
func shouldAlert(ip string) bool {
alertMu.Lock()
defer alertMu.Unlock()
alertCounts[ip]++
return alertCounts[ip] <= 3
}
Call shouldAlert(hit.RemoteAddr) before sendTelegramAlert and reset the map hourly with a background goroutine. This keeps notifications actionable rather than overwhelming.
What You'll Actually See
After deploying on a fresh VPS with a public IP and no DNS record, traffic starts within minutes:
- Port scanners hit every path in the first hour
- Automated bots probe
/.env,/config/database.yml,/wp-login.phpwithin a few hours - Credential stuffing attempts surface within 24 hours if port 22 is exposed
The Telegram alerts create a real-time picture of active campaigns. Cross-reference the top 20 most-probed paths against your actual services. If attackers are probing paths that exist in your production stack, that is worth acting on immediately. For concrete steps to harden those services, the security hardening checklists we maintain cover Linux servers, Docker hosts, and Go APIs with specific configuration changes.
The Takeaway
A Go honeypot logger is under 150 lines of code, has zero external dependencies, and deploys in minutes. It gives you real-time visibility into what is actively being probed in your environment. The JSON log format makes it straightforward to feed into a SIEM, blocklist generation, or WAF rule updates.
Start with one VPS, tune the sensitive path list based on what you observe, and add rate limiting once the volume picks up. The intelligence you collect from a honeypot is often more actionable than a vulnerability scan — because it reflects what attackers are doing right now, not what they could theoretically do.
I run AYI NEDJIMI Consultants, a cybersecurity consulting firm. We publish free security hardening checklists — PDF and Excel.
Top comments (0)