Sign inSign up

onesystems/tools

By onesystems

Updated about 1 month ago

This project packages SMTP ingress, an optional web test UI, and IT tools.

Image
Networking
Security
API management
0

1.8K

onesystems/tools repository overview

OneSystems Tools (Docker)

This project packages SMTP ingress (Echo Mail with SPF, DKIM, DMARC analysis), an optional web test UI for one-shot recipient addresses, and IT tools (DNS, GeoIP, TLS/certs, and more) in a single Alpine-based Python image. The main entrypoint is echo_maild.py; orchestration examples live in docker-compose.yml.

The image is designed to be safely exposed publicly: SSRF-filtered outbound targets, HTTP token-bucket rate limits per client subnet, strict Content-Security-Policy with per-request nonces, trusted-proxy validation for X-Forwarded-*, non-root container process, and no open SMTP relay by default.


Features

  • SMTP listener (aiosmtpd) for configured addresses — classic Echo reply and/or test-<token>@… web flow
  • SPF, DKIM, DMARC evaluation (DNS-based; optional fixed resolvers via TOOLS_DNS_SERVERS)
  • Optional HTTP UI (Uvicorn + FastAPI) — dashboard, tool forms, JSON API under /api/tools/…
  • IT tools: DNS, PTR, subdomains, ping/traceroute, WHOIS/RDAP, GeoIP (local MaxMind MMDB), TLS/cert helpers, Base64/password/hashes, and more
  • Static assets bundled under files/ (Bootstrap 5.3.3, no CDN required)
  • Optional Matomo (TOOLS_MATOMO_*) and ad slots (TOOLS_WEB_ADS_*) for trusted HTML snippets only
  • Hardened for public exposure: SSRF filter (net_guard.py), HTTP rate limits (rate_limit.py), CSP with per-request nonce, HSTS, no-new-privileges, non-root runtime

Quick start (Docker Compose)

Pick exactly one Compose profile (see comments in docker-compose.yml):

  • bridge — published port mapping (e.g. host 25 → container 8025)
  • host-tls — host network + Caddy for HTTPS + web UI on localhost 8080

Do not run conflicting profiles on the same host port (e.g. both binding port 25).

services:
  tools:
    image: onesystems/tools:latest
    build: .
    restart: unless-stopped
    ports:
      - "25:8025"
      # With TOOLS_WEB_ENABLED=true, also expose HTTP:
      # - "8080:8080"
    environment:
      TOOLS_MAIL_ADDRESSES: [email protected]
      TOOLS_MAIL_HOSTNAME: tools.example.com
      # TOOLS_WEB_ENABLED: "true"
      # WEB_LISTEN_PORT: "8080"

Image

  • Docker Hub: onesystems/tools (tags e.g. latest, v1.1.0)
  • Multi-arch build (example):
docker buildx build --platform linux/amd64,linux/arm64 --push \
  --build-arg VERSION="1.1.0" \
  --build-arg BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
  --tag onesystems/tools:v1.1.0 --tag onesystems/tools:latest .

Configuration overview

Legacy ECHO_* / ECHO_TOOLS_* names are not read anymore — use TOOLS_* everywhere (e.g. TOOLS_MAIL_ADDRESSES, TOOLS_GEOIP_DB).

HTTP / Web UI (web_ui.py)
VariablePurpose
TOOLS_WEB_SUBPATHBase path for the web test UI (default /echo). Short URLs like …/new, …/w/, …/r/ hang off this.
TOOLS_WEB_ENABLEDtrue/1: start the HTTP server in echo_maild (with WEB_LISTEN_*).
TOOLS_WEB_FILES_DIRStatic files directory (logo/favicon); default files/ next to web_ui.py.
TOOLS_WEB_LOGO / TOOLS_WEB_FAVICONFilenames inside the files dir.
TOOLS_WEB_BRANDNavbar brand text (default Tools).
TOOLS_WEB_APP_NAMEapplication-name / og:site_name. If empty, uses TOOLS_WEB_BRAND, else Tools.
TOOLS_WEB_OG_LOCALEOpen Graph og:locale (default en_US).
TOOLS_WEB_SUBTITLEOptional subtitle in the compact top bar (no main nav).
TOOLS_WEB_FOOTER_COPYRIGHTFooter text; {YEAR} → current year. Empty → built-in default.
TOOLS_WEB_PRIVACY_URL / TOOLS_WEB_IMPRINT_URLOptional footer links.
TOOLS_WEB_ADS_*Raw HTML slots: AFTER_NAV, MAIN_TOP, MAIN_BOTTOM, FOOTER (trusted admins only).
TOOLS_IT_TOOLS_ENABLEDtrue/1: IT tools (HTML + /api/tools/…). false: mail/web test only.
TOOLS_WEB_CURL_ROOT_IPDefault true: GET / with curl/… User-Agent returns visible client IP as plain text.
TOOLS_WEB_META_*SEO / Open Graph (DESCRIPTION, KEYWORDS, AUTHOR, SITE_URL, …).
TOOLS_MATOMO_URLMatomo base URL (optional).
TOOLS_MATOMO_SITE_IDMatomo site id (required with TOOLS_MATOMO_URL).
TOOLS_MATOMO_COOKIE_DOMAINe.g. *.tools.example.com for cross-domain cookies; empty → omit.

