Pfxhttp is a lightweight HTTP proxy designed to integrate Postfix with external HTTP APIs for socket maps and policy services. This enables dynamic and flexible email workflows by connecting Postfix to modern APIs.
- Pfxhttp – HTTP Proxy for Postfix
- Table of contents
Pfxhttp allows you to:
- Perform dynamic lookups via socket maps, such as resolving virtual mailboxes or domains.
- Implement custom mail policy checks through HTTP-based policy services.
The application is configured using a YAML file, specifying HTTP endpoints, the format of requests, and field mappings. It supports key Postfix features like query lookups and policy service hooks.
Pfxhttp is written in Go. It can be compiled with the following commands:
make
make install- Go 1.24 or later
- For JWT support only:
- SQLite development libraries (libsqlite3-dev on Debian/Ubuntu, sqlite-devel on RHEL/CentOS)
- GCC or another C compiler for CGO
When building with JWT support, you may need to customize the SQLite library and include paths:
# For custom SQLite installation paths (only needed for JWT support)
make TAGS=jwt SQLITE_LIB_PATH=/path/to/sqlite/lib SQLITE_INCLUDE_PATH=/path/to/sqlite/include
# On macOS with Homebrew (only needed for JWT support)
make TAGS=jwt SQLITE_LIB_PATH=/usr/local/opt/sqlite/lib SQLITE_INCLUDE_PATH=/usr/local/opt/sqlite/includeYou can also set these as environment variables when building with JWT support:
export CGO_LDFLAGS="-L/path/to/sqlite/lib -lsqlite3"
export CGO_CFLAGS="-I/path/to/sqlite/include"
make TAGS=jwtPfxhttp supports optional features through build tags:
- jwt: Enables JWT authentication support (requires SQLite)
To build with JWT support:
make TAGS=jwtTo build without JWT support (no SQLite dependency):
makeIf you're building with JWT support, you can check your SQLite configuration with:
make sqlite-configAnd run the tests to ensure everything is working correctly:
# Run all tests
make test
# For JWT builds only: Run the customsql package tests (SQLite-specific)
# This is recommended for testing the SQLite functionality when using JWT
make test-customsqlNote: When testing JWT functionality with SQLite, always use the
test-customsqltarget rather than trying to test individual files, as this ensures all dependencies are properly included.
By default, Pfxhttp and its associated man pages are installed in /usr/local.
The configuration is located in one of the following directories, based on priority:
/usr/local/etc/pfxhttp//etc/pfxhttp/$HOME/.pfxhttp/- Current directory (
.)
The expected configuration file name is pfxhttp.yml.
Pfxhttp is typically run as a systemd service. Below is an example unit file:
[Unit]
Description=PfxHTTP Postfix-to-HTTP server
After=network.target
[Service]
Type=simple
Restart=always
User=pfxhttp
Group=pfxhttp
EnvironmentFile=-/etc/default/pfxhttp
ExecStart=/usr/local/sbin/pfxhttp
StandardOutput=journal
StandardError=journal
SyslogIdentifier=pfxhttp
MemoryMax=50M
CPUQuota=10%
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_CHOWN
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
NoNewPrivileges=true
ReadOnlyPaths=/etc
ProtectKernelModules=true
MemoryDenyWriteExecute=true
ProtectControlGroups=true
ProtectKernelLogs=true
ProtectClock=true
RestrictSUIDSGID=true
ProtectProc=invisible
LimitNOFILE=1024
#RestrictAddressFamilies=AF_INET AF_INET6
[Install]
WantedBy=multi-user.targetYou must create a user pfxhttp and a group pfxhttp before using this unit file!
To install and start the service:
sudo systemctl daemon-reload
sudo systemctl enable pfxhttp
sudo systemctl start pfxhttpPfxhttp provides the following command-line flags:
-
--config: Specifies the path to the configuration file. Overrides the default configuration file location.
./pfxhttp --config=/path/to/config.yml
-
--format: Sets the logging format. Available options are
yaml,tomlorjson../pfxhttp --format=json
Use these flags as needed to customize the behavior of the application during runtime.
Pfxhttp is configured through a YAML file named pfxhttp.yml (or a custom file specified with the --config and --format flags). The following are the main sections:
The server section contains global options, including:
- Listeners: Define socket map and policy service listeners for Postfix integration.
- Logging: Enable JSON-formatted logs and set verbosity (
debug,info, orerror). - HTTP Client Options: Configure connection limits, timeouts, and optional TLS settings.
- JWT Authentication: Configure JWT authentication for HTTP requests with automatic token management.
- Response Cache: Optional in-memory cache to serve responses when the backend is unavailable.
Below is a detailed example configuration for pfxhttp.yml:
server:
listen:
- kind: "socket_map"
name: "demo_map"
type: "tcp"
address: "[::]"
port: 23450
- kind: "policy_service"
name: "example_policy"
type: "tcp"
address: "[::]"
port: 23451
logging:
json: true
level: info
tls:
enabled: true
http_client_skip_verify: true
# Path to SQLite database for JWT token storage
jwt_db_path: "/var/lib/pfxhttp/jwt.db"
# Optional response cache to serve data during backend outages
response_cache:
enabled: true
ttl: 5m # cache lifetime per entry
socket_maps:
demo_map:
target: "https://127.0.0.1:9443/api/v1/custom/map"
custom_headers:
- "Authorization: Bearer <token>"
payload: >
{
"key": "{{ .Key }}"
}
status_code: 200
value_field: "data.result"
error_field: "error"
no_error_value: "not-found"
jwt_demo_map:
target: "https://127.0.0.1:9443/api/v1/custom/map"
jwt_auth:
enabled: true
token_endpoint: "https://example.com/api/token"
credentials:
username: "foobar"
password: "secret"
payload: >
{
"key": "{{ .Key }}"
}
status_code: 200
value_field: "data.result"
error_field: "error"
no_error_value: "not-found"
policy_services:
example_policy:
target: "https://127.0.0.1:9443/api/v1/custom/policy"
custom_headers:
- "Authorization: Bearer <token>"
payload: "{{ .Key }}"
status_code: 200
value_field: "policy.result"
error_field: "policy.error"
no_error_value: "OK"
jwt_example_policy:
target: "https://127.0.0.1:9443/api/v1/custom/policy"
jwt_auth:
enabled: true
token_endpoint: "https://example.com/api/token"
credentials:
username: "foobar"
password: "secret"
payload: "{{ .Key }}"
status_code: 200
value_field: "policy.result"
error_field: "policy.error"
no_error_value: "OK"Important: Postfix has a hardcoded socket map reply size limit of 100,000 bytes (Postfix 3.9.1 or older).
Pfxhttp includes an optional in-memory response cache. It always forwards responses from your backend, but if the backend becomes unavailable, it can serve a previously cached response for a configurable time (TTL).
Behavior:
- On backend failure: if a valid cache entry exists for the same map/policy name and key, it is returned.
- Cache population:
- Socket maps: cache only definitive successes (status "OK").
- Policy services: cache only definitive actions (anything other than empty).
- Expiration: entries expire after the TTL.
Configuration (server section):
server:
response_cache:
enabled: true
ttl: 5mNotes:
- TTL must be between 1s and 168h (7 days).
- Cache is in-memory and per-process; it is cleared on restart.
- Keys are derived from the tuple (name, key). For policy services, the key is the JSON of the policy payload.
Pfxhttp supports JWT authentication for HTTP requests to the target endpoints. This allows you to securely authenticate with APIs that require JWT tokens.
Note: JWT support is optional and requires building Pfxhttp with the
jwtbuild tag (make TAGS=jwt). This feature depends on SQLite for token storage.
The JWT authentication feature includes:
- Automatic token fetching from a token endpoint
- Token storage in a SQLite database
- Automatic token refresh when tokens expire
To configure JWT authentication:
-
Make sure you've built Pfxhttp with JWT support:
make TAGS=jwt
-
Set the
jwt_db_pathin the server section to specify where tokens will be stored:server: jwt_db_path: "/var/lib/pfxhttp/jwt.db"
-
Configure JWT authentication for each socket map or policy service that requires it:
socket_maps: example: target: "https://api.example.com/endpoint" jwt_auth: enabled: true token_endpoint: "https://api.example.com/token" credentials: some_username_identifier: "your_username" some_password_identifier: "your_password" content_type: "application/json" # Optional: "application/x-www-form-urlencoded" (default) or "application/json"
The JWT token will be automatically fetched from the token endpoint and included in the Authorization header as a Bearer token for all requests to the target endpoint.
If you build Pfxhttp without the jwt build tag, the JWT configuration in the YAML file will be ignored, and no JWT authentication will be performed.
Pfxhttp allows you to control HTTP compression per target (socket map or policy service). This is useful when your backend supports gzip and you want to reduce bandwidth or comply with specific API requirements. Nauthilus backends support gzip exclusively.
- http_request_compression: When true, Pfxhttp gzips the request body and sets Content-Encoding: gzip. Disabled by default.
- http_response_compression: When true, Pfxhttp advertises Accept-Encoding: gzip and will transparently decompress gzip responses if the server replies with Content-Encoding: gzip. Disabled by default.
Notes:
- Compression settings are defined per target, not globally.
- The HTTP client's automatic gzip handling is disabled to ensure per-target control.
- Only gzip is supported currently.
Example:
socket_maps: demo: target: https://127.0.0.1:9443/api/v1/custom/postfix/socket_map http_request_compression: true http_response_compression: true payload: > { "key": "{{ .Key }}" } status_code: 200 value_field: "demo_value"
policy_services: policy: target: https://127.0.0.1:9443/api/v1/custom/postfix/policy_service http_request_compression: false http_response_compression: true payload: "{{ .Key }}" status_code: 200 value_field: "result"
To configure Postfix to use a socket map, simply add it to your main.cf:
# main.cf
virtual_mailbox_domains = socketmap:tcp:127.0.0.1:23450:demo_map
Here, Postfix connects to the TCP socket map listener defined in pfxhttp.yml for demo_map.
To use a policy service, include it in your recipient restrictions list in main.cf:
# main.cf
smtpd_recipient_restrictions =
permit_mynetworks,
reject_unauth_destination,
check_policy_service inet:127.0.0.1:23451
This setup enables Postfix to query the policy service defined in pfxhttp.yml for example_policy.
Logs are output to the console by default and should be captured by the service manager (e.g., systemd). Log verbosity is configurable in the pfxhttp.yml file.
If Pfxhttp fails to start, verify the following:
- Ensure the configuration file (
/etc/pfxhttp/pfxhttp.yml) is valid and complete. - Ensure the service is running with the appropriate permissions for the configured resources.
Contributions are welcome! Feel free to submit pull requests or issues to improve the project. The project is distributed under the MIT License.
- Postfix Documentation
- Nauthilus
- Manpages:
pfxhttp(8): Overview and service managementpfxhttp.yml(5): Detailed configuration guide