This project packages SMTP ingress, an optional web test UI, and IT tools.
1.8K
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.
test-<token>@… web flowTOOLS_DNS_SERVERS)/api/tools/…files/ (Bootstrap 5.3.3, no CDN required)TOOLS_MATOMO_*) and ad slots (TOOLS_WEB_ADS_*) for trusted HTML snippets onlynet_guard.py), HTTP rate limits (rate_limit.py), CSP with per-request nonce, HSTS, no-new-privileges, non-root runtimePick 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 8080Do 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"
onesystems/tools (tags e.g. latest, v1.1.0)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 .
Legacy ECHO_* / ECHO_TOOLS_* names are not read anymore — use TOOLS_* everywhere (e.g. TOOLS_MAIL_ADDRESSES, TOOLS_GEOIP_DB).
web_ui.py)| Variable | Purpose |
|---|---|
TOOLS_WEB_SUBPATH | Base path for the web test UI (default /echo). Short URLs like …/new, …/w/, …/r/ hang off this. |
TOOLS_WEB_ENABLED | true/1: start the HTTP server in echo_maild (with WEB_LISTEN_*). |
TOOLS_WEB_FILES_DIR | Static files directory (logo/favicon); default files/ next to web_ui.py. |
TOOLS_WEB_LOGO / TOOLS_WEB_FAVICON | Filenames inside the files dir. |
TOOLS_WEB_BRAND | Navbar brand text (default Tools). |
TOOLS_WEB_APP_NAME | application-name / og:site_name. If empty, uses TOOLS_WEB_BRAND, else Tools. |
TOOLS_WEB_OG_LOCALE | Open Graph og:locale (default en_US). |
TOOLS_WEB_SUBTITLE | Optional subtitle in the compact top bar (no main nav). |
TOOLS_WEB_FOOTER_COPYRIGHT | Footer text; {YEAR} → current year. Empty → built-in default. |
TOOLS_WEB_PRIVACY_URL / TOOLS_WEB_IMPRINT_URL | Optional footer links. |
TOOLS_WEB_ADS_* | Raw HTML slots: AFTER_NAV, MAIN_TOP, MAIN_BOTTOM, FOOTER (trusted admins only). |
TOOLS_IT_TOOLS_ENABLED | true/1: IT tools (HTML + /api/tools/…). false: mail/web test only. |
TOOLS_WEB_CURL_ROOT_IP | Default 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_URL | Matomo base URL (optional). |
TOOLS_MATOMO_SITE_ID | Matomo site id (required with TOOLS_MATOMO_URL). |
TOOLS_MATOMO_COOKIE_DOMAIN | e.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.
echo_maild)| Variable | Purpose |
|---|---|
WEB_LISTEN_HOST / WEB_LISTEN_PORT | Bind 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_IPS | Comma-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_PROXIES | Additional 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_LEVEL | Uvicorn log level (default warning). |
WEB_GEO_* | Optional HTTP geo allow/deny by country (MaxMind Country/City MMDB). |
| Variable | Purpose |
|---|---|
TOOLS_WEB_TTL_PENDING | Seconds to wait for mail after minting a token (default 3600). |
TOOLS_WEB_TTL_STORED | Seconds to keep stored reports (default 86400). |
echo_maild)| Variable | Purpose |
|---|---|
TOOLS_MAIL_ADDRESSES | Comma-separated recipient addresses. |
TOOLS_RECEIVER_DOMAIN | Optional explicit domain for analysis. |
TOOLS_MAIL_HOSTNAME | HELO / hostname in messages. |
TOOLS_REPLY_ENABLED | true: 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_SERVERS | Optional comma-separated resolver IPs for public lookups. |
SMTP_LISTEN_HOST / SMTP_LISTEN_PORT | SMTP 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.py)| Variable | Purpose |
|---|---|
TOOLS_GEOIP_DB | Path 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.
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.
| Variable | Purpose |
|---|---|
TOOLS_ALLOW_PRIVATE_TARGETS | Default false. Opt-in bypass to allow private/loopback/link-local targets. Only enable in trusted, isolated deployments. |
TOOLS_EXTRA_BLOCK_CIDRS | Comma-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.
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/…) — heavy work: DNS, outbound TCP, subprocess.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.
| Variable | Default | Purpose |
|---|---|---|
TOOLS_HTTP_API_RATE_PER_MIN | 60 | Sustained requests per minute per client bucket for /api/…. |
TOOLS_HTTP_API_BURST | 20 | Burst capacity for /api/…. |
TOOLS_HTTP_UI_RATE_PER_MIN | 300 | Sustained requests per minute per client bucket for UI / static assets. |
TOOLS_HTTP_UI_BURST | 120 | Burst capacity for UI / static. |
TOOLS_HTTP_IPV6_PREFIX | 64 | IPv6 prefix length used as the bucket key (/64 recommended). |
TOOLS_HTTP_IPV4_PREFIX | 32 | IPv4 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_LOG | true | Log 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).
With TOOLS_WEB_ENABLED=true and HTTP exposed:
http://<host>:8080/<TOOLS_WEB_SUBPATH>/new (default …/echo/new) or the dashboard at /.test-<token>@<domain> using a domain from TOOLS_MAIL_ADDRESSES./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.
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.
127.0.0.1 at startup are normal (listener check).Connection lost during _handle_client() after QUIT is normal.MAIL FROM:<> DSN-style messages are accepted without generating reply loops.The image is designed for public exposure of the HTTP API without authentication. Hardening in place:
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.ping, traceroute, openssl s_client (whitelist chars + -- end-of-options).rate_limit.py) — token bucket per client subnet, separate API/UI limits, IPv6 /64 normalization, self-purging.X-Forwarded-For / -Proto / -Host are only honored when the direct peer is inside TOOLS_TRUSTED_PROXIES. Prevents IP spoofing, geoblock bypass, and cache poisoning.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).'strict-dynamic' — no 'unsafe-inline' for scripts, frame-ancestors 'none', form-action 'self', base-uri 'self'.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.TOOLS_REPLY_ENABLED=false. Only turn on if the SMTP port is not exposed to arbitrary senders.~=X.Y.0) in requirements.txt for reproducible builds.Operational notes:
SMTP_*) and limits.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.WEB_FORWARDED_ALLOW_IPS (Uvicorn's own filter) and TOOLS_TRUSTED_PROXIES (app-level guard) to the proxy's peer address / CIDR.Michael Kleger — OneSystems GmbH
https://www.onesystems.ch · [email protected]
MIT — free for commercial and private use.
Content type
Image
Digest
sha256:f151c5bba…
Size
39.1 MB
Last updated
about 1 month ago
docker pull onesystems/tools