Static files are served under /web-assets/…. Mounting ./files:/app/files replaces the entire directory from the image — include css/, js/, vendor/ on the host or mount single files only.

HTTP server (Uvicorn, in echo_maild)
VariablePurpose
WEB_LISTEN_HOST / WEB_LISTEN_PORTBind address / port (default 0.0.0.0 / 8080). In host-tls profile bind to 127.0.0.1 so only Caddy talks to Uvicorn.
WEB_FORWARDED_ALLOW_IPSComma-separated IPs whose X-Forwarded-* headers Uvicorn processes at all (default 127.0.0.1,::1). Set to your Caddy/nginx peer only.
TOOLS_TRUSTED_PROXIESAdditional app-level guard: comma-separated CIDRs whose forwarded headers the app trusts for client-IP / scheme / host resolution. Empty (default in bridge mode) = never trust X-Forwarded-*. Prevents IP spoofing, geoblock bypass, and cache-poisoning via X-Forwarded-Host.
UVICORN_LOG_LEVELUvicorn log level (default warning).
WEB_GEO_*Optional HTTP geo allow/deny by country (MaxMind Country/City MMDB).
Web test storage (one-shot addresses)
VariablePurpose
TOOLS_WEB_TTL_PENDINGSeconds to wait for mail after minting a token (default 3600).
TOOLS_WEB_TTL_STOREDSeconds to keep stored reports (default 86400).
SMTP ingress (echo_maild)
VariablePurpose
TOOLS_MAIL_ADDRESSESComma-separated recipient addresses.
TOOLS_RECEIVER_DOMAINOptional explicit domain for analysis.
TOOLS_MAIL_HOSTNAMEHELO / hostname in messages.
TOOLS_REPLY_ENABLEDtrue: send classic Echo replies; false (default): ingest + web tests only. Left off by default to prevent backscatter/reflection when the SMTP port is exposed publicly.
TOOLS_DNS_SERVERSOptional comma-separated resolver IPs for public lookups.
SMTP_LISTEN_HOST / SMTP_LISTEN_PORTSMTP bind (default 0.0.0.0 / 8025).
SMTP_*Rate limits, size limits, allow/deny lists, geo blocking, relay settings — see source and comments in docker-compose.yml.
IT tools API (it_tools.py)
VariablePurpose
TOOLS_GEOIP_DBPath to GeoLite2-City or GeoLite2-Country .mmdb inside the container.
TOOLS_SUBDOMAIN_CAP / TOOLS_PORTSCAN_CAP / …Caps for enumeration, port scans, RBL, certs — see README tables in repo history or it_tools.py.

Full German/legacy tables from older docs are superseded by this file; refer to the code for the exact default values and clamps.

SSRF filter for outbound targets (net_guard.py)

Every user-supplied host / IP / URL (TLS cert inspection, SMTP diagnose, port scan, ping, traceroute, SMTP RCPT probe, WHOIS/GeoIP, subdomain probing…) is validated before any outbound connection: syntactic hostname check, DNS resolution, and a public-only IP filter (loopback, RFC1918, link-local, CGNAT 100.64.0.0/10, cloud metadata 169.254.169.254, multicast, reserved — all rejected). The resolved IP is then used for socket.create_connection / openssl s_client while the original hostname is only kept as TLS SNI / SMTP HELO — this closes DNS-rebinding TOCTOU races against the filter.

VariablePurpose
TOOLS_ALLOW_PRIVATE_TARGETSDefault false. Opt-in bypass to allow private/loopback/link-local targets. Only enable in trusted, isolated deployments.
TOOLS_EXTRA_BLOCK_CIDRSComma-separated extra CIDRs that must always be blocked, regardless of is_global (e.g. your own 10.0.0.0/8 peers).

Argument injection into subprocesses (ping, traceroute, openssl s_client) is prevented with a whitelist-based safe_argv_arg() and an explicit -- end-of-options marker.

HTTP rate limit (rate_limit.py)

Per-client token-bucket middleware protects the whole HTTP surface — required because the API is publicly reachable without authentication. Two independent buckets:

  • API (paths under /api/…) — heavy work: DNS, outbound TCP, subprocess.
  • UI/assets — everything else.

Clients are grouped by subnet (default IPv6 /64, IPv4 /32) to prevent trivial IPv6 bypass. Idle buckets are periodically purged so the process RAM stays bounded even under IP rotation.

