ipabusedb-docker-ssh: autoload .env via godotenv, document DOCKER_HOST passthrough, surface docker CLI errors

This commit is contained in:
sneak committed 2026-11-06 19:59:33 -08:00
1 parent 21bda80f88
commit b8c36689ae
6 files changed
+56 -2

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
# Copy to .env (gitignored). Variables already set in your shell win.
IPABUSEDB_API_KEY=
DOCKER_CONTAINER_NAME=sshd
# Point the docker CLI at a remote daemon; credentials above stay local.
DOCKER_HOST=ssh://user@sshd-host.example.com
+1
View File
@@ -1,2 +1,3 @@
/ipabusedb-docker-ssh
/reports.csv
/.env
+13 -1
View File
@@ -21,10 +21,21 @@ Categories are `18,22` (Brute-Force, SSH).
## Configuration
A `.env` file in the working directory is loaded at startup (see
`.env.example`; `.env` is gitignored). Variables already set in the
shell take precedence over the file.
| Variable | Purpose |
| ----------------------- | ------------------------------------------------ |
| ----------------------- | -------------------------------------------------------- |
| `DOCKER_CONTAINER_NAME` | container whose logs are read (required) |
| `IPABUSEDB_API_KEY` | AbuseIPDB API key (required only with `--submit`) |
| `DOCKER_HOST` | passed through to the `docker` CLI to target a remote daemon |
Because the program shells out to the `docker` CLI with its own
environment, `DOCKER_HOST` (and `DOCKER_CONTEXT`, `DOCKER_TLS_VERIFY`,
`DOCKER_CERT_PATH`) work exactly as they do for `docker` itself. Set
`DOCKER_HOST=ssh://user@host` in `.env` to read logs from a remote
daemon while the AbuseIPDB key never leaves the local machine.
Flags:
@@ -34,6 +45,7 @@ Flags:
| `-o FILE` | write the CSV to `FILE` instead of stdout |
| `--input FILE` | parse `FILE` (or `-` for stdin) instead of `docker logs` |
| `--submit` | POST the CSV to the AbuseIPDB bulk-report endpoint |
| `--env FILE` | env file to autoload (default `.env`; missing is ignored) |
## Usage
+2
View File
@@ -1,3 +1,5 @@
module sneak.berlin/go/hacks/ipabusedb-docker-ssh
go 1.26.5
require github.com/joho/godotenv v1.5.1
+2
View File
@@ -0,0 +1,2 @@
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+33 -1
View File
@@ -2,10 +2,13 @@
// "docker logs -t", extracts brute-force / invalid-user attempts, and
// emits a CSV suitable for the AbuseIPDB bulk-report endpoint.
//
// Environment:
// Environment (a .env file in the working directory is loaded first;
// variables already set in the process environment take precedence):
//
// DOCKER_CONTAINER_NAME container whose logs are read (required)
// IPABUSEDB_API_KEY AbuseIPDB API key (required with --submit)
// DOCKER_HOST passed through to the docker CLI, so e.g.
// ssh://user@host reads logs from a remote daemon
//
// Flags:
//
@@ -35,6 +38,8 @@ import (
"strings"
"sync"
"time"
"github.com/joho/godotenv"
)
const (
@@ -74,8 +79,13 @@ type parser struct {
mu sync.Mutex
events map[netip.Addr]*event
skipped int
// unparsed keeps the last few lines without a valid timestamp; the
// docker CLI writes its own errors to stderr without one.
unparsed []string
}
const keepUnparsed = 5
func newParser(cutoff time.Time) *parser {
return &parser{cutoff: cutoff, events: map[netip.Addr]*event{}}
}
@@ -92,6 +102,10 @@ func (p *parser) handleLine(line string) {
if err != nil {
p.mu.Lock()
p.skipped++
p.unparsed = append(p.unparsed, line)
if len(p.unparsed) > keepUnparsed {
p.unparsed = p.unparsed[1:]
}
p.mu.Unlock()
return
}
@@ -193,7 +207,13 @@ func readDockerLogs(ctx context.Context, p *parser, container string, since time
args = append(args, "--since", since.String())
}
args = append(args, container)
// cmd.Env is left nil so the docker CLI inherits the full process
// environment, including DOCKER_HOST / DOCKER_CONTEXT / DOCKER_TLS_*
// loaded from .env, and can therefore target a remote daemon.
cmd := exec.CommandContext(ctx, "docker", args...)
if host := os.Getenv("DOCKER_HOST"); host != "" {
slog.Info("reading logs via docker", "DOCKER_HOST", host, "container", container)
}
stdout, err := cmd.StdoutPipe()
if err != nil {
@@ -221,6 +241,12 @@ func readDockerLogs(ctx context.Context, p *parser, container string, since time
wg.Wait()
close(errs)
if err := cmd.Wait(); err != nil {
p.mu.Lock()
detail := strings.Join(p.unparsed, "\n")
p.mu.Unlock()
if detail != "" {
return fmt.Errorf("docker logs %s: %w\n%s", container, err, detail)
}
return fmt.Errorf("docker logs %s: %w", container, err)
}
for err := range errs {
@@ -286,8 +312,14 @@ func run() error {
outPath := flag.String("o", "", "write CSV to this file instead of stdout")
inPath := flag.String("input", "", "parse this file (or - for stdin) instead of running docker logs")
doSubmit := flag.Bool("submit", false, "POST the CSV to the AbuseIPDB bulk-report endpoint")
envFile := flag.String("env", ".env", "env file to autoload (missing file is ignored)")
flag.Parse()
// Load .env without overriding variables already in the environment.
if err := godotenv.Load(*envFile); err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("loading %s: %w", *envFile, err)
}
apiKey := os.Getenv("IPABUSEDB_API_KEY")
container := os.Getenv("DOCKER_CONTAINER_NAME")
if *doSubmit && apiKey == "" {