Skip to content

reinvented-stuff/nginx-mail-auth-http-server

Repository files navigation

Vulnerabilities Bugs Codacy Badge

Nginx Mail Auth Server

Nginx Mail Auth HTTP Server provides an auth service for Nginx Mail module.

Benifits of using nginx as a mail proxy:

  1. Nginx is fast and thin
  2. You can do load balancing
  3. You can use multiple upstream servers
  4. Configuration is dynamic

Workflow Diagram


      +-------------+           +---------------+          +--------------+
      |             |           |               |          |              |
      |     MTA     <----7------+     Nginx     <----2-----+    Gmail     |
      |             |   SMTP    |               |   SMTP   |              |
      +------+------+           +-----^---+-----+          +------^-------+
             |                        |   |                       |
             |                        |   |                       |
             8                        6   3  HTTP(S)              1 SMTP
             |                        |   |                       |
             |                        |   |                       |
      +------v------+           +-----+---v-----+          +------+-------+
      |             +-----5----->               |          |              |
      |    MySQL    |           |  Auth Server  |          |    Client    |
      |             <-----4-----+               |          |              |
      +-------------+   MySQL   +---------------+          +--------------+


Run as binary

./nginx-mail-auth-http-server -h
Usage of ./nginx-mail-auth-http-server:
  -config string
      Path to configuration file (default "nginx-mail-auth-http-server.conf")
  -log-secrets
      Show plaintext passwords in logs
  -verbose
      Verbose output
  -version
      Show version

Run in Docker/Podman

We currently publish docker images on github and quay.io.

In order to pull any images from there you need to have a personal github token. Please, refer to the official documantation: https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token#creating-a-token

docker run \
  --log-driver=journald \
  --log-opt=tag="nginx-auth" \
  --network host \
  --interactive \
  --tty \
  --name nginx-mail-auth-http-server \
  -v /opt/nginx-mail-auth-http-server.conf:/nginx-mail-auth-http-server.conf:ro \
  "docker.pkg.github.com/reinvented-stuff/nginx-mail-auth-http-server/nginx-mail-auth-http-server:1.4.2"
podman run \
  --log-driver=journald \
  --log-opt=tag="nginx-auth" \
  --network host \
  --interactive \
  --tty \
  --name nginx-mail-auth-http-server \
  -v /opt/nginx-mail-auth-http-server.conf:/nginx-mail-auth-http-server.conf:ro \
  "quay.io/reinventedstuff/nginx-mail-auth-http-server:1.4.2"

Nginx

nginx should be listening on 25/tcp port of your mail server.

nginx.conf

user nginx;
worker_processes auto;

...

http {
    ...
}

mail {
    server_name          mx.example.com;

    auth_http            http:https://localhost:8080/auth;
    auth_http_header     X-Origin-Mail-Key 9TlBLGKoOa;

    starttls             on;
    ssl_certificate      /etc/pki/tls/certs/mx.example.com.crt;
    ssl_certificate_key  /etc/pki/tls/private/mx.example.com.key;
    ssl_protocols        TLSv1.2 TLSv1.3;
    ssl_ciphers          HIGH:!aNULL:!MD5;
    ssl_session_cache    shared:SSL:10m;
    ssl_session_timeout  10m;


    server {
        listen                    25;
        protocol                  smtp;
        smtp_auth                 login plain none;
        auth_http_header          X-Origin-Server-Key zb4xKm9XmD;

        error_log                 /var/log/nginx/mx.example.com-mail-error.log;
        proxy_pass_error_message  on;
    }
}

MTA

Postfix configuration

postfix is supposed to be listening a different port from the one nginx does listen.

main.cf

mynetworks should contain your nginx host. This will let postfix accept all mail from nginx. smtpd_authorized_xclient_hosts should contain your nginx host. This allows Nginx to pass XCLIENT command.

inet_interfaces = localhost
mynetworks = 127.0.0.0/8
smtpd_authorized_xclient_hosts = 127.0.0.0/8
smtpd_recipient_restrictions =
    permit_mynetworks,
    ...

master.cf