VariableDefaultPurpose
TOOLS_HTTP_API_RATE_PER_MIN60Sustained requests per minute per client bucket for /api/….
TOOLS_HTTP_API_BURST20Burst capacity for /api/….
TOOLS_HTTP_UI_RATE_PER_MIN300Sustained requests per minute per client bucket for UI / static assets.
TOOLS_HTTP_UI_BURST120Burst capacity for UI / static.
TOOLS_HTTP_IPV6_PREFIX64IPv6 prefix length used as the bucket key (/64 recommended).
TOOLS_HTTP_IPV4_PREFIX32IPv4 prefix length used as the bucket key.
TOOLS_HTTP_RATE_EXEMPT_CIDRS(empty)Networks that skip the rate limit entirely (e.g. your monitoring).
TOOLS_HTTP_RATE_LOGtrueLog every rejected request (not every hit).

A limit hit returns HTTP 429 Too Many Requests with a Retry-After header (JSON body for /api/…, plain text otherwise).


Web test flow (no return mail required)

With TOOLS_WEB_ENABLED=true and HTTP exposed:

  1. Open http://<host>:8080/<TOOLS_WEB_SUBPATH>/new (default …/echo/new) or the dashboard at /.
  2. You get test-<token>@<domain> using a domain from TOOLS_MAIL_ADDRESSES.
  3. Send a test message to that address.
  4. /w/<token> auto-refreshes; /r/<token> shows the report (headers, raw source, SPF/DKIM/DMARC).

Unlike the fixed echo@… address, test-* accepts with 250 quickly and evaluates auth when you open the report — use TOOLS_DKIM_DNS_TIMEOUT_SEC and TOOLS_SMTP_DATA_ANALYSIS_TIMEOUT_SEC to bound work during classic Echo replies.


Networking notes (NAT / Docker)

Outbound connections from the container often use an internal source IP. Remote MX/SPF checks see that IP, not necessarily your public NAT address — plan SPF, PTR, and optional SMTP_RELAY_* (submission with auth) accordingly.


Logs (basics)

  • Short internal connections to 127.0.0.1 at startup are normal (listener check).
  • Connection lost during _handle_client() after QUIT is normal.
  • Empty MAIL FROM:<> DSN-style messages are accepted without generating reply loops.

Security

The image is designed for public exposure of the HTTP API without authentication. Hardening in place:

  • SSRF filter on every outbound target (net_guard.py) — blocks loopback / RFC1918 / link-local / CGNAT / cloud metadata / multicast. TOOLS_ALLOW_PRIVATE_TARGETS=true to opt out, TOOLS_EXTRA_BLOCK_CIDRS to add sites.
  • DNS-rebinding TOCTOU protection — TLS cert inspect, SMTP diagnose, port scan and SMTP RCPT probe connect to the validated IP, keeping the hostname only as SNI/HELO.
  • Argument-injection guard for ping, traceroute, openssl s_client (whitelist chars + -- end-of-options).
  • HTTP rate limit (rate_limit.py) — token bucket per client subnet, separate API/UI limits, IPv6 /64 normalization, self-purging.
  • Trusted-proxy validationX-Forwarded-For / -Proto / -Host are only honored when the direct peer is inside TOOLS_TRUSTED_PROXIES. Prevents IP spoofing, geoblock bypass, and cache poisoning.
  • Security headers — HSTS (on HTTPS), X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy, Permissions-Policy, Cross-Origin-Opener-Policy, Cross-Origin-Resource-Policy (set by Caddy in host-tls).
  • Content-Security-Policy with per-request nonce and 'strict-dynamic' — no 'unsafe-inline' for scripts, frame-ancestors 'none', form-action 'self', base-uri 'self'.
  • Non-root container — process runs as nobody in both profiles. host-tls uses cap_add: NET_BIND_SERVICE + security_opt: no-new-privileges:true so it can bind port 25 without root.
  • SMTP backscatter/reflection off by defaultTOOLS_REPLY_ENABLED=false. Only turn on if the SMTP port is not exposed to arbitrary senders.
  • Dependencies pinned with the compatible-release operator (~=X.Y.0) in requirements.txt for reproducible builds.

Operational notes:

  • Not a public mailbox server — configure allowlists (SMTP_*) and limits.
  • Do not expose raw SMTP to the open internet without allow/deny lists and geo rules.
  • Treat TOOLS_WEB_ADS_* HTML slots as privileged input. The CSP with nonce + 'strict-dynamic' blocks unnonced inline scripts injected via ads, but stylistic XSS / phishing surface remains — only use trusted content.
  • When running behind a reverse proxy, set both WEB_FORWARDED_ALLOW_IPS (Uvicorn's own filter) and TOOLS_TRUSTED_PROXIES (app-level guard) to the proxy's peer address / CIDR.

Author

Michael Kleger — OneSystems GmbH

https://www.onesystems.ch · [email protected]


License

MIT — free for commercial and private use.

Tag summary

Content type

Image

Digest

sha256:f151c5bba

Size

39.1 MB

Last updated

about 1 month ago

docker pull onesystems/tools