{"id":40811672,"url":"https://github.com/croessner/geoip-policyd","last_synced_at":"2026-05-25T10:03:57.327Z","repository":{"id":255385438,"uuid":"844396184","full_name":"croessner/geoip-policyd","owner":"croessner","description":"Policy server that checks IPs and blocks senders, if they come from different countries or if they come from too many different IP addresses","archived":false,"fork":false,"pushed_at":"2026-05-09T15:07:59.000Z","size":6859,"stargazers_count":2,"open_issues_count":4,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-05-09T16:28:39.960Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/croessner.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2024-08-19T07:10:36.000Z","updated_at":"2026-05-09T15:08:03.000Z","dependencies_parsed_at":null,"dependency_job_id":"8eaade75-4824-4103-a73f-56def7239a50","html_url":"https://github.com/croessner/geoip-policyd","commit_stats":null,"previous_names":["croessner/geoip-policyd"],"tags_count":76,"template":false,"template_full_name":null,"purl":"pkg:github/croessner/geoip-policyd","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/croessner%2Fgeoip-policyd","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/croessner%2Fgeoip-policyd/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/croessner%2Fgeoip-policyd/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/croessner%2Fgeoip-policyd/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/croessner","download_url":"https://codeload.github.com/croessner/geoip-policyd/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/croessner%2Fgeoip-policyd/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33469420,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-25T06:32:55.349Z","status":"ssl_error","status_checked_at":"2026-05-25T06:32:35.322Z","response_time":57,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-01-21T21:18:21.887Z","updated_at":"2026-05-25T10:03:57.319Z","avatar_url":"https://github.com/croessner.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# geoip-policyd\n\n## About\n\nPostfix-Submission policy server that checks sender IPs and blocks senders, if they come from too many countries or if\nthey come from too many IP addresses.\n\n## Features\n\n* GeoIP policy service for Postfix\n* Custom settings to define different limits for IPs and countries per sender\n* LDAP support (optional)\n* REST interface to interact with the service on the fly\n* Actions\n* Prometheus metrics and OpenTelemetry export (optional)\n\n# Table of contents\n\n1. [Install](#install)\n    * [Postfix integration](#postfix-integration)\n    * [Custom settings](#custom-settings)\n    * [Preparing a docker image](#preparing-a-docker-image)\n    * [Server options](#server-options)\n2. [Environment variables](#environment-variables)\n    * [Server](#server)\n3. [Observability](#observability)\n    * [Prometheus](#prometheus)\n    * [Grafana dashboard](#grafana-dashboard)\n    * [OpenTelemetry](#opentelemetry)\n4. [REST interface](#rest-interface)\n    * [GET request /reload](#get-request-reload)\n    * [GET request /custom-settings](#get-request-custom-settings)\n    * [POST request /remove](#post-request-remove)\n    * [POST request /query](#post-request-query)\n    * [PUT request /update](#put-request-update)\n    * [PATCH request /modify](#patch-request-modify)\n    * [DELETE request /remove](#delete-request-remove)\n5. [Endpoint test client](#endpoint-test-client)\n    * [OpenTelemetry and Prometheus smoke test](#opentelemetry-and-prometheus-smoke-test)\n6. [Actions](#actions)\n    * [Operator action](#operator-action)\n7. [LDAP](#ldap)\n    * [docker-compose.yml](#docker-composeyml)\n    * [custom.json](#customjson)\n\n# Install\n\n## Postfix integration\n\nThe service is configured in Postfix like this...\n\n```\nsmtpd_sender_restrictions =\n    ...\n    check_policy_service inet:127.0.0.1:4646\n    ...\n```\n\n... if you use the docker-compose.yml file as provided.\n\nBack to [table of contents](#table-of-contents)\n\n## Custom settings\n\nYou can specify custom settings, which must be written in valid JSON. The format is:\n\n```\n{\n  \"data\": [\n    {\n      \"comment\": \"Whatever comment you like...\",\n      \"sender\": \"localpart@domain.tld\",\n      \"ips\": NUMBER,\n      \"countries\": NUMBER\n    },\n    ...\n  ]\n}\n```\n\nIt is possible to only specify *ips* or *countries*. The missing parameter will be set to its default. Furthermore, the\ndata structure is read one by one and the rules are evaluated as first match wins. By redefining a sender more than\nonce, only the first will be used.\n\nBack to [table of contents](#table-of-contents)\n\n## Preparing a docker image\n\nThe simplest way to use the program is by using a docker image. You can build your own, as the default repository is not\npublic for other people.\n\n```shell\ncd /path/to/Dockerfile\ndocker build -t geoip-policyd:latest .\n```\n\nYou need to change the docker-compose.yml file as well. If you prefer, you can add a Redis service and run the *\ngeoip-policyd* container in bridged mode.\n\nFor a complete example see [here](docker-compose.yml)\n\nBack to [table of contents](#table-of-contents)\n\n## Server options\n\n```shell\ngeoip-policyd server --help\n```\n\nproduces the following output:\n\n```\n...\n\nArguments:\n\n  -h  --help                          Print help information\n  -a  --server-address                IPv4 or IPv6 address for the policy service. Default: 127.0.0.1\n  -p  --server-port                   Port for the policy service. Default: 4646\n      --disable-policy-service        Do not start the policy TCP service. Default: false\n      --disable-http-service          Do not start the HTTP service. Default: false\n      --http-address                  HTTP address for incoming requests. Default: 127.0.0.1\n      --http-port                     HTTP port for incoming requests. Default 8080\n      --sasl-username                 Use 'sasl_username' instead of the 'sender' attribute. Default: false\n  -A  --redis-address                 IPv4 or IPv6 address for the Redis service. Default: 127.0.0.1\n  -P  --redis-port                    Port for the Redis service. Default: 6379\n      --redis-username                Redis username. Default: \n      --redis-password                Redis password. Default: \n      --redis-replica-address         IPv4 or IPv6 address for a Redis service (replica). Default: 127.0.0.1\n      --redis-replica-port            Port for a Redis service (replica). Default: 6379\n      --redis-sentinels               List of space seperated sentinel servers. Default:\n      --redis-sentinel-master-name    Sentinel master name. Default:\n      --redis-sentinel-username       Redis sentinel username. Default:\n      --redis-sentinel-password       Redis sentinel password. Defailt:\n      --redis-prefix                  Redis prefix. Default: geopol_\n      --redis-database-number         Redis database number. Default: 0\n      --redis-ttl                     Redis TTL in seconds. Default: 3600\n  -g  --geoip-path                    Full path to the GeoIP database file. Default: /usr/share/GeoIP/GeoLite2-City.mmdb\n      --max-countries                 Maximum number of countries before rejecting e-mails. Default: 3\n      --max-ips                       Maximum number of IP addresses before rejecting e-mails. Default: 10\n      --home-countries                List of known home country codes. Default:\n      --max-home-countries            Maximum number of home countries before rejecting e-mails. Default: 3\n      --max-home-ips                  Maximum number of home IP addresses before rejecting e-mails. Default: 10\n      --block-permanent               Do not expire senders from Redis, if they were blocked in the past. Default: false\n      --force-user-known              Senders are already known by an upstream service. Default: false\n  -c  --custom-settings-path          Custom settings with different IP and country limits. Default: \n      --http-use-basic-auth           Enable basic HTTP auth. Default: false\n      --http-use-ssl                  Enable HTTPS. Default: false\n      --http-basic-auth-username      HTTP basic auth username. Default: \n      --http-basic-auth-password      HTTP basic auth password. Default: \n      --http-tls-cert                 HTTP TLS server certificate (full chain). Default: /localhost.pem\n      --http-tls-key                  HTTP TLS server key. Default: /localhost-key.pem\n      --prometheus-enabled            Enable Prometheus metrics on the HTTP service. Default: false\n      --prometheus-path               HTTP path for Prometheus metrics. Default: /metrics\n      --prometheus-runtime-metrics    Include Go runtime and process metrics. Default: false\n      --otel-enabled                  Enable OpenTelemetry export. Default: false\n      --otel-traces-enabled           Export OpenTelemetry traces when OTel is enabled. Default: false\n      --otel-metrics-enabled          Export OpenTelemetry metrics when OTel is enabled. Default: false\n      --otel-service-name             OpenTelemetry service.name resource attribute. Default: geoip-policyd\n      --otel-service-version          OpenTelemetry service.version resource attribute. Default: current binary version\n      --otel-exporter-otlp-endpoint   OTLP HTTP endpoint as host:port or base URL. Default:\n      --otel-exporter-otlp-headers    Comma-separated OTLP HTTP headers as key=value pairs. Default:\n      --otel-exporter-otlp-insecure   Use insecure OTLP HTTP transport. Default: false\n      --otel-sample-ratio             OpenTelemetry trace sampling ratio between 0.0 and 1.0. Default: 1.0\n      --use-ldap                      Enable LDAP support. Default: false\n      --ldap-server-uri               Server URI. Specify multiple times, if you need more than one server. Default: [ldap://127.0.0.1:389/]\n      --ldap-basedn                   Base DN. Default: \n      --ldap-binddn                   Bind DN. Default: \n      --ldap-bindpw                   Bind password. Default: \n      --ldap-filter                   Filter with %s placeholder. Default: (\u0026(objectClass=*)(mailAlias=%s))\n      --ldap-result-attribute         Result attribute for the requested mail sender. Default: mailAccount\n      --ldap-starttls                 If this option is given, use StartTLS. Default: false\n      --ldap-skip-tls-verify          Skip TLS server name verification. Default: false\n      --ldap-tls-cafile               File containing TLS CA certificate(s). Default: \n      --ldap-tls-client-cert          File containing a TLS client certificate. Default: \n      --ldap-tls-client-key           File containing a TLS client key. Default: \n      --ldap-sasl-external            Use SASL/EXTERNAL instead of a simple bind. Default: false\n      --ldap-scope                    LDAP search scope [base, one, sub]. Default: sub\n      --ldap-idle-pool-size           LDAP pre-forked (idle) pool size. Default 3\n      --ldap-pool-size                LDAP max pool size. Default: 10\n      --run-actions                   Run actions, if a sender is over limits. Default: false\n      --run-action-operator           Run the operator action. Default: false\n      --operator-to                   E-Mail To-header for the operator action. Default: \n      --operator-from                 E-Mail From-header for the operator action. Default: \n      --operator-subject              E-Mail Subject-header for the operator action. Default: [geoip-policyd] An e-mail account was compromised\n      --operator-message-ct           E-Mail Content-Type-header for the operator action. Default: text/plain\n      --operator-message-path         Full path to the e-mail message file for the operator action. Default: \n      --mail-server-address           E-mail server address for notifications. Default: \n      --mail-server-port              E-mail server port number. Default: \n      --mail-helo                     E-mail server HELO/EHLO hostname. Default: localhost\n      --mail-port                     E-mail server port number. Default: 587\n      --mail-username                 E-mail server username. Default: \n      --mail-password                 E-mail server password. Default: \n      --mail-ssl-on-connect           Use TLS on connect for the e-mail server. Default: false\n  -v  --verbose                       Verbose mode. Repeat this for an increased log level\n      --version                       Current version\n```\n\nBack to [table of contents](#table-of-contents)\n\n# Environment variables\n\nThe following environment variables can be used to configure the policy service. This is especially useful, if you plan\non running the service as a docker service.\n\n## Server\n\n| Variable                                 | Description                                                                                               |\n|------------------------------------------|-----------------------------------------------------------------------------------------------------------|\n| GEOIPPOLICYD_DISABLE_POLICY_SERVICE      | Do not start the policy TCP service; default(false)                                                       |\n| GEOIPPOLICYD_SERVER_ADDRESS              | IPv4 or IPv6 address for the policy service; default(127.0.0.1)                                           |\n| GEOIPPOLICYD_SERVER_PORT                 | Port for the policy service; default(4646)                                                                |\n| GEOIPPOLICYD_DISABLE_HTTP_SERVICE        | Do not start the HTTP service; default(false)                                                             |\n| GEOIPPOLICYD_HTTP_ADDRESS                | HTTP address for incoming requests; default(127.0.0.1:8080)                                               |\n| GEOIPPOLICYD_HTTP_PORT                   | HTTP port for incoming requests; default(8080)                                                            |\n| GEOIPPOLICYD_USE_SASL_USERNAME           | Use 'sasl_username' instead of the 'sender' attribute; default(false)                                     |\n| GEOIPPOLICYD_REDIS_ADDRESS               | IPv4 or IPv6 address for the Redis service; default(127.0.0.1)                                            |\n| GEOIPPOLICYD_REDIS_PORT                  | Port for the Redis service; default(6379)                                                                 |\n| GEOIPPOLICYD_REDIS_USERNAME              | Redis username                                                                                            |\n| GEOIPPOLICYD_REDIS_PASSWORD              | Redis password                                                                                            |\n| GEOIPPOLICYD_REDIS_REPLICA_ADDRESS       | IPv4 or IPv6 address for a Redis service (replica)                                                        |\n| GEOIPPOLICYD_REDIS_REPLICA_PORT          | Port for a Redis service (replica)                                                                        |\n| GEOIPPOLICYD_REDIS_SENTINELS             | List of space seperated sentinel servers                                                                  |\n| GEOIPPOLICYD_REDIS_SENTINEL_MASTER_NAME  | Sentinel master name                                                                                      |\n| GEOIPPOLICYD_REDIS_SENTINEL_USERNAME     | Redis sentinel username                                                                                   |\n| GEOIPPOLICYD_REDIS_SENTINEL_PASSWORD     | Redis sentinel password                                                                                   |\n| GEOIPPOLICYD_REDIS_PREFIX                | Redis prefix; default(geopol_)                                                                            |\n| GEOIPPOLICYD_REDIS_DATABASE_NUMBER       | Redis database number                                                                                     |\n| GEOIPPOLICYD_REDIS_TTL                   | Redis TTL; default(3600)                                                                                  |\n| GEOIPPOLICYD_GEOIP_PATH                  | Full path to the GeoIP database file; default(/usr/share/GeoIP/GeoLite2-City.mmdb)                        |\n| GEOIPPOLICYD_MAX_COUNTRIES               | Maximum number of countries before rejecting e-mails; default(3)                                          |\n| GEOIPPOLICYD_MAX_IPS                     | Maximum number of IP addresses before rejecting e-mails; default(10)                                      |\n| GEOIPPOLICYD_HOME_COUNTRIES              | List of known home country codes                                                                          |\n| GEOIPPOLICYD_MAX_HOME_COUNTRIES          | Maximum number of home countries before rejecting e-mails; default(3)                                     |\n| GEOIPPOLICYD_MAX_HOME_IPS                | Maximum number of home IP addresses before rejecting e-mails; default(10)                                 |\n| GEOIPPOLICYD_BLOCK_PERMANENT             | Do not expire senders from Redis, if they were blocked in the past                                        |\n| GEOIPPOLICYD_CUSTOM_SETTINGS_PATH        | Custom settings with different IP and country limits                                                      |\n| GEOIPPOLICYD_HTTP_USE_BASIC_AUTH         | Enable basic HTTP auth; default(false)                                                                    |\n| GEOIPPOLICYD_HTTP_USE_SSL                | Enable HTTPS; default(false)                                                                              |\n| GEOIPPOLICYD_HTTP_BASIC_AUTH_USERNAME    | HTTP basic auth username                                                                                  |\n| GEOIPPOLICYD_HTTP_BASIC_AUTH_PASSWORD    | HTTP basic auth password                                                                                  |\n| GEOIPPOLICYD_HTTP_TLS_CERT               | HTTP TLS server certificate (full chain); default(/localhost.pem)                                         |\n| GEOIPPOLICYD_HTTP_TLS_KEY                | HTTP TLS server key; default(/localhost-key.pem)                                                          |\n| GEOIPPOLICYD_PROMETHEUS_ENABLED          | Enable Prometheus metrics on the HTTP service; default(false)                                             |\n| GEOIPPOLICYD_PROMETHEUS_PATH             | HTTP path for Prometheus metrics; default(/metrics)                                                       |\n| GEOIPPOLICYD_PROMETHEUS_RUNTIME_METRICS  | Include Go runtime and process metrics; default(false)                                                    |\n| GEOIPPOLICYD_OTEL_ENABLED                | Enable OpenTelemetry OTLP HTTP export; default(false)                                                     |\n| GEOIPPOLICYD_OTEL_TRACES_ENABLED         | Export OpenTelemetry traces when OTel is enabled; default(false)                                          |\n| GEOIPPOLICYD_OTEL_METRICS_ENABLED        | Export OpenTelemetry metrics when OTel is enabled; default(false)                                         |\n| GEOIPPOLICYD_OTEL_SERVICE_NAME           | OpenTelemetry service.name resource attribute; default(geoip-policyd)                                     |\n| GEOIPPOLICYD_OTEL_SERVICE_VERSION        | OpenTelemetry service.version resource attribute; default(current binary version)                         |\n| GEOIPPOLICYD_OTEL_EXPORTER_OTLP_ENDPOINT | OTLP HTTP endpoint as host:port or base URL                                                               |\n| GEOIPPOLICYD_OTEL_EXPORTER_OTLP_HEADERS  | Comma-separated OTLP HTTP headers as key=value pairs                                                      |\n| GEOIPPOLICYD_OTEL_EXPORTER_OTLP_INSECURE | Use insecure OTLP HTTP transport; default(false)                                                          |\n| GEOIPPOLICYD_OTEL_SAMPLE_RATIO           | OpenTelemetry trace sampling ratio between 0.0 and 1.0; default(1.0)                                      |\n| GEOIPPOLICYD_USE_LDAP                    | Enable LDAP support; default(false)                                                                       |\n| GEOIPPOLICYD_LDAP_SERVER_URIS            | Server URI. Specify multiple times, if you need more than one server; default(ldap://127.0.0.1:389/)      |\n| GEOIPPOLICYD_LDAP_BASEDN                 | Base DN                                                                                                   |\n| GEOIPPOLICYD_LDAP_BINDPW                 | Bind PW                                                                                                   |\n| GEOIPPOLICYD_LDAP_FILTER                 | Filter with %s placeholder; default( (\u0026(objectClass=*)(mailAlias=%s)) )                                   |\n| GEOIPPOLICYD_LDAP_RESULT_ATTRIBUTE       | Result attribute for the requested mail sender; default(mailAccount)                                      |\n| GEOIPPOLICYD_LDAP_STARTTLS               | If this option is given, use StartTLS                                                                     |\n| GEOIPPOLICYD_LDAP_TLS_SKIP_VERIFY        | Skip TLS server name verification                                                                         |\n| GEOIPPOLICYD_LDAP_TLS_CAFILE             | File containing TLS CA certificate(s)                                                                     |\n| GEOIPPOLICYD_LDAP_TLS_CLIENT_CERT        | File containing a TLS client certificate                                                                  |\n| GEOIPPOLICYD_LDAP_TLS_CLIENT_KEY         | File containing a TLS client key                                                                          |\n| GEOIPPOLICYD_LDAP_SASL_EXTERNAL          | Use SASL/EXTERNAL instead of a simple bind; default(false)                                                |\n| GEOIPPOLICYD_LDAP_SCOPE                  | LDAP search scope [base, one, sub]; default(sub)                                                          |\n| GEOIPPOLICYD_LDAP_IDLE_POOL_SIZE         | LDAP pre-forked (idle) pool size; default(3)                                                              |\n| GEOIPPOLICYD_LDAP_POOL_SIZE              | LDAP max pool size; default(10)                                                                           |\n| GEOIPPOLICYD_RUN_ACTIONS                 | Run actions, if a sender is over limits; default(false)                                                   |\n| GEOIPPOLICYD_RUN_ACTION_OPERATOR         | Run the operator action; default(false)                                                                   |\n| GEOIPPOLICYD_OPERATOR_TO                 | E-Mail To-header for the operator action                                                                  |\n| GEOIPPOLICYD_OPERATOR_FROM               | E-Mail From-header for the operator action                                                                |\n| GEOIPPOLICYD_OPERATOR_SUBJECT            | E-Mail Subject-header for the operator action; default([geoip-policyd] An e-mail account was compromised) |\n| GEOIPPOLICYD_OPERATOR_MESSAGE_CT         | E-Mail Content-Type-header for the operator action; default(text/plain)                                   |\n| GEOIPPOLICYD_OPERATOR_MESSAGE_PATH       | Full path to the e-mail message file for the operator action                                              |\n| GEOIPPOLICYD_MAIL_SERVER_ADDRESS         | E-mail server address for notifications                                                                   |\n| GEOIPPOLICYD_MAIL_SERVER_PORT            | E-mail server port number                                                                                 |\n| GEOIPPOLICYD_MAIL_HELO                   | E-mail server HELO/EHLO hostname; default(localhost)                                                      |\n| GEOIPPOLICYD_MAIL_PORT                   | E-mail server port number; default(587)                                                                   |\n| GEOIPPOLICYD_MAIL_USERNAME               | E-mail server username                                                                                    |\n| GEOIPPOLICYD_MAIL_PASSWORD               | E-mail server password                                                                                    |\n| GEOIPPOLICYD_MAIL_SSL_ON_CONNECT         | Use TLS on connect for the e-mail server; default(false)                                                  |\n| GEOIPPOLICYD_VERBOSE_LEVEL               | Log level. One of 'none', 'info' or 'debug'                                                               |\n\nAt least one listener must remain enabled. Do not set both\n`GEOIPPOLICYD_DISABLE_POLICY_SERVICE=true` and\n`GEOIPPOLICYD_DISABLE_HTTP_SERVICE=true`. Prometheus export needs the HTTP\nservice because the scrape endpoint is registered there.\n\nBack to [table of contents](#table-of-contents)\n\n# Observability\n\nObservability is disabled by default. Enabling it adds process-local metrics and optional OTLP HTTP export so operators\ncan observe HTTP requests, Postfix policy requests, policy decisions, Redis operations, LDAP operations and pool state,\nGeoIP lookups and reloads, CDB lookups, operator actions, and TCP connection lifecycle events.\n\nMetric and trace labels intentionally do not include sender addresses, client IP addresses, Redis keys, or raw LDAP\nfilters. This keeps label cardinality predictable and avoids exporting sensitive request identifiers.\n\n## Prometheus\n\nEnable the Prometheus endpoint on the existing HTTP service:\n\n```shell\ngeoip-policyd server --prometheus-enabled\n```\n\nThe default endpoint is:\n\n```text\nhttp://127.0.0.1:8080/metrics\n```\n\n`--disable-http-service` cannot be combined with `--prometheus-enabled`.\n\nIf HTTP basic auth is enabled, the metrics endpoint uses the same credentials as the REST API:\n\n```shell\ncurl -u testuser:testsecret http://127.0.0.1:8080/metrics\n```\n\nUseful options:\n\n| Option                           | Default    | Description                                      |\n|----------------------------------|------------|--------------------------------------------------|\n| --prometheus-enabled             | false      | Enables the `/metrics` endpoint                  |\n| --prometheus-path                | /metrics   | Changes the metrics endpoint path                |\n| --prometheus-runtime-metrics     | false      | Adds Go runtime and process collectors           |\n\nSide effects:\n\n* The HTTP listener serves one additional route when Prometheus is enabled.\n* Go runtime and process collectors add standard `go_*` and `process_*` metrics only when explicitly enabled.\n* `/metrics` is not instrumented by the HTTP middleware to avoid self-scrape noise.\n\n## Grafana dashboard\n\nThe repository ships an importable Grafana 11 dashboard at\n`contrib/grafana/geoip-policyd-grafana11-dashboard.json`. It visualizes the\nPrometheus metrics for policy decisions, HTTP routes, GeoIP lookups and reloads,\nRedis operations, LDAP operations and pool state, CDB lookups, operator actions,\nTCP policy-service connections, and optional Go runtime and process collectors.\n\nPrerequisites:\n\n| Requirement                | Default or note                                                     |\n|----------------------------|---------------------------------------------------------------------|\n| Grafana                    | Dashboard JSON targets Grafana 11 schema version 41                 |\n| Prometheus datasource      | Selected through the `DS_PROMETHEUS` dashboard datasource variable  |\n| geoip-policyd Prometheus   | Start the service with `--prometheus-enabled`                       |\n| Runtime/process panels     | Also enable `--prometheus-runtime-metrics`; otherwise these stay empty |\n\nImport it through the Grafana UI with **Dashboards \u003e New \u003e Import**. For API\nimports, wrap the dashboard JSON for Grafana's create/update endpoint:\n\n```shell\npython3 -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\"}))' \\\n  contrib/grafana/geoip-policyd-grafana11-dashboard.json |\ncurl -X POST \\\n  -H \"Authorization: Bearer ${GRAFANA_TOKEN}\" \\\n  -H \"Content-Type: application/json\" \\\n  --data-binary @- \\\n  http://127.0.0.1:3000/api/dashboards/db\n```\n\nAfter import, choose the Prometheus datasource and, if needed, narrow the\ndashboard variables for `job`, `instance`, policy `source`, and HTTP `route`.\n\nSide effects:\n\n* Importing the JSON creates or updates only the Grafana dashboard with UID\n  `geoip-policyd-g11`.\n* Dashboard queries read from Prometheus only and do not call geoip-policyd.\n* Runtime panels are intentionally optional because `go_*` and `process_*`\n  metrics are exported only when runtime metrics are enabled.\n\n## OpenTelemetry\n\nOpenTelemetry uses OTLP over HTTP. Enable it with an endpoint:\n\n```shell\ngeoip-policyd server \\\n  --otel-enabled \\\n  --otel-traces-enabled \\\n  --otel-exporter-otlp-endpoint http://127.0.0.1:4318\n```\n\nThe endpoint may be `host:port` or a base URL such as `http://collector.example:4318`. The exporter uses the standard\nOTLP HTTP paths for traces and metrics. At least one of `--otel-traces-enabled` or `--otel-metrics-enabled` must be set\nwhen `--otel-enabled` is set.\n\nUseful options:\n\n| Option                         | Default          | Description                                                     |\n|--------------------------------|------------------|-----------------------------------------------------------------|\n| --otel-enabled                 | false            | Enables OpenTelemetry export                                    |\n| --otel-traces-enabled          | false            | Exports traces when OTel is enabled                             |\n| --otel-metrics-enabled         | false            | Exports OTel metrics when OTel is enabled                       |\n| --otel-service-name            | geoip-policyd    | Sets the `service.name` resource attribute                      |\n| --otel-service-version         | current version  | Sets the `service.version` resource attribute                   |\n| --otel-exporter-otlp-endpoint  | empty            | OTLP HTTP collector endpoint; required when OTel is enabled     |\n| --otel-exporter-otlp-headers   | empty            | Comma-separated `key=value` headers for the OTLP HTTP exporter  |\n| --otel-exporter-otlp-insecure  | false            | Uses insecure transport for OTLP HTTP                           |\n| --otel-sample-ratio            | 1.0              | Trace sampling ratio from `0.0` to `1.0`                        |\n\nExample with headers:\n\n```shell\ngeoip-policyd server \\\n  --otel-enabled \\\n  --otel-traces-enabled \\\n  --otel-exporter-otlp-endpoint https://collector.example:4318 \\\n  --otel-exporter-otlp-headers \"authorization=Bearer token\"\n```\n\nSide effects:\n\n* The process opens outbound HTTP connections to the configured collector.\n* SIGINT and SIGTERM trigger a best-effort telemetry flush before process exit.\n* If OTel is enabled, at least one of traces or metrics must remain enabled and the OTLP endpoint is required.\n\nBack to [table of contents](#table-of-contents)\n\n# REST interface\n\n## GET request /reload\n\nRequest: reload     \nResponse: No results\n\nExample:\n\n```shell\n# Plain http without basic auth\ncurl \"http://localhost:8080/reload\"\n\n# Plain with basic auth\ncurl \"http://localhost:8080/reload\" -u testuser:testsecret\n\n# Secured with basic auth\ncurl -k \"https://localhost:8443/reload\" -u testuser:testsecret\n```\n\nBack to [table of contents](#table-of-contents)\n\n## GET request /custom-settings\n\nRequest: get current custom settings in JSON format     \nResponse: JSON output of the currently loaded custom settings\n\nExample:\n\n```shell\n# Plain http without basic auth\ncurl \"http://localhost:8080/custom-settings\" | jq\n\n# Plain with basic auth\ncurl \"http://localhost:8080/custom-settings\" -u testuser:testsecret | jq\n\n# Secured with basic auth\ncurl -k \"https://localhost:8443/custom-settings\" -u testuser:testsecret | jq\n```\n\nExample result from default [custom.json](custom.json):\n\n```json\n[\n  {\n    \"comment\": \"Allow only two countries and a maximum of 5 IP addresses\",\n    \"sender\": \"christian@roessner.email\",\n    \"ips\": 5,\n    \"countries\": 2\n  },\n  {\n    \"comment\": \"Allow at least 4 countries and go with the default IP address limit\",\n    \"sender\": \"test1@example.com\",\n    \"ips\": 0,\n    \"countries\": 4\n  },\n  {\n    \"comment\": \"Go with the default country limit, but allow up to 30 IP addresses\",\n    \"sender\": \"test2@example.com\",\n    \"ips\": 30,\n    \"countries\": 0\n  }\n]\n```\n\nBack to [table of contents](#table-of-contents)\n\n## POST request /remove\n\nRequest: Submit an email account that should be unlocked        \nResponse: No results\n\nExample:\n\n```shell\n# Plain http without basic auth\ncurl -d '{\"key\":\"sender\",\"value\":\"user@example.com\"}' -H \"Content-Type: application/json\" -X POST \"http://localhost:8080/remove\"\n\n# Plain with basic auth\ncurl -d '{\"key\":\"sender\",\"value\":\"user@example.com\"}' -H \"Content-Type: application/json\" -X POST \"http://localhost:8080/remove\" -u testuser:testsecret\n\n# Secured with basic auth\ncurl -k -d '{\"key\":\"sender\",\"value\":\"user@example.com}\"' -H \"Content-Type: application/json\" -X POST \"https://localhost:8443/remove\" -u testuser:testsecret\n```\n\nBack to [table of contents](#table-of-contents)\n\n## POST request /query\n\nRequest: Submit a client address and a sender name to get a policy result\nRequest format: JSON\nResponse: JSON formatted policy decision\n\nExample request:\n```shell\n# Plain http without basic auth\ncurl -d '{ \"key\": \"client\", \"value\": { \"address\": \"1.2.3.4\", \"sender\": \"user@example.com\" } }' -H \"Content-Type: application/json\" -X POST \"http://localhost:8080/query\"\n\n# Plain with basic auth\ncurl -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\n\n# Secured with basic auth\ncurl -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\n```\n\nAllowed policy response example:\n\n````json\n{\n  \"guid\": \"2FOLGkYQwUB8XTQhdTY3csRkNV2\",\n  \"object\": \"client\",\n  \"operation\": \"query\",\n  \"result\": true\n}\n````\n\nForbidden policy response example:\n\n````json\n{\n  \"guid\": \"2FOLGkYQwUB8XTQhdTY3csRkNV2\",\n  \"object\": \"client\",\n  \"operation\": \"query\",\n  \"result\": false\n}\n````\n\nIf \"?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.\n\nBack to [table of contents](#table-of-contents)\n\n## PUT request /update\n\nRequest: Set custom settings. This will overwrite a custom settings file or initiates settings, if there have not been\nany settings before (no config file given).       \nResponse: No results\n\n---\n***Note***\n\nIf you use a custom settings file and send new data with a PUT request, the settings are updated in memory. But if you\ndo a GET request afterwards and reloading data, the settings from the file will be loaded again!\n\n---\n\nExample:\n\n```shell\n# Plain http without basic auth\ncurl -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\"\n\n# Plain with basic auth\ncurl -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\n\n# Secured with basic auth\ncurl -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\n```\n\nBack to [table of contents](#table-of-contents)\n\n## PATCH request /modify\n\nRequest: Send changed settings for a given sender. If the sender does not exist, add a new record to the custom\nsettings.       \nResponse: No results\n\nExample:\n\n````shell\n# Plain http without basic auth\ncurl -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\"\n\n# Plain with basic auth\ncurl -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\n\n# Secured with basic auth\ncurl -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\n````\n\n\u003e Note:\n\u003e \n\u003e This endpoint is not yet implemented nor tested for home countries!\n\nBack to [table of contents](#table-of-contents)\n\n## DELETE request /remove\n\nRequest: Remove an entry from the custom settings by using the sender as the key.        \nResponse: No results\n\nExample:\n\n````shell\n# Plain http without basic auth\ncurl -d '{\"key\":\"sender\",\"value\":\"christian@roessner.email\"}' -H \"Content-Type: application/json\" -X DELETE \"http://localhost:8080/remove\"\n\n# Plain with basic auth\ncurl -d '{\"key\":\"sender\",\"value\":\"christian@roessner.email\"}' -H \"Content-Type: application/json\" -X DELETE \"http://localhost:8080/remove\" -u testuser:testsecret\n\n# Secured with basic auth\ncurl -k -d '{\"key\":\"sender\",\"value\":\"christian@roessner.email\"}\"' -H \"Content-Type: application/json\" -X DELETE \"https://localhost:8443/remove\" -u testuser:testsecret\n````\n\nBack to [table of contents](#table-of-contents)\n\n# Endpoint test client\n\nThe `contrib/geoip-policyd-test.py` script exercises the REST interface and the\nraw Postfix policy socket with useful local defaults. It uses only the Python\nstandard library and does not require a virtual environment.\n\nThe repository contains `testdata/GeoIP2-City-Test.mmdb` for local smoke tests\nthat need a valid MaxMind database without depending on an operator-provided\nGeoLite file.\n\nDefault targets:\n\n| Option          | Default                          |\n|-----------------|----------------------------------|\n| `--base-url`    | `http://127.0.0.1:8080`          |\n| `--policy-host` | `127.0.0.1`                      |\n| `--policy-port` | `4646`                           |\n| `--sender`      | `geoip-policyd-test@example.com` |\n| `--address`     | `127.0.0.1`                      |\n| `--recipient`   | `postmaster@example.com`         |\n\nRun a single endpoint check:\n\n```shell\ncontrib/geoip-policyd-test.py --address 8.8.8.8 --sender user@example.com query\ncontrib/geoip-policyd-test.py --address 8.8.8.8 dovecotpolicy --command allow\ncontrib/geoip-policyd-test.py --address 8.8.8.8 policy\n```\n\nRun the complete endpoint suite:\n\n```shell\ncontrib/geoip-policyd-test.py all\n```\n\nThe `all` command checks `GET /custom-settings`, `POST /query`,\n`POST /dovecotpolicy?command=report`, `POST /dovecotpolicy?command=allow`,\nthe raw Postfix policy socket, `PUT /update`, `PATCH /modify`,\n`DELETE /remove`, `POST /remove`, and `GET /reload`. The suite intentionally\ntouches mutating custom-settings and unlock endpoints, so run it against a\ndedicated test instance or with a sender and Redis prefix that are safe to\nmodify.\n\nFor HTTPS with a local or self-signed certificate, add `--insecure`. For HTTP\nbasic authentication, add `--username` and `--password`.\n\nExample against isolated local test ports:\n\n```shell\ncontrib/geoip-policyd-test.py \\\n  --base-url http://127.0.0.1:18080 \\\n  --policy-port 14646 \\\n  --address 8.8.8.8 \\\n  all\n```\n\nThe output is a compact table with the endpoint name, method, target, status,\nexpected status, result, and a short response summary. The process exits with\nstatus `0` only when all selected checks pass.\n\n## OpenTelemetry and Prometheus smoke test\n\nThe `contrib/otel_prometheus_smoke.go` helper runs an external observability\nsmoke test. It starts a fake Redis server, a fake OTLP/HTTP collector, and a\nreal `geoip-policyd server` process on random loopback ports. It then sends a\nreal `POST /query`, scrapes Prometheus, terminates the child process cleanly,\nand validates the exported OTLP protobuf payloads.\n\nRun it through the Makefile:\n\n```shell\nmake smoke-observability\n```\n\nThe smoke checks that Prometheus contains HTTP, policy, Redis, and GeoIP\nmetrics. It also checks that OTLP traces contain this graph:\n\n```text\nHTTP POST /query\n`-- policy.request\n    |-- geoip.lookup\n    |   `-- geoip.maxmind.lookup\n    |-- redis.command GET\n    `-- redis.command SET\n```\n\nOTLP metrics are checked for `geoip_policyd_http_requests`,\n`geoip_policyd_policy_requests`, `geoip_policyd_redis_operations`, and\n`geoip_policyd_geoip_lookups`.\n\nThe helper uses only local loopback ports and temporary files. To run it with an\nexisting binary or a different GeoIP database, use:\n\n```shell\ngo run -mod=vendor ./contrib \\\n  --binary /path/to/geoip-policyd \\\n  --geoip-path ./GeoIP2-Country.mmdb \\\n  --address 8.8.8.8\n```\n\nBack to [table of contents](#table-of-contents)\n\n# Actions\n\n## Operator action\n\nYou can activate actions that will be taken, if a sender was declared compromised. At the moment you can send a\nnotification to an e-mail operator. To do this, you must activate actions in general as well as the operator action. You\nneed also to define all the required operator parameters as To, From, Subject, CT and of course an e-mail server (\nincluding all required settings) to get things done.\n\nExample:\n\n```shell\ngeoip-policyd ...other-options... \\\n  --run-actions \\\n  --run-action-operator \\\n  --operator-to \"\u003coperator@example.com\u003e\" \\\n  --operator-from \"\u003cno-reply@submission.example.com\u003e\" \\\n  --operator-message-ct \"text/plain\" \\\n  --operator-message-path ./mailtemplate.txt \\\n  --mail-server submission.example.com \\\n  --mail-port 587 \\\n  --mail-username \"some_username\" \\\n  --mail-password some-secret\n```\n\nBack to [table of contents](#table-of-contents)\n\n# LDAP\n\nYou can use LDAP to send the sender attribute and to retrieve whatever that makes your request unique. If you have\ncustomers that use virtual aliases and that belong to exactly one account, this may help you to aggregate e-mail sender\nrequests.\n\nExample:\n\n| virtual alias     | real account       |\n|-------------------|--------------------|\n| user1@example.com | unique@account.net |\n| foo@bar.org       | unique@account.net |\n\nBoth belong to one and the same account. Without LDAP this would result in two records in Redis. With LDAP it results\ninto the real unique account.\n\nIt is also possible to not retrieve another unique mail account from LDAP. You can also return the entryUUID field or\nsome other field like uid or uniqueIdentifier (LDAP overlay unique to enforce uniqueness!).\n\nHere is my personal example of a docker-compose.yml file that makes use of LDAP:\n\n### docker-compose.yml\n\n```yaml\nversion: \"3.8\"\n\nservices:\n\n  geoip-policyd:\n    image: ...whatever.../geoip-policyd:latest\n    logging:\n      driver: journald\n      options:\n        tag: geoip-policyd\n    network_mode: host\n    environment:\n      VERBOSE: \"debug\"\n      SERVER_ADDRESS: \"127.0.0.1\"\n      SERVER_PORT: 4646\n      HTTP_ADDRESS: \"127.0.0.1:8080\"\n      REDIS_ADDRESS: \"127.0.0.1\"\n      REDIS_PORT: 6379\n      REDIS_DATABASE_NUMBER: 0\n      GEOIP_PATH: \"/GeoLite2-City.mmdb\"\n      CUSTOM_SETTINGS_PATH: \"/custom.json\"\n      USE_LDAP: \"true\"\n      LDAP_STARTTLS: \"true\"\n      LDAP_SASL_EXTERNAL: \"true\"\n      LDAP_SERVER_URIS: \"ldap://****:389/, ldap://****:389/\"\n      LDAP_BASEDN: \"ou=people,...\"\n      LDAP_TLS_CAFILE: \"/cacert.pem\"\n      LDAP_TLS_CLIENT_CERT: \"/cert.pem\"\n      LDAP_TLS_CLIENT_KEY: \"/key.pem\"\n      LDAP_FILTER: \"(\u0026(objectClass=rnsMSDovecotAccount)(objectClass=rnsMSPostfixAccount)(rnsMSRecipientAddress=%s))\"\n      LDAP_RESULT_ATTRIBUTE: \"uid\"\n    volumes:\n      - /usr/share/GeoIP/GeoLite2-City.mmdb:/GeoLite2-City.mmdb:ro,Z\n      - ./custom.json:/custom.json:ro,Z\n      - /etc/pki/tls/certs/cacert.pem:/cacert.pem:ro,Z\n      - /etc/ssl/certs/cert.pem:/cert.pem:ro,Z\n      - /etc/ssl/private/key.pem:/key.pem:ro,Z\n```\n\nA result in the logs looks like this:\n\n```\ngeoip-policyd_1  | 2021/09/14 06:53:28 Info: sender=\u003c2F7032A7-D2BE-4178-87B2-A8D3AC0F32F1\u003e; countries=[DE]; ip_addresses=[x.x.x.x]; #countries=1/1; #ip_addresses=1/1; action=DUNNO\n```\n\nRedis-result:\n\n    127.0.0.1:6379\u003e get geopol_2F7032A7-D2BE-4178-87B2-A8D3AC0F32F1\n\n```json\n\"{\\\"Ips\\\":[\\\"x.x.x.x\\\"],\\\"Countries\\\":[\\\"DE\\\"]}\"\n```\n\nThis way you get some pseudo anonymization.\n\nIf you do so, you also have to modify your custom.json file, if you use one:\n\n### custom.json\n\n```json\n{\n  \"data\": [\n    {\n      \"comment\": \"Some comment\",\n      \"sender\": \"4FFDDFD3-BE1B-4639-8465-32A9A709F4CF\",\n      \"ips\": 5,\n      \"countries\": 2\n    },\n    {\n      \"comment\": \"Whatever else\",\n      \"sender\": \"2F7032A7-D2BE-4178-87B2-A8D3AC0F32F1\",\n      \"ips\": 1,\n      \"countries\": 1\n    },\n    {\n      \"comment\": \"And another one goes here\",\n      \"sender\": \"6B806FF8-8BA5-40CC-A0FE-602CF2AEEDE2\",\n      \"countries\": 1\n    }\n  ]\n}\n```\n\nBack to [table of contents](#table-of-contents)\n\n## License\n\nThis project is licensed under the GPLv3 License - see the [LICENSE](LICENSE) file for details.\n\n\u003e Note\n\u003e\n\u003e The license has changed from AGPL-3 to GPL-3! This step is required to provide docker images.\n\u003e \n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcroessner%2Fgeoip-policyd","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcroessner%2Fgeoip-policyd","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcroessner%2Fgeoip-policyd/lists"}