To make postfix listen on a custom port you can comment out the default smtp ... line and add a new one as proposed below.

# ==========================================================================
# service type  private unpriv  chroot  wakeup  maxproc command + args
#               (yes)   (yes)   (no)    (never) (100)
# ==========================================================================
# smtp      inet  n       -       n       -       -       smtpd
31025     inet  n       -       n       -       -       smtpd -o smtpd_tls_auth_only=no

...

Application configuration

The Auth Server shold be reachable by nginx.

nginx-mail-auth-http-server.conf

{
    "listen": "127.0.0.1:8080",
    "database": {
        "uri": "mysqluser:mysqlpass@tcp(127.0.0.1:3306)/postfix",
    "driver": "mysql",
        
    "auth_lookup_queries": [
      "SELECT '127.0.0.1' as address, 25 as port WHERE :User = 'root';",
      "SELECT '127.0.0.1' as address, 10025 as port;",
    ],
        
    "relay_lookup_queries": [
      "SELECT '127.0.0.1' as address, 25 as port WHERE :RcptTo = 'nobody';"
      "SELECT '127.0.0.1' as address, 10025 as port;"
    ]
    }
}

On-fly configuration reload

The application supports hot configuration file reload. Use SIGUSR1 signal for that.

kill -s SIGUSR1 $(pgrep nginx-mail-auth-http-server)

Lookup queries

It is required for queries to return two named values: address and port (of the upstream mail server).

You can use the following named parameters in your lookup queries:

  • :User – Username part of the authentication request (only on AUTH command)
  • :Pass – Password part of the authentication request (only on AUTH command)
  • :RcptTo – RCPT TO command content (if no AUTH command passed)
  • :MailFrom – MAIL FROM command content (if no AUTH command passed)
  • :ClientIP – Client IP address passed by nginx

Example:

SELECT address, port
FROM transport
JOIN account ON account.transport_id = transport.id
WHERE account.username = :User AND account.password = MD5(:Pass);

VERP (Variable envelope return path)

Currently the server strips everything from the first found "+" symbol until the first "@" symbol.

Prometheus exporter

Grafana dashboard: https://grafana.com/grafana/dashboards/16427

There is a /metrics endpoint with a few things:

# TYPE AuthRequests counter
# HELP Number of events happened in Nginx Mail Auth Server
AuthRequests{kind="AuthRequestsSuccessLogin"} 3
AuthRequests{kind="RequestIndex"} 3
AuthRequests{kind="AuthRequests"} 8
AuthRequests{kind="AuthRequestsRelay"} 5
AuthRequests{kind="AuthRequestsSuccess"} 8
AuthRequests{kind="AuthRequestsSuccessRelay"} 5
AuthRequests{kind="AuthRequestsLogin"} 3

# TYPE InternalErrors counter
InternalErrors 32

IPv6 support

To be done.

Test

Request authentication with login and password:

curl -v -k \
 -H "Auth-Method: none" \
 -H "Auth-User: pepe_likes" \
 -H "Auth-Pass: koalas" \
 -H "Auth-Protocol: smtp" \
 -H "Auth-Login-Attempt: 1" \
 -H "Client-IP: 10.13.199.8" \
 -H "Client-Host: [UNAVAILABLE]" \
 -H "Auth-SMTP-Helo: pepes_workstation" \
 -H "Auth-SMTP-From: MAIL FROM:<[email protected]>" \
 -H "Auth-SMTP-To: RCPT TO:<[email protected]>" \
 http:https://127.0.0.1:8080/auth

Request authentication via relay:

curl -v -k \
 -H "Auth-Method: none" \
 -H "Auth-Protocol: smtp" \
 -H "Auth-Login-Attempt: 1" \
 -H "Client-IP: 10.13.199.8" \
 -H "Client-Host: [UNAVAILABLE]" \
 -H "Auth-SMTP-Helo: pepes_workstation" \
 -H "Auth-SMTP-From: MAIL FROM:<[email protected]>" \
 -H "Auth-SMTP-To: RCPT TO:<[email protected]>" \
 http:https://127.0.0.1:8080/auth