diff --git a/README.md b/README.md
index e69de29..d919617 100644
--- a/README.md
+++ b/README.md
@@ -0,0 +1,462 @@
+# Dovecot Web Authenticator for App Passwords
+
+This projects purpose is to provide a small App Passwords
+Web API for Dovecot to authenticate against. It is designed to
+interoperate with the Roundcube Plugin `mpipks/imap_apppasswd`
+while simultaneously providing audit logging similar to
+[`dovecot_badclients`](https://github.com/bennet0496/dovecot_badclients).
+
+While it is possible to implement App Password with a normal SQL
+passdb in Dovecot. It becomes more difficult if precise last-login
+tracking is desired, as it is only reliably possible to get Dovecot
+to run a post-login script for the IMAP (and POP3) service. If an MTA
+is connected via SASL, these login would not be tracked.
+
+# Requirements and Setup
+
+To get this work you'll need the **Dovecot LUA Plugin**, a **Python runtime**,
+a **Redis-compatible server**, a **MySQL-compatible Database** and the **MaxMind
+GeoLite2** City and ASN MMDB-Databases.
+
+You'll find the Python requirements in the `requirements.txt`. Either install
+them to your system or create a virtual environment with `venv` or `conda`.
+
+It is recommended to use [KeyDB](https://docs.keydb.dev/) (as Redis Drop-in)
+and MariaDB (as MySQL Drop-in)
+
+```bash
+apt install dovecot-auth-lua python3-venv python3-systemd \
+ keydb-server mariadb-server geoipupdate
+
+cd /path/to/install/to
+git clone ...
+
+cd dovecot_web_auth
+
+python3 -m venv --system-site-packages --symlinks .venv
+source .venv/bin/activate
+pip install -r requirements.txt
+```
+
+## Configuration
+Before stating it you will need to configure you database and set up your
+`config.toml`.
+
+First get a GeoLite License and configure it according to the [MaxMind Docs](https://dev.maxmind.com/geoip/geolite2-free-geolocation-data).
+
+Then make sure Redis/KeyDB, MariaDB and the GeoIP Updater are running
+```bash
+systemctl enable --now keydb-server.service
+systemctl enable --now mariadb.service
+systemctl enable --now geoipupdate.timer
+```
+
+Then seed the database (or import the old dump)
+```sql
+CREATE DATABASE mail_auth;
+GRANT USAGE ON *.* TO `mailserver`@`localhost` IDENTIFIED BY 'password123';
+GRANT USAGE ON *.* TO `roundcube`@`webmail.example.com` IDENTIFIED BY 'password123';
+
+GRANT SELECT ON `mail_auth`.`log` TO `roundcube`@`webmail.example.com`;
+GRANT SELECT, SHOW VIEW ON `mail_auth`.`app_passwords_with_log` TO `roundcube`@`webmail.example.com`;
+GRANT SELECT, INSERT, UPDATE (`comment`), DELETE ON `mail_auth`.`app_passwords` TO `roundcube`@`webmail.example.com`;
+
+GRANT SELECT ON `mail_auth`.`app_passwords` TO `mailserver`@`localhost`;
+GRANT SELECT, INSERT ON `mail_auth`.`log` TO `mailserver`@`localhost`;
+```
+
+Then import the DDL (or previous dump)
+```bash
+mysql mail_auth < database/DDL.sql
+```
+
+Now set up the `config.toml`. An example of the available options is in
+`config.toml.dist`. A minimal configuration could be the following
+```toml
+[database]
+dsn = "mysql+pymysql://user:pass@host/mail"
+[ldap]
+host = "ldap.example.com"
+basedn = "ou=users,dc=example,dc=com"
+[audit]
+audit_result_success = "unknown"
+[audit.maxmind]
+city = "./mmdb/GeoLite2-City.mmdb"
+asn = "./mmdb/GeoLite2-ASN.mmdb"
+[audit.lists]
+ip_networks = "/path/to/list/..."
+reverse_hostname = "/path/to/list/..."
+network_name = "/path/to/list/..."
+network_cc = "/path/to/list/..."
+entities = "/path/to/list/..."
+as_numbers = "/path/to/list/..."
+as_names = "/path/to/list/..."
+as_cc = "/path/to/list/..."
+geo_location_ids = "/path/to/list/..."
+```
+
+The list setup will be detailed below.
+
+## Running
+To run the API use `uvicorn` or run `main.py` directly
+```bash
+uvicorn main:app --host 127.0.0.1 --port 8000 --workers 4
+```
+If the `config.toml` is not in your current working directory, set the environment variable
+`CONFIG_PATH` to the path to the file.
+
+To run it as a system service, you can use the `dovecot-web-auth.service`
+SystemD unit as an example
+
+## Setup Dovecot
+
+This Web API can be used for App Password authentication, as well as just for auditing akin to
+[`dovecot_badclients`](https://github.com/bennet0496/dovecot_badclients). To integrate this functionality
+into Dovecot, two small LUA Scripts are used.
+
+### Auth setup
+Create a passdb with the following config
+```
+passdb {
+ driver = lua
+ args = file=/etc/dovecot/auth.lua blocking=yes
+ skip = authenticated
+}
+```
+You may want to reconfigure your real passdb, to only allow its usages from certain hosts
+like a Webmailer with 2 Factor. Add the following to that passdb
+```
+...
+passdb {
+ ...
+ # Replace with Roundcube IP
+ override_fields = allow_nets=1.2.3.4/32
+ ...
+}
+...
+```
+
+### Audit setup
+If you just need auditing without App Password, e.g. in a transitional period, add the following as
+your last passdb
+```
+passdb {
+ driver = lua
+ args = file=/etc/dovecot/audit.lua blocking=yes
+ skip = unauthenticated
+}
+```
+And add `result_success = continue-ok` to any previous passdb you want auditing for
+```
+passdb {
+...
+ result_success = continue-ok
+}
+```
+
+# Using the lists
+
+With the `[audit.lists]` config section you set up the path to lists that are parsed during auditing.
+If an entry matches, the login request is denied. However, baring the service limitation none of lists
+will ever (with a few exceptions) block request from IPs not globally reachable by [iana-ipv4-special-registry](https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml)
+(for IPv4) or [iana-ipv6-special-registry](https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml)
+(for IPv6). These exceptions are as follows
+- 100.64.0.0/10 is not considered private
+- For IPv4-mapped IPv6-addresses the result is determined by the semantics of the underlying IPv4 addresses
+
+I.e. everything that is considered private by [`ipaddress._BaseAddress.is_private`](https://docs.python.org/3.12/library/ipaddress.html#ipaddress.IPv4Address.is_private).
+
+### `asn.deny.lst`
+List of literal Autonomous System Numbers to block
+
+E.g.
+```
+# Vodafone, DE
+AS3209
+```
+
+### Autonomous System Names: `as_names`
+List of Python Regular Expressions matched against IPWhois' `as_desc` and MaxMind's `as_org`.
+The IPWhois `as_desc` consists of the AS', first line of `descr` and the CC when running WHOIS
+against the AS. And MaxMind's `as_org` is similar or just the human-readable company name.
+
+E.g.`whois AS3209`
+```
+% This is the RIPE Database query service.
+% The objects are in RPSL format.
+%
+% The RIPE Database is subject to Terms and Conditions.
+% See https://apps.db.ripe.net/docs/HTML-Terms-And-Conditions
+
+% Note: this output has been filtered.
+% To receive output for a database update, use the "-B" flag.
+
+% Information related to 'AS3209 - AS3353'
+
+as-block: AS3209 - AS3353
+descr: RIPE NCC ASN block
+remarks: These AS Numbers are assigned to network operators in the RIPE NCC service region.
+mnt-by: RIPE-NCC-HM-MNT
+created: 2018-11-22T15:27:19Z
+last-modified: 2018-11-22T15:27:19Z
+source: RIPE
+
+% Information related to 'AS3209'
+
+% Abuse contact for 'AS3209' is 'abuse.de@vodafone.com'
+
+aut-num: AS3209
+as-name: VODANET
+org: ORG-MAT1-RIPE
+descr: International IP-Backbone of Vodafone
+descr: Duesseldorfer Strasse 15
+descr: D-65760 Eschborn
+descr: Germany
+descr: http://www.vodafone.de
+```
+Becomes `VODANET International IP-Backbone of Vodafone, DE`, for IPWhois. And MaxMind outputs
+`Vodafone GmbH`.
+
+Each line needs to be a valid Python Regular expression that need to match the entire
+description e.g. `VODANET.*` or `.*Vodafone.*` for `VODANET International IP-Backbone of Vodafone, DE`
+
+### Network and AS Country Codes: `as_cc` and `network_cc`
+List [ISO 3166-2 country codes](https://www.iso.org/iso-3166-country-codes.html), associated to the Autonomous System or Network. One per line.
+
+
+
+| CC | Country | CC | Country | CC | Country | CC | Country |
+|:--:|:----------------------------------|:--:|:-------------------------------------------|:--:|:-------------------------------------------|:--:|:-----------------------------------|
+| `AD` | Andorra | `EG` | Egypt | `LB` | Lebanon | `RO` | Romania |
+| `AE` | United Arab Emirates | `EH` | Western Sahara | `LC` | Saint Lucia | `RS` | Serbia |
+| `AF` | Afghanistan | `ER` | Eritrea | `LI` | Liechtenstein | `RU` | Russian Federation |
+| `AG` | Antigua & Barbuda | `ES` | Spain | `LK` | Sri Lanka | `RW` | Rwanda |
+| `AI` | Anguilla | `ET` | Ethiopia | `LR` | Liberia | `SA` | Saudi Arabia |
+| `AL` | Albania | `FI` | Finland | `LS` | Lesotho | `SB` | Solomon Islands |
+| `AM` | Armenia | `FJ` | Fiji | `LT` | Lithuania | `SC` | Seychelles |
+| `AN` | Netherlands Antilles | `FK` | Falkland Islands (Malvinas) | `LU` | Luxembourg | `SD` | Sudan |
+| `AO` | Angola | `FM` | Micronesia, Federated States Of | `LV` | Latvia | `SE` | Sweden |
+| `AQ` | Antarctica | `FO` | Faroe Islands | `LY` | Libyan Arab Jamahiriya | `SG` | Singapore |
+| `AR` | Argentina | `FR` | France | `MA` | Morocco | `SH` | St. Helena |
+| `AS` | American Samoa | `GA` | Gabon | `MC` | Monaco | `SI` | Slovenia |
+| `AT` | Austria | `GB` | United Kingdom | `MD` | Moldova, Republic Of | `SJ` | Svalbard & Jan Mayen Islands |
+| `AU` | Australia | `GD` | Grenada | `ME` | Montenegro | `SK` | Slovakia (Slovak Republic) |
+| `AW` | Aruba | `GE` | Georgia | `MF` | Saint Martin | `SL` | Sierra Leone |
+| `AX` | Aland Islands | `GF` | French Guiana | `MG` | Madagascar | `SM` | San Marino |
+| `AZ` | Azerbaijan | `GG` | Guernsey | `MH` | Marshall Islands | `SN` | Senegal |
+| `BA` | Bosnia & Herzegovina | `GH` | Ghana | `MK` | Macedonia, The Former Yugoslav Republic Of | `SO` | Somalia |
+| `BB` | Barbados | `GI` | Gibraltar | `ML` | Mali | `SR` | Suriname |
+| `BD` | Bangladesh | `GL` | Greenland | `MM` | Myanmar | `ST` | Sao Tome & Principe |
+| `BE` | Belgium | `GM` | Gambia | `MN` | Mongolia | `SV` | El Salvador |
+| `BF` | Burkina Faso | `GN` | Guinea | `MO` | Macau | `SY` | Syrian Arab Republic |
+| `BG` | Bulgaria | `GP` | Guadeloupe | `MP` | Northern Mariana Islands | `SZ` | Swaziland |
+| `BH` | Bahrain | `GQ` | Equatorial Guinea | `MQ` | Martinique | `TC` | Turks & Caicos Islands |
+| `BI` | Burundi | `GR` | Greece | `MR` | Mauritania | `TD` | Chad |
+| `BJ` | Benin | `GS` | South Georgia & The South Sandwich Islands | `MS` | Montserrat | `TF` | French Southern Territories |
+| `BM` | Bermuda | `GT` | Guatemala | `MT` | Malta | `TG` | Togo |
+| `BN` | Brunei Darussalam | `GU` | Guam | `MU` | Mauritius | `TH` | Thailand |
+| `BO` | Bolivia | `GW` | Guinea-Bissau | `MV` | Maldives | `TJ` | Tajikistan |
+| `BR` | Brazil | `GY` | Guyana | `MW` | Malawi | `TK` | Tokelau |
+| `BS` | Bahamas | `HK` | Hong Kong | `MX` | Mexico | `TL` | Timor-Leste |
+| `BT` | Bhutan | `HM` | Heard & Mc Donald Islands | `MY` | Malaysia | `TM` | Turkmenistan |
+| `BV` | Bouvet Island | `HN` | Honduras | `MZ` | Mozambique | `TN` | Tunisia |
+| `BW` | Botswana | `HR` | Croatia (Hrvatska) | `NA` | Namibia | `TO` | Tonga |
+| `BY` | Belarus | `HT` | Haiti | `NC` | New Caledonia | `TR` | Turkey |
+| `BZ` | Belize | `HU` | Hungary | `NE` | Niger | `TT` | Trinidad & Tobago |
+| `CA` | Canada | `ID` | Indonesia | `NF` | Norfolk Island | `TV` | Tuvalu |
+| `CC` | Cocos (Keeling) Islands | `IE` | Ireland | `NG` | Nigeria | `TW` | Taiwan |
+| `CD` | Congo, Democratic Republic Of The | `IL` | Israel | `NI` | Nicaragua | `TZ` | Tanzania, United Republic Of |
+| `CF` | Central African Republic | `IM` | Isle Of Man | `NL` | Netherlands | `UA` | Ukraine |
+| `CG` | Congo | `IN` | India | `NO` | Norway | `UG` | Uganda |
+| `CH` | Switzerland | `IO` | British Indian Ocean Territory | `NP` | Nepal | `UM` | United States Minor Outlying Islands|
+| `CI` | Cote D’Ivoire | `IQ` | Iraq | `NR` | Nauru | `US` | United States |
+| `CK` | Cook Islands | `IR` | Iran (Islamic Republic Of) | `NU` | Niue | `UY` | Uruguay |
+| `CL` | Chile | `IS` | Iceland | `NZ` | New Zealand | `UZ` | Uzbekistan |
+| `CM` | Cameroon | `IT` | Italy | `OM` | Oman | `VA` | Holy See (Vatican City State) |
+| `CN` | China | `JE` | Jersey | `PA` | Panama | `VC` | Saint Vincent & The Grenadines |
+| `CO` | Colombia | `JM` | Jamaica | `PE` | Peru | `VE` | Venezuela, Bolivarian Republic Of |
+| `CR` | Costa Rica | `JO` | Jordan | `PF` | French Polynesia | `VG` | Virgin Islands (British) |
+| `CU` | Cuba | `JP` | Japan | `PG` | Papua New Guinea | `VI` | Virgin Islands (U.S.) |
+| `CV` | Cape Verde | `KE` | Kenya | `PH` | Philippines | `VN` | Viet Nam |
+| `CX` | Christmas Island | `KG` | Kyrgyzstan | `PK` | Pakistan | `VU` | Vanuatu |
+| `CY` | Cyprus | `KH` | Cambodia | `PL` | Poland | `WF` | Wallis & Futuna Islands |
+| `CZ` | Czech Republic | `KI` | Kiribati | `PM` | St. Pierre & Miquelon | `WS` | Samoa |
+| `DE` | Germany | `KM` | Comoros | `PN` | Pitcairn | `YE` | Yemen |
+| `DJ` | Djibouti | `KN` | Saint Kitts & Nevis | `PR` | Puerto Rico | `YT` | Mayotte |
+| `DK` | Denmark | `KP` | Korea, Democratic People’S Republic Of | `PS` | Palestinian Territory | `ZA` | South Africa |
+| `DM` | Dominica | `KR` | Korea, Republic Of | `PT` | Portugal | `ZM` | Zambia |
+| `DO` | Dominican Republic | `KW` | Kuwait | `PW` | Palau | `ZW` | Zimbabwe |
+| `DZ` | Algeria | `KY` | Cayman Islands | `PY` | Paraguay | `ZZ` | Local Country |
+| `EC` | Ecuador | `KZ` | Kazakhstan | `QA` | Qatar | | |
+| `EE` | Estonia | `LA` | Lao People’S Democratic Republic | `RE` | Reunion | | |
+
+
+
+`whois 139.162.133.252`
+```
+% This is the RIPE Database query service.
+% The objects are in RPSL format.
+%
+% The RIPE Database is subject to Terms and Conditions.
+% See https://apps.db.ripe.net/docs/HTML-Terms-And-Conditions
+
+% Note: this output has been filtered.
+% To receive output for a database update, use the "-B" flag.
+
+% Information related to '139.162.0.0 - 139.162.255.255'
+
+% Abuse contact for '139.162.0.0 - 139.162.255.255' is 'abuse@linode.com'
+
+inetnum: 139.162.0.0 - 139.162.255.255
+netname: EU-LINODE-20141229
+descr: 139.162.0.0/16
+org: ORG-LL72-RIPE
+country: US
+admin-c: TA2589-RIPE
+abuse-c: LAS85-RIPE
+tech-c: TA2589-RIPE
+status: LEGACY
+remarks: Please send abuse reports to abuse@linode.com
+mnt-by: linode-mnt
+created: 2004-02-02T16:20:09Z
+last-modified: 2022-12-12T21:26:29Z
+source: RIPE
+```
+Is Network CC `US` but the associated AS, `AS63949` would have (interestingly enough) `NL`
+(which makes little sense, but this what `ipwhois` detects)
+
+### ISP Network Name: `network_name`
+List of Python regexes of provider specified network names in WHOIS.
+
+Names might be `EU-LINODE-20141229`, `DE-D2VODAFONE-20220628`, `DTAG-DIAL16` or `AMAZON-IAD`, `MSFT`
+
+These are not necessarily unique.
+
+Each line needs to be a valid Lua Regular expression that need to match the entire name like
+`EU-LINODE-20141229` or `.*LINODE.*` for `EU-LINODE-20141229`
+
+### Reverse Hostnames: `reverse_hostname`
+List of Python regexes of reverse hostnames resolvable via the local DNS resolver.
+
+E.g.
+```
+$ nslookup 52.23.158.188
+188.158.23.52.in-addr.arpa name = ec2-52-23-158-188.compute-1.amazonaws.com.
+```
+Empty (NXDOMAIN) results will be matched as `<>`
+```
+$ nslookup 52.97.246.245
+** server can't find 245.246.97.52.in-addr.arpa: NXDOMAIN
+```
+Each line needs to be a valid Python Regular expression that need to match the entire reverse name
+(without trailing dots), e.g. `.*\.compute-1\.amazonaws\.com` for
+`ec2-52-23-158-188.compute-1.amazonaws.com`
+
+### WHOIS Related Entities: `entities`
+List of related WHOIS entities like administrators or organizations.
+
+E.g. Related with `176.112.169.192` (ASN 7764) are `EY1327-RIPE` (VK admin-c),
+`ORG-LLCn4-RIPE` (VK LLC), `RIPE-NCC-END-MNT` (RIPE Contact), `VKCOMPANY-MNT`
+(Maintainer for VK objects), `VKNC` (VK admin-c), `MAIL-RU` (abuse-c)
+
+### IP CIDR Networks: `ip_networks`
+List of IPv4 or IPv6 CIDR networks to block access from. E.g. `176.112.168.0/21`
+
+There is no check for set host-bits, the mask is just applied to both addresses to compare
+network addresses, if they match the request is blocked. This means for example
+`176.112.170.0/21` is equivalent to `176.112.168.0/21`
+
+### Geonames IDs (MaxMind): `geo_location_ids`
+A list of numeric [Geoname IDs](https://www.geonames.org/). Matched against the City's,
+Most specific Subdivision's, Country's Geoname ID.
+
+E.g.
+```
+# Virgina, USA
+6254928
+
+# Ashburn, Virgina, USA
+4744870
+```
+
+# Logs
+The script additionally generates log lines like this for later examination
+```
+Mai 01 04:06:34 honeypot mail-audit[1084587]: user=, service=imap, ip=176.112.169.218, host=rimap26.i.mail.ru, asn=AS47764, as_cc=RU, as_desc=, as_org=, net_name=, net_cc=RU, entity=EY1327-RIPE, entity=ORG-LLCn4-RIPE, entity=RIPE-NCC-END-MNT, entity=VKCOMPANY-MNT, entity=VKNC, entity=MAIL-RU, city=<550478/Khasavyurt>, subdivision=<567293/Dagestan>, country=<2017370/Russia>, represented_country=, registered_country=<2017370/Russia>, lat=43.2465, lon=46.59, rad=20km
+Mai 03 13:07:45 honeypot mail-audit[1084587]: user=, service=imap, ip=172.17.1.204, host=dhcp204.internal, asn=None, as_cc=ZZ, as_desc=, as_org=, net_name=, net_cc=ZZ, entity=
+Mai 06 04:20:25 honeypot mail-audit[1084587]: user=, service=imap, ip=139.162.133.252, host=node-eu-0001.email2-cloud.com, asn=AS63949, as_cc=NL, as_desc=, as_org=, net_name=, net_cc=US, entity=linode-mnt, entity=ORG-LL72-RIPE, entity=TA2589-RIPE, entity=LAS85-RIPE, city=<2925533/Frankfurt am Main>, subdivision=<2905330/Hesse>, country=<2921044/Germany>, represented_country=, registered_country=<2750405/The Netherlands>, lat=50.1188, lon=8.6843, rad=20km
+Mai 06 11:00:20 honeypot mail-audit[1084587]: user=, service=pop3, ip=209.85.218.15, host=mail-ej1-f15.google.com, asn=AS15169, as_cc=US, as_desc=, as_org=, net_name=, net_cc=None, entity=GOGL, city=, country=<6252001/United States>, represented_country=, registered_country=<6252001/United States>, lat=37.751, lon=-97.822, rad=1000km
+Mai 06 11:05:24 honeypot mail-audit[1084587]: user=, service=smtp, ip=209.85.218.53, host=mail-ej1-f53.google.com, asn=AS15169, as_cc=US, as_desc=, as_org=, net_name=, net_cc=None, entity=GOGL, city=, country=<6252001/United States>, represented_country=, registered_country=<6252001/United States>, lat=37.751, lon=-97.822, rad=1000km
+```
+The information is also deconstructed into journal meta information.
+
+
+
+```json
+{
+ "AUDIT_AS_DESC":"GOOGLE, US",
+ "_RUNTIME_SCOPE":"system",
+ "AUDIT_MAXMIND_COUNTRY":"{'name': 'United States', 'geoname_id': 6252001, 'code': 'US'}",
+ "_BOOT_ID":"...",
+ "AUDIT_NET_NAME":"GOOGLE",
+ "AUDIT_IP":"209.85.218.53",
+ "AUDIT_MATCHED":"None",
+ "_EXE":"/usr/bin/python3.12",
+ "_MACHINE_ID":"...",
+ "AUDIT_MAXMIND_CITY":"{'name': None, 'geoname_id': None, 'code': None}",
+ "AUDIT_MAXMIND_REGISTERED_COUNTRY":"{'name': 'United States', 'geoname_id': 6252001, 'code': 'US'}",
+ "_CMDLINE":"...",
+ "CODE_LINE":"129",
+ "_AUDIT_LOGINUID":"1000",
+ "AUDIT_RESERVED":"False",
+ "_CAP_EFFECTIVE":"0",
+ "AUDIT_SERVICE":"smtp",
+ "AUDIT_NET_CC":"None",
+ "_SYSTEMD_CGROUP":"...",
+ "__CURSOR":"...",
+ "_SYSTEMD_INVOCATION_ID":"...",
+ "_AUDIT_SESSION":"4",
+ "_PID":"4161694",
+ "_HOSTNAME":"...",
+ "__REALTIME_TIMESTAMP":"1727355927030069",
+ "AUDIT_MAXMIND_POSTAL_CODE":"None",
+ "__SEQNUM_ID":"...",
+ "AUDIT_BLOCKED":"False",
+ "_SOURCE_REALTIME_TIMESTAMP":"1727355927030042",
+ "AUDIT_MAXMIND_REPRESENTED_COUNTRY":"{'name': None, 'geoname_id': None, 'code': None}",
+ "AUDIT_MAXMIND_LOCATION":"{'accuracy_radius': 1000, 'latitude': 37.751, 'longitude': -97.822, 'time_zone': 'America/Chicago'}",
+ "_SYSTEMD_UNIT":"user@1000.service",
+ "_SYSTEMD_USER_UNIT":"...",
+ "SYSLOG_IDENTIFIER":"mail-audit",
+ "_SYSTEMD_SLICE":"user-1000.slice",
+ "_COMM":"python",
+ "_SYSTEMD_USER_SLICE":"app.slice",
+ "AUDIT_ENTITIES":"['GOGL']",
+ "AUDIT_USER":"bbecker",
+ "_SYSTEMD_OWNER_UID":"1000",
+ "_UID":"1000",
+ "CODE_FUNC":"audit_log",
+ "__SEQNUM":"4339035",
+ "AUDIT_ASN":"AS15169",
+ "AUDIT_LOG":"True",
+ "__MONOTONIC_TIMESTAMP":"872567118506",
+ "_TRANSPORT":"journal",
+ "AUDIT_MAXMIND_SUBDIVISIONS":"()",
+ "AUDIT_REV_HOST":"mail-ej1-f53.google.com",
+ "AUDIT_MAXMIND_CONTINENT":"{'name': 'North America', 'geoname_id': 6255149, 'code': 'NA'}",
+ "_GID":"1000",
+ "MESSAGE":"user=, service=smtp, ip=209.85.218.53, host=mail-ej1-f53.google.com, asn=AS15169, as_cc=US, as_desc=, as_org=, net_name=, net_cc=None, entity=GOGL, city=, country=<6252001/United States>, represented_country=, registered_country=<6252001/United States>, lat=37.751, lon=-97.822, rad=1000km",
+ "CODE_FILE":"/path/to/dovecot_web_auth/audit.py",
+ "AUDIT_AS_CC":"US"
+}
+```
+
+
+
+A script to retrieve user statistics of the last 24h might look something like this
+```bash
+journalctl -S "24 hours ago" -g "mail-audit" | awk -F : '{print $4}' | sort | uniq -c | sort -h
+```
+
+More sophisticated analytics can be generated with `audit_mail.py`, which need a yaml file describing
+additional information about ASN that should be considered, meant for generating an email.
\ No newline at end of file
diff --git a/config.py b/config.py
index af1f243..262a563 100644
--- a/config.py
+++ b/config.py
@@ -28,8 +28,8 @@ class Ldap(BaseModel):
class MaxMind(BaseModel):
- city: str | None = None
- asn: str | None = None
+ city: str
+ asn: str
country: str | None = None
@@ -56,7 +56,7 @@ class Audit(BaseModel):
audit_result_success : str
audit_process_unknown : bool = False
log_local : bool = True
- local_locationname: str | None = None
+ local_locationname: str = "local network"
local_networks: Dict[str, str] | None = None
maxmind: MaxMind | None = None
lists: AuditLists | None = None
@@ -64,14 +64,14 @@ class Audit(BaseModel):
class Cache(BaseModel):
mode : str = "redis"
- host : str
+ host : str = "localhost"
port : int = 6379
class Auth(BaseModel):
disallow_passwords_from: List[str] = []
class EnvSettings(BaseSettings):
- config_path: str
+ config_path: str = "config.toml"
model_config = SettingsConfigDict()
@@ -122,7 +122,7 @@ class LogConfig(BaseSettings):
class Settings(BaseSettings):
database: Database
ldap: Ldap
- cache: Cache
+ cache: Cache = Cache()
audit: Audit
auth: Auth | None = None
log: LogConfig = LogConfig()
diff --git a/config.toml.dist b/config.toml.dist
index b49092a..641c8b7 100644
--- a/config.toml.dist
+++ b/config.toml.dist
@@ -3,11 +3,22 @@ dsn = "mysql+pymysql://user:pass@host/mail"
[ldap]
host = "ldap.example.com"
+port = 389 # Optional
tls = false
+tls_cert = "/path/to/cert" # Optional
basedn = "ou=users,dc=example,dc=com"
+bind = "cn=binduser,dc=example,dc=com" # Optional
+password = "bindpassword" # Optional
[cache]
+# Redis host
host = "localhost"
+port = 6379 # Optional
+
+[auth]
+# Hosts or Networks where app password should be forbidden from
+# like the webmail host, that should use the real password
+disallow_passwords_from = ["1.2.3.4/32"]
[audit]
# Comma separated lists fof services to always deny for
@@ -36,6 +47,8 @@ log_local = true
[audit.maxmind]
city = "./mmdb/GeoLite2-City.mmdb"
asn = "./mmdb/GeoLite2-ASN.mmdb"
+# Currently unused
+# country = "./mmdb/GeoLite2-Country.mmdb"
[audit.lists]
ip_networks = "./lists/ip_net.deny.lst"
@@ -51,4 +64,26 @@ as_names = "./lists/as_dscr.deny.lst"
as_cc = "./lists/as_cc.deny.list"
geo_location_ids = "./lists/maxmind/geo_loc.deny.lst"
-coordinates = "./lists/maxmind.deny.lst"
\ No newline at end of file
+# currently not implemented
+coordinates = "./lists/maxmind.deny.lst"
+
+[log]
+# Application logging settings. Do not affect the systemd audit logging
+version = 1
+disable_existing_loggers = true
+
+[log.formatters]
+default = { "()" = "uvicorn.logging.DefaultFormatter", fmt = "%(asctime)s %(name)-30.30s [%(levelname)5.5s] %(message)s", datefmt = "%Y-%m-%d %H:%M:%S"}
+
+[log.handlers]
+stderr = { formatter = "default", class = "logging.StreamHandler", stream = "ext://sys.stderr" }
+stdout = { formatter = "default", class = "logging.StreamHandler", stream = "ext://sys.stdout" }
+
+[log.loggers]
+dovecot_web_auth = {handlers = ["stdout"], level = "DEBUG"}
+fastapi = {handlers = ["stderr"], level = "INFO"}
+uvicorn.access = {handlers = ["stdout"], level = "INFO"}
+uvicorn.error = {handlers = ["stderr"], level = "INFO"}
+uvicorn.asgi = {handlers = ["stdout"], level = "INFO"}
+sqlalchemy = {handlers = ["stderr"], level = "ERROR"}
+ipwhois = {handlers = ["stdout"], level = "INFO"}
\ No newline at end of file