Skip to content

About

Policy server that checks IPs and blocks senders, if they come from different countries or if they come from too many different IP addresses

Resources

Stars

2 stars

Watchers

1 watching

Forks

Latest commit

 

History

351 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

geoip-policyd

About

Postfix-Submission policy server that checks sender IPs and blocks senders, if they come from too many countries or if they come from too many IP addresses.

Features

  • GeoIP policy service for Postfix
  • Custom settings to define different limits for IPs and countries per sender
  • LDAP support (optional)
  • REST interface to interact with the service on the fly
  • Actions
  • Prometheus metrics and OpenTelemetry export (optional)

Table of contents

  1. Install
  2. Environment variables
  3. Observability
  4. REST interface
  5. Endpoint test client
  6. Actions
  7. LDAP

Install

Postfix integration

The service is configured in Postfix like this...

smtpd_sender_restrictions =
    ...
    check_policy_service inet:127.0.0.1:4646
    ...

... if you use the docker-compose.yml file as provided.

Back to table of contents

Custom settings

You can specify custom settings, which must be written in valid JSON. The format is:

{
  "data": [
    {
      "comment": "Whatever comment you like...",
      "sender": "localpart@domain.tld",
      "ips": NUMBER,
      "countries": NUMBER
    },
    ...
  ]
}

It is possible to only specify ips or countries. The missing parameter will be set to its default. Furthermore, the data structure is read one by one and the rules are evaluated as first match wins. By redefining a sender more than once, only the first will be used.

Back to table of contents

Preparing a docker image

Builds require Go 1.27.1 or a newer patched Go 1.27 toolchain. Module metadata, CI, release packaging, and the container builder use Go 1.27.1. Dependencies are vendored; after updates, run go mod tidy, go mod vendor, and make guardrails. Run make smoke-observability to verify the local HTTP, Redis, Prometheus, and OTLP integration after dependency updates.

The Docker image uses Alpine 3.24 and runs as UID/GID 10001:10001. Mounted GeoIP databases, custom settings, TLS keys, CA files, and action templates must be readable by that identity, with searchable parent directories. Grant access narrowly, especially for private keys. The systemd package continues to use its existing DynamicUser identity. Buildx selects the target architecture for both bundled executables. The OCI export below requires a Buildx builder with the docker-container driver:

docker buildx build --platform linux/amd64,linux/arm64 --output type=oci,dest=/tmp/geoip-policyd.tar .

There is no built-in container health check. Operators must configure checks for their enabled listeners, addresses, TLS, and authentication settings; either listener can be disabled. A process-only check does not verify policy admission or Redis/LDAP availability. Use the endpoint test client below only against a dedicated test instance when exercising mutating endpoints.

The simplest way to use the program is by using a docker image. You can build your own, as the default repository is not public for other people.

cd /path/to/Dockerfile
docker build -t geoip-policyd:latest .

You need to change the docker-compose.yml file as well. If you prefer, you can add a Redis service and run the * geoip-policyd* container in bridged mode.

For a complete example see here

Back to table of contents

GeoIP data sources

geoip-policyd reads country data from a local MMDB file. Two providers are supported; the file is never bundled with the binary, Docker image, or package, so operators download and update it themselves.

Provider Typical file Record layout used License
MaxMind GeoLite2-City.mmdb, GeoLite2-Country.mmdb, GeoIP2-* country.iso_code GeoLite2 EULA
IPinfo Lite ipinfo_lite.mmdb country_code CC BY-SA 4.0 (attribution)

Any other database with a MaxMind-compatible country.iso_code layout, such as DB-IP Country Lite, uses the MaxMind schema.

--geoip-provider (GEOIPPOLICYD_GEOIP_PROVIDER) selects how records are decoded:

  • auto (default) reads the database_type metadata. IPinfo Lite databases use the IPinfo schema, other IPinfo products (for example the legacy country_asn.mmdb) are rejected, and everything else uses the MaxMind schema.
  • maxmind or ipinfo enforce one schema.

Every load verifies the schema by decoding the first networks of the file. A database without usable country codes, such as a GeoLite2-ASN file or a file that does not match the enforced provider, is rejected with a schema_mismatch error instead of silently returning empty country codes.

On startup, a load failure terminates the process. On reload, either through the five-minute file modification check or GET /reload, a failure keeps the active database in service, increments geoip_policyd_geoip_reloads_total with result="error" or result="schema_mismatch", and the file check retries on the next tick. The initial load at startup is not counted as a reload. Each successful load logs the provider, database type, and build time, and warns when the build is older than 30 days.

MaxMind

Create a free GeoLite account at https://www.maxmind.com/en/geolite2/signup and keep the database current with MaxMind's geoipupdate, which replaces files atomically. Configure EditionIDs GeoLite2-City (or GeoLite2-Country) and point GEOIPPOLICYD_GEOIP_PATH at the resulting file, by default /usr/share/GeoIP/GeoLite2-City.mmdb.

IPinfo Lite

Create a free account at https://ipinfo.io/lite to obtain a download token. contrib/update-ipinfo-lite.sh downloads the database and replaces the target atomically:

IPINFO_TOKEN=... contrib/update-ipinfo-lite.sh /usr/share/GeoIP/ipinfo_lite.mmdb
Setting Default
target argument /usr/share/GeoIP/ipinfo_lite.mmdb
IPINFO_TOKEN required
IPINFO_URL https://ipinfo.io/data/ipinfo_lite.mmdb
IPINFO_MIN_SIZE 1048576 bytes; smaller downloads are refused

Downloads time out after 10 minutes and are retried up to three times. Files without the MMDB metadata marker, such as HTML error pages, are refused.

The script writes a temporary file in the target directory, checks its size, sets mode 0644, and moves it over the target. It needs write access to the target directory and passes the token to curl via standard input, so the token does not show up in the process list. Run it from cron or a systemd timer; IPinfo refreshes the data daily. geoip-policyd picks up the new file automatically.

Then point geoip-policyd at the file:

GEOIPPOLICYD_GEOIP_PATH="/usr/share/GeoIP/ipinfo_lite.mmdb"
GEOIPPOLICYD_GEOIP_PROVIDER="ipinfo"   # optional, auto detects it as well

IPinfo Lite data is licensed under CC BY-SA 4.0. If you publish or pass on data derived from it, credit IPinfo, for example with "IP address data powered by IPinfo". geoip-policyd itself does not redistribute the data.

Back to table of contents

Server options

geoip-policyd server --help

produces the following output:

...

Arguments:

  -h  --help                          Print help information
  -a  --server-address                IPv4 or IPv6 address for the policy service. Default: 127.0.0.1
  -p  --server-port                   Port for the policy service. Default: 4646
      --disable-policy-service        Do not start the policy TCP service. Default: false
      --disable-http-service          Do not start the HTTP service. Default: false
      --http-address                  HTTP address for incoming requests. Default: 127.0.0.1
      --http-port                     HTTP port for incoming requests. Default 8080
      --sasl-username                 Use 'sasl_username' instead of the 'sender' attribute. Default: false
  -A  --redis-address                 IPv4 or IPv6 address for the Redis service. Default: 127.0.0.1
  -P  --redis-port                    Port for the Redis service. Default: 6379
      --redis-username                Redis username. Default: 
      --redis-password                Redis password. Default: 
      --redis-replica-address         IPv4 or IPv6 address for a Redis service (replica). Default: 127.0.0.1
      --redis-replica-port            Port for a Redis service (replica). Default: 6379
      --redis-sentinels               List of space seperated sentinel servers. Default:
      --redis-sentinel-master-name    Sentinel master name. Default:
      --redis-sentinel-username       Redis sentinel username. Default:
      --redis-sentinel-password       Redis sentinel password. Defailt:
      --redis-prefix                  Redis prefix. Default: geopol_
      --redis-database-number         Redis database number. Default: 0
      --redis-ttl                     Redis TTL in seconds. Default: 3600
  -g  --geoip-path                    Full path to the GeoIP database file. Default: /usr/share/GeoIP/GeoLite2-City.mmdb
      --geoip-provider                GeoIP database provider: auto, maxmind or ipinfo. Default: auto
      --max-countries                 Maximum number of countries before rejecting e-mails. Default: 3
      --max-ips                       Maximum number of IP addresses before rejecting e-mails. Default: 10
      --home-countries                List of known home country codes. Default:
      --max-home-countries            Maximum number of home countries before rejecting e-mails. Default: 3
      --max-home-ips                  Maximum number of home IP addresses before rejecting e-mails. Default: 10
      --block-permanent               Do not expire senders from Redis, if they were blocked in the past. Default: false
      --force-user-known              Senders are already known by an upstream service. Default: false
  -c  --custom-settings-path          Custom settings with different IP and country limits. Default: 
      --http-use-basic-auth           Enable basic HTTP auth. Default: false
      --http-use-ssl                  Enable HTTPS. Default: false
      --http-basic-auth-username      HTTP basic auth username. Default: 
      --http-basic-auth-password      HTTP basic auth password. Default: 
      --http-tls-cert                 HTTP TLS server certificate (full chain). Default: /localhost.pem
      --http-tls-key                  HTTP TLS server key. Default: /localhost-key.pem
      --prometheus-enabled            Enable Prometheus metrics on the HTTP service. Default: false
      --prometheus-path               HTTP path for Prometheus metrics. Default: /metrics
      --prometheus-runtime-metrics    Include Go runtime and process metrics. Default: false
      --otel-enabled                  Enable OpenTelemetry export. Default: false
      --otel-traces-enabled           Export OpenTelemetry traces when OTel is enabled. Default: false
      --otel-metrics-enabled          Export OpenTelemetry metrics when OTel is enabled. Default: false
      --otel-service-name             OpenTelemetry service.name resource attribute. Default: geoip-policyd
      --otel-service-version          OpenTelemetry service.version resource attribute. Default: current binary version
      --otel-exporter-otlp-endpoint   OTLP HTTP endpoint as host:port or base URL. Default:
      --otel-exporter-otlp-headers    Comma-separated OTLP HTTP headers as key=value pairs. Default:
      --otel-exporter-otlp-insecure   Use insecure OTLP HTTP transport. Default: false
      --otel-sample-ratio             OpenTelemetry trace sampling ratio between 0.0 and 1.0. Default: 1.0
      --use-ldap                      Enable LDAP support. Default: false
      --ldap-server-uri               Server URI. Specify multiple times, if you need more than one server. Default: [ldap://127.0.0.1:389/]
      --ldap-basedn                   Base DN. Default: 
      --ldap-binddn                   Bind DN. Default: 
      --ldap-bindpw                   Bind password. Default: 
      --ldap-filter                   Filter with %s placeholder. Default: (&(objectClass=*)(mailAlias=%s))
      --ldap-result-attribute         Result attribute for the requested mail sender. Default: mailAccount
      --ldap-starttls                 If this option is given, use StartTLS. Default: false
      --ldap-skip-tls-verify          Skip TLS server name verification. Default: false
      --ldap-tls-cafile               File containing TLS CA certificate(s). Default: 
      --ldap-tls-client-cert          File containing a TLS client certificate. Default: 
      --ldap-tls-client-key           File containing a TLS client key. Default: 
      --ldap-sasl-external            Use SASL/EXTERNAL instead of a simple bind. Default: false
      --ldap-scope                    LDAP search scope [base, one, sub]. Default: sub
      --ldap-idle-pool-size           LDAP pre-forked (idle) pool size. Default 3
      --ldap-pool-size                LDAP max pool size. Default: 10
      --run-actions                   Run actions, if a sender is over limits. Default: false
      --run-action-operator           Run the operator action. Default: false
      --operator-to                   E-Mail To-header for the operator action. Default: 
      --operator-from                 E-Mail From-header for the operator action. Default: 
      --operator-subject              E-Mail Subject-header for the operator action. Default: [geoip-policyd] An e-mail account was compromised
      --operator-message-ct           E-Mail Content-Type-header for the operator action. Default: text/plain
      --operator-message-path         Full path to the e-mail message file for the operator action. Default: 
      --mail-server-address           E-mail server address for notifications. Default: 
      --mail-server-port              E-mail server port number. Default: 
      --mail-helo                     E-mail server HELO/EHLO hostname. Default: localhost
      --mail-port                     E-mail server port number. Default: 587
      --mail-username                 E-mail server username. Default: 
      --mail-password                 E-mail server password. Default: 
      --mail-ssl-on-connect           Use TLS on connect for the e-mail server. Default: false
  -v  --verbose                       Verbose mode. Repeat this for an increased log level
      --version                       Current version

Back to table of contents

Environment variables

The following environment variables can be used to configure the policy service. This is especially useful, if you plan on running the service as a docker service.

Server

Variable Description
GEOIPPOLICYD_DISABLE_POLICY_SERVICE Do not start the policy TCP service; default(false)
GEOIPPOLICYD_SERVER_ADDRESS IPv4 or IPv6 address for the policy service; default(127.0.0.1)
GEOIPPOLICYD_SERVER_PORT Port for the policy service; default(4646)
GEOIPPOLICYD_DISABLE_HTTP_SERVICE Do not start the HTTP service; default(false)
GEOIPPOLICYD_HTTP_ADDRESS HTTP address for incoming requests; default(127.0.0.1:8080)
GEOIPPOLICYD_HTTP_PORT HTTP port for incoming requests; default(8080)
GEOIPPOLICYD_USE_SASL_USERNAME Use 'sasl_username' instead of the 'sender' attribute; default(false)
GEOIPPOLICYD_REDIS_ADDRESS IPv4 or IPv6 address for the Redis service; default(127.0.0.1)
GEOIPPOLICYD_REDIS_PORT Port for the Redis service; default(6379)
GEOIPPOLICYD_REDIS_USERNAME Redis username
GEOIPPOLICYD_REDIS_PASSWORD Redis password
GEOIPPOLICYD_REDIS_REPLICA_ADDRESS IPv4 or IPv6 address for a Redis service (replica)
GEOIPPOLICYD_REDIS_REPLICA_PORT Port for a Redis service (replica)
GEOIPPOLICYD_REDIS_SENTINELS List of space seperated sentinel servers
GEOIPPOLICYD_REDIS_SENTINEL_MASTER_NAME Sentinel master name
GEOIPPOLICYD_REDIS_SENTINEL_USERNAME Redis sentinel username
GEOIPPOLICYD_REDIS_SENTINEL_PASSWORD Redis sentinel password
GEOIPPOLICYD_REDIS_PREFIX Redis prefix; default(geopol_)
GEOIPPOLICYD_REDIS_DATABASE_NUMBER Redis database number
GEOIPPOLICYD_REDIS_TTL Redis TTL; default(3600)
GEOIPPOLICYD_GEOIP_PATH Full path to the GeoIP database file; default(/usr/share/GeoIP/GeoLite2-City.mmdb)
GEOIPPOLICYD_GEOIP_PROVIDER GeoIP database provider: auto, maxmind or ipinfo; default(auto)
GEOIPPOLICYD_MAX_COUNTRIES Maximum number of countries before rejecting e-mails; default(3)
GEOIPPOLICYD_MAX_IPS Maximum number of IP addresses before rejecting e-mails; default(10)
GEOIPPOLICYD_HOME_COUNTRIES List of known home country codes
GEOIPPOLICYD_MAX_HOME_COUNTRIES Maximum number of home countries before rejecting e-mails; default(3)
GEOIPPOLICYD_MAX_HOME_IPS Maximum number of home IP addresses before rejecting e-mails; default(10)
GEOIPPOLICYD_BLOCK_PERMANENT Do not expire senders from Redis, if they were blocked in the past
GEOIPPOLICYD_CUSTOM_SETTINGS_PATH Custom settings with different IP and country limits
GEOIPPOLICYD_HTTP_USE_BASIC_AUTH Enable basic HTTP auth; default(false)
GEOIPPOLICYD_HTTP_USE_SSL Enable HTTPS; default(false)
GEOIPPOLICYD_HTTP_BASIC_AUTH_USERNAME HTTP basic auth username
GEOIPPOLICYD_HTTP_BASIC_AUTH_PASSWORD HTTP basic auth password
GEOIPPOLICYD_HTTP_TLS_CERT HTTP TLS server certificate (full chain); default(/localhost.pem)
GEOIPPOLICYD_HTTP_TLS_KEY HTTP TLS server key; default(/localhost-key.pem)
GEOIPPOLICYD_PROMETHEUS_ENABLED Enable Prometheus metrics on the HTTP service; default(false)
GEOIPPOLICYD_PROMETHEUS_PATH HTTP path for Prometheus metrics; default(/metrics)
GEOIPPOLICYD_PROMETHEUS_RUNTIME_METRICS Include Go runtime and process metrics; default(false)
GEOIPPOLICYD_OTEL_ENABLED Enable OpenTelemetry OTLP HTTP export; default(false)
GEOIPPOLICYD_OTEL_TRACES_ENABLED Export OpenTelemetry traces when OTel is enabled; default(false)
GEOIPPOLICYD_OTEL_METRICS_ENABLED Export OpenTelemetry metrics when OTel is enabled; default(false)
GEOIPPOLICYD_OTEL_SERVICE_NAME OpenTelemetry service.name resource attribute; default(geoip-policyd)
GEOIPPOLICYD_OTEL_SERVICE_VERSION OpenTelemetry service.version resource attribute; default(current binary version)
GEOIPPOLICYD_OTEL_EXPORTER_OTLP_ENDPOINT OTLP HTTP endpoint as host:port or base URL
GEOIPPOLICYD_OTEL_EXPORTER_OTLP_HEADERS Comma-separated OTLP HTTP headers as key=value pairs
GEOIPPOLICYD_OTEL_EXPORTER_OTLP_INSECURE Use insecure OTLP HTTP transport; default(false)
GEOIPPOLICYD_OTEL_SAMPLE_RATIO OpenTelemetry trace sampling ratio between 0.0 and 1.0; default(1.0)
GEOIPPOLICYD_USE_LDAP Enable LDAP support; default(false)
GEOIPPOLICYD_LDAP_SERVER_URIS Server URI. Specify multiple times, if you need more than one server; default(ldap://127.0.0.1:389/)
GEOIPPOLICYD_LDAP_BASEDN Base DN
GEOIPPOLICYD_LDAP_BINDPW Bind PW
GEOIPPOLICYD_LDAP_FILTER Filter with %s placeholder; default( (&(objectClass=*)(mailAlias=%s)) )
GEOIPPOLICYD_LDAP_RESULT_ATTRIBUTE Result attribute for the requested mail sender; default(mailAccount)
GEOIPPOLICYD_LDAP_STARTTLS If this option is given, use StartTLS
GEOIPPOLICYD_LDAP_TLS_SKIP_VERIFY Skip TLS server name verification
GEOIPPOLICYD_LDAP_TLS_CAFILE File containing TLS CA certificate(s)
GEOIPPOLICYD_LDAP_TLS_CLIENT_CERT File containing a TLS client certificate
GEOIPPOLICYD_LDAP_TLS_CLIENT_KEY File containing a TLS client key
GEOIPPOLICYD_LDAP_SASL_EXTERNAL Use SASL/EXTERNAL instead of a simple bind; default(false)
GEOIPPOLICYD_LDAP_SCOPE LDAP search scope [base, one, sub]; default(sub)
GEOIPPOLICYD_LDAP_IDLE_POOL_SIZE LDAP pre-forked (idle) pool size; default(3)
GEOIPPOLICYD_LDAP_POOL_SIZE LDAP max pool size; default(10)
GEOIPPOLICYD_RUN_ACTIONS Run actions, if a sender is over limits; default(false)
GEOIPPOLICYD_RUN_ACTION_OPERATOR Run the operator action; default(false)
GEOIPPOLICYD_OPERATOR_TO E-Mail To-header for the operator action
GEOIPPOLICYD_OPERATOR_FROM E-Mail From-header for the operator action
GEOIPPOLICYD_OPERATOR_SUBJECT E-Mail Subject-header for the operator action; default([geoip-policyd] An e-mail account was compromised)
GEOIPPOLICYD_OPERATOR_MESSAGE_CT E-Mail Content-Type-header for the operator action; default(text/plain)
GEOIPPOLICYD_OPERATOR_MESSAGE_PATH Full path to the e-mail message file for the operator action
GEOIPPOLICYD_MAIL_SERVER_ADDRESS E-mail server address for notifications
GEOIPPOLICYD_MAIL_SERVER_PORT E-mail server port number
GEOIPPOLICYD_MAIL_HELO E-mail server HELO/EHLO hostname; default(localhost)
GEOIPPOLICYD_MAIL_PORT E-mail server port number; default(587)
GEOIPPOLICYD_MAIL_USERNAME E-mail server username
GEOIPPOLICYD_MAIL_PASSWORD E-mail server password
GEOIPPOLICYD_MAIL_SSL_ON_CONNECT Use TLS on connect for the e-mail server; default(false)
GEOIPPOLICYD_VERBOSE_LEVEL Log level. One of 'none', 'info' or 'debug'

At least one listener must remain enabled. Do not set both GEOIPPOLICYD_DISABLE_POLICY_SERVICE=true and GEOIPPOLICYD_DISABLE_HTTP_SERVICE=true. Prometheus export needs the HTTP service because the scrape endpoint is registered there.

Back to table of contents

Observability

Observability is disabled by default. Enabling it adds process-local metrics and optional OTLP HTTP export so operators can observe HTTP requests, Postfix policy requests, policy decisions, Redis operations, LDAP operations and pool state, GeoIP lookups and reloads, CDB lookups, operator actions, and TCP connection lifecycle events.

Metric and trace labels intentionally do not include sender addresses, client IP addresses, Redis keys, or raw LDAP filters. This keeps label cardinality predictable and avoids exporting sensitive request identifiers.

Prometheus

Enable the Prometheus endpoint on the existing HTTP service:

geoip-policyd server --prometheus-enabled

The default endpoint is:

http://127.0.0.1:8080/metrics

--disable-http-service cannot be combined with --prometheus-enabled.

If HTTP basic auth is enabled, the metrics endpoint uses the same credentials as the REST API:

curl -u testuser:testsecret http://127.0.0.1:8080/metrics

Useful options:

Option Default Description
--prometheus-enabled false Enables the /metrics endpoint
--prometheus-path /metrics Changes the metrics endpoint path
--prometheus-runtime-metrics false Adds Go runtime and process collectors

Side effects:

  • The HTTP listener serves one additional route when Prometheus is enabled.
  • Go runtime and process collectors add standard go_* and process_* metrics only when explicitly enabled.
  • /metrics is not instrumented by the HTTP middleware to avoid self-scrape noise.

Grafana dashboard

The repository ships an importable Grafana 11 dashboard at contrib/grafana/geoip-policyd-grafana11-dashboard.json. It visualizes the Prometheus metrics for policy decisions, HTTP routes, GeoIP lookups and reloads, Redis operations, LDAP operations and pool state, CDB lookups, operator actions, TCP policy-service connections, and optional Go runtime and process collectors.

Prerequisites:

Requirement Default or note
Grafana Dashboard JSON targets Grafana 11 schema version 41
Prometheus datasource Selected through the DS_PROMETHEUS dashboard datasource variable
geoip-policyd Prometheus Start the service with --prometheus-enabled
Runtime/process panels Also enable --prometheus-runtime-metrics; otherwise these stay empty

Import it through the Grafana UI with Dashboards > New > Import. For API imports, wrap the dashboard JSON for Grafana's create/update endpoint:

python3 -c 'import json, sys; dashboard = json.load(open(sys.argv[1], encoding="utf-8")); print(json.dumps({"dashboard": dashboard, "overwrite": True, "message": "Import geoip-policyd dashboard"}))' \
  contrib/grafana/geoip-policyd-grafana11-dashboard.json |
curl -X POST \
  -H "Authorization: Bearer ${GRAFANA_TOKEN}" \
  -H "Content-Type: application/json" \
  --data-binary @- \
  http://127.0.0.1:3000/api/dashboards/db

After import, choose the Prometheus datasource and, if needed, narrow the dashboard variables for job, instance, policy source, and HTTP route.

Side effects:

  • Importing the JSON creates or updates only the Grafana dashboard with UID geoip-policyd-g11.
  • Dashboard queries read from Prometheus only and do not call geoip-policyd.
  • Runtime panels are intentionally optional because go_* and process_* metrics are exported only when runtime metrics are enabled.

OpenTelemetry

OpenTelemetry uses OTLP over HTTP. Enable it with an endpoint:

geoip-policyd server \
  --otel-enabled \
  --otel-traces-enabled \
  --otel-exporter-otlp-endpoint http://127.0.0.1:4318

The endpoint may be host:port or a base URL such as http://collector.example:4318. The exporter uses the standard OTLP HTTP paths for traces and metrics. At least one of --otel-traces-enabled or --otel-metrics-enabled must be set when --otel-enabled is set.

Useful options:

Option Default Description
--otel-enabled false Enables OpenTelemetry export
--otel-traces-enabled false Exports traces when OTel is enabled
--otel-metrics-enabled false Exports OTel metrics when OTel is enabled
--otel-service-name geoip-policyd Sets the service.name resource attribute
--otel-service-version current version Sets the service.version resource attribute
--otel-exporter-otlp-endpoint empty OTLP HTTP collector endpoint; required when OTel is enabled
--otel-exporter-otlp-headers empty Comma-separated key=value headers for the OTLP HTTP exporter
--otel-exporter-otlp-insecure false Uses insecure transport for OTLP HTTP
--otel-sample-ratio 1.0 Trace sampling ratio from 0.0 to 1.0

Example with headers:

geoip-policyd server \
  --otel-enabled \
  --otel-traces-enabled \
  --otel-exporter-otlp-endpoint https://collector.example:4318 \
  --otel-exporter-otlp-headers "authorization=Bearer token"

Side effects:

  • The process opens outbound HTTP connections to the configured collector.
  • SIGINT and SIGTERM trigger a best-effort telemetry flush before process exit.
  • If OTel is enabled, at least one of traces or metrics must remain enabled and the OTLP endpoint is required.

Back to table of contents

REST interface

GET request /reload

Request: reload
Response: No results

The GeoIP database is reloaded through the same verified loader as on startup. If it cannot be loaded, the endpoint returns 500 and the active database stays in service (see GeoIP data sources).

Example:

# Plain http without basic auth
curl "http://localhost:8080/reload"

# Plain with basic auth
curl "http://localhost:8080/reload" -u testuser:testsecret

# Secured with basic auth
curl -k "https://localhost:8443/reload" -u testuser:testsecret

Back to table of contents

GET request /custom-settings

Request: get current custom settings in JSON format
Response: JSON output of the currently loaded custom settings

Example:

# Plain http without basic auth
curl "http://localhost:8080/custom-settings" | jq

# Plain with basic auth
curl "http://localhost:8080/custom-settings" -u testuser:testsecret | jq

# Secured with basic auth
curl -k "https://localhost:8443/custom-settings" -u testuser:testsecret | jq

Example result from default custom.json:

[
  {
    "comment": "Allow only two countries and a maximum of 5 IP addresses",
    "sender": "christian@roessner.email",
    "ips": 5,
    "countries": 2
  },
  {
    "comment": "Allow at least 4 countries and go with the default IP address limit",
    "sender": "test1@example.com",
    "ips": 0,
    "countries": 4
  },
  {
    "comment": "Go with the default country limit, but allow up to 30 IP addresses",
    "sender": "test2@example.com",
    "ips": 30,
    "countries": 0
  }
]

Back to table of contents

POST request /remove

Request: Submit an email account that should be unlocked
Response: No results

Example:

# Plain http without basic auth
curl -d '{"key":"sender","value":"user@example.com"}' -H "Content-Type: application/json" -X POST "http://localhost:8080/remove"

# Plain with basic auth
curl -d '{"key":"sender","value":"user@example.com"}' -H "Content-Type: application/json" -X POST "http://localhost:8080/remove" -u testuser:testsecret

# Secured with basic auth
curl -k -d '{"key":"sender","value":"user@example.com}"' -H "Content-Type: application/json" -X POST "https://localhost:8443/remove" -u testuser:testsecret

Back to table of contents

POST request /query

Request: Submit a client address and a sender name to get a policy result Request format: JSON Response: JSON formatted policy decision

Example request:

# Plain http without basic auth
curl -d '{ "key": "client", "value": { "address": "1.2.3.4", "sender": "user@example.com" } }' -H "Content-Type: application/json" -X POST "http://localhost:8080/query"

# Plain with basic auth
curl -d '{ "key": "client", "value": { "address": "1.2.3.4", "sender": "user@example.com" } }' -H "Content-Type: application/json" -X POST "http://localhost:8080/query" -u testuser:testsecret

# Secured with basic auth
curl -k -d '{ "key": "client", "value": { "address": "1.2.3.4", "sender": "user@example.com" } }' -H "Content-Type: application/json" -X POST "https://localhost:8443/query" -u testuser:testsecret

Allowed policy response example:

{
  "guid": "2FOLGkYQwUB8XTQhdTY3csRkNV2",
  "object": "client",
  "operation": "query",
  "result": true
}

Forbidden policy response example:

{
  "guid": "2FOLGkYQwUB8XTQhdTY3csRkNV2",
  "object": "client",
  "operation": "query",
  "result": false
}

If "?info=1" is appended to the query-string of the HTTP request, the service will only return the country code for the current client IP address.

Back to table of contents

PUT request /update

Request: Set custom settings. This will overwrite a custom settings file or initiates settings, if there have not been any settings before (no config file given).
Response: No results


Note

If you use a custom settings file and send new data with a PUT request, the settings are updated in memory. But if you do a GET request afterwards and reloading data, the settings from the file will be loaded again!


Example:

# Plain http without basic auth
curl -d '{"data":[{ "sender":"christian@roessner.email","ips":3,"countries":1},{"sender":"test1@example.com","countries":1},{"sender":"test2@example.com","ips":20}]}' -H "Content-Type: application/json" -X PUT "http://localhost:8080/update"

# Plain with basic auth
curl -d '{"data":[{ "sender":"christian@roessner.email","ips":3,"countries":1},{"sender":"test1@example.com","countries":1},{"sender":"test2@example.com","ips":20}]}' -H "Content-Type: application/json" -X PUT "http://localhost:8080/update" -u testuser:testsecret

# Secured with basic auth
curl -k -d '{"data":[{ "sender":"christian@roessner.email","ips":3,"countries":1},{"sender":"test1@example.com","countries":1},{"sender":"test2@example.com","ips":20}]}' -H "Content-Type: application/json" -X PUT "https://localhost:8443/update" -u testuser:testsecret

Back to table of contents

PATCH request /modify

Request: Send changed settings for a given sender. If the sender does not exist, add a new record to the custom settings.
Response: No results

Example:

# Plain http without basic auth
curl -d '{"key":"sender","value":{"comment":"Test","sender":"christian@roessner.email","ips":100,"countries":100}}' -H "Content-Type: application/json" -X PATCH "http://localhost:8080/modify"

# Plain with basic auth
curl -d '{"key":"sender","value":{"comment":"Test","sender":"christian@roessner.email","ips":100,"countries":100}}' -H "Content-Type: application/json" -X PATCH "http://localhost:8080/modify" -u testuser:testsecret

# Secured with basic auth
curl -k -d '{"key":"sender","value":{"comment":"Test","sender":"christian@roessner.email","ips":100,"countries":100}}"' -H "Content-Type: application/json" -X PATCH "https://localhost:8443/modify" -u testuser:testsecret

Note:

This endpoint is not yet implemented nor tested for home countries!

Back to table of contents

DELETE request /remove

Request: Remove an entry from the custom settings by using the sender as the key.
Response: No results

Example:

# Plain http without basic auth
curl -d '{"key":"sender","value":"christian@roessner.email"}' -H "Content-Type: application/json" -X DELETE "http://localhost:8080/remove"

# Plain with basic auth
curl -d '{"key":"sender","value":"christian@roessner.email"}' -H "Content-Type: application/json" -X DELETE "http://localhost:8080/remove" -u testuser:testsecret

# Secured with basic auth
curl -k -d '{"key":"sender","value":"christian@roessner.email"}"' -H "Content-Type: application/json" -X DELETE "https://localhost:8443/remove" -u testuser:testsecret

Back to table of contents

Endpoint test client

The contrib/geoip-policyd-test.py script exercises the REST interface and the raw Postfix policy socket with useful local defaults. It uses only the Python standard library and does not require a virtual environment.

The repository contains testdata/GeoIP2-City-Test.mmdb for local smoke tests that need a valid MaxMind database without depending on an operator-provided GeoLite file. testdata/IPinfo-Lite-Test.mmdb is a small synthetic IPinfo Lite database (see MMDB test fixtures).

Default targets:

Option Default
--base-url http://127.0.0.1:8080
--policy-host 127.0.0.1
--policy-port 4646
--sender geoip-policyd-test@example.com
--address 127.0.0.1
--recipient postmaster@example.com

Run a single endpoint check:

contrib/geoip-policyd-test.py --address 8.8.8.8 --sender user@example.com query
contrib/geoip-policyd-test.py --address 8.8.8.8 dovecotpolicy --command allow
contrib/geoip-policyd-test.py --address 8.8.8.8 policy

Run the complete endpoint suite:

contrib/geoip-policyd-test.py all

The all command checks GET /custom-settings, POST /query, POST /dovecotpolicy?command=report, POST /dovecotpolicy?command=allow, the raw Postfix policy socket, PUT /update, PATCH /modify, DELETE /remove, POST /remove, and GET /reload. The suite intentionally touches mutating custom-settings and unlock endpoints, so run it against a dedicated test instance or with a sender and Redis prefix that are safe to modify.

For HTTPS with a local or self-signed certificate, add --insecure. For HTTP basic authentication, add --username and --password.

Example against isolated local test ports:

contrib/geoip-policyd-test.py \
  --base-url http://127.0.0.1:18080 \
  --policy-port 14646 \
  --address 8.8.8.8 \
  all

The output is a compact table with the endpoint name, method, target, status, expected status, result, and a short response summary. The process exits with status 0 only when all selected checks pass.

OpenTelemetry and Prometheus smoke test

The contrib/otel_prometheus_smoke.go helper runs an external observability smoke test. It starts a fake Redis server, a fake OTLP/HTTP collector, and a real geoip-policyd server process on random loopback ports. It then sends a real POST /query, scrapes Prometheus, terminates the child process cleanly, and validates the exported OTLP protobuf payloads.

Run it through the Makefile:

make smoke-observability

The smoke checks that Prometheus contains HTTP, policy, Redis, and GeoIP metrics. It also checks that OTLP traces contain this graph:

HTTP POST /query
`-- policy.request
    |-- geoip.lookup
    |   `-- geoip.mmdb.lookup
    |-- redis.command GET
    `-- redis.command SET

OTLP metrics are checked for geoip_policyd_http_requests, geoip_policyd_policy_requests, geoip_policyd_redis_operations, and geoip_policyd_geoip_lookups.

The helper uses only local loopback ports and temporary files. To run it with an existing binary or a different GeoIP database, use:

go run -mod=vendor ./contrib \
  --binary /path/to/geoip-policyd \
  --geoip-path ./GeoIP2-Country.mmdb \
  --address 8.8.8.8

Both GeoIP spans carry the geoip.provider attribute (maxmind or ipinfo).

MMDB test fixtures

contrib/mmdbfixture generates the synthetic databases used by the unit tests:

File Purpose
testdata/IPinfo-Lite-Test.mmdb IPinfo Lite layout, IPv4 and IPv6 networks, one without ASN
testdata/IPinfo-Legacy-Test.mmdb Legacy IPinfo country_asn layout, must be rejected
testdata/GeoLite2-ASN-Test.mmdb ASN-only database, must fail schema verification

The generator is a separate Go module with its own go.mod, so its github.com/maxmind/mmdbwriter dependency never enters the vendored runtime module or ./.... Regenerate the fixtures with:

make fixtures

The target downloads the generator dependencies into the Go module cache, overwrites the three files in testdata/, and changes nothing else. Commit the regenerated files together with generator changes.

Back to table of contents

Actions

Operator action

You can activate actions that will be taken, if a sender was declared compromised. At the moment you can send a notification to an e-mail operator. To do this, you must activate actions in general as well as the operator action. You need also to define all the required operator parameters as To, From, Subject, CT and of course an e-mail server ( including all required settings) to get things done.

Example:

geoip-policyd ...other-options... \
  --run-actions \
  --run-action-operator \
  --operator-to "<operator@example.com>" \
  --operator-from "<no-reply@submission.example.com>" \
  --operator-message-ct "text/plain" \
  --operator-message-path ./mailtemplate.txt \
  --mail-server submission.example.com \
  --mail-port 587 \
  --mail-username "some_username" \
  --mail-password some-secret

Back to table of contents

LDAP

You can use LDAP to send the sender attribute and to retrieve whatever that makes your request unique. If you have customers that use virtual aliases and that belong to exactly one account, this may help you to aggregate e-mail sender requests.

Example:

virtual alias real account
user1@example.com unique@account.net
foo@bar.org unique@account.net

Both belong to one and the same account. Without LDAP this would result in two records in Redis. With LDAP it results into the real unique account.

It is also possible to not retrieve another unique mail account from LDAP. You can also return the entryUUID field or some other field like uid or uniqueIdentifier (LDAP overlay unique to enforce uniqueness!).

Here is my personal example of a docker-compose.yml file that makes use of LDAP:

docker-compose.yml

version: "3.8"

services:

  geoip-policyd:
    image: ...whatever.../geoip-policyd:latest
    logging:
      driver: journald
      options:
        tag: geoip-policyd
    network_mode: host
    environment:
      VERBOSE: "debug"
      SERVER_ADDRESS: "127.0.0.1"
      SERVER_PORT: 4646
      HTTP_ADDRESS: "127.0.0.1:8080"
      REDIS_ADDRESS: "127.0.0.1"
      REDIS_PORT: 6379
      REDIS_DATABASE_NUMBER: 0
      GEOIP_PATH: "/GeoLite2-City.mmdb"
      CUSTOM_SETTINGS_PATH: "/custom.json"
      USE_LDAP: "true"
      LDAP_STARTTLS: "true"
      LDAP_SASL_EXTERNAL: "true"
      LDAP_SERVER_URIS: "ldap://****:389/, ldap://****:389/"
      LDAP_BASEDN: "ou=people,..."
      LDAP_TLS_CAFILE: "/cacert.pem"
      LDAP_TLS_CLIENT_CERT: "/cert.pem"
      LDAP_TLS_CLIENT_KEY: "/key.pem"
      LDAP_FILTER: "(&(objectClass=rnsMSDovecotAccount)(objectClass=rnsMSPostfixAccount)(rnsMSRecipientAddress=%s))"
      LDAP_RESULT_ATTRIBUTE: "uid"
    volumes:
      - /usr/share/GeoIP/GeoLite2-City.mmdb:/GeoLite2-City.mmdb:ro,Z
      - ./custom.json:/custom.json:ro,Z
      - /etc/pki/tls/certs/cacert.pem:/cacert.pem:ro,Z
      - /etc/ssl/certs/cert.pem:/cert.pem:ro,Z
      - /etc/ssl/private/key.pem:/key.pem:ro,Z

A result in the logs looks like this:

geoip-policyd_1  | 2021/09/14 06:53:28 Info: sender=<2F7032A7-D2BE-4178-87B2-A8D3AC0F32F1>; countries=[DE]; ip_addresses=[x.x.x.x]; #countries=1/1; #ip_addresses=1/1; action=DUNNO

Redis-result:

127.0.0.1:6379> get geopol_2F7032A7-D2BE-4178-87B2-A8D3AC0F32F1
"{\"Ips\":[\"x.x.x.x\"],\"Countries\":[\"DE\"]}"

This way you get some pseudo anonymization.

If you do so, you also have to modify your custom.json file, if you use one:

custom.json

{
  "data": [
    {
      "comment": "Some comment",
      "sender": "4FFDDFD3-BE1B-4639-8465-32A9A709F4CF",
      "ips": 5,
      "countries": 2
    },
    {
      "comment": "Whatever else",
      "sender": "2F7032A7-D2BE-4178-87B2-A8D3AC0F32F1",
      "ips": 1,
      "countries": 1
    },
    {
      "comment": "And another one goes here",
      "sender": "6B806FF8-8BA5-40CC-A0FE-602CF2AEEDE2",
      "countries": 1
    }
  ]
}

Back to table of contents

License

This project is licensed under the GPLv3 License - see the LICENSE file for details.

Note

The license has changed from AGPL-3 to GPL-3! This step is required to provide docker images.

About

Policy server that checks IPs and blocks senders, if they come from different countries or if they come from too many different IP addresses

